Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1cc812cd3 | ||
|
|
e273a1fa3a | ||
|
|
e297168eef | ||
|
|
984a5384a5 | ||
|
|
b37476785c | ||
|
|
997d5b691f | ||
|
|
63e37a76bf | ||
|
|
66b8a7f74b | ||
|
|
35b07aeacd | ||
|
|
fe2a30456f | ||
|
|
e23d6f3ba7 | ||
|
|
f167435cfc | ||
|
|
3c93bfc943 | ||
|
|
9e617db031 | ||
|
|
258dd35754 | ||
|
|
126ee2ac35 | ||
|
|
188e895d6a | ||
|
|
08ecb92fdc | ||
|
|
683b9db3d9 | ||
|
|
880c1bed31 | ||
|
|
aba4972866 | ||
|
|
2758b202fa | ||
|
|
f797d0bf8d | ||
|
|
5602dc5089 | ||
|
|
19722eeec7 | ||
|
|
fc558e41ea | ||
|
|
daf070f876 | ||
|
|
2cd02f1ea8 | ||
|
|
e1ce26896e | ||
|
|
bd2245eee3 | ||
|
|
0b03791672 | ||
|
|
7250214259 | ||
|
|
46c3b5079d | ||
|
|
3e618923ea |
@@ -59,6 +59,9 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
|
|||||||
SendCartridgeUiReadyEvent(_activeProgram.Value);
|
SendCartridgeUiReadyEvent(_activeProgram.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_activeCartridgeUI is IDisposable disposableUi) // Funky: Handle cleanup for popups and such.
|
||||||
|
disposableUi.Dispose();
|
||||||
|
|
||||||
_activeCartridgeUI = ui;
|
_activeCartridgeUI = ui;
|
||||||
_activeUiFragment?.Dispose();
|
_activeUiFragment?.Dispose();
|
||||||
_activeUiFragment = control;
|
_activeUiFragment = control;
|
||||||
@@ -121,8 +124,13 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
|
|||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|
||||||
if (disposing)
|
if (disposing) // Funky: Handle cleanup for popups and such.
|
||||||
|
{
|
||||||
|
if (_activeCartridgeUI is IDisposable disposableUi)
|
||||||
|
disposableUi.Dispose();
|
||||||
|
|
||||||
_activeUiFragment?.Dispose();
|
_activeUiFragment?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CartridgeComponent? RetrieveCartridgeComponent(EntityUid? cartridgeUid)
|
protected CartridgeComponent? RetrieveCartridgeComponent(EntityUid? cartridgeUid)
|
||||||
|
|||||||
@@ -26,6 +26,16 @@
|
|||||||
</PanelContainer.PanelOverride>
|
</PanelContainer.PanelOverride>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
|
|
||||||
|
<!-- Group chat icon -->
|
||||||
|
<TextureRect Name="GroupIcon"
|
||||||
|
TexturePath="/Textures/_Funkystation/Interface/NanoChat/members.png"
|
||||||
|
MinSize="16 16"
|
||||||
|
MaxSize="16 16"
|
||||||
|
Stretch="KeepAspectCentered"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="4 0 6 0"
|
||||||
|
Visible="False" />
|
||||||
|
|
||||||
<!-- Text container -->
|
<!-- Text container -->
|
||||||
<BoxContainer Orientation="Vertical"
|
<BoxContainer Orientation="Vertical"
|
||||||
HorizontalExpand="True"
|
HorizontalExpand="True"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public sealed partial class NanoChatEntry : BoxContainer
|
|||||||
JobLabel.Text = FormattedMessage.EscapeText(SharedNanoChatSystem.Truncate(recipient.JobTitle ?? string.Empty, _maxIdJobLength));
|
JobLabel.Text = FormattedMessage.EscapeText(SharedNanoChatSystem.Truncate(recipient.JobTitle ?? string.Empty, _maxIdJobLength));
|
||||||
JobLabel.Visible = !string.IsNullOrWhiteSpace(JobLabel.Text);
|
JobLabel.Visible = !string.IsNullOrWhiteSpace(JobLabel.Text);
|
||||||
UnreadIndicator.Visible = recipient.HasUnread;
|
UnreadIndicator.Visible = recipient.HasUnread;
|
||||||
|
GroupIcon.Visible = recipient.IsGroup;
|
||||||
|
|
||||||
ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null;
|
ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
||||||
|
using Content.Shared.Roles;
|
||||||
using Robust.Client.AutoGenerated;
|
using Robust.Client.AutoGenerated;
|
||||||
using Robust.Client.UserInterface;
|
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Client._DV.CartridgeLoader.Cartridges;
|
namespace Content.Client._DV.CartridgeLoader.Cartridges;
|
||||||
|
|
||||||
@@ -26,12 +29,67 @@ public sealed partial class NanoChatLookupView : PanelContainer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var idx = 0; idx < contacts.Count; idx++)
|
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||||
|
var departmentGroups = new Dictionary<DepartmentPrototype, List<NanoChatRecipient>>();
|
||||||
|
var noDepartmentContacts = new List<NanoChatRecipient>();
|
||||||
|
|
||||||
|
foreach (var contact in contacts)
|
||||||
{
|
{
|
||||||
var contact = contacts[idx];
|
if (!string.IsNullOrWhiteSpace(contact.Department) &&
|
||||||
var isEvenRow = idx % 2 == 0;
|
prototypeManager.TryIndex<DepartmentPrototype>(contact.Department, out var dept))
|
||||||
var contactControl = new ContactContainer(contact, state, isEvenRow, OnStartChat);
|
{
|
||||||
ContactsList.AddChild(contactControl);
|
departmentGroups.GetOrNew(dept).Add(contact);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
noDepartmentContacts.Add(contact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowIndex = 0;
|
||||||
|
foreach (var (department, departmentContacts) in departmentGroups.OrderBy(entry => entry.Key, DepartmentUIComparer.Instance))
|
||||||
|
{
|
||||||
|
departmentContacts.Sort();
|
||||||
|
|
||||||
|
// Add department header
|
||||||
|
ContactsList.AddChild(new Label
|
||||||
|
{
|
||||||
|
Text = Loc.GetString(department.Name),
|
||||||
|
StyleClasses = { "LabelBig" },
|
||||||
|
Margin = new Thickness(0, rowIndex == 0 ? 0 : 8, 0, 2)
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var contact in departmentContacts)
|
||||||
|
{
|
||||||
|
var isEvenRow = rowIndex % 2 == 0;
|
||||||
|
var contactControl = new ContactContainer(contact, state, isEvenRow, OnStartChat);
|
||||||
|
ContactsList.AddChild(contactControl);
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add contacts without departments at the end
|
||||||
|
if (noDepartmentContacts.Count > 0)
|
||||||
|
{
|
||||||
|
noDepartmentContacts.Sort();
|
||||||
|
|
||||||
|
if (departmentGroups.Count > 0)
|
||||||
|
{
|
||||||
|
ContactsList.AddChild(new Label
|
||||||
|
{
|
||||||
|
Text = Loc.GetString("nano-chat-other-department"),
|
||||||
|
StyleClasses = { "LabelBig" },
|
||||||
|
Margin = new Thickness(0, 8, 0, 2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var contact in noDepartmentContacts)
|
||||||
|
{
|
||||||
|
var isEvenRow = rowIndex % 2 == 0;
|
||||||
|
var contactControl = new ContactContainer(contact, state, isEvenRow, OnStartChat);
|
||||||
|
ContactsList.AddChild(contactControl);
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,9 +100,13 @@ public sealed partial class NanoChatLookupView : PanelContainer
|
|||||||
HorizontalExpand = true;
|
HorizontalExpand = true;
|
||||||
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
|
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
|
||||||
|
|
||||||
|
var displayName = contact.Name;
|
||||||
|
if (!string.IsNullOrWhiteSpace(contact.JobTitle))
|
||||||
|
displayName = $"{contact.Name} ({contact.JobTitle})";
|
||||||
|
|
||||||
var nameLabel = new Label()
|
var nameLabel = new Label()
|
||||||
{
|
{
|
||||||
Text = contact.Name,
|
Text = displayName,
|
||||||
HorizontalAlignment = HAlignment.Left,
|
HorizontalAlignment = HAlignment.Left,
|
||||||
HorizontalExpand = true
|
HorizontalExpand = true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<cartridges:NanoChatMessageBubble
|
<cartridges:NanoChatMessageBubble
|
||||||
xmlns="https://spacestation14.io"
|
xmlns="https://spacestation14.io"
|
||||||
|
xmlns:nanochatui="clr-namespace:Content.Client._DV.NanoChat.UI"
|
||||||
xmlns:cartridges="clr-namespace:Content.Client._DV.CartridgeLoader.Cartridges"
|
xmlns:cartridges="clr-namespace:Content.Client._DV.CartridgeLoader.Cartridges"
|
||||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||||
HorizontalExpand="True">
|
HorizontalExpand="True">
|
||||||
@@ -7,31 +8,34 @@
|
|||||||
<BoxContainer Name="MessageContainer"
|
<BoxContainer Name="MessageContainer"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
HorizontalExpand="True">
|
HorizontalExpand="True">
|
||||||
<!-- Left spacer for other's messages -->
|
<!-- Left spacer for other's messages -->
|
||||||
<Control Name="LeftSpacer"
|
<Control MinSize="12 0" Name="LeftSpacer" />
|
||||||
MinSize="12 0" />
|
|
||||||
|
|
||||||
<!-- Message panel -->
|
<!-- Message panel -->
|
||||||
<BoxContainer Name="MessageBox"
|
<BoxContainer Name="MessageBox"
|
||||||
Orientation="Vertical"
|
Orientation="Vertical"
|
||||||
MaxWidth="320"
|
MaxWidth="320"
|
||||||
HorizontalExpand="True">
|
HorizontalExpand="True">
|
||||||
|
|
||||||
|
<!-- Sender name -->
|
||||||
|
<Label HorizontalAlignment="Left"
|
||||||
|
Margin="10,0,10,2"
|
||||||
|
Name="SenderNameLabel"
|
||||||
|
StyleClasses="LabelSmall"
|
||||||
|
Visible="False" />
|
||||||
|
|
||||||
<PanelContainer Name="MessagePanel"
|
<PanelContainer Name="MessagePanel"
|
||||||
MaxWidth="320"
|
MaxWidth="320"
|
||||||
HorizontalExpand="True">
|
HorizontalExpand="True">
|
||||||
<PanelContainer.PanelOverride>
|
<PanelContainer.PanelOverride>
|
||||||
<graphics:StyleBoxFlat
|
<graphics:StyleBoxFlat BorderThickness="1">
|
||||||
ContentMarginLeftOverride="10"
|
|
||||||
ContentMarginRightOverride="10"
|
|
||||||
ContentMarginTopOverride="6"
|
|
||||||
ContentMarginBottomOverride="6"
|
|
||||||
BorderThickness="1">
|
|
||||||
<!-- Colors set in code based on message sender -->
|
<!-- Colors set in code based on message sender -->
|
||||||
</graphics:StyleBoxFlat>
|
</graphics:StyleBoxFlat>
|
||||||
</PanelContainer.PanelOverride>
|
</PanelContainer.PanelOverride>
|
||||||
|
|
||||||
<RichTextLabel Name="MessageText"
|
<RichTextLabel Name="MessageText"
|
||||||
HorizontalExpand="True" />
|
HorizontalExpand="True"
|
||||||
|
Margin="10,6,10,6" />
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
|
|
||||||
<!-- Delivery failed text -->
|
<!-- Delivery failed text -->
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
||||||
using Robust.Client.AutoGenerated;
|
using Robust.Client.AutoGenerated;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
using Content.Client._Funkystation.NanoChat;
|
||||||
|
|
||||||
namespace Content.Client._DV.CartridgeLoader.Cartridges;
|
namespace Content.Client._DV.CartridgeLoader.Cartridges;
|
||||||
|
|
||||||
[GenerateTypedNameReferences]
|
[GenerateTypedNameReferences]
|
||||||
public sealed partial class NanoChatMessageBubble : BoxContainer
|
public sealed partial class NanoChatMessageBubble : BoxContainer
|
||||||
{
|
{
|
||||||
|
private static readonly Regex EmojiRegex = new(@":(\w+):", RegexOptions.Compiled);
|
||||||
|
|
||||||
public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green
|
public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green
|
||||||
public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray
|
public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray
|
||||||
public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border
|
public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border
|
||||||
@@ -21,7 +25,7 @@ public sealed partial class NanoChatMessageBubble : BoxContainer
|
|||||||
RobustXamlLoader.Load(this);
|
RobustXamlLoader.Load(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetMessage(NanoChatMessage message, bool isOwnMessage)
|
public void SetMessage(NanoChatMessage message, bool isOwnMessage, string? senderName = null, bool showSenderName = false)
|
||||||
{
|
{
|
||||||
if (MessagePanel.PanelOverride is not StyleBoxFlat)
|
if (MessagePanel.PanelOverride is not StyleBoxFlat)
|
||||||
return;
|
return;
|
||||||
@@ -31,8 +35,20 @@ public sealed partial class NanoChatMessageBubble : BoxContainer
|
|||||||
style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor;
|
style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor;
|
||||||
style.BorderColor = BorderColor;
|
style.BorderColor = BorderColor;
|
||||||
|
|
||||||
// Set message content
|
var contentWithEmoji = EmojiRegex.Replace(FormattedMessage.EscapeText(message.Content), "[emoji=\"$1\"]");
|
||||||
MessageText.Text = FormattedMessage.EscapeText(message.Content);
|
|
||||||
|
MessageText.SetMessage(
|
||||||
|
FormattedMessage.FromMarkupPermissive(contentWithEmoji),
|
||||||
|
new[] { typeof(EmojiTag) }
|
||||||
|
);
|
||||||
|
|
||||||
|
SenderNameLabel.Visible = showSenderName && !string.IsNullOrEmpty(senderName);
|
||||||
|
if (SenderNameLabel.Visible)
|
||||||
|
{
|
||||||
|
SenderNameLabel.Text = senderName;
|
||||||
|
SenderNameLabel.Modulate = TextColor;
|
||||||
|
}
|
||||||
|
|
||||||
MessageText.Modulate = TextColor;
|
MessageText.Modulate = TextColor;
|
||||||
|
|
||||||
// Show delivery failed text if needed (only for own messages)
|
// Show delivery failed text if needed (only for own messages)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Robust.Client.UserInterface;
|
|||||||
|
|
||||||
namespace Content.Client._DV.CartridgeLoader.Cartridges;
|
namespace Content.Client._DV.CartridgeLoader.Cartridges;
|
||||||
|
|
||||||
public sealed partial class NanoChatUi : UIFragment
|
public sealed partial class NanoChatUi : UIFragment, IDisposable
|
||||||
{
|
{
|
||||||
private NanoChatUiFragment? _fragment;
|
private NanoChatUiFragment? _fragment;
|
||||||
|
|
||||||
@@ -30,6 +30,12 @@ public sealed partial class NanoChatUi : UIFragment
|
|||||||
_fragment?.UpdateState(cast);
|
_fragment?.UpdateState(cast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Close all popups when switching apps or closing the PDA
|
||||||
|
_fragment?.CloseAllPopups();
|
||||||
|
}
|
||||||
|
|
||||||
private static void SendNanoChatUiMessage(NanoChatUiMessageType type,
|
private static void SendNanoChatUiMessage(NanoChatUiMessageType type,
|
||||||
uint? number,
|
uint? number,
|
||||||
string? content,
|
string? content,
|
||||||
|
|||||||
@@ -60,6 +60,17 @@
|
|||||||
Margin="0 0 4 0"
|
Margin="0 0 4 0"
|
||||||
StyleClasses="OpenBoth"
|
StyleClasses="OpenBoth"
|
||||||
ToolTip="{Loc nano-chat-new-chat}" />
|
ToolTip="{Loc nano-chat-new-chat}" />
|
||||||
|
<Button Name="CreateGroupChatButton"
|
||||||
|
MinSize="32 32"
|
||||||
|
MaxSize="32 32"
|
||||||
|
Margin="0 0 4 0"
|
||||||
|
StyleClasses="OpenBoth"
|
||||||
|
ToolTip="{Loc nano-chat-create-group}">
|
||||||
|
<TextureRect StyleClasses="ButtonSquare"
|
||||||
|
TexturePath="/Textures/_Funkystation/Interface/NanoChat/members.png"
|
||||||
|
Stretch="KeepAspectCentered"
|
||||||
|
MinSize="18 18" />
|
||||||
|
</Button>
|
||||||
<Button Name="LookupButton"
|
<Button Name="LookupButton"
|
||||||
MaxSize="32 32"
|
MaxSize="32 32"
|
||||||
StyleClasses="OpenBoth"
|
StyleClasses="OpenBoth"
|
||||||
@@ -84,12 +95,20 @@
|
|||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</controls:StripeBack>
|
</controls:StripeBack>
|
||||||
|
|
||||||
|
<Label Name="NoIdLabel"
|
||||||
|
Text="{Loc 'nano-chat-no-id-card'}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
VerticalExpand="True"
|
||||||
|
HorizontalExpand="True"
|
||||||
|
Visible="True" />
|
||||||
<!-- Main content split -->
|
<!-- Main content split -->
|
||||||
<BoxContainer Name="ChatView"
|
<BoxContainer Name="ChatView"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
VerticalExpand="True"
|
VerticalExpand="True"
|
||||||
HorizontalExpand="True"
|
HorizontalExpand="True"
|
||||||
Margin="0 5 0 0">
|
Margin="0 5 0 0"
|
||||||
|
Visible="False">
|
||||||
<!-- Left panel: Chat list -->
|
<!-- Left panel: Chat list -->
|
||||||
<PanelContainer StyleClasses="AngleRect"
|
<PanelContainer StyleClasses="AngleRect"
|
||||||
VerticalExpand="True"
|
VerticalExpand="True"
|
||||||
@@ -167,6 +186,18 @@
|
|||||||
Stretch="KeepAspectCentered"
|
Stretch="KeepAspectCentered"
|
||||||
MinSize="18 18" />
|
MinSize="18 18" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<!-- View Members should take place of edit button in group chats, to avoid layout shift when moving between normal and group -->
|
||||||
|
<Button Name="ViewMembersButton"
|
||||||
|
MaxSize="32 32"
|
||||||
|
Visible="False"
|
||||||
|
StyleClasses="OpenBoth"
|
||||||
|
Margin="0 0 4 0"
|
||||||
|
ToolTip="{Loc nano-chat-view-members}">
|
||||||
|
<TextureRect StyleClasses="ButtonSquare"
|
||||||
|
TexturePath="/Textures/_Funkystation/Interface/NanoChat/members.png"
|
||||||
|
Stretch="KeepAspectCentered"
|
||||||
|
MinSize="18 18" />
|
||||||
|
</Button>
|
||||||
<Button Name="DeleteChatButton"
|
<Button Name="DeleteChatButton"
|
||||||
MaxSize="32 32"
|
MaxSize="32 32"
|
||||||
Visible="False"
|
Visible="False"
|
||||||
@@ -197,11 +228,34 @@
|
|||||||
MinSize="18 18" />
|
MinSize="18 18" />
|
||||||
</Control>
|
</Control>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button Name="InviteToGroupButton"
|
||||||
|
MaxSize="32 32"
|
||||||
|
Visible="False"
|
||||||
|
StyleClasses="OpenBoth"
|
||||||
|
Margin="0 0 4 0"
|
||||||
|
ToolTip="{Loc nano-chat-invite-to-group}">
|
||||||
|
<TextureRect StyleClasses="ButtonSquare"
|
||||||
|
TexturePath="/Textures/_Funkystation/Interface/NanoChat/invite.png"
|
||||||
|
Stretch="KeepAspectCentered"
|
||||||
|
MinSize="18 18" />
|
||||||
|
</Button>
|
||||||
<!-- Input row -->
|
<!-- Input row -->
|
||||||
<LineEdit Name="MessageInput"
|
<LineEdit Name="MessageInput"
|
||||||
PlaceHolder="{Loc nano-chat-message-placeholder}"
|
PlaceHolder="{Loc nano-chat-message-placeholder}"
|
||||||
HorizontalExpand="True"
|
HorizontalExpand="True"
|
||||||
StyleClasses="OpenRight" />
|
StyleClasses="OpenRight" />
|
||||||
|
<!-- Emoji Picker -->
|
||||||
|
<Button Name="EmojiButton"
|
||||||
|
MinSize="32 32"
|
||||||
|
MaxSize="32 32"
|
||||||
|
StyleClasses="OpenBoth"
|
||||||
|
Margin="4 0 0 0"
|
||||||
|
ToolTip="{Loc nano-chat-emoji-picker}">
|
||||||
|
<TextureRect StyleClasses="ButtonSquare"
|
||||||
|
TexturePath="/Textures/Interface/Emotes/laugh.png"
|
||||||
|
Stretch="KeepAspectCentered"
|
||||||
|
MinSize="24 24" />
|
||||||
|
</Button>
|
||||||
<Button Name="SendButton"
|
<Button Name="SendButton"
|
||||||
MinSize="32 32"
|
MinSize="32 32"
|
||||||
Disabled="True"
|
Disabled="True"
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Skubman <ba.fallaria@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2024 Tadeo <td12233a@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
// SPDX-FileCopyrightText: 2025 Icepick <122653407+Icepicked@users.noreply.github.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 corresp0nd <46357632+corresp0nd@users.noreply.github.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 taydeo <td12233a@gmail.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
|
using Content.Client._Funkystation.NanoChat;
|
||||||
using Robust.Client.AutoGenerated;
|
using Robust.Client.AutoGenerated;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
@@ -18,10 +29,14 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
|
|
||||||
private int _maxNameLength;
|
private readonly int _maxNameLength;
|
||||||
private int _maxIdJobLength;
|
private readonly int _maxIdJobLength;
|
||||||
private readonly NewChatPopup _newChatPopup;
|
private readonly NewChatPopup _newChatPopup;
|
||||||
private readonly EditChatPopup _editChatPopup;
|
private readonly EditChatPopup _editChatPopup;
|
||||||
|
private readonly CreateGroupChatPopup _createGroupChatPopup;
|
||||||
|
private readonly InviteToGroupPopup _inviteToGroupPopup;
|
||||||
|
private readonly GroupMembersPopup _groupMembersPopup;
|
||||||
|
private readonly EmojiPickerPopup _emojiPickerPopup;
|
||||||
private uint? _currentChat;
|
private uint? _currentChat;
|
||||||
private uint? _pendingChat;
|
private uint? _pendingChat;
|
||||||
private uint _ownNumber;
|
private uint _ownNumber;
|
||||||
@@ -30,6 +45,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
private Dictionary<uint, NanoChatRecipient> _recipients = [];
|
private Dictionary<uint, NanoChatRecipient> _recipients = [];
|
||||||
private Dictionary<uint, List<NanoChatMessage>> _messages = [];
|
private Dictionary<uint, List<NanoChatMessage>> _messages = [];
|
||||||
private HashSet<uint> _mutedChats = [];
|
private HashSet<uint> _mutedChats = [];
|
||||||
|
private List<NanoChatRecipient>? _contacts; // Used in order to show names for people not in recipients
|
||||||
|
|
||||||
public event Action<NanoChatUiMessageType, uint?, string?, string?>? OnMessageSent;
|
public event Action<NanoChatUiMessageType, uint?, string?, string?>? OnMessageSent;
|
||||||
|
|
||||||
@@ -43,6 +59,10 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
|
|
||||||
_newChatPopup = new(_maxNameLength, _maxIdJobLength);
|
_newChatPopup = new(_maxNameLength, _maxIdJobLength);
|
||||||
_editChatPopup = new(_maxNameLength, _maxIdJobLength);
|
_editChatPopup = new(_maxNameLength, _maxIdJobLength);
|
||||||
|
_createGroupChatPopup = new(_maxNameLength);
|
||||||
|
_inviteToGroupPopup = new();
|
||||||
|
_groupMembersPopup = new();
|
||||||
|
_emojiPickerPopup = new();
|
||||||
SetupEventHandlers();
|
SetupEventHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +78,57 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
OnMessageSent?.Invoke(NanoChatUiMessageType.EditChat, number, name, job);
|
OnMessageSent?.Invoke(NanoChatUiMessageType.EditChat, number, name, job);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_createGroupChatPopup.OnGroupCreated += (name) =>
|
||||||
|
{
|
||||||
|
OnMessageSent?.Invoke(NanoChatUiMessageType.CreateGroupChat, null, name, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
_inviteToGroupPopup.OnInvite += (number) =>
|
||||||
|
{
|
||||||
|
if (_currentChat is not uint currentChat)
|
||||||
|
return;
|
||||||
|
|
||||||
|
OnMessageSent?.Invoke(NanoChatUiMessageType.InviteToGroup, currentChat, number.ToString(), null);
|
||||||
|
};
|
||||||
|
|
||||||
|
_groupMembersPopup.OnKick += (number) =>
|
||||||
|
{
|
||||||
|
if (_currentChat is not uint currentChat)
|
||||||
|
return;
|
||||||
|
|
||||||
|
OnMessageSent?.Invoke(NanoChatUiMessageType.KickFromGroup, currentChat, number.ToString(), null);
|
||||||
|
};
|
||||||
|
|
||||||
|
_groupMembersPopup.OnAdmin += (number) =>
|
||||||
|
{
|
||||||
|
if (_currentChat is not uint currentChat)
|
||||||
|
return;
|
||||||
|
|
||||||
|
OnMessageSent?.Invoke(NanoChatUiMessageType.AdminUser, currentChat, number.ToString(), null);
|
||||||
|
};
|
||||||
|
|
||||||
|
_groupMembersPopup.OnDeadmin += (number) =>
|
||||||
|
{
|
||||||
|
if (_currentChat is not uint currentChat)
|
||||||
|
return;
|
||||||
|
|
||||||
|
OnMessageSent?.Invoke(NanoChatUiMessageType.DeadminUser, currentChat, number.ToString(), null);
|
||||||
|
};
|
||||||
|
|
||||||
|
_emojiPickerPopup.OnEmojiSelected += InsertEmoji;
|
||||||
|
|
||||||
NewChatButton.OnPressed += _ =>
|
NewChatButton.OnPressed += _ =>
|
||||||
{
|
{
|
||||||
_newChatPopup.ClearInputs();
|
_newChatPopup.ClearInputs();
|
||||||
_newChatPopup.OpenCentered();
|
_newChatPopup.OpenCentered();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
CreateGroupChatButton.OnPressed += _ =>
|
||||||
|
{
|
||||||
|
_createGroupChatPopup.ClearInputs();
|
||||||
|
_createGroupChatPopup.OpenCentered();
|
||||||
|
};
|
||||||
|
|
||||||
MuteChatButton.OnPressed += _ =>
|
MuteChatButton.OnPressed += _ =>
|
||||||
{
|
{
|
||||||
if (_currentChat is not uint currentChat)
|
if (_currentChat is not uint currentChat)
|
||||||
@@ -134,8 +199,39 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
|
|
||||||
MessageInput.OnTextEntered += _ => SendMessage();
|
MessageInput.OnTextEntered += _ => SendMessage();
|
||||||
SendButton.OnPressed += _ => SendMessage();
|
SendButton.OnPressed += _ => SendMessage();
|
||||||
|
EmojiButton.OnPressed += _ => OpenEmojiPicker();
|
||||||
EditChatButton.OnPressed += _ => BeginEditChat();
|
EditChatButton.OnPressed += _ => BeginEditChat();
|
||||||
DeleteChatButton.OnPressed += _ => DeleteCurrentChat();
|
DeleteChatButton.OnPressed += _ => DeleteCurrentChat();
|
||||||
|
InviteToGroupButton.OnPressed += _ => OpenInvitePopup();
|
||||||
|
ViewMembersButton.OnPressed += _ => OpenMembersPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenEmojiPicker()
|
||||||
|
{
|
||||||
|
// Close any existing emoji picker to prevent duplicates (shouldn't be happening anyway but it sure did)
|
||||||
|
if (_emojiPickerPopup.IsOpen)
|
||||||
|
_emojiPickerPopup.Close();
|
||||||
|
|
||||||
|
_emojiPickerPopup.OpenCentered();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InsertEmoji(string emoji)
|
||||||
|
{
|
||||||
|
MessageInput.InsertAtCursor(emoji);
|
||||||
|
MessageInput.GrabKeyboardFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes all open popups. Called when switching apps or closing the PDA.
|
||||||
|
/// </summary>
|
||||||
|
public void CloseAllPopups()
|
||||||
|
{
|
||||||
|
_emojiPickerPopup.Close();
|
||||||
|
_inviteToGroupPopup.Close();
|
||||||
|
_groupMembersPopup.Close();
|
||||||
|
_newChatPopup.Close();
|
||||||
|
_editChatPopup.Close();
|
||||||
|
_createGroupChatPopup.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleView()
|
private void ToggleView()
|
||||||
@@ -156,7 +252,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
if (_recipients.Count == 0)
|
if (_recipients.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var orderedRecipients = _recipients.OrderBy(r => r.Value.Name).Select(r => r.Key).ToArray();
|
var orderedRecipients = _recipients.OrderBy(r => r.Value).Select(r => r.Key).ToArray();
|
||||||
var currentChatIndex = (direction, _currentChat) switch
|
var currentChatIndex = (direction, _currentChat) switch
|
||||||
{
|
{
|
||||||
(CycleDirection.Up, null) => _recipients.Count,
|
(CycleDirection.Up, null) => _recipients.Count,
|
||||||
@@ -252,6 +348,89 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null);
|
OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OpenInvitePopup()
|
||||||
|
{
|
||||||
|
if (_currentChat is not uint currentChat ||
|
||||||
|
!_recipients.TryGetValue(currentChat, out var recipient) ||
|
||||||
|
!recipient.IsGroup)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
|
||||||
|
var members = recipient.Members ?? new HashSet<uint>();
|
||||||
|
_inviteToGroupPopup.SetContacts(availableContacts, members, _ownNumber);
|
||||||
|
_inviteToGroupPopup.ClearSearch();
|
||||||
|
_inviteToGroupPopup.OpenCentered();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenMembersPopup()
|
||||||
|
{
|
||||||
|
if (_currentChat is not uint currentChat ||
|
||||||
|
!_recipients.TryGetValue(currentChat, out var recipient) ||
|
||||||
|
!recipient.IsGroup)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
|
||||||
|
var members = recipient.Members ?? new HashSet<uint>();
|
||||||
|
var admins = recipient.Admins ?? new HashSet<uint>();
|
||||||
|
_groupMembersPopup.SetMembers(availableContacts, members, _ownNumber, recipient.CreatorId, admins);
|
||||||
|
_groupMembersPopup.OpenCentered();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshOpenPopups()
|
||||||
|
{
|
||||||
|
// If popup is open, refresh it or close if no longer a member
|
||||||
|
if (_inviteToGroupPopup.IsOpen)
|
||||||
|
{
|
||||||
|
if (_currentChat is uint currentChat &&
|
||||||
|
_recipients.TryGetValue(currentChat, out var recipient) &&
|
||||||
|
recipient.IsGroup)
|
||||||
|
{
|
||||||
|
var members = recipient.Members ?? new HashSet<uint>();
|
||||||
|
if (!members.Contains(_ownNumber))
|
||||||
|
{
|
||||||
|
_inviteToGroupPopup.Close();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use the contacts list from the server (same as Look Up Numbers)
|
||||||
|
// Don't fall back to recipients as they contain outdated cached information
|
||||||
|
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
|
||||||
|
_inviteToGroupPopup.SetContacts(availableContacts, members, _ownNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_inviteToGroupPopup.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_groupMembersPopup.IsOpen)
|
||||||
|
{
|
||||||
|
if (_currentChat is uint currentChat &&
|
||||||
|
_recipients.TryGetValue(currentChat, out var recipient) &&
|
||||||
|
recipient.IsGroup)
|
||||||
|
{
|
||||||
|
var members = recipient.Members ?? new HashSet<uint>();
|
||||||
|
if (!members.Contains(_ownNumber))
|
||||||
|
{
|
||||||
|
_groupMembersPopup.Close();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use the contacts list from the server (same as Look Up Numbers)
|
||||||
|
// Don't fall back to recipients as they contain outdated cached information
|
||||||
|
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
|
||||||
|
var admins = recipient.Admins ?? new HashSet<uint>();
|
||||||
|
_groupMembersPopup.SetMembers(availableContacts, members, _ownNumber, recipient.CreatorId, admins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_groupMembersPopup.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void BeginEditChat()
|
private void BeginEditChat()
|
||||||
{
|
{
|
||||||
if (_currentChat is not uint currentChat)
|
if (_currentChat is not uint currentChat)
|
||||||
@@ -294,17 +473,40 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
var activeChat = _pendingChat ?? _currentChat;
|
var activeChat = _pendingChat ?? _currentChat;
|
||||||
var hasActiveChat = activeChat != null;
|
var hasActiveChat = activeChat != null;
|
||||||
|
|
||||||
|
var isGroupChat = false;
|
||||||
|
var canManageGroup = false;
|
||||||
|
NanoChatRecipient recipient = default;
|
||||||
|
var hasRecipient = false;
|
||||||
|
|
||||||
|
if (hasActiveChat && activeChat != null && _recipients.TryGetValue(activeChat.Value, out recipient))
|
||||||
|
{
|
||||||
|
hasRecipient = true;
|
||||||
|
isGroupChat = recipient.IsGroup;
|
||||||
|
canManageGroup = isGroupChat && (recipient.CreatorId == _ownNumber || recipient.Admins?.Contains(_ownNumber) == true);
|
||||||
|
}
|
||||||
|
|
||||||
// Update UI state
|
// Update UI state
|
||||||
MessagesScroll.Visible = hasActiveChat;
|
MessagesScroll.Visible = hasActiveChat;
|
||||||
CurrentChatName.Visible = !hasActiveChat;
|
CurrentChatName.Visible = !hasActiveChat;
|
||||||
MessageInputContainer.Visible = hasActiveChat;
|
MessageInputContainer.Visible = hasActiveChat;
|
||||||
DeleteChatButton.Visible = hasActiveChat;
|
DeleteChatButton.Visible = hasActiveChat;
|
||||||
EditChatButton.Visible = hasActiveChat;
|
EditChatButton.Visible = hasActiveChat && !isGroupChat;
|
||||||
DeleteChatButton.Disabled = !hasActiveChat;
|
DeleteChatButton.Disabled = !hasActiveChat;
|
||||||
|
|
||||||
if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient))
|
|
||||||
|
InviteToGroupButton.Visible = canManageGroup;
|
||||||
|
ViewMembersButton.Visible = isGroupChat;
|
||||||
|
|
||||||
|
if (hasRecipient)
|
||||||
{
|
{
|
||||||
CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})");
|
if (isGroupChat)
|
||||||
|
{
|
||||||
|
CurrentChatName.Text = recipient.Name;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -321,14 +523,44 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages))
|
if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var isGroupChat = _recipients.TryGetValue(activeChat.Value, out var recipient) && recipient.IsGroup;
|
||||||
foreach (var message in chatMessages)
|
foreach (var message in chatMessages)
|
||||||
{
|
{
|
||||||
var messageBubble = new NanoChatMessageBubble();
|
var messageBubble = new NanoChatMessageBubble();
|
||||||
messageBubble.SetMessage(message, message.SenderId == _ownNumber);
|
var isOwnMessage = message.SenderId == _ownNumber;
|
||||||
MessageList.AddChild(messageBubble);
|
|
||||||
|
// For group chats, show sender name for other people's messages
|
||||||
|
string? senderName = null;
|
||||||
|
if (isGroupChat && !isOwnMessage)
|
||||||
|
{
|
||||||
|
// Try to get sender name from contacts first, then recipients
|
||||||
|
if (_recipients.TryGetValue(message.SenderId, out var senderRecipient))
|
||||||
|
{
|
||||||
|
senderName = senderRecipient.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in recipients, try contacts list (station-wide directory)
|
||||||
|
if (senderName == null && _contacts != null)
|
||||||
|
{
|
||||||
|
var contact = _contacts.FirstOrDefault(c => c.Number == message.SenderId);
|
||||||
|
if (contact.Number != 0)
|
||||||
|
senderName = contact.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to "Unknown" with number if name not found
|
||||||
|
if (senderName == null)
|
||||||
|
{
|
||||||
|
senderName = $"{Loc.GetString("nano-chat-unknown-sender")}#{message.SenderId:D4}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messageBubble.SetMessage(message, isOwnMessage, senderName, isGroupChat && !isOwnMessage);
|
||||||
|
|
||||||
// Add spacing between messages
|
// Add spacing between messages
|
||||||
MessageList.AddChild(new Control { MinSize = new Vector2(0, 4) });
|
if (MessageList.ChildCount > 0)
|
||||||
|
MessageList.AddChild(new Control { Name = "MessagePadding", MinSize = new Vector2(0, 4) });
|
||||||
|
|
||||||
|
MessageList.AddChild(messageBubble);
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageList.InvalidateMeasure();
|
MessageList.InvalidateMeasure();
|
||||||
@@ -357,13 +589,41 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
ListNumberButton.Pressed = _listNumber;
|
ListNumberButton.Pressed = _listNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CheckValidNumber()
|
||||||
|
{
|
||||||
|
if (_ownNumber == 0)
|
||||||
|
{
|
||||||
|
NoIdLabel.Visible = true;
|
||||||
|
ChatView.Visible = false;
|
||||||
|
LookupView.Visible = false;
|
||||||
|
|
||||||
|
MuteButton.Disabled = true;
|
||||||
|
NewChatButton.Disabled = true;
|
||||||
|
CreateGroupChatButton.Disabled = true;
|
||||||
|
LookupButton.Disabled = true;
|
||||||
|
ListNumberButton.Disabled = true;
|
||||||
|
}
|
||||||
|
else if (NoIdLabel.Visible)
|
||||||
|
{
|
||||||
|
NoIdLabel.Visible = false;
|
||||||
|
ToggleView();
|
||||||
|
|
||||||
|
MuteButton.Disabled = false;
|
||||||
|
NewChatButton.Disabled = false;
|
||||||
|
CreateGroupChatButton.Disabled = false;
|
||||||
|
LookupButton.Disabled = false;
|
||||||
|
ListNumberButton.Disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void UpdateState(NanoChatUiState state)
|
public void UpdateState(NanoChatUiState state)
|
||||||
{
|
{
|
||||||
_ownNumber = state.OwnNumber;
|
_ownNumber = state.OwnNumber;
|
||||||
_notificationsMuted = state.NotificationsMuted;
|
_notificationsMuted = state.NotificationsMuted;
|
||||||
_listNumber = state.ListNumber;
|
_listNumber = state.ListNumber;
|
||||||
_mutedChats = state.MutedChats;
|
_mutedChats = state.MutedChats;
|
||||||
OwnNumberLabel.Text = $"#{state.OwnNumber:D4}";
|
_contacts = state.Contacts;
|
||||||
|
OwnNumberLabel.Text = $"#{_ownNumber:D4}";
|
||||||
UpdateMuteButton();
|
UpdateMuteButton();
|
||||||
UpdateListNumber();
|
UpdateListNumber();
|
||||||
|
|
||||||
@@ -387,10 +647,15 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||||||
if (_pendingChat == null)
|
if (_pendingChat == null)
|
||||||
_currentChat = state.CurrentChat;
|
_currentChat = state.CurrentChat;
|
||||||
|
|
||||||
UpdateCurrentChat();
|
// Update recipients first, then update UI that depends on it
|
||||||
UpdateMuteChatButton();
|
|
||||||
UpdateChatList(state.Recipients);
|
UpdateChatList(state.Recipients);
|
||||||
UpdateMessages(state.Messages);
|
UpdateMessages(state.Messages);
|
||||||
|
UpdateCurrentChat();
|
||||||
|
UpdateMuteChatButton();
|
||||||
LookupView.UpdateContactList(state);
|
LookupView.UpdateContactList(state);
|
||||||
|
|
||||||
|
CheckValidNumber();
|
||||||
|
|
||||||
|
RefreshOpenPopups();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.ResourceManagement;
|
||||||
|
using Robust.Client.UserInterface;
|
||||||
|
|
||||||
|
namespace Content.Client._DV.NanoChat.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extremely specialized version of <see cref="Robust.Client.UserInterface.Controls.TextureRect" /> designed for drawing Textures at fixed size with an additional offset
|
||||||
|
/// Circumvents the problem of not having "negative mergins" on TextureRect for manual alignment
|
||||||
|
/// This is fucking evil, please do not use this :3
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EmojiControl : Control
|
||||||
|
{
|
||||||
|
public const string StylePropertyTexture = "texture";
|
||||||
|
public const string StylePropertyOffset = "offset";
|
||||||
|
|
||||||
|
private Texture? _texture;
|
||||||
|
private Vector2? _offset;
|
||||||
|
|
||||||
|
private string? _texturePath;
|
||||||
|
|
||||||
|
public string TexturePath
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Texture = IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(value);
|
||||||
|
_texturePath = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnThemeUpdated()
|
||||||
|
{
|
||||||
|
if (_texturePath != null) Texture = Theme.ResolveTexture(_texturePath);
|
||||||
|
base.OnThemeUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The texture to draw.
|
||||||
|
/// </summary>
|
||||||
|
public Texture? Texture
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_texture is null)
|
||||||
|
{
|
||||||
|
if (TryGetStyleProperty(StylePropertyTexture, out Texture? texture))
|
||||||
|
{
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _texture;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var oldSize = _texture?.Size;
|
||||||
|
_texture = value;
|
||||||
|
|
||||||
|
if (value?.Size != oldSize)
|
||||||
|
{
|
||||||
|
InvalidateMeasure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vertical offset of the texture
|
||||||
|
/// </summary>
|
||||||
|
public Vector2 Offset
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_offset is null)
|
||||||
|
{
|
||||||
|
if (TryGetStyleProperty(StylePropertyOffset, out Vector2 offset))
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _offset ?? Vector2.Zero;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var oldOffset = _offset;
|
||||||
|
_offset = value;
|
||||||
|
|
||||||
|
if (oldOffset != value)
|
||||||
|
{
|
||||||
|
InvalidateArrange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ArrangeCore(UIBox2 finalRect)
|
||||||
|
{
|
||||||
|
finalRect.Left += Offset.X;
|
||||||
|
finalRect.Right += Offset.X;
|
||||||
|
finalRect.Top += Offset.Y;
|
||||||
|
finalRect.Bottom += Offset.Y;
|
||||||
|
base.ArrangeCore(finalRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Draw(DrawingHandleScreen handle)
|
||||||
|
{
|
||||||
|
var texture = Texture;
|
||||||
|
|
||||||
|
if (texture is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
handle.DrawTextureRect(texture, GetDrawDimensions());
|
||||||
|
}
|
||||||
|
|
||||||
|
private UIBox2 GetDrawDimensions()
|
||||||
|
{
|
||||||
|
var (texWidth, texHeight) = Texture?.Size ?? Vector2i.Zero;
|
||||||
|
var width = texWidth * (PixelSize.Y / texHeight);
|
||||||
|
var height = (float)PixelSize.Y;
|
||||||
|
if (width > PixelSize.X)
|
||||||
|
{
|
||||||
|
width = PixelSize.X;
|
||||||
|
height = texHeight * (PixelSize.X / texWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = new Vector2(width, height);
|
||||||
|
var position = (PixelSize - size) / 2 + Offset;
|
||||||
|
|
||||||
|
return UIBox2.FromDimensions(position, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
-->
|
||||||
|
|
||||||
|
<DefaultWindow xmlns="https://spacestation14.io"
|
||||||
|
Title="{Loc nano-chat-create-group-title}"
|
||||||
|
MinSize="300 150">
|
||||||
|
<PanelContainer StyleClasses="AngleRect">
|
||||||
|
<BoxContainer Orientation="Vertical" Margin="4">
|
||||||
|
<!-- Group Name input -->
|
||||||
|
<BoxContainer Orientation="Vertical" Margin="0 4">
|
||||||
|
<Label Text="{Loc nano-chat-group-name-label}"
|
||||||
|
StyleClasses="LabelHeading" />
|
||||||
|
<PanelContainer StyleClasses="ButtonSquare">
|
||||||
|
<LineEdit Name="GroupNameInput"
|
||||||
|
PlaceHolder="{Loc nano-chat-group-name-placeholder}" />
|
||||||
|
</PanelContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<BoxContainer Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0 8 0 0">
|
||||||
|
<Button Name="CancelButton"
|
||||||
|
Text="{Loc nano-chat-cancel}"
|
||||||
|
StyleClasses="OpenRight"
|
||||||
|
MinSize="80 0" />
|
||||||
|
<Button Name="CreateButton"
|
||||||
|
Text="{Loc nano-chat-create}"
|
||||||
|
StyleClasses="OpenLeft"
|
||||||
|
MinSize="80 0"
|
||||||
|
Disabled="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
</PanelContainer>
|
||||||
|
</DefaultWindow>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
|
||||||
|
namespace Content.Client._Funkystation.NanoChat;
|
||||||
|
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public sealed partial class CreateGroupChatPopup : DefaultWindow
|
||||||
|
{
|
||||||
|
public event Action<string>? OnGroupCreated;
|
||||||
|
|
||||||
|
public CreateGroupChatPopup(int maxFullNameLength)
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
ContentsContainer.Margin = new Thickness(3);
|
||||||
|
|
||||||
|
// Button handlers
|
||||||
|
CancelButton.OnPressed += _ => Close();
|
||||||
|
CreateButton.OnPressed += _ => CreateGroup();
|
||||||
|
|
||||||
|
// Input validation
|
||||||
|
GroupNameInput.OnTextEntered += _ => CreateGroup();
|
||||||
|
|
||||||
|
GroupNameInput.OnTextChanged += args =>
|
||||||
|
{
|
||||||
|
if (args.Text.Length > maxFullNameLength)
|
||||||
|
GroupNameInput.Text = args.Text[..maxFullNameLength];
|
||||||
|
ValidateInputs();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateInputs()
|
||||||
|
{
|
||||||
|
var isValid = !string.IsNullOrWhiteSpace(GroupNameInput.Text);
|
||||||
|
CreateButton.Disabled = !isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateGroup()
|
||||||
|
{
|
||||||
|
if (CreateButton.Disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var name = GroupNameInput.Text.Trim();
|
||||||
|
OnGroupCreated?.Invoke(name);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearInputs()
|
||||||
|
{
|
||||||
|
GroupNameInput.Text = string.Empty;
|
||||||
|
ValidateInputs();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
-->
|
||||||
|
|
||||||
|
<DefaultWindow xmlns="https://spacestation14.io"
|
||||||
|
Title="{Loc nano-chat-emoji-picker}"
|
||||||
|
MinSize="300 400">
|
||||||
|
<PanelContainer StyleClasses="AngleRect">
|
||||||
|
<BoxContainer Orientation="Vertical" Margin="4">
|
||||||
|
<!-- Search box -->
|
||||||
|
<LineEdit Name="SearchInput"
|
||||||
|
PlaceHolder="{Loc nano-chat-emoji-search}"
|
||||||
|
HorizontalExpand="True"
|
||||||
|
Margin="0 0 0 4" />
|
||||||
|
|
||||||
|
<!-- Emoji grid -->
|
||||||
|
<ScrollContainer VerticalExpand="True"
|
||||||
|
HorizontalExpand="True"
|
||||||
|
Margin="0 4"
|
||||||
|
HScroll="0"
|
||||||
|
VScroll="1">
|
||||||
|
<GridContainer Name="EmojiGrid"
|
||||||
|
Columns="6"
|
||||||
|
HorizontalExpand="True" />
|
||||||
|
</ScrollContainer>
|
||||||
|
|
||||||
|
<!-- Close button -->
|
||||||
|
<BoxContainer Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0 8 0 0">
|
||||||
|
<Button Name="EmojiCloseButton"
|
||||||
|
Text="{Loc nano-chat-close}"
|
||||||
|
MinSize="80 0" />
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
</PanelContainer>
|
||||||
|
</DefaultWindow>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Shared._Funkystation.NanoChat;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Client.ResourceManagement;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Content.Client._Funkystation.NanoChat;
|
||||||
|
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public sealed partial class EmojiPickerPopup : DefaultWindow
|
||||||
|
{
|
||||||
|
private List<(string name, SpriteSpecifier specifier)> _filteredEmojis;
|
||||||
|
private SpriteSystem? _spriteSystem;
|
||||||
|
|
||||||
|
public event Action<string>? OnEmojiSelected;
|
||||||
|
|
||||||
|
public EmojiPickerPopup()
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
_filteredEmojis = NanoChatEmojis.EmojiSpecifiers.Select(kv => (kv.Key, kv.Value)).ToList();
|
||||||
|
|
||||||
|
ContentsContainer.Margin = new Thickness(3);
|
||||||
|
|
||||||
|
EmojiCloseButton.OnPressed += _ => Close();
|
||||||
|
SearchInput.OnTextChanged += OnSearchChanged;
|
||||||
|
|
||||||
|
OnResized += UpdateGridColumns;
|
||||||
|
|
||||||
|
PopulateEmojiGrid();
|
||||||
|
UpdateGridColumns();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateGridColumns()
|
||||||
|
{
|
||||||
|
const int buttonSize = 48;
|
||||||
|
const int scrollbarWidth = 12;
|
||||||
|
|
||||||
|
var scrollContainer = EmojiGrid.Parent as ScrollContainer;
|
||||||
|
if (scrollContainer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var availableWidth = scrollContainer.PixelSize.X - scrollbarWidth;
|
||||||
|
|
||||||
|
if (availableWidth <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var columns = Math.Max(1, (int) (availableWidth / buttonSize));
|
||||||
|
if (EmojiGrid.Columns != columns)
|
||||||
|
{
|
||||||
|
EmojiGrid.Columns = columns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSearchChanged(LineEdit.LineEditEventArgs args)
|
||||||
|
{
|
||||||
|
var search = args.Text.ToLower().Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(search))
|
||||||
|
{
|
||||||
|
_filteredEmojis = NanoChatEmojis.EmojiSpecifiers.Select(kv => (kv.Key, kv.Value)).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_filteredEmojis = NanoChatEmojis.EmojiSpecifiers
|
||||||
|
.Where(kv => kv.Key.Contains(search, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(kv => (kv.Key, kv.Value))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
PopulateEmojiGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateEmojiGrid()
|
||||||
|
{
|
||||||
|
EmojiGrid.RemoveAllChildren();
|
||||||
|
|
||||||
|
var resCache = IoCManager.Resolve<IResourceCache>();
|
||||||
|
_spriteSystem ??= IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||||
|
|
||||||
|
foreach (var (name, specifier) in _filteredEmojis)
|
||||||
|
{
|
||||||
|
var button = new Button
|
||||||
|
{
|
||||||
|
MinSize = new Vector2(48, 48),
|
||||||
|
MaxSize = new Vector2(48, 48),
|
||||||
|
ToolTip = $":{name}:",
|
||||||
|
};
|
||||||
|
|
||||||
|
Texture texture;
|
||||||
|
switch (specifier)
|
||||||
|
{
|
||||||
|
case SpriteSpecifier.Texture texSpecifier:
|
||||||
|
if (!resCache.TryGetResource<TextureResource>(texSpecifier.TexturePath, out var textureResource))
|
||||||
|
continue;
|
||||||
|
texture = textureResource.Texture;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpriteSpecifier.Rsi rsiSpecifier:
|
||||||
|
var state = _spriteSystem.GetState(rsiSpecifier);
|
||||||
|
texture = state.Frame0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textureRect = new TextureRect
|
||||||
|
{
|
||||||
|
Texture = texture,
|
||||||
|
Stretch = TextureRect.StretchMode.KeepAspect,
|
||||||
|
MinSize = new Vector2(32, 32),
|
||||||
|
MaxSize = new Vector2(32, 32),
|
||||||
|
HorizontalAlignment = HAlignment.Center,
|
||||||
|
VerticalAlignment = VAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
button.AddChild(textureRect);
|
||||||
|
|
||||||
|
button.OnPressed += _ =>
|
||||||
|
{
|
||||||
|
OnEmojiSelected?.Invoke($":{name}:");
|
||||||
|
Close();
|
||||||
|
};
|
||||||
|
|
||||||
|
EmojiGrid.AddChild(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate columns after populating
|
||||||
|
UpdateGridColumns();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SpriteSpecifier? GetEmojiSpecifier(string emojiName)
|
||||||
|
{
|
||||||
|
return NanoChatEmojis.EmojiSpecifiers.TryGetValue(emojiName, out var specifier) ? specifier : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, SpriteSpecifier> GetEmojis() => new Dictionary<string, SpriteSpecifier>(NanoChatEmojis.EmojiSpecifiers);
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.ResourceManagement;
|
||||||
|
using Robust.Client.UserInterface;
|
||||||
|
using Robust.Client.UserInterface.RichText;
|
||||||
|
using Content.Shared._Funkystation.NanoChat;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
using Content.Client._DV.NanoChat.UI; // DeltaV - Fix emoji alignment
|
||||||
|
using Robust.Shared.Prototypes; // DeltaV - Emoji id for debugging
|
||||||
|
|
||||||
|
namespace Content.Client._Funkystation.NanoChat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Markup tag for rendering emoji images inline in RichTextLabel.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EmojiTag : IMarkupTagHandler
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||||
|
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||||
|
|
||||||
|
private SpriteSystem? _spriteSystem;
|
||||||
|
|
||||||
|
public EmojiTag()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => "emoji";
|
||||||
|
|
||||||
|
// Begin DeltaV - Render invalid emoji as text
|
||||||
|
|
||||||
|
private static ProtoId<NanoChatEmojiPrototype>? GetEmoji(MarkupNode node, out string text, out SpriteSpecifier? emoji)
|
||||||
|
{
|
||||||
|
if (!node.Value.TryGetString(out var emojiName))
|
||||||
|
{
|
||||||
|
// If we don't have a value, then render nothing
|
||||||
|
emoji = null;
|
||||||
|
text = string.Empty;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (NanoChatEmojis.EmojiSpecifiers.TryGetValue(emojiName, out emoji))
|
||||||
|
{
|
||||||
|
// If we have a valid emoji, render the emoji, but no text
|
||||||
|
text = string.Empty;
|
||||||
|
return emojiName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If we have an invalid emoji, render only text
|
||||||
|
text = $":{emojiName}:";
|
||||||
|
return emojiName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string IMarkupTagHandler.TextBefore(MarkupNode node)
|
||||||
|
{
|
||||||
|
GetEmoji(node, out var text, out _);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End DeltaV - Render invalid emoji as text
|
||||||
|
|
||||||
|
public bool TryCreateControl(MarkupNode node, out Control control)
|
||||||
|
{
|
||||||
|
control = default!;
|
||||||
|
|
||||||
|
var emojiId = GetEmoji(node, out _, out var maybeSpecifier);
|
||||||
|
if (maybeSpecifier is not { } specifier)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_spriteSystem ??= _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||||
|
|
||||||
|
Texture texture;
|
||||||
|
switch (specifier)
|
||||||
|
{
|
||||||
|
case SpriteSpecifier.Texture texSpecifier:
|
||||||
|
if (!_resourceCache.TryGetResource<TextureResource>(texSpecifier.TexturePath, out var textureResource))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
texture = textureResource.Texture;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpriteSpecifier.Rsi rsiSpecifier:
|
||||||
|
var state = _spriteSystem.GetState(rsiSpecifier);
|
||||||
|
texture = state.Frame0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin DeltaV - Fix emoji alignment, replace TextureRect with custom EmojiControl
|
||||||
|
control = new EmojiControl
|
||||||
|
{
|
||||||
|
Texture = texture,
|
||||||
|
SetWidth = 32f,
|
||||||
|
SetHeight = 32f,
|
||||||
|
Name = $"Emoji[{emojiId}]",
|
||||||
|
// Evilly hard-coded Margin and Offset, don't ask me where they come from please
|
||||||
|
Margin = new(5f, 0f, 2f, 0),
|
||||||
|
Offset = new(5f, -5f),
|
||||||
|
};
|
||||||
|
// End DeltaV
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
-->
|
||||||
|
|
||||||
|
<DefaultWindow xmlns="https://spacestation14.io"
|
||||||
|
Title="{Loc nano-chat-group-members-title}"
|
||||||
|
MinSize="400 300">
|
||||||
|
<PanelContainer StyleClasses="AngleRect">
|
||||||
|
<BoxContainer Orientation="Vertical" Margin="4">
|
||||||
|
<!-- Member list -->
|
||||||
|
<ScrollContainer VerticalExpand="True"
|
||||||
|
HorizontalExpand="True"
|
||||||
|
Margin="0 4">
|
||||||
|
<BoxContainer Name="MembersList"
|
||||||
|
Orientation="Vertical"
|
||||||
|
VerticalExpand="True"
|
||||||
|
HorizontalExpand="True" />
|
||||||
|
</ScrollContainer>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<BoxContainer Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0 8 0 0">
|
||||||
|
<Button Name="MembersCloseButton"
|
||||||
|
Text="{Loc nano-chat-close}"
|
||||||
|
MinSize="80 0" />
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
</PanelContainer>
|
||||||
|
</DefaultWindow>
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Content.Client.Resources;
|
||||||
|
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Client.ResourceManagement;
|
||||||
|
|
||||||
|
namespace Content.Client._Funkystation.NanoChat;
|
||||||
|
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public sealed partial class GroupMembersPopup : DefaultWindow
|
||||||
|
{
|
||||||
|
private List<NanoChatRecipient> _allContacts = new();
|
||||||
|
private HashSet<uint> _members = new();
|
||||||
|
private HashSet<uint> _admins = new();
|
||||||
|
private uint _ownNumber;
|
||||||
|
private uint? _creatorId;
|
||||||
|
|
||||||
|
public event Action<uint>? OnKick;
|
||||||
|
public event Action<uint>? OnAdmin;
|
||||||
|
public event Action<uint>? OnDeadmin;
|
||||||
|
|
||||||
|
public GroupMembersPopup()
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
ContentsContainer.Margin = new Thickness(3);
|
||||||
|
|
||||||
|
MembersCloseButton.OnPressed += _ => Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetMembers(List<NanoChatRecipient> allContacts, HashSet<uint> members, uint ownNumber, uint? creatorId, HashSet<uint>? admins = null)
|
||||||
|
{
|
||||||
|
_allContacts = allContacts;
|
||||||
|
_members = members;
|
||||||
|
_ownNumber = ownNumber;
|
||||||
|
_creatorId = creatorId;
|
||||||
|
_admins = admins ?? new HashSet<uint>();
|
||||||
|
UpdateMembersList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMembersList()
|
||||||
|
{
|
||||||
|
MembersList.RemoveAllChildren();
|
||||||
|
|
||||||
|
if (_members.Count == 0)
|
||||||
|
{
|
||||||
|
MembersList.AddChild(new Label { Text = Loc.GetString("nano-chat-no-members") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isCreator = _creatorId == _ownNumber;
|
||||||
|
var isAdmin = _admins.Contains(_ownNumber);
|
||||||
|
var canManage = isCreator || isAdmin;
|
||||||
|
|
||||||
|
var memberContacts = _allContacts
|
||||||
|
.Where(c => _members.Contains(c.Number))
|
||||||
|
.OrderBy(c => c.Name)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
for (var idx = 0; idx < memberContacts.Count; idx++)
|
||||||
|
{
|
||||||
|
var contact = memberContacts[idx];
|
||||||
|
var isEvenRow = idx % 2 == 0;
|
||||||
|
var isMemberAdmin = _admins.Contains(contact.Number);
|
||||||
|
var isMemberCreator = contact.Number == _creatorId;
|
||||||
|
var isSelf = contact.Number == _ownNumber;
|
||||||
|
|
||||||
|
var canKick = false;
|
||||||
|
if (isSelf) // can always leave yourself.
|
||||||
|
{
|
||||||
|
canKick = true;
|
||||||
|
}
|
||||||
|
else if (!isSelf && canManage && !isMemberCreator)
|
||||||
|
{
|
||||||
|
if (isCreator)
|
||||||
|
canKick = true;
|
||||||
|
else if (isAdmin && !isMemberAdmin)
|
||||||
|
canKick = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canAdmin = canManage && !isSelf && !isMemberCreator && !isMemberAdmin;
|
||||||
|
|
||||||
|
var canDeadmin = canManage && !isSelf && !isMemberCreator && isMemberAdmin;
|
||||||
|
if (!isCreator && canDeadmin)
|
||||||
|
canDeadmin = false;
|
||||||
|
|
||||||
|
var contactControl = new MemberContainer(contact, isEvenRow, isMemberCreator, isMemberAdmin, isSelf, canKick, canAdmin, canDeadmin, OnKick, OnAdmin, OnDeadmin);
|
||||||
|
MembersList.AddChild(contactControl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class MemberContainer : PanelContainer
|
||||||
|
{
|
||||||
|
public MemberContainer(NanoChatRecipient contact, bool isEvenRow, bool isCreator, bool isAdmin, bool isSelf, bool canKick, bool canMakeAdmin, bool canDeadmin, Action<uint>? onKick, Action<uint>? onAdmin, Action<uint>? onDeadmin)
|
||||||
|
{
|
||||||
|
HorizontalExpand = true;
|
||||||
|
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
|
||||||
|
|
||||||
|
var resCache = IoCManager.Resolve<IResourceCache>();
|
||||||
|
|
||||||
|
// Show name with job title if available
|
||||||
|
var displayName = contact.Name;
|
||||||
|
if (!string.IsNullOrWhiteSpace(contact.JobTitle))
|
||||||
|
displayName = $"{contact.Name} ({contact.JobTitle})";
|
||||||
|
|
||||||
|
var nameLabel = new Label
|
||||||
|
{
|
||||||
|
Text = displayName + (isCreator ? $" {Loc.GetString("nano-chat-owner-suffix")}" : isAdmin ? $" {Loc.GetString("nano-chat-admin-suffix")}" : ""),
|
||||||
|
HorizontalAlignment = HAlignment.Left,
|
||||||
|
HorizontalExpand = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var numberLabel = new Label
|
||||||
|
{
|
||||||
|
Text = $"#{contact.Number:D4}",
|
||||||
|
HorizontalAlignment = HAlignment.Right,
|
||||||
|
Margin = new Thickness(0, 0, (canKick || canMakeAdmin || canDeadmin) ? 108 : 4, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
AddChild(nameLabel);
|
||||||
|
AddChild(numberLabel);
|
||||||
|
|
||||||
|
// Admin/Deadmin button
|
||||||
|
if (canMakeAdmin)
|
||||||
|
{
|
||||||
|
var adminButton = new Button
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HAlignment.Right,
|
||||||
|
MinSize = new Vector2(32, 32),
|
||||||
|
MaxSize = new Vector2(32, 32),
|
||||||
|
ToolTip = Loc.GetString("nano-chat-admin"),
|
||||||
|
Margin = new Thickness(0, 0, canKick ? 72 : 36, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
adminButton.AddStyleClass("OpenBoth");
|
||||||
|
adminButton.AddChild(new TextureRect
|
||||||
|
{
|
||||||
|
Texture = resCache.GetTexture("/Textures/_Funkystation/Interface/NanoChat/admin.png"),
|
||||||
|
Stretch = TextureRect.StretchMode.KeepAspectCentered,
|
||||||
|
MinSize = new Vector2(28, 28),
|
||||||
|
MaxSize = new Vector2(28, 28),
|
||||||
|
});
|
||||||
|
adminButton.OnPressed += _ => onAdmin?.Invoke(contact.Number);
|
||||||
|
|
||||||
|
AddChild(adminButton);
|
||||||
|
}
|
||||||
|
else if (canDeadmin)
|
||||||
|
{
|
||||||
|
var deadminButton = new Button
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HAlignment.Right,
|
||||||
|
MinSize = new Vector2(32, 32),
|
||||||
|
MaxSize = new Vector2(32, 32),
|
||||||
|
ToolTip = Loc.GetString("nano-chat-deadmin"),
|
||||||
|
Margin = new Thickness(0, 0, canKick ? 72 : 36, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
deadminButton.AddStyleClass("OpenBoth");
|
||||||
|
deadminButton.AddChild(new TextureRect
|
||||||
|
{
|
||||||
|
Texture = resCache.GetTexture("/Textures/_Funkystation/Interface/NanoChat/deadmin.png"),
|
||||||
|
Stretch = TextureRect.StretchMode.KeepAspectCentered,
|
||||||
|
MinSize = new Vector2(28, 28),
|
||||||
|
MaxSize = new Vector2(28, 28),
|
||||||
|
});
|
||||||
|
deadminButton.OnPressed += _ => onDeadmin?.Invoke(contact.Number);
|
||||||
|
|
||||||
|
AddChild(deadminButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick/Leave button
|
||||||
|
if (canKick)
|
||||||
|
{
|
||||||
|
var kickButton = new Button
|
||||||
|
{
|
||||||
|
Text = isSelf ? Loc.GetString("nano-chat-leave") : "X",
|
||||||
|
HorizontalAlignment = HAlignment.Right,
|
||||||
|
MinSize = new Vector2(isSelf ? 64 : 32, 32),
|
||||||
|
MaxSize = new Vector2(isSelf ? 64 : 32, 32),
|
||||||
|
ToolTip = isSelf ? Loc.GetString("nano-chat-leave") : Loc.GetString("nano-chat-kick"),
|
||||||
|
Margin = new Thickness(0, 0, 36, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
kickButton.AddStyleClass("OpenBoth");
|
||||||
|
kickButton.OnPressed += _ => onKick?.Invoke(contact.Number);
|
||||||
|
|
||||||
|
AddChild(kickButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
-->
|
||||||
|
|
||||||
|
<DefaultWindow xmlns="https://spacestation14.io"
|
||||||
|
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||||
|
Title="{Loc nano-chat-invite-to-group-title}"
|
||||||
|
MinSize="400 300">
|
||||||
|
<PanelContainer StyleClasses="AngleRect">
|
||||||
|
<BoxContainer Orientation="Vertical" Margin="4">
|
||||||
|
<!-- Search input -->
|
||||||
|
<BoxContainer Orientation="Vertical" Margin="0 4">
|
||||||
|
<Label Text="{Loc nano-chat-search-label}"
|
||||||
|
StyleClasses="LabelHeading" />
|
||||||
|
<PanelContainer StyleClasses="ButtonSquare">
|
||||||
|
<LineEdit Name="SearchInput"
|
||||||
|
PlaceHolder="{Loc nano-chat-search-placeholder}" />
|
||||||
|
</PanelContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<!-- Contact list -->
|
||||||
|
<ScrollContainer VerticalExpand="True"
|
||||||
|
HorizontalExpand="True"
|
||||||
|
Margin="0 4">
|
||||||
|
<BoxContainer Name="ContactsList"
|
||||||
|
Orientation="Vertical"
|
||||||
|
VerticalExpand="True"
|
||||||
|
HorizontalExpand="True" />
|
||||||
|
</ScrollContainer>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<BoxContainer Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0 8 0 0">
|
||||||
|
<Button Name="InviteCloseButton"
|
||||||
|
Text="{Loc nano-chat-close}"
|
||||||
|
MinSize="80 0" />
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
</PanelContainer>
|
||||||
|
</DefaultWindow>
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
||||||
|
using Content.Shared.Roles;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Client._Funkystation.NanoChat;
|
||||||
|
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public sealed partial class InviteToGroupPopup : DefaultWindow
|
||||||
|
{
|
||||||
|
private List<NanoChatRecipient> _allContacts = new();
|
||||||
|
private HashSet<uint> _existingMembers = new();
|
||||||
|
private uint _ownNumber;
|
||||||
|
|
||||||
|
public event Action<uint>? OnInvite;
|
||||||
|
|
||||||
|
public InviteToGroupPopup()
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
ContentsContainer.Margin = new Thickness(3);
|
||||||
|
|
||||||
|
InviteCloseButton.OnPressed += _ => Close();
|
||||||
|
|
||||||
|
SearchInput.OnTextChanged += _ => UpdateContactList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetContacts(List<NanoChatRecipient> contacts, HashSet<uint> existingMembers, uint ownNumber)
|
||||||
|
{
|
||||||
|
_allContacts = contacts;
|
||||||
|
_existingMembers = existingMembers;
|
||||||
|
_ownNumber = ownNumber;
|
||||||
|
UpdateContactList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateContactList()
|
||||||
|
{
|
||||||
|
ContactsList.RemoveAllChildren();
|
||||||
|
|
||||||
|
if (_allContacts.Count == 0)
|
||||||
|
{
|
||||||
|
ContactsList.AddChild(new Label { Text = Loc.GetString("nano-chat-look-up-no-server") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchText = SearchInput.Text.ToLower();
|
||||||
|
var filteredContacts = _allContacts
|
||||||
|
.Where(c => c.Number != _ownNumber &&
|
||||||
|
!_existingMembers.Contains(c.Number) &&
|
||||||
|
(string.IsNullOrWhiteSpace(searchText) ||
|
||||||
|
c.Name.ToLower().Contains(searchText) ||
|
||||||
|
c.Number.ToString().Contains(searchText) ||
|
||||||
|
(!string.IsNullOrWhiteSpace(c.JobTitle) && c.JobTitle.ToLower().Contains(searchText))))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (filteredContacts.Count == 0)
|
||||||
|
{
|
||||||
|
ContactsList.AddChild(new Label { Text = Loc.GetString("nano-chat-no-results") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group contacts by department like crew manifest
|
||||||
|
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||||
|
var departmentGroups = new Dictionary<DepartmentPrototype, List<NanoChatRecipient>>();
|
||||||
|
var noDepartmentContacts = new List<NanoChatRecipient>();
|
||||||
|
|
||||||
|
foreach (var contact in filteredContacts)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(contact.Department) &&
|
||||||
|
prototypeManager.TryIndex<DepartmentPrototype>(contact.Department, out var dept))
|
||||||
|
{
|
||||||
|
departmentGroups.GetOrNew(dept).Add(contact);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
noDepartmentContacts.Add(contact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort departments by weight
|
||||||
|
var sortedDepartments = departmentGroups.Keys.ToList();
|
||||||
|
sortedDepartments.Sort(DepartmentUIComparer.Instance);
|
||||||
|
|
||||||
|
var rowIndex = 0;
|
||||||
|
foreach (var department in sortedDepartments)
|
||||||
|
{
|
||||||
|
var departmentContacts = departmentGroups[department];
|
||||||
|
departmentContacts.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name));
|
||||||
|
|
||||||
|
// Add department header
|
||||||
|
ContactsList.AddChild(new Label
|
||||||
|
{
|
||||||
|
Text = Loc.GetString(department.Name),
|
||||||
|
StyleClasses = { "LabelBig" },
|
||||||
|
Margin = new Thickness(0, rowIndex == 0 ? 0 : 8, 0, 2)
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var contact in departmentContacts)
|
||||||
|
{
|
||||||
|
var isEvenRow = rowIndex % 2 == 0;
|
||||||
|
var contactControl = new ContactContainer(contact, isEvenRow, OnInvite);
|
||||||
|
ContactsList.AddChild(contactControl);
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add contacts without departments at the end
|
||||||
|
if (noDepartmentContacts.Count > 0)
|
||||||
|
{
|
||||||
|
noDepartmentContacts.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name));
|
||||||
|
|
||||||
|
if (sortedDepartments.Count > 0)
|
||||||
|
{
|
||||||
|
ContactsList.AddChild(new Label
|
||||||
|
{
|
||||||
|
Text = Loc.GetString("nano-chat-other-department"),
|
||||||
|
StyleClasses = { "LabelBig" },
|
||||||
|
Margin = new Thickness(0, 8, 0, 2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var contact in noDepartmentContacts)
|
||||||
|
{
|
||||||
|
var isEvenRow = rowIndex % 2 == 0;
|
||||||
|
var contactControl = new ContactContainer(contact, isEvenRow, OnInvite);
|
||||||
|
ContactsList.AddChild(contactControl);
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearSearch()
|
||||||
|
{
|
||||||
|
SearchInput.Text = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ContactContainer : PanelContainer
|
||||||
|
{
|
||||||
|
public ContactContainer(NanoChatRecipient contact, bool isEvenRow, Action<uint>? onInvite)
|
||||||
|
{
|
||||||
|
HorizontalExpand = true;
|
||||||
|
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
|
||||||
|
|
||||||
|
// Show name with job title if available
|
||||||
|
var displayName = contact.Name;
|
||||||
|
if (!string.IsNullOrWhiteSpace(contact.JobTitle))
|
||||||
|
displayName = $"{contact.Name} ({contact.JobTitle})";
|
||||||
|
|
||||||
|
var nameLabel = new Label
|
||||||
|
{
|
||||||
|
Text = displayName,
|
||||||
|
HorizontalAlignment = HAlignment.Left,
|
||||||
|
HorizontalExpand = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var numberLabel = new Label
|
||||||
|
{
|
||||||
|
Text = $"#{contact.Number:D4}",
|
||||||
|
HorizontalAlignment = HAlignment.Right,
|
||||||
|
Margin = new Thickness(0, 0, 36, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
var inviteButton = new Button
|
||||||
|
{
|
||||||
|
Text = "+",
|
||||||
|
HorizontalAlignment = HAlignment.Right,
|
||||||
|
MinSize = new Vector2(32, 32),
|
||||||
|
MaxSize = new Vector2(32, 32),
|
||||||
|
ToolTip = Loc.GetString("nano-chat-invite"),
|
||||||
|
};
|
||||||
|
|
||||||
|
inviteButton.AddStyleClass("OpenBoth");
|
||||||
|
inviteButton.OnPressed += _ => onInvite?.Invoke(contact.Number);
|
||||||
|
|
||||||
|
AddChild(nameLabel);
|
||||||
|
AddChild(numberLabel);
|
||||||
|
AddChild(inviteButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,11 +46,17 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
|
|
||||||
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
|
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
|
||||||
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeMessageEvent>(OnMessage);
|
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeMessageEvent>(OnMessage);
|
||||||
|
SubscribeLocalEvent<NanoChatCartridgeComponent, MapInitEvent>(OnMapInit);
|
||||||
|
|
||||||
Subs.CVar(_cfg, CCVars.MaxNameLength, x => _maxNameLength = x, true);
|
Subs.CVar(_cfg, CCVars.MaxNameLength, x => _maxNameLength = x, true);
|
||||||
Subs.CVar(_cfg, CCVars.MaxIdJobLength, x => _maxIdJobLength = x, true);
|
Subs.CVar(_cfg, CCVars.MaxIdJobLength, x => _maxIdJobLength = x, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnMapInit(Entity<NanoChatCartridgeComponent> ent, ref MapInitEvent args)
|
||||||
|
{
|
||||||
|
UpdateUIForAllCards();
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateClosed(Entity<NanoChatCartridgeComponent> ent)
|
private void UpdateClosed(Entity<NanoChatCartridgeComponent> ent)
|
||||||
{
|
{
|
||||||
if (!TryComp<CartridgeComponent>(ent, out var cartridge) ||
|
if (!TryComp<CartridgeComponent>(ent, out var cartridge) ||
|
||||||
@@ -124,7 +130,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
HandleCloseChat(card);
|
HandleCloseChat(card);
|
||||||
break;
|
break;
|
||||||
case NanoChatUiMessageType.ToggleMute:
|
case NanoChatUiMessageType.ToggleMute:
|
||||||
HandleToggleMute(card);
|
HandleToggleMute(card, msg);
|
||||||
break;
|
break;
|
||||||
case NanoChatUiMessageType.ToggleMuteChat:
|
case NanoChatUiMessageType.ToggleMuteChat:
|
||||||
HandleToggleMuteChat(card, msg);
|
HandleToggleMuteChat(card, msg);
|
||||||
@@ -136,7 +142,22 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
HandleSendMessage(ent, card, msg);
|
HandleSendMessage(ent, card, msg);
|
||||||
break;
|
break;
|
||||||
case NanoChatUiMessageType.ToggleListNumber:
|
case NanoChatUiMessageType.ToggleListNumber:
|
||||||
HandleToggleListNumber(card);
|
HandleToggleListNumber(card, msg);
|
||||||
|
break;
|
||||||
|
case NanoChatUiMessageType.CreateGroupChat:
|
||||||
|
HandleCreateGroupChat(card, msg);
|
||||||
|
break;
|
||||||
|
case NanoChatUiMessageType.InviteToGroup:
|
||||||
|
HandleInviteToGroup(card, msg);
|
||||||
|
break;
|
||||||
|
case NanoChatUiMessageType.KickFromGroup:
|
||||||
|
HandleKickFromGroup(card, msg);
|
||||||
|
break;
|
||||||
|
case NanoChatUiMessageType.AdminUser:
|
||||||
|
HandleAdminUser(card, msg);
|
||||||
|
break;
|
||||||
|
case NanoChatUiMessageType.DeadminUser:
|
||||||
|
HandleDeadminUser(card, msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,16 +211,24 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add new recipient
|
// Add new recipient
|
||||||
var recipient = new NanoChatRecipient(msg.RecipientNumber.Value,
|
var recipient = new NanoChatRecipient(
|
||||||
|
msg.RecipientNumber.Value,
|
||||||
name,
|
name,
|
||||||
jobTitle);
|
jobTitle,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize or update recipient
|
// Initialize or update recipient
|
||||||
_nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient);
|
_nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient);
|
||||||
|
|
||||||
_adminLogger.Add(LogType.Action,
|
_adminLogger.Add(LogType.Action,
|
||||||
LogImpact.Low,
|
LogImpact.Low,
|
||||||
$"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({name})");
|
$"{ToPrettyString(msg.Actor)} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({name})");
|
||||||
|
|
||||||
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
|
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
|
||||||
RaiseLocalEvent(ref recipientEv);
|
RaiseLocalEvent(ref recipientEv);
|
||||||
@@ -277,15 +306,65 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
if (msg.RecipientNumber == null || card.Comp.Number == null)
|
if (msg.RecipientNumber == null || card.Comp.Number == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var chatNumber = msg.RecipientNumber.Value;
|
||||||
|
var recipient = _nanoChat.GetRecipient((card, card.Comp), chatNumber);
|
||||||
|
|
||||||
|
if (recipient is { IsGroup: true } groupChat)
|
||||||
|
{
|
||||||
|
var members = groupChat.Members ?? new HashSet<uint>();
|
||||||
|
if (members.Remove(card.Comp.Number.Value))
|
||||||
|
{
|
||||||
|
var admins = groupChat.Admins ?? new HashSet<uint>();
|
||||||
|
admins.Remove(card.Comp.Number.Value);
|
||||||
|
|
||||||
|
// If the creator is leaving, transfer ownership
|
||||||
|
uint? newCreatorId = groupChat.CreatorId;
|
||||||
|
var isCreatorLeaving = card.Comp.Number.Value == groupChat.CreatorId;
|
||||||
|
if (isCreatorLeaving)
|
||||||
|
{
|
||||||
|
if (admins.Count > 0)
|
||||||
|
{
|
||||||
|
newCreatorId = admins.First();
|
||||||
|
}
|
||||||
|
else if (members.Count > 0)
|
||||||
|
{
|
||||||
|
newCreatorId = members.First();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newCreatorId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the group for all remaining members
|
||||||
|
var updatedRecipient = newCreatorId != null
|
||||||
|
? groupChat with { Members = members, Admins = admins, CreatorId = newCreatorId }
|
||||||
|
: groupChat with { Members = members, Admins = admins };
|
||||||
|
|
||||||
|
foreach (var memberNumber in members)
|
||||||
|
{
|
||||||
|
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
|
||||||
|
while (cardQuery.MoveNext(out var memberCardUid, out var memberCard))
|
||||||
|
{
|
||||||
|
if (memberCard.Number == memberNumber)
|
||||||
|
{
|
||||||
|
_nanoChat.SetRecipient((memberCardUid, memberCard), chatNumber, updatedRecipient);
|
||||||
|
UpdateUIForCard(memberCardUid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Delete chat but keep the messages
|
// Delete chat but keep the messages
|
||||||
var deleted = _nanoChat.TryDeleteChat((card, card.Comp), msg.RecipientNumber.Value, true);
|
var deleted = _nanoChat.TryDeleteChat((card, card.Comp), chatNumber, true);
|
||||||
|
|
||||||
if (!deleted)
|
if (!deleted)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_adminLogger.Add(LogType.Action,
|
_adminLogger.Add(LogType.Action,
|
||||||
LogImpact.Low,
|
LogImpact.Low,
|
||||||
$"{ToPrettyString(msg.Actor):user} deleted NanoChat conversation with #{msg.RecipientNumber:D4}");
|
$"{ToPrettyString(msg.Actor)} deleted NanoChat conversation with #{chatNumber:D4}");
|
||||||
|
|
||||||
UpdateUIForCard(card);
|
UpdateUIForCard(card);
|
||||||
}
|
}
|
||||||
@@ -293,9 +372,15 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles toggling notification mute state.
|
/// Handles toggling notification mute state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void HandleToggleMute(Entity<NanoChatCardComponent> card)
|
private void HandleToggleMute(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||||
{
|
{
|
||||||
_nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp)));
|
var muted = _nanoChat.GetNotificationsMuted((card, card.Comp));
|
||||||
|
_nanoChat.SetNotificationsMuted((card, card.Comp), !muted);
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Chat,
|
||||||
|
LogImpact.Low,
|
||||||
|
$"{ToPrettyString(msg.Actor)} {(muted ? "un" : "")}muted the NanoChat on {ToPrettyString(card)}");
|
||||||
|
|
||||||
UpdateUIForCard(card);
|
UpdateUIForCard(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,13 +388,25 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
{
|
{
|
||||||
if (msg.RecipientNumber is not uint chat)
|
if (msg.RecipientNumber is not uint chat)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_nanoChat.ToggleChatMuted((card, card.Comp), chat);
|
_nanoChat.ToggleChatMuted((card, card.Comp), chat);
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Chat,
|
||||||
|
LogImpact.Low,
|
||||||
|
$"{ToPrettyString(msg.Actor)} toggled NanoChat notifcations for chat #{chat:D4} on {ToPrettyString(card)}");
|
||||||
|
|
||||||
UpdateUIForCard(card);
|
UpdateUIForCard(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleToggleListNumber(Entity<NanoChatCardComponent> card)
|
private void HandleToggleListNumber(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||||
{
|
{
|
||||||
_nanoChat.SetListNumber((card, card.Comp), !_nanoChat.GetListNumber((card, card.Comp)));
|
var numberListed = _nanoChat.GetListNumber((card, card.Comp));
|
||||||
|
_nanoChat.SetListNumber((card, card.Comp), !numberListed);
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Chat,
|
||||||
|
LogImpact.Low,
|
||||||
|
$"{ToPrettyString(msg.Actor)} toggled the NanoChat listing of {ToPrettyString(card)} {(numberListed ? "off" : "on")}");
|
||||||
|
|
||||||
UpdateUIForAllCards();
|
UpdateUIForAllCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +420,11 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
|
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!EnsureRecipientExists(card, msg.RecipientNumber.Value))
|
var recipient = _nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value);
|
||||||
|
var isGroupChat = recipient?.IsGroup ?? false;
|
||||||
|
|
||||||
|
// Only ensure recipient exists for non-group chats
|
||||||
|
if (!isGroupChat && !EnsureRecipientExists(card, msg.RecipientNumber.Value))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var content = msg.Content;
|
var content = msg.Content;
|
||||||
@@ -341,8 +442,19 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
(uint)card.Comp.Number
|
(uint)card.Comp.Number
|
||||||
);
|
);
|
||||||
|
|
||||||
// Attempt delivery
|
List<Entity<NanoChatCardComponent>> recipients;
|
||||||
var (deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value);
|
bool deliveryFailed;
|
||||||
|
|
||||||
|
if (isGroupChat && recipient != null)
|
||||||
|
{
|
||||||
|
// For group chats, deliver to all members
|
||||||
|
(deliveryFailed, recipients) = AttemptGroupMessageDelivery(cartridge, recipient.Value, card.Comp.Number.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// For regular chats, deliver to single recipient
|
||||||
|
(deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value);
|
||||||
|
}
|
||||||
|
|
||||||
// Update delivery status
|
// Update delivery status
|
||||||
message = message with { DeliveryFailed = deliveryFailed };
|
message = message with { DeliveryFailed = deliveryFailed };
|
||||||
@@ -357,7 +469,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
|
|
||||||
_adminLogger.Add(LogType.Chat,
|
_adminLogger.Add(LogType.Chat,
|
||||||
LogImpact.Low,
|
LogImpact.Low,
|
||||||
$"{ToPrettyString(card):user} sent NanoChat message to {recipientsText}: {content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
|
$"{ToPrettyString(msg.Actor)} sent NanoChat message to {recipientsText}: {content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
|
||||||
|
|
||||||
var msgEv = new NanoChatMessageReceivedEvent(card);
|
var msgEv = new NanoChatMessageReceivedEvent(card);
|
||||||
RaiseLocalEvent(ref msgEv);
|
RaiseLocalEvent(ref msgEv);
|
||||||
@@ -365,9 +477,9 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
if (deliveryFailed)
|
if (deliveryFailed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var recipient in recipients)
|
foreach (var recipientCard in recipients)
|
||||||
{
|
{
|
||||||
DeliverMessageToRecipient(card, recipient, message);
|
DeliverMessageToRecipient(card, recipientCard, message, msg.RecipientNumber.Value, recipient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,27 +592,88 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
/// <param name="sender">The sender's card entity</param>
|
/// <param name="sender">The sender's card entity</param>
|
||||||
/// <param name="recipient">The recipient's card entity</param>
|
/// <param name="recipient">The recipient's card entity</param>
|
||||||
/// <param name="message">The <see cref="NanoChatMessage" /> to deliver</param>
|
/// <param name="message">The <see cref="NanoChatMessage" /> to deliver</param>
|
||||||
|
/// <param name="chatNumber">The chat number (for group chats, this is the group number)</param>
|
||||||
|
/// <param name="groupRecipient">Optional group recipient info if this is a group chat</param>
|
||||||
private void DeliverMessageToRecipient(Entity<NanoChatCardComponent> sender,
|
private void DeliverMessageToRecipient(Entity<NanoChatCardComponent> sender,
|
||||||
Entity<NanoChatCardComponent> recipient,
|
Entity<NanoChatCardComponent> recipient,
|
||||||
NanoChatMessage message)
|
NanoChatMessage message,
|
||||||
|
uint chatNumber,
|
||||||
|
NanoChatRecipient? groupRecipient = null)
|
||||||
{
|
{
|
||||||
if (sender.Comp.Number is not uint senderNumber)
|
if (sender.Comp.Number is not uint senderNumber)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Always try to get and add sender info to recipient's contacts
|
// For group chats, use the group number. For regular chats, use the sender's number
|
||||||
if (!EnsureRecipientExists(recipient, senderNumber))
|
var recipientNumber = (groupRecipient != null && groupRecipient.Value.IsGroup) ? chatNumber : senderNumber;
|
||||||
|
|
||||||
|
if (groupRecipient != null && groupRecipient.Value.IsGroup)
|
||||||
|
{
|
||||||
|
var existingRecipient = _nanoChat.GetRecipient((recipient, recipient.Comp), recipientNumber);
|
||||||
|
|
||||||
|
if (existingRecipient == null || !existingRecipient.Value.IsGroup)
|
||||||
|
{
|
||||||
|
_nanoChat.SetRecipient((recipient, recipient.Comp), recipientNumber, groupRecipient.Value);
|
||||||
|
}
|
||||||
|
else if (groupRecipient.Value.Members != null)
|
||||||
|
{
|
||||||
|
var existingMembers = existingRecipient.Value.Members ?? new HashSet<uint>();
|
||||||
|
if (!existingMembers.SetEquals(groupRecipient.Value.Members))
|
||||||
|
{
|
||||||
|
_nanoChat.SetRecipient((recipient, recipient.Comp), recipientNumber, groupRecipient.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var existingAdmins = existingRecipient.Value.Admins ?? new HashSet<uint>();
|
||||||
|
var groupAdmins = groupRecipient.Value.Admins ?? new HashSet<uint>();
|
||||||
|
if (!existingAdmins.SetEquals(groupAdmins))
|
||||||
|
{
|
||||||
|
_nanoChat.SetRecipient((recipient, recipient.Comp), recipientNumber, groupRecipient.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!EnsureRecipientExists(recipient, recipientNumber))
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_nanoChat.AddMessage((recipient, recipient.Comp), senderNumber, message with { DeliveryFailed = false });
|
_nanoChat.AddMessage((recipient, recipient.Comp), recipientNumber, message with { DeliveryFailed = false });
|
||||||
|
|
||||||
if (recipient.Comp.IsClosed || _nanoChat.GetCurrentChat((recipient, recipient.Comp)) != senderNumber)
|
if (recipient.Comp.IsClosed || _nanoChat.GetCurrentChat((recipient, recipient.Comp)) != recipientNumber)
|
||||||
HandleUnreadNotification(recipient, message, senderNumber);
|
HandleUnreadNotification(recipient, message, recipientNumber);
|
||||||
|
|
||||||
var msgEv = new NanoChatMessageReceivedEvent(recipient);
|
var msgEv = new NanoChatMessageReceivedEvent(recipient);
|
||||||
RaiseLocalEvent(ref msgEv);
|
RaiseLocalEvent(ref msgEv);
|
||||||
UpdateUIForCard(recipient);
|
UpdateUIForCard(recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to deliver a message to all members of a group chat.
|
||||||
|
/// </summary>
|
||||||
|
private (bool failed, List<Entity<NanoChatCardComponent>> recipients) AttemptGroupMessageDelivery(
|
||||||
|
Entity<NanoChatCartridgeComponent> sender,
|
||||||
|
NanoChatRecipient groupRecipient,
|
||||||
|
uint senderNumber)
|
||||||
|
{
|
||||||
|
if (groupRecipient.Members == null)
|
||||||
|
return (true, new List<Entity<NanoChatCardComponent>>());
|
||||||
|
|
||||||
|
var deliverableRecipients = new List<Entity<NanoChatCardComponent>>();
|
||||||
|
|
||||||
|
foreach (var memberNumber in groupRecipient.Members)
|
||||||
|
{
|
||||||
|
if (memberNumber == senderNumber)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var (failed, memberCards) = AttemptMessageDelivery(sender, memberNumber);
|
||||||
|
if (!failed)
|
||||||
|
deliverableRecipients.AddRange(memberCards);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delivery failed if no recipients could receive the message
|
||||||
|
return (deliverableRecipients.Count == 0, deliverableRecipients);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles unread message notifications and updates unread status.
|
/// Handles unread message notifications and updates unread status.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -510,21 +683,21 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
{
|
{
|
||||||
// Get sender name from contacts or fall back to number
|
// Get sender name from contacts or fall back to number
|
||||||
var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp));
|
var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp));
|
||||||
var senderName = recipients.TryGetValue(message.SenderId, out var senderRecipient)
|
var senderName = recipients.TryGetValue(senderNumber, out var senderRecipient)
|
||||||
? senderRecipient.Name
|
? senderRecipient.Name
|
||||||
: $"#{message.SenderId:D4}";
|
: $"#{senderNumber:D4}";
|
||||||
var hasSelectedCurrentChat = _nanoChat.GetCurrentChat((recipient, recipient.Comp)) == senderNumber;
|
var hasSelectedCurrentChat = _nanoChat.GetCurrentChat((recipient, recipient.Comp)) == senderNumber;
|
||||||
|
|
||||||
// Update unread status
|
// Update unread status
|
||||||
if (!hasSelectedCurrentChat)
|
if (!hasSelectedCurrentChat)
|
||||||
_nanoChat.SetRecipient((recipient, recipient.Comp),
|
_nanoChat.SetRecipient((recipient, recipient.Comp),
|
||||||
message.SenderId,
|
senderNumber,
|
||||||
senderRecipient with { HasUnread = true });
|
senderRecipient with { HasUnread = true });
|
||||||
|
|
||||||
// Temporary local to avoid trouble with read-only access; Contains doesn't modify the collection
|
// Temporary local to avoid trouble with read-only access; Contains doesn't modify the collection
|
||||||
HashSet<uint> mutedChats = recipient.Comp.MutedChats;
|
HashSet<uint> mutedChats = recipient.Comp.MutedChats;
|
||||||
if (recipient.Comp.NotificationsMuted ||
|
if (recipient.Comp.NotificationsMuted ||
|
||||||
mutedChats.Contains(message.SenderId) ||
|
mutedChats.Contains(senderNumber) ||
|
||||||
recipient.Comp.PdaUid is not { } pdaUid ||
|
recipient.Comp.PdaUid is not { } pdaUid ||
|
||||||
!TryComp<CartridgeLoaderComponent>(pdaUid, out var loader) ||
|
!TryComp<CartridgeLoaderComponent>(pdaUid, out var loader) ||
|
||||||
// Don't notify if the recipient has the NanoChat program open with this chat selected.
|
// Don't notify if the recipient has the NanoChat program open with this chat selected.
|
||||||
@@ -593,14 +766,27 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
|
|
||||||
// Try to get job title from ID card if possible
|
// Try to get job title from ID card if possible
|
||||||
string? jobTitle = null;
|
string? jobTitle = null;
|
||||||
|
string? department = null;
|
||||||
var name = "Unknown";
|
var name = "Unknown";
|
||||||
if (TryComp<IdCardComponent>(uid, out var idCard))
|
if (TryComp<IdCardComponent>(uid, out var idCard))
|
||||||
{
|
{
|
||||||
jobTitle = idCard.LocalizedJobTitle;
|
jobTitle = idCard.LocalizedJobTitle;
|
||||||
name = idCard.FullName ?? name;
|
name = idCard.FullName ?? name;
|
||||||
|
if (idCard.JobDepartments is { } depts && depts.Count > 0)
|
||||||
|
department = depts[0].ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NanoChatRecipient(number, name, jobTitle);
|
return new NanoChatRecipient(
|
||||||
|
number,
|
||||||
|
name,
|
||||||
|
jobTitle,
|
||||||
|
department,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -624,12 +810,34 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
var query = AllEntityQuery<NanoChatCardComponent, IdCardComponent>();
|
var query = AllEntityQuery<NanoChatCardComponent, IdCardComponent>();
|
||||||
while (query.MoveNext(out var entityId, out var nanoChatCard, out var idCardComponent))
|
while (query.MoveNext(out var entityId, out var nanoChatCard, out var idCardComponent))
|
||||||
{
|
{
|
||||||
if (nanoChatCard.ListNumber && nanoChatCard.Number is uint nanoChatNumber && idCardComponent.FullName is string fullName && _station.GetOwningStation(entityId) == station)
|
if (!nanoChatCard.ListNumber)
|
||||||
{
|
continue;
|
||||||
contacts.Add(new NanoChatRecipient(nanoChatNumber, fullName));
|
if (nanoChatCard.PdaUid is null)
|
||||||
}
|
continue;
|
||||||
|
if (nanoChatCard.Number is not { } nanoChatNumber)
|
||||||
|
continue;
|
||||||
|
if (idCardComponent.FullName is not { } fullName)
|
||||||
|
continue;
|
||||||
|
if (_station.GetOwningStation(entityId) != station)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string? department = null;
|
||||||
|
if (idCardComponent.JobDepartments is { } depts && depts.Count > 0)
|
||||||
|
department = depts[0].ToString();
|
||||||
|
|
||||||
|
contacts.Add(new NanoChatRecipient(
|
||||||
|
nanoChatNumber,
|
||||||
|
fullName,
|
||||||
|
idCardComponent.LocalizedJobTitle,
|
||||||
|
department,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
));
|
||||||
}
|
}
|
||||||
contacts.Sort((contactA, contactB) => string.CompareOrdinal(contactA.Name, contactB.Name));
|
contacts.Sort();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -668,4 +876,314 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||||||
listNumber);
|
listNumber);
|
||||||
_cartridge.UpdateCartridgeUiState(loader, state);
|
_cartridge.UpdateCartridgeUiState(loader, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles creation of a new group chat.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleCreateGroupChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||||
|
{
|
||||||
|
if (msg.Content == null || card.Comp.Number == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var name = msg.Content;
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
name = name.Trim();
|
||||||
|
if (name.Length > _maxNameLength)
|
||||||
|
name = name[.._maxNameLength];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique group number (surely unique I actually have no idea how to generate good unique numbers.)
|
||||||
|
var groupNumber = (uint)(HashCode.Combine(card.Comp.Number.Value, _timing.CurTime.Ticks) & 0x7FFFFFFF);
|
||||||
|
|
||||||
|
// This fucking sucks (fire emoji)
|
||||||
|
while (_nanoChat.GetRecipient((card, card.Comp), groupNumber) != null)
|
||||||
|
{
|
||||||
|
groupNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create group chat recipient
|
||||||
|
var recipient = new NanoChatRecipient(
|
||||||
|
groupNumber,
|
||||||
|
name,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
[card.Comp.Number.Value],
|
||||||
|
card.Comp.Number.Value,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
_nanoChat.SetRecipient((card, card.Comp), groupNumber, recipient);
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Action,
|
||||||
|
LogImpact.Low,
|
||||||
|
$"{ToPrettyString(msg.Actor)} created group chat '{name}' (#{groupNumber:D4})");
|
||||||
|
|
||||||
|
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
|
||||||
|
RaiseLocalEvent(ref recipientEv);
|
||||||
|
UpdateUIForCard(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles inviting a user to a group chat.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleInviteToGroup(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||||
|
{
|
||||||
|
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var groupNumber = msg.RecipientNumber.Value;
|
||||||
|
if (!uint.TryParse(msg.Content, out var inviteeNumber))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
|
||||||
|
if (recipient == null || !recipient.Value.IsGroup)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Only the creator or admins can invite
|
||||||
|
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
|
||||||
|
var admins = recipient.Value.Admins ?? new HashSet<uint>();
|
||||||
|
var isAdmin = admins.Contains(card.Comp.Number.Value);
|
||||||
|
|
||||||
|
if (!isCreator && !isAdmin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var members = recipient.Value.Members ?? new HashSet<uint>();
|
||||||
|
if (members.Contains(inviteeNumber))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Add member to group
|
||||||
|
members.Add(inviteeNumber);
|
||||||
|
var updatedRecipient = recipient.Value with { Members = members };
|
||||||
|
_nanoChat.SetRecipient((card, card.Comp), groupNumber, updatedRecipient);
|
||||||
|
|
||||||
|
// Update member lists for all members
|
||||||
|
var memberCards = new List<Entity<NanoChatCardComponent>>();
|
||||||
|
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
|
||||||
|
while (cardQuery.MoveNext(out var cardUid, out var memberCard))
|
||||||
|
{
|
||||||
|
if (memberCard.Number != null && members.Contains(memberCard.Number.Value))
|
||||||
|
{
|
||||||
|
memberCards.Add((cardUid, memberCard));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var memberCard in memberCards)
|
||||||
|
{
|
||||||
|
_nanoChat.SetRecipient((memberCard, memberCard.Comp), groupNumber, updatedRecipient);
|
||||||
|
UpdateUIForCard(memberCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Action,
|
||||||
|
LogImpact.Low,
|
||||||
|
$"{ToPrettyString(msg.Actor)} invited #{inviteeNumber:D4} to group chat #{groupNumber:D4}");
|
||||||
|
|
||||||
|
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
|
||||||
|
RaiseLocalEvent(ref recipientEv);
|
||||||
|
UpdateUIForCard(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles kicking a user from a group chat.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleKickFromGroup(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||||
|
{
|
||||||
|
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var groupNumber = msg.RecipientNumber.Value;
|
||||||
|
if (!uint.TryParse(msg.Content, out var kickeeNumber))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
|
||||||
|
if (recipient == null || !recipient.Value.IsGroup)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
|
||||||
|
var admins = recipient.Value.Admins ?? new HashSet<uint>();
|
||||||
|
var isAdmin = admins.Contains(card.Comp.Number.Value);
|
||||||
|
var isSelfKick = kickeeNumber == card.Comp.Number.Value;
|
||||||
|
|
||||||
|
if (!isSelfKick && !isCreator && !isAdmin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Allow creator to leave the group but not be kicked by other members.
|
||||||
|
var isCreatorLeaving = kickeeNumber == recipient.Value.CreatorId && isCreator;
|
||||||
|
if (kickeeNumber == recipient.Value.CreatorId && !isCreatorLeaving)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Find all cards belonging to the kickee
|
||||||
|
var kickeeCards = new List<Entity<NanoChatCardComponent>>();
|
||||||
|
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
|
||||||
|
while (cardQuery.MoveNext(out var cardUid, out var kickeeCard))
|
||||||
|
{
|
||||||
|
if (kickeeCard.Number == kickeeNumber)
|
||||||
|
{
|
||||||
|
kickeeCards.Add((cardUid, kickeeCard));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kickeeCard in kickeeCards)
|
||||||
|
{
|
||||||
|
var deleteMsg = new NanoChatUiMessageEvent(NanoChatUiMessageType.DeleteChat, groupNumber, null, null)
|
||||||
|
{
|
||||||
|
Actor = msg.Actor
|
||||||
|
};
|
||||||
|
HandleDeleteChat(kickeeCard, deleteMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCreatorLeaving)
|
||||||
|
{
|
||||||
|
_adminLogger.Add(LogType.Action,
|
||||||
|
LogImpact.Low,
|
||||||
|
$"{ToPrettyString(msg.Actor)} left group chat #{groupNumber:D4}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_adminLogger.Add(LogType.Action,
|
||||||
|
LogImpact.Low,
|
||||||
|
$"{ToPrettyString(msg.Actor)} kicked #{kickeeNumber:D4} from group chat #{groupNumber:D4}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
|
||||||
|
RaiseLocalEvent(ref recipientEv);
|
||||||
|
UpdateUIForCard(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles promoting a user to admin in a group chat.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleAdminUser(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||||
|
{
|
||||||
|
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var groupNumber = msg.RecipientNumber.Value;
|
||||||
|
if (!uint.TryParse(msg.Content, out var targetNumber))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
|
||||||
|
if (recipient == null || !recipient.Value.IsGroup)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Only the creator or admins can make admins
|
||||||
|
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
|
||||||
|
var admins = recipient.Value.Admins ?? new HashSet<uint>();
|
||||||
|
var isAdmin = admins.Contains(card.Comp.Number.Value);
|
||||||
|
|
||||||
|
if (!isCreator && !isAdmin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var members = recipient.Value.Members ?? new HashSet<uint>();
|
||||||
|
if (!members.Contains(targetNumber))
|
||||||
|
return; // how tf
|
||||||
|
|
||||||
|
if (targetNumber == recipient.Value.CreatorId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!isCreator && admins.Contains(targetNumber))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!admins.Add(targetNumber))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var updatedRecipient = recipient.Value with { Admins = admins };
|
||||||
|
_nanoChat.SetRecipient((card, card.Comp), groupNumber, updatedRecipient);
|
||||||
|
|
||||||
|
// Sync to all members
|
||||||
|
foreach (var memberNumber in members)
|
||||||
|
{
|
||||||
|
var memberCards = new List<Entity<NanoChatCardComponent>>();
|
||||||
|
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
|
||||||
|
while (cardQuery.MoveNext(out var cardUid, out var memberCard))
|
||||||
|
{
|
||||||
|
if (memberCard.Number == memberNumber)
|
||||||
|
{
|
||||||
|
memberCards.Add((cardUid, memberCard));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var memberCard in memberCards)
|
||||||
|
{
|
||||||
|
_nanoChat.SetRecipient((memberCard, memberCard.Comp), groupNumber, updatedRecipient);
|
||||||
|
UpdateUIForCard(memberCard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Action,
|
||||||
|
LogImpact.Low,
|
||||||
|
$"{ToPrettyString(msg.Actor)} promoted #{targetNumber:D4} to admin in group chat #{groupNumber:D4}");
|
||||||
|
|
||||||
|
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
|
||||||
|
RaiseLocalEvent(ref recipientEv);
|
||||||
|
UpdateUIForCard(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles demoting a user from admin in a group chat.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleDeadminUser(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||||
|
{
|
||||||
|
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var groupNumber = msg.RecipientNumber.Value;
|
||||||
|
if (!uint.TryParse(msg.Content, out var targetNumber))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
|
||||||
|
if (recipient == null || !recipient.Value.IsGroup)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Only the creator or admins can remove admins
|
||||||
|
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
|
||||||
|
var admins = recipient.Value.Admins ?? new HashSet<uint>();
|
||||||
|
var isAdmin = admins.Contains(card.Comp.Number.Value);
|
||||||
|
|
||||||
|
if (!isCreator && !isAdmin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var members = recipient.Value.Members ?? new HashSet<uint>();
|
||||||
|
if (!members.Contains(targetNumber))
|
||||||
|
return; // how
|
||||||
|
|
||||||
|
if (!isCreator && (targetNumber == recipient.Value.CreatorId || admins.Contains(targetNumber)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!admins.Remove(targetNumber))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var updatedRecipient = recipient.Value with { Admins = admins };
|
||||||
|
_nanoChat.SetRecipient((card, card.Comp), groupNumber, updatedRecipient);
|
||||||
|
|
||||||
|
// Sync to all members
|
||||||
|
foreach (var memberNumber in members)
|
||||||
|
{
|
||||||
|
var memberCards = new List<Entity<NanoChatCardComponent>>();
|
||||||
|
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
|
||||||
|
while (cardQuery.MoveNext(out var cardUid, out var memberCard))
|
||||||
|
{
|
||||||
|
if (memberCard.Number == memberNumber)
|
||||||
|
{
|
||||||
|
memberCards.Add((cardUid, memberCard));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var memberCard in memberCards)
|
||||||
|
{
|
||||||
|
_nanoChat.SetRecipient((memberCard, memberCard.Comp), groupNumber, updatedRecipient);
|
||||||
|
UpdateUIForCard(memberCard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Action,
|
||||||
|
LogImpact.Low,
|
||||||
|
$"{ToPrettyString(msg.Actor)} removed admin from #{targetNumber:D4} in group chat #{groupNumber:D4}");
|
||||||
|
|
||||||
|
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
|
||||||
|
RaiseLocalEvent(ref recipientEv);
|
||||||
|
UpdateUIForCard(card);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Shared.CartridgeLoader;
|
using Content.Shared.CartridgeLoader;
|
||||||
|
using Content.Shared.Roles;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
namespace Content.Shared._DV.CartridgeLoader.Cartridges;
|
namespace Content.Shared._DV.CartridgeLoader.Cartridges;
|
||||||
@@ -57,45 +58,107 @@ public enum NanoChatUiMessageType : byte
|
|||||||
ToggleMute,
|
ToggleMute,
|
||||||
ToggleMuteChat,
|
ToggleMuteChat,
|
||||||
ToggleListNumber,
|
ToggleListNumber,
|
||||||
|
CreateGroupChat,
|
||||||
|
InviteToGroup,
|
||||||
|
KickFromGroup,
|
||||||
|
ViewGroupMembers,
|
||||||
|
AdminUser,
|
||||||
|
DeadminUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
// putting this here because i can
|
/// <summary>
|
||||||
|
/// Represents an actual NanoChat Chat
|
||||||
|
/// Either a Group, if <paramref name="isGroup"/> is <see langword="true" />, else a normal user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="number">The recipient's NanoChat number</param>
|
||||||
|
/// <param name="name">The recipient's display name</param>
|
||||||
|
/// <param name="jobTitle">Optional job title for the recipient</param>
|
||||||
|
/// <param name="department">Optional department ID for the recipient</param>
|
||||||
|
/// <param name="hasUnread">Whether there are unread messages from this recipient</param>
|
||||||
|
/// <param name="isGroup">Whether this is a group chat</param>
|
||||||
|
/// <param name="members">For group chats: list of member NanoChat numbers</param>
|
||||||
|
/// <param name="creatorId">For group chats: the creator's NanoChat number</param>
|
||||||
|
/// <param name="admins">For group chats: set of admin NanoChat numbers</param>
|
||||||
[Serializable, NetSerializable, DataRecord]
|
[Serializable, NetSerializable, DataRecord]
|
||||||
public struct NanoChatRecipient
|
public struct NanoChatRecipient(
|
||||||
|
uint number,
|
||||||
|
string name,
|
||||||
|
string? jobTitle,
|
||||||
|
string? department,
|
||||||
|
bool hasUnread,
|
||||||
|
bool isGroup,
|
||||||
|
HashSet<uint>? members,
|
||||||
|
uint? creatorId,
|
||||||
|
HashSet<uint>? admins
|
||||||
|
) : IComparable<NanoChatRecipient>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The recipient's unique NanoChat number.
|
/// The recipient's unique NanoChat number.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint Number;
|
public uint Number = number;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The recipient's display name, typically from their ID card.
|
/// The recipient's display name, typically from their ID card.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name;
|
public string Name = name;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The recipient's job title, if available.
|
/// The recipient's job title, if available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? JobTitle;
|
public string? JobTitle = jobTitle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The recipient's department ID, if available.
|
||||||
|
/// </summary>
|
||||||
|
public string? Department = department;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this recipient has unread messages.
|
/// Whether this recipient has unread messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasUnread;
|
public bool HasUnread = hasUnread;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new NanoChat recipient.
|
/// Whether this is a group chat.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="number">The recipient's NanoChat number</param>
|
public bool IsGroup = isGroup;
|
||||||
/// <param name="name">The recipient's display name</param>
|
|
||||||
/// <param name="jobTitle">Optional job title for the recipient</param>
|
/// <summary>
|
||||||
/// <param name="hasUnread">Whether there are unread messages from this recipient</param>
|
/// For group chats: list of member NanoChat numbers.
|
||||||
public NanoChatRecipient(uint number, string name, string? jobTitle = null, bool hasUnread = false)
|
/// </summary>
|
||||||
|
public HashSet<uint>? Members = members;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For group chats: the NanoChat number of the creator.
|
||||||
|
/// </summary>
|
||||||
|
public uint? CreatorId = creatorId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For group chats: set of admin NanoChat numbers who can invite/kick.
|
||||||
|
/// </summary>
|
||||||
|
public HashSet<uint>? Admins = admins;
|
||||||
|
|
||||||
|
readonly int IComparable<NanoChatRecipient>.CompareTo(NanoChatRecipient other)
|
||||||
{
|
{
|
||||||
Number = number;
|
// Groups should go before normal users
|
||||||
Name = name;
|
if (IsGroup && !other.IsGroup)
|
||||||
JobTitle = jobTitle;
|
return -1;
|
||||||
HasUnread = hasUnread;
|
else if (!IsGroup && other.IsGroup)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
// FIXME: Order by Department?
|
||||||
|
|
||||||
|
// Order by name
|
||||||
|
var nameCompare = string.CompareOrdinal(Name, other.Name);
|
||||||
|
if (nameCompare != 0)
|
||||||
|
return nameCompare;
|
||||||
|
|
||||||
|
// Smaller number goes first
|
||||||
|
if (Number > other.Number)
|
||||||
|
return 1;
|
||||||
|
else if (Number < other.Number)
|
||||||
|
return -1;
|
||||||
|
else
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
||||||
|
using Content.Shared._Funkystation.NanoChat;
|
||||||
using Content.Shared.Examine;
|
using Content.Shared.Examine;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Shared._DV.NanoChat;
|
namespace Content.Shared._DV.NanoChat;
|
||||||
@@ -10,11 +12,13 @@ namespace Content.Shared._DV.NanoChat;
|
|||||||
public abstract class SharedNanoChatSystem : EntitySystem
|
public abstract class SharedNanoChatSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
SubscribeLocalEvent<NanoChatCardComponent, ExaminedEvent>(OnExamined);
|
SubscribeLocalEvent<NanoChatCardComponent, ExaminedEvent>(OnExamined);
|
||||||
|
_prototypeManager.PrototypesReloaded += (_) => NanoChatEmojis.ClearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnExamined(Entity<NanoChatCardComponent> ent, ref ExaminedEvent args)
|
private void OnExamined(Entity<NanoChatCardComponent> ent, ref ExaminedEvent args)
|
||||||
@@ -36,6 +40,9 @@ public abstract class SharedNanoChatSystem : EntitySystem
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static string Truncate(string text, int maxLength, string overflowText = "...")
|
public static string Truncate(string text, int maxLength, string overflowText = "...")
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
return text.Length > maxLength
|
return text.Length > maxLength
|
||||||
? text[..(maxLength - overflowText.Length)] + overflowText
|
? text[..(maxLength - overflowText.Length)] + overflowText
|
||||||
: text;
|
: text;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Shared._Funkystation.NanoChat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prototype for NanoChat emoji definitions.
|
||||||
|
/// </summary>
|
||||||
|
[Prototype]
|
||||||
|
public sealed partial class NanoChatEmojiPrototype : IPrototype
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The unique identifier for this emoji (e.g., "godo")
|
||||||
|
/// </summary>
|
||||||
|
[IdDataField]
|
||||||
|
public string ID { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional category for grouping emojis (e.g., "Godo", "Plushies")
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public string Category { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The sprite specifier for this emoji
|
||||||
|
/// </summary>
|
||||||
|
[DataField(required: true)]
|
||||||
|
public SpriteSpecifier Sprite = default!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Shared._Funkystation.NanoChat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared emoji definitions for NanoChat.
|
||||||
|
/// This class provides access to emoji prototypes.
|
||||||
|
/// </summary>
|
||||||
|
public static class NanoChatEmojis
|
||||||
|
{
|
||||||
|
private static Dictionary<string, SpriteSpecifier>? _cachedEmojis;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dictionary mapping emoji names to their sprite specifiers.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyDictionary<string, SpriteSpecifier> EmojiSpecifiers
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_cachedEmojis == null)
|
||||||
|
{
|
||||||
|
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||||
|
_cachedEmojis = new Dictionary<string, SpriteSpecifier>();
|
||||||
|
|
||||||
|
var sortedEmojis = prototypeManager.EnumeratePrototypes<NanoChatEmojiPrototype>()
|
||||||
|
.OrderBy(proto => proto.Category)
|
||||||
|
.ThenBy(proto => proto.ID);
|
||||||
|
|
||||||
|
foreach (var proto in sortedEmojis)
|
||||||
|
{
|
||||||
|
_cachedEmojis[proto.ID] = proto.Sprite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _cachedEmojis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the emoji cache. Call this if prototypes are reloaded.
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearCache()
|
||||||
|
{
|
||||||
|
_cachedEmojis = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -188,6 +188,7 @@ nano-chat-title = NanoChat
|
|||||||
nano-chat-new-chat = New Chat
|
nano-chat-new-chat = New Chat
|
||||||
nano-chat-contacts = CONTACTS
|
nano-chat-contacts = CONTACTS
|
||||||
nano-chat-no-chats = No active chats
|
nano-chat-no-chats = No active chats
|
||||||
|
nano-chat-no-id-card = No ID card inserted
|
||||||
nano-chat-select-chat = Select a chat to begin
|
nano-chat-select-chat = Select a chat to begin
|
||||||
nano-chat-message-placeholder = Type a message...
|
nano-chat-message-placeholder = Type a message...
|
||||||
nano-chat-send = Send
|
nano-chat-send = Send
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
|
||||||
|
# Group chat
|
||||||
|
nano-chat-create-group = Create Group Chat
|
||||||
|
nano-chat-invite-to-group = Invite to Group
|
||||||
|
nano-chat-view-members = View Members
|
||||||
|
nano-chat-invite = Invite
|
||||||
|
nano-chat-kick = Kick
|
||||||
|
nano-chat-leave = Leave
|
||||||
|
nano-chat-admin = Make Admin
|
||||||
|
nano-chat-deadmin = Remove Admin
|
||||||
|
nano-chat-no-results = No results found
|
||||||
|
nano-chat-no-members = No members
|
||||||
|
nano-chat-close = Close
|
||||||
|
|
||||||
|
# Create group chat popup
|
||||||
|
nano-chat-create-group-title = Create Group Chat
|
||||||
|
nano-chat-group-name-label = Group Name
|
||||||
|
nano-chat-group-name-placeholder = Enter a group name
|
||||||
|
|
||||||
|
# Invite to group popup
|
||||||
|
nano-chat-invite-to-group-title = Invite to Group
|
||||||
|
nano-chat-search-label = Search
|
||||||
|
nano-chat-search-placeholder = Search by name or number...
|
||||||
|
|
||||||
|
# Group members popup
|
||||||
|
nano-chat-group-members-title = Group Members
|
||||||
|
nano-chat-owner-suffix = (Owner)
|
||||||
|
nano-chat-admin-suffix = (Admin)
|
||||||
|
|
||||||
|
# Emoji picker
|
||||||
|
nano-chat-emoji-picker = Emoji Picker
|
||||||
|
nano-chat-emoji-search = Search emojis...
|
||||||
|
|
||||||
|
# Department sorting
|
||||||
|
nano-chat-other-department = Other
|
||||||
|
nano-chat-unknown-sender = Unknown
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
|
||||||
|
# SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
|
||||||
|
|
||||||
|
# Godo emotes
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: godo
|
||||||
|
category: Godo
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/godo.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: do
|
||||||
|
category: Godo
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/do.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: donow
|
||||||
|
category: Godo
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/donow.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: gocrazy
|
||||||
|
category: Godo
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/donot.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: gosad
|
||||||
|
category: Godo
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/dontdo.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: godover
|
||||||
|
category: Godo
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/godover.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: golove
|
||||||
|
category: Godo
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/golove.png
|
||||||
|
|
||||||
|
# Character emotes
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: buzz
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/buzztwo.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: chime
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/chime.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: chirp
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/chirp.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: clap
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/clap.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: click
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/click.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: cough
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/cough.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: cry
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/cry.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: deathgasp
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/deathgasp.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: hew
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/hew.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: honk
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/honk.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: laugh
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/laugh.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: salute
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/salute.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: scream
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Actions/scream.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: sigh
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/sigh.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: snap
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/snap.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: squeak
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/squeak.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: tailslap
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/tailslap.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: vocal
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/vocal.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: weh
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/weh.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: whistle
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/whistle.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: yawn
|
||||||
|
category: Character
|
||||||
|
sprite: /Textures/Interface/Emotes/yawn.png
|
||||||
|
|
||||||
|
# Plushie emotes
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: bee
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/bee.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: ratvar
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/ratvar.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: penguin
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/penguin.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: moth
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/moth.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: rouny
|
||||||
|
category: Plushies
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/rouny.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: lizard
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/lizard.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: spacelizard
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/lizard.rsi
|
||||||
|
state: plushie_spacelizard
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: hampter
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/hampter.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: arachnid
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/arachnid.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: lamp
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/lamp.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: nukie
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/nukie.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: atmosian
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/atmosian.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: slime
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/slime.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: snake
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/snake.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: vox
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/vox.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: diona
|
||||||
|
category: Plushies
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/plushie_diona.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: human
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/human.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: xeno
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/xeno.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: carp
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/carp.rsi
|
||||||
|
state: carpplush
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: rainbowcarp
|
||||||
|
category: Plushies
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/rainbowcarp.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: rainbowlizard
|
||||||
|
category: Plushies
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/rainbowlizardplush.png
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: nar
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/Objects/Fun/Plushies/narsie.rsi
|
||||||
|
state: icon
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: mothroach
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/_Funkystation/Objects/Fun/toys.rsi
|
||||||
|
state: plushie_mothroach
|
||||||
|
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: cosmic
|
||||||
|
category: Plushies
|
||||||
|
sprite:
|
||||||
|
sprite: /Textures/_DV/CosmicCult/Objects/unknownplushie.rsi
|
||||||
|
state: plushie_cosmic
|
||||||
|
|
||||||
|
# Other
|
||||||
|
- type: nanoChatEmoji
|
||||||
|
id: floorgoblin
|
||||||
|
category: Misc
|
||||||
|
sprite: /Textures/_Funkystation/Interface/NanoChat/Emotes/floorgoblin.png
|
||||||
|
After Width: | Height: | Size: 177 B |
|
After Width: | Height: | Size: 270 B |
|
After Width: | Height: | Size: 191 B |
|
After Width: | Height: | Size: 276 B |
|
After Width: | Height: | Size: 516 B |
|
After Width: | Height: | Size: 580 B |
|
After Width: | Height: | Size: 739 B |
|
After Width: | Height: | Size: 738 B |
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 710 B |
|
After Width: | Height: | Size: 707 B |
|
After Width: | Height: | Size: 776 B |
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 734 B |
|
After Width: | Height: | Size: 611 B |
|
After Width: | Height: | Size: 273 B |
|
After Width: | Height: | Size: 230 B |
|
After Width: | Height: | Size: 436 B |
|
After Width: | Height: | Size: 744 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 517 B |
|
After Width: | Height: | Size: 741 B |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"license": "CC-BY-SA-3.0",
|
||||||
|
"copyright": "meijunkie on Discord - https://funkystation.org",
|
||||||
|
"size": {
|
||||||
|
"x": 32,
|
||||||
|
"y": 32
|
||||||
|
},
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"name": "plushie_mothroach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 349 B |