XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEToolBox

重构聊天模块:单栏模式、统一导航、通知优化

本次提交带来以下主要改进: - 新增“聊天单栏模式”,适配小屏/移动端体验,支持会话/联系人列表与消息详情分离展示。 - 会话列表重构为统一导航列表,整合好友、群聊、最近会话,支持未读数、免打扰、最后消息等信息,顶部新增“群聊大厅”入口。 - 支持群聊“消息免打扰”设置,配置持久化,免打扰群聊不再弹出 Windows 通知。 - 新增 `ChatDesktopNotificationCoordinator`,统一管理聊天消息、好友申请、群聊邀请等 Windows 通知,支持免打扰过滤。 - 移除 InfoBar 状态栏,所有状态提示统一通过 Windows 通知弹窗展示。 - 聊天页面 UI 结构优化,入口合并、弹窗统一管理、语音通话按钮更换为 Path 图标,文本绑定声明为 OneWay。 - 新增多项回归测试,确保统一导航、免打扰、通知、布局切换等功能的正确性。 - 辅助类如 `DesktopNotificationService`、`ChatNotificationPreferences` 实现通知和免打扰配置统一管理。 整体提升聊天模块的易用性、可维护性和多端适配能力。

89e8515
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

14 个文件 +759 -51
Added XFEToolBox.Client.Wpf.Test/ChatNavigationTests.cs +63 -0
@@ -0,0 +1,63 @@
1 using XFEToolBox.Client.Models.Chat;
2 using XFEToolBox.Client.Utilities.Chat;
3 using XFEToolBox.Client.ViewModel.Chat;
4 using XFEToolBox.Core.Chat;
5
6 namespace XFEToolBox.Client.Wpf.Test;
7
8 public static class ChatNavigationTests
9 {
10 [Test]
11 public static void NavigationItemsExposeLatestMessageAndMutedStateSafely()
12 {
13 var now = DateTimeOffset.UtcNow;
14 var friend = new ChatFriendInfo
15 {
16 User = new ChatUserSummary { Id = "friend", UserName = "friend-account", NickName = "好友" },
17 ConversationId = "direct"
18 };
19 var conversation = new ChatConversationInfo
20 {
21 Id = "direct",
22 Kind = ChatConversationKind.Direct,
23 Friend = friend.User,
24 LastMessage = new ChatMessageInfo
25 {
26 Id = "message",
27 ConversationId = "direct",
28 Sender = friend.User,
29 Text = "最新消息",
30 CreatedAtUtc = now
31 }
32 };
33 var item = new ChatNavigationItem(conversation, friend);
34
35 Ensure(item.Key == "friend:friend", "好友统一列表键不稳定。 ");
36 Ensure(item.Subtitle == "最新消息" && item.LastMessageAtUtc == now, "统一列表没有使用最后一条消息。 ");
37 Ensure(!item.IsGroup && !item.IsMuted, "好友被错误识别成免打扰群聊。 ");
38 Ensure(ChatNotificationPreferences.ParseMutedGroupIds("[\"g1\",\"g1\",\"\"]").SetEquals(["g1"]),
39 "群聊免打扰配置没有去重或过滤空值。 ");
40 Ensure(ChatNotificationPreferences.ParseMutedGroupIds("{损坏").Count == 0,
41 "损坏的群聊免打扰配置没有安全恢复。 ");
42 }
43
44 [Test]
45 public static void SinglePaneModeSwitchesBetweenListAndDetail()
46 {
47 var viewModel = new ChatPageViewModel { IsSinglePaneMode = true };
48 Ensure(viewModel.ShowListPane && !viewModel.ShowDetailPane, "单栏模式初始状态没有只显示列表。 ");
49
50 viewModel.OpenLobbyCommand.ExecuteAsync(null).GetAwaiter().GetResult();
51 Ensure(!viewModel.ShowListPane && viewModel.ShowDetailPane && viewModel.ShowBackButton,
52 "打开大厅后单栏模式没有切换到详情。 ");
53
54 viewModel.CloseDetailCommand.Execute(null);
55 Ensure(viewModel.ShowListPane && !viewModel.ShowDetailPane && !viewModel.ShowBackButton,
56 "返回后单栏模式没有恢复会话列表。 ");
57 }
58
59 private static void Ensure(bool condition, string message)
60 {
61 if (!condition) throw new InvalidOperationException(message);
62 }
63 }
Added XFEToolBox.Client.Wpf.Test/ChatPageXamlTests.cs +72 -0
@@ -0,0 +1,72 @@
1 using System.IO;
2 using System.Xml;
3 using System.Xml.Linq;
4
5 namespace XFEToolBox.Client.Wpf.Test;
6
7 public static class ChatPageXamlTests
8 {
9 [Test]
10 public static void InlineReadOnlyTextBindingsAreExplicitlyOneWay()
11 {
12 var repositoryRoot = FindRepositoryRoot();
13 var xamlPath = Path.Combine(repositoryRoot, "XFEToolBox", "Views", "Pages", "ChatPage.xaml");
14 var document = XDocument.Load(xamlPath, LoadOptions.SetLineInfo);
15 XNamespace presentation = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
16 var bindings = document
17 .Descendants(presentation + "Run")
18 .Select(run => run.Attribute("Text"))
19 .Where(attribute => attribute?.Value.StartsWith("{Binding", StringComparison.Ordinal) == true)
20 .ToArray();
21
22 Ensure(bindings.Length >= 6, "聊天页面的动态 Run.Text 绑定数量异常,回归测试可能已失效。");
23 foreach (var binding in bindings)
24 {
25 Ensure(binding!.Value.Contains("Mode=OneWay", StringComparison.Ordinal),
26 $"只读内联文本绑定没有显式使用 OneWay:{binding.Value}(第 {((IXmlLineInfo)binding).LineNumber} 行)。");
27 }
28 }
29
30 [Test]
31 public static void ChatUsesUnifiedNavigationAndWindowsNotificationLayout()
32 {
33 var repositoryRoot = FindRepositoryRoot();
34 var chatPath = Path.Combine(repositoryRoot, "XFEToolBox", "Views", "Pages", "ChatPage.xaml");
35 var settingPath = Path.Combine(repositoryRoot, "XFEToolBox", "Views", "Pages", "SettingPage.xaml");
36 var chatText = File.ReadAllText(chatPath);
37 var settingText = File.ReadAllText(settingPath);
38 var document = XDocument.Load(chatPath);
39 XNamespace presentation = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
40
41 Ensure(!document.Descendants(presentation + "InfoBar").Any(), "聊天页仍显示顶部 InfoBar。 ");
42 Ensure(chatText.Contains("ItemsSource=\"{Binding NavigationItems}\"", StringComparison.Ordinal),
43 "聊天页没有绑定好友与群聊统一列表。 ");
44 Ensure(chatText.Contains("Command=\"{Binding OpenLobbyCommand}\"", StringComparison.Ordinal),
45 "统一列表顶部缺少大厅入口。 ");
46 Ensure(chatText.Contains("IsChecked=\"{Binding ManagedGroupIsMuted}\"", StringComparison.Ordinal),
47 "群聊管理缺少消息免打扰设置。 ");
48 Ensure(!chatText.Contains("Content=\"☎\"", StringComparison.Ordinal) &&
49 document.Descendants(presentation + "Path").Any(path => path.Attribute("Data")?.Value.Contains("M6.62,10.79", StringComparison.Ordinal) == true),
50 "语音按钮没有使用简约电话路径图标。 ");
51 Ensure(settingText.Contains("SystemProfile.ChatSinglePaneMode", StringComparison.Ordinal),
52 "设置页缺少聊天单栏模式开关。 ");
53 }
54
55 private static string FindRepositoryRoot()
56 {
57 var directory = new DirectoryInfo(AppContext.BaseDirectory);
58 while (directory is not null)
59 {
60 if (File.Exists(Path.Combine(directory.FullName, "XFEToolBox.sln")))
61 return directory.FullName;
62 directory = directory.Parent;
63 }
64
65 throw new DirectoryNotFoundException("无法定位 XFEToolBox 仓库根目录。");
66 }
67
68 private static void Ensure(bool condition, string message)
69 {
70 if (!condition) throw new InvalidOperationException(message);
71 }
72 }
Modified XFEToolBox/App.xaml.cs +6 -0
@@ -17,6 +17,7 @@ public partial class App : Application
17 17 private bool chatRuntimeInitialized;
18 18 private bool chatRuntimeEventsDetached;
19 19 private Task? chatRuntimeShutdownTask;
20 private ChatDesktopNotificationCoordinator? chatNotificationCoordinator;
20 21 private readonly SemaphoreSlim chatSessionGate = new(1, 1);
21 22
22 23 public App() => InitializeComponent();
@@ -57,6 +58,7 @@ public partial class App : Application
57 58 () => ShowMainWindow("home"),
58 59 () => ShowCommandPalette(),
59 60 RequestExit);
61 chatNotificationCoordinator = new ChatDesktopNotificationCoordinator();
60 62 singleInstanceService.StartListening(message => Dispatcher.BeginInvoke(() =>
61 63 {
62 64 if (message.Equals("show-palette", StringComparison.OrdinalIgnoreCase)) ShowCommandPalette();
@@ -86,6 +88,9 @@ public partial class App : Application
86 88 commandPaletteWindow.ShowPalette(initialQuery);
87 89 }
88 90
91 public void ShowDesktopNotification(string title, string message, DesktopNotificationLevel level = DesktopNotificationLevel.Information) =>
92 trayIconService?.ShowNotification(title, message, level);
93
89 94 public bool ConfigureGlobalHotkey(string gestureText)
90 95 {
91 96 if (!GlobalHotkeyService.TryNormalize(gestureText, out var normalized, out _)) return false;
@@ -149,6 +154,7 @@ public partial class App : Application
149 154 ChatRealtimeClient.Shared.RequestStop();
150 155 }
151 156 globalHotkeyService?.Dispose();
157 chatNotificationCoordinator?.Dispose();
152 158 trayIconService?.Dispose();
153 159 singleInstanceService?.Dispose();
154 160 base.OnExit(e);
Modified XFEToolBox/Models/Chat/ChatUiModels.cs +62 -0
@@ -1,4 +1,5 @@
1 1 using CommunityToolkit.Mvvm.ComponentModel;
2 using XFEToolBox.Client.Utilities.Chat;
2 3 using XFEToolBox.Core.Chat;
3 4
4 5 namespace XFEToolBox.Client.Models.Chat;
@@ -28,6 +29,67 @@ public sealed class ChatConversationItem(ChatConversationInfo conversation)
28 29 public bool HasUnread => Conversation.UnreadCount > 0;
29 30 }
30 31
32 public sealed class ChatNavigationItem
33 {
34 public ChatNavigationItem(ChatConversationInfo? conversation, ChatFriendInfo friend)
35 {
36 Conversation = conversation;
37 Friend = friend;
38 Key = $"friend:{friend.User.Id}";
39 Title = friend.User.NickName;
40 Initials = Title;
41 }
42
43 public ChatNavigationItem(ChatConversationInfo? conversation, ChatGroupSummary group)
44 {
45 Conversation = conversation;
46 Group = group;
47 Key = $"group:{group.Id}";
48 Title = group.Name;
49 Initials = Title;
50 }
51
52 public ChatNavigationItem(ChatConversationInfo conversation)
53 {
54 Conversation = conversation;
55 Friend = conversation.Kind == ChatConversationKind.Direct && conversation.Friend is not null
56 ? new ChatFriendInfo
57 {
58 User = conversation.Friend,
59 ConversationId = conversation.Id,
60 FriendsSinceUtc = conversation.CreatedAtUtc
61 }
62 : null;
63 Group = conversation.Kind == ChatConversationKind.Group ? conversation.Group : null;
64 Key = conversation.Kind == ChatConversationKind.Group
65 ? $"group:{conversation.Group?.Id ?? conversation.Id}"
66 : $"friend:{conversation.Friend?.Id ?? conversation.Id}";
67 Title = conversation.Kind == ChatConversationKind.Group
68 ? conversation.Group?.Name ?? "群聊"
69 : conversation.Friend?.NickName ?? "好友";
70 Initials = Title;
71 }
72
73 public string Key { get; }
74 public ChatConversationInfo? Conversation { get; }
75 public ChatFriendInfo? Friend { get; }
76 public ChatGroupSummary? Group { get; }
77 public string Title { get; }
78 public string Initials { get; }
79 public bool IsGroup => Group is not null || Conversation?.Kind == ChatConversationKind.Group;
80 public bool IsFriend => !IsGroup;
81 public string Subtitle => Conversation?.LastMessage is null
82 ? "暂无消息"
83 : ChatMessageItem.DescribeMessage(Conversation.LastMessage);
84 public DateTimeOffset? LastMessageAtUtc => Conversation?.LastMessage?.CreatedAtUtc;
85 public string TimeText => LastMessageAtUtc?.ToLocalTime().ToString("MM-dd HH:mm") ?? string.Empty;
86 public long UnreadCount => Conversation?.UnreadCount ?? 0;
87 public string UnreadText => UnreadCount > 99 ? "99+" : UnreadCount.ToString();
88 public bool HasUnread => UnreadCount > 0;
89 public bool IsMuted => IsGroup && ChatNotificationPreferences.IsGroupMuted(Group?.Id ?? Conversation?.Group?.Id);
90 public string MutedText => IsMuted ? "免打扰" : string.Empty;
91 }
92
31 93 public sealed class ChatGroupItem(ChatGroupSummary group)
32 94 {
33 95 public ChatGroupSummary Group { get; } = group;
Modified XFEToolBox/Profiles/CrossVersionProfiles/SystemProfile.cs +10 -0
@@ -85,6 +85,16 @@ public partial class SystemProfile : XFEProfile
85 85 /// </summary>
86 86 [ProfileProperty]
87 87 private bool trayCloseHintShown = false;
88 /// <summary>
89 /// 聊天页是否使用单栏会话布局。默认保留列表和消息并列的双栏布局。
90 /// </summary>
91 [ProfileProperty]
92 private bool chatSinglePaneMode = false;
93 /// <summary>
94 /// 已开启消息免打扰的群聊 ID(JSON 数组)。
95 /// </summary>
96 [ProfileProperty]
97 private string chatMutedGroupIdsJson = "[]";
88 98 public SystemProfile() => ProfilePath = @$"{AppPath.LocalProfile}\{typeof(SystemProfile)}.xprofile";
89 99 /// <summary>
90 100 /// 工具箱现在是否可以被关闭
Added XFEToolBox/Utilities/Chat/ChatDesktopNotificationCoordinator.cs +137 -0
@@ -0,0 +1,137 @@
1 using System.Text.Json;
2 using XFEToolBox.Client.Models.Chat;
3 using XFEToolBox.Client.Utilities.Server;
4 using XFEToolBox.Core.Chat;
5
6 namespace XFEToolBox.Client.Utilities.Chat;
7
8 public sealed class ChatDesktopNotificationCoordinator : IDisposable
9 {
10 private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
11 {
12 PropertyNameCaseInsensitive = true
13 };
14
15 private readonly ChatApiClient apiClient = new();
16 private readonly SemaphoreSlim notificationGate = new(1, 1);
17 private bool disposed;
18
19 public ChatDesktopNotificationCoordinator() =>
20 ChatRealtimeClient.Shared.EnvelopeReceived += RealtimeClient_EnvelopeReceived;
21
22 private async void RealtimeClient_EnvelopeReceived(object? sender, ChatRealtimeEnvelopeEventArgs e)
23 {
24 if (disposed || !ClientSession.IsLoggedIn) return;
25 await notificationGate.WaitAsync();
26 try
27 {
28 switch (e.Envelope.Type)
29 {
30 case "chat.message.created":
31 await NotifyMessageAsync(e.Envelope.Payload.Deserialize<ChatMessageInfo>(JsonOptions));
32 break;
33 case "chat.friend.requested":
34 NotifyFriendRequest(e.Envelope.Payload.Deserialize<ChatFriendRequestInfo>(JsonOptions));
35 break;
36 case "chat.friend.request.updated":
37 NotifyFriendRequestUpdate(e.Envelope.Payload.Deserialize<ChatFriendRequestInfo>(JsonOptions));
38 break;
39 case "chat.group.invited":
40 NotifyGroupInvitation(e.Envelope.Payload.Deserialize<ChatGroupInvitationInfo>(JsonOptions));
41 break;
42 case "chat.group.invitation.updated":
43 NotifyGroupInvitationUpdate(e.Envelope.Payload.Deserialize<ChatGroupInvitationInfo>(JsonOptions));
44 break;
45 }
46 }
47 catch
48 {
49 // A notification failure must never interrupt the realtime connection.
50 }
51 finally
52 {
53 notificationGate.Release();
54 }
55 }
56
57 private async Task NotifyMessageAsync(ChatMessageInfo? message)
58 {
59 if (message is null ||
60 message.MessageType == ChatMessageType.GroupInvitation ||
61 string.Equals(message.Sender.Id, ClientSession.CurrentUser?.Id, StringComparison.Ordinal))
62 return;
63
64 var conversation = (await apiClient.GetConversationsAsync())
65 .FirstOrDefault(item => string.Equals(item.Id, message.ConversationId, StringComparison.Ordinal));
66 if (conversation?.Kind == ChatConversationKind.Group &&
67 ChatNotificationPreferences.IsGroupMuted(conversation.Group?.Id))
68 return;
69
70 var title = conversation?.Kind == ChatConversationKind.Group
71 ? $"{conversation.Group?.Name ?? "群聊"} · {message.Sender.NickName}"
72 : message.Sender.NickName;
73 var text = ChatMessageItem.DescribeMessage(message);
74 DesktopNotificationService.Show(title, string.IsNullOrWhiteSpace(text) ? "收到一条新消息" : text);
75 }
76
77 private static void NotifyFriendRequest(ChatFriendRequestInfo? request)
78 {
79 if (request is null ||
80 string.Equals(request.FromUser.Id, ClientSession.CurrentUser?.Id, StringComparison.Ordinal))
81 return;
82 DesktopNotificationService.Show(
83 "新的好友申请",
84 string.IsNullOrWhiteSpace(request.Message)
85 ? $"{request.FromUser.NickName} 希望添加你为好友"
86 : $"{request.FromUser.NickName}:{request.Message}");
87 }
88
89 private static void NotifyGroupInvitation(ChatGroupInvitationInfo? invitation)
90 {
91 if (invitation is null ||
92 !string.Equals(invitation.InvitedUser.Id, ClientSession.CurrentUser?.Id, StringComparison.Ordinal))
93 return;
94 DesktopNotificationService.Show(
95 "新的群聊邀请",
96 $"{invitation.InvitedBy.NickName} 邀请你加入“{invitation.Group.Name}”");
97 }
98
99 private static void NotifyFriendRequestUpdate(ChatFriendRequestInfo? request)
100 {
101 if (request is null ||
102 !string.Equals(request.FromUser.Id, ClientSession.CurrentUser?.Id, StringComparison.Ordinal))
103 return;
104 var action = request.Status switch
105 {
106 ChatFriendRequestStatus.Accepted => "已同意你的好友申请",
107 ChatFriendRequestStatus.Rejected => "已拒绝你的好友申请",
108 _ => null
109 };
110 if (action is not null)
111 DesktopNotificationService.Show("好友申请已处理", $"{request.ToUser.NickName}{action}");
112 }
113
114 private static void NotifyGroupInvitationUpdate(ChatGroupInvitationInfo? invitation)
115 {
116 if (invitation is null ||
117 !string.Equals(invitation.InvitedBy.Id, ClientSession.CurrentUser?.Id, StringComparison.Ordinal))
118 return;
119 var action = invitation.Status switch
120 {
121 ChatGroupInvitationStatus.Accepted => "已接受群聊邀请",
122 ChatGroupInvitationStatus.Rejected => "已拒绝群聊邀请",
123 _ => null
124 };
125 if (action is not null)
126 DesktopNotificationService.Show(
127 "群聊邀请已处理",
128 $"{invitation.InvitedUser.NickName}{action}:{invitation.Group.Name}");
129 }
130
131 public void Dispose()
132 {
133 if (disposed) return;
134 disposed = true;
135 ChatRealtimeClient.Shared.EnvelopeReceived -= RealtimeClient_EnvelopeReceived;
136 }
137 }
Added XFEToolBox/Utilities/Chat/ChatNotificationPreferences.cs +45 -0
@@ -0,0 +1,45 @@
1 using System.Text.Json;
2 using XFEToolBox.Client.Profiles.CrossVersionProfiles;
3
4 namespace XFEToolBox.Client.Utilities.Chat;
5
6 public static class ChatNotificationPreferences
7 {
8 private static readonly object SyncRoot = new();
9
10 public static bool IsGroupMuted(string? groupId)
11 {
12 if (string.IsNullOrWhiteSpace(groupId)) return false;
13 lock (SyncRoot)
14 return ParseMutedGroupIds(SystemProfile.ChatMutedGroupIdsJson).Contains(groupId);
15 }
16
17 public static void SetGroupMuted(string? groupId, bool muted)
18 {
19 if (string.IsNullOrWhiteSpace(groupId)) return;
20 lock (SyncRoot)
21 {
22 var ids = ParseMutedGroupIds(SystemProfile.ChatMutedGroupIdsJson);
23 var changed = muted ? ids.Add(groupId) : ids.Remove(groupId);
24 if (!changed) return;
25 SystemProfile.ChatMutedGroupIdsJson = JsonSerializer.Serialize(ids.Order(StringComparer.Ordinal));
26 SystemProfile.SaveProfile();
27 }
28 }
29
30 public static HashSet<string> ParseMutedGroupIds(string? json)
31 {
32 if (string.IsNullOrWhiteSpace(json)) return new(StringComparer.Ordinal);
33 try
34 {
35 var values = JsonSerializer.Deserialize<string[]>(json) ?? [];
36 return values
37 .Where(value => !string.IsNullOrWhiteSpace(value))
38 .ToHashSet(StringComparer.Ordinal);
39 }
40 catch (JsonException)
41 {
42 return new(StringComparer.Ordinal);
43 }
44 }
45 }
Added XFEToolBox/Utilities/DesktopNotificationService.cs +25 -0
@@ -0,0 +1,25 @@
1 using System.Windows;
2
3 namespace XFEToolBox.Client.Utilities;
4
5 public enum DesktopNotificationLevel
6 {
7 Information,
8 Warning,
9 Error
10 }
11
12 public static class DesktopNotificationService
13 {
14 public static void Show(
15 string title,
16 string message,
17 DesktopNotificationLevel level = DesktopNotificationLevel.Information)
18 {
19 if (string.IsNullOrWhiteSpace(title) || Application.Current is not App app) return;
20 if (app.Dispatcher.CheckAccess())
21 app.ShowDesktopNotification(title, message, level);
22 else
23 app.Dispatcher.BeginInvoke(() => app.ShowDesktopNotification(title, message, level));
24 }
25 }
Modified XFEToolBox/Utilities/TrayIconService.cs +12 -0
@@ -38,6 +38,18 @@ internal sealed class TrayIconService : IDisposable
38 38 "可使用全局快捷键打开命令面板,或从托盘菜单退出。",
39 39 Forms.ToolTipIcon.Info);
40 40
41 public void ShowNotification(string title, string message, DesktopNotificationLevel level) =>
42 notifyIcon.ShowBalloonTip(
43 5000,
44 title,
45 message,
46 level switch
47 {
48 DesktopNotificationLevel.Warning => Forms.ToolTipIcon.Warning,
49 DesktopNotificationLevel.Error => Forms.ToolTipIcon.Error,
50 _ => Forms.ToolTipIcon.Info
51 });
52
41 53 public void Dispose()
42 54 {
43 55 notifyIcon.Visible = false;
Modified XFEToolBox/ViewModel/Chat/ChatPageViewModel.cs +166 -9
@@ -1,10 +1,11 @@
1 1 using System.Collections.ObjectModel;
2 2 using System.IO;
3 3 using System.Text.Json;
4 using System.Windows;
5 4 using CommunityToolkit.Mvvm.ComponentModel;
6 5 using CommunityToolkit.Mvvm.Input;
7 6 using XFEToolBox.Client.Models.Chat;
7 using XFEToolBox.Client.Profiles.CrossVersionProfiles;
8 using XFEToolBox.Client.Utilities;
8 9 using XFEToolBox.Client.Utilities.Chat;
9 10 using XFEToolBox.Client.Utilities.Server;
10 11 using XFEToolBox.Core.Chat;
@@ -26,6 +27,7 @@ public partial class ChatPageViewModel : ObservableObject
26 27 private CancellationTokenSource sessionCancellation = new();
27 28
28 29 public ObservableCollection<ChatConversationItem> Conversations { get; } = [];
30 public ObservableCollection<ChatNavigationItem> NavigationItems { get; } = [];
29 31 public ObservableCollection<ChatGroupItem> RecommendedGroups { get; } = [];
30 32 public ObservableCollection<ChatGroupItem> MyGroups { get; } = [];
31 33 public ObservableCollection<ChatFriendItem> Friends { get; } = [];
@@ -83,11 +85,37 @@ public partial class ChatPageViewModel : ObservableObject
83 85 [NotifyPropertyChangedFor(nameof(ConversationSubtitle))]
84 86 [NotifyPropertyChangedFor(nameof(IsSelectedConversationGroup))]
85 87 [NotifyPropertyChangedFor(nameof(CanCompose))]
88 [NotifyPropertyChangedFor(nameof(HasOpenDetail))]
89 [NotifyPropertyChangedFor(nameof(ShowListPane))]
90 [NotifyPropertyChangedFor(nameof(ShowDetailPane))]
91 [NotifyPropertyChangedFor(nameof(ShowBackButton))]
86 92 private ChatConversationItem? selectedConversation;
87 93
94 [ObservableProperty]
95 private ChatNavigationItem? selectedNavigationItem;
96
97 [ObservableProperty]
98 [NotifyPropertyChangedFor(nameof(IsMessageAreaOpen))]
99 [NotifyPropertyChangedFor(nameof(HasOpenDetail))]
100 [NotifyPropertyChangedFor(nameof(ShowListPane))]
101 [NotifyPropertyChangedFor(nameof(ShowDetailPane))]
102 [NotifyPropertyChangedFor(nameof(ShowBackButton))]
103 private bool isLobbyOpen;
104
105 [ObservableProperty]
106 [NotifyPropertyChangedFor(nameof(ShowListPane))]
107 [NotifyPropertyChangedFor(nameof(ShowDetailPane))]
108 [NotifyPropertyChangedFor(nameof(ShowBackButton))]
109 private bool isSinglePaneMode = SystemProfile.ChatSinglePaneMode;
110
88 111 public bool HasSelectedConversation => SelectedConversation is not null;
89 112 public bool HasNoSelectedConversation => SelectedConversation is null;
90 113 public bool IsSelectedConversationGroup => SelectedConversation?.Conversation.Kind == ChatConversationKind.Group;
114 public bool IsMessageAreaOpen => !IsLobbyOpen;
115 public bool HasOpenDetail => IsLobbyOpen || HasSelectedConversation;
116 public bool ShowListPane => !IsSinglePaneMode || !HasOpenDetail;
117 public bool ShowDetailPane => !IsSinglePaneMode || HasOpenDetail;
118 public bool ShowBackButton => IsSinglePaneMode && HasOpenDetail;
91 119 public string ConversationTitle => SelectedConversation?.Title ?? "选择一个会话";
92 120 public string ConversationSubtitle => SelectedConversation is null
93 121 ? "从左侧选择好友、群聊或最近会话"
@@ -133,6 +161,9 @@ public partial class ChatPageViewModel : ObservableObject
133 161 [ObservableProperty]
134 162 private bool isCreateGroupDialogOpen;
135 163
164 [ObservableProperty]
165 private bool isContactManagementDialogOpen;
166
136 167 [ObservableProperty]
137 168 private string newGroupName = string.Empty;
138 169
@@ -162,9 +193,21 @@ public partial class ChatPageViewModel : ObservableObject
162 193 [ObservableProperty]
163 194 private bool managedGroupIsPublic;
164 195
196 [ObservableProperty]
197 private bool managedGroupIsMuted;
198
165 199 public bool CanManageSelectedGroup => ManagedGroup?.CanManage == true;
166 200 public bool IsManagedGroupOwner => ManagedGroup?.IsOwner == true;
167 201
202 partial void OnManagedGroupIsMutedChanged(bool value)
203 {
204 if (ManagedGroup is null) return;
205 ChatNotificationPreferences.SetGroupMuted(ManagedGroup.Group.Id, value);
206 RebuildNavigationItems();
207 }
208
209 public void RefreshLayoutPreference() => IsSinglePaneMode = SystemProfile.ChatSinglePaneMode;
210
168 211 public async Task InitializeAsync()
169 212 {
170 213 IsLoggedIn = ClientSession.IsLoggedIn;
@@ -254,6 +297,56 @@ public partial class ChatPageViewModel : ObservableObject
254 297 [RelayCommand]
255 298 private void SelectSection(ChatSection section) => SelectedSection = section;
256 299
300 [RelayCommand]
301 private async Task OpenLobbyAsync()
302 {
303 IsLobbyOpen = true;
304 SelectedNavigationItem = null;
305 SelectedConversation = null;
306 Messages.Clear();
307 await RefreshRecommendedGroupsAsync();
308 }
309
310 [RelayCommand]
311 private void CloseDetail()
312 {
313 IsLobbyOpen = false;
314 SelectedConversation = null;
315 SelectedNavigationItem = null;
316 Messages.Clear();
317 nextBeforeSequence = null;
318 HasEarlierMessages = false;
319 }
320
321 [RelayCommand]
322 private void OpenContactManagementDialog()
323 {
324 IsCreateGroupDialogOpen = false;
325 IsContactManagementDialogOpen = true;
326 }
327
328 [RelayCommand]
329 private void CloseContactManagementDialog() => IsContactManagementDialogOpen = false;
330
331 [RelayCommand]
332 private async Task OpenNavigationItemAsync(ChatNavigationItem? item)
333 {
334 if (item is null) return;
335 SelectedNavigationItem = item;
336 if (item.Conversation is not null)
337 {
338 await OpenConversationAsync(new ChatConversationItem(item.Conversation));
339 return;
340 }
341 if (item.Friend is not null)
342 {
343 await OpenFriendConversationAsync(new ChatFriendItem(item.Friend));
344 return;
345 }
346 if (item.Group is not null)
347 await OpenGroupConversationCoreAsync(item.Group);
348 }
349
257 350 [RelayCommand]
258 351 private Task RefreshAllAsync() => RefreshAllCoreAsync(allowWhileBusy: false);
259 352
@@ -286,6 +379,7 @@ public partial class ChatPageViewModel : ObservableObject
286 379 var refreshed = Conversations.FirstOrDefault(item => item.Id == SelectedConversation.Id);
287 380 if (refreshed is not null) SelectedConversation = refreshed;
288 381 }
382 RebuildNavigationItems();
289 383 ShowStatus("聊天已同步", "好友、群聊与最近会话已刷新。", InfoBarSeverity.Success, autoClose: true);
290 384 }
291 385 catch (Exception exception)
@@ -363,6 +457,9 @@ public partial class ChatPageViewModel : ObservableObject
363 457 {
364 458 if (item is null) return;
365 459 SelectedConversation = item;
460 IsLobbyOpen = false;
461 SelectedNavigationItem = NavigationItems.FirstOrDefault(entry =>
462 string.Equals(entry.Conversation?.Id, item.Id, StringComparison.Ordinal));
366 463 await LoadMessagesAsync(reset: true, allowWhileBusy: true);
367 464 }
368 465
@@ -375,6 +472,10 @@ public partial class ChatPageViewModel : ObservableObject
375 472 var conversation = await apiClient.OpenDirectConversationAsync(item.Friend.User.Id);
376 473 UpsertConversation(conversation);
377 474 SelectedConversation = new ChatConversationItem(conversation);
475 IsLobbyOpen = false;
476 RebuildNavigationItems();
477 SelectedNavigationItem = NavigationItems.FirstOrDefault(entry =>
478 string.Equals(entry.Conversation?.Id, conversation.Id, StringComparison.Ordinal));
378 479 SelectedSection = ChatSection.Conversations;
379 480 await LoadMessagesAsync(reset: true, allowWhileBusy: true);
380 481 });
@@ -393,6 +494,10 @@ public partial class ChatPageViewModel : ObservableObject
393 494 CreatedAtUtc = group.CreatedAtUtc,
394 495 UpdatedAtUtc = group.UpdatedAtUtc
395 496 });
497 IsLobbyOpen = false;
498 RebuildNavigationItems();
499 SelectedNavigationItem = NavigationItems.FirstOrDefault(entry =>
500 string.Equals(entry.Conversation?.Id, SelectedConversation.Id, StringComparison.Ordinal));
396 501 SelectedSection = ChatSection.Conversations;
397 502 await LoadMessagesAsync(reset: true, allowWhileBusy: true);
398 503 }
@@ -607,6 +712,7 @@ public partial class ChatPageViewModel : ObservableObject
607 712 [RelayCommand]
608 713 private void OpenCreateGroupDialog()
609 714 {
715 IsContactManagementDialogOpen = false;
610 716 NewGroupName = string.Empty;
611 717 NewGroupDescription = string.Empty;
612 718 NewGroupIsPublic = true;
@@ -659,6 +765,7 @@ public partial class ChatPageViewModel : ObservableObject
659 765 ManagedGroupName = group.Name;
660 766 ManagedGroupDescription = group.Description;
661 767 ManagedGroupIsPublic = group.Visibility == ChatGroupVisibility.Public;
768 ManagedGroupIsMuted = ChatNotificationPreferences.IsGroupMuted(group.Id);
662 769 IsGroupManagementDialogOpen = true;
663 770 await RunBusyAsync("群成员加载失败", LoadManagedGroupMembersAsync);
664 771 }
@@ -693,6 +800,7 @@ public partial class ChatPageViewModel : ObservableObject
693 800 {
694 801 var id = ManagedGroup.Group.Id;
695 802 await apiClient.LeaveGroupAsync(id);
803 ChatNotificationPreferences.SetGroupMuted(id, false);
696 804 IsGroupManagementDialogOpen = false;
697 805 ManagedGroup = null;
698 806 ManagedGroupMembers.Clear();
@@ -810,6 +918,7 @@ public partial class ChatPageViewModel : ObservableObject
810 918 HasEarlierMessages = false;
811 919 }
812 920 }
921 RebuildNavigationItems();
813 922 }
814 923
815 924 private async Task RefreshFriendDataAsync()
@@ -819,6 +928,7 @@ public partial class ChatPageViewModel : ObservableObject
819 928 Replace(Friends, friends.Select(item => new ChatFriendItem(item)));
820 929 Replace(FriendRequests, requests.Select(item => new ChatFriendRequestItem(item)));
821 930 RebuildInviteCandidates(friends);
931 RebuildNavigationItems();
822 932 }
823 933
824 934 private async Task RefreshGroupsAsync()
@@ -835,6 +945,7 @@ public partial class ChatPageViewModel : ObservableObject
835 945 var refreshed = groups.FirstOrDefault(item => item.Id == LookupGroupResult.Group.Id);
836 946 if (refreshed is not null) LookupGroupResult = new ChatGroupItem(refreshed);
837 947 }
948 RebuildNavigationItems();
838 949 }
839 950
840 951 private async Task LoadManagedGroupMembersAsync()
@@ -862,6 +973,48 @@ public partial class ChatPageViewModel : ObservableObject
862 973 var existing = Conversations.FirstOrDefault(item => item.Id == conversation.Id);
863 974 if (existing is not null) Conversations.Remove(existing);
864 975 Conversations.Insert(0, new ChatConversationItem(conversation));
976 RebuildNavigationItems();
977 }
978
979 private void RebuildNavigationItems()
980 {
981 var selectedKey = SelectedNavigationItem?.Key;
982 var conversationByFriend = Conversations
983 .Where(item => item.Conversation.Kind == ChatConversationKind.Direct && item.Conversation.Friend is not null)
984 .GroupBy(item => item.Conversation.Friend!.Id, StringComparer.Ordinal)
985 .ToDictionary(group => group.Key, group => group.First().Conversation, StringComparer.Ordinal);
986 var conversationByGroup = Conversations
987 .Where(item => item.Conversation.Kind == ChatConversationKind.Group && item.Conversation.Group is not null)
988 .GroupBy(item => item.Conversation.Group!.Id, StringComparer.Ordinal)
989 .ToDictionary(group => group.Key, group => group.First().Conversation, StringComparer.Ordinal);
990
991 var values = new List<ChatNavigationItem>();
992 var usedConversationIds = new HashSet<string>(StringComparer.Ordinal);
993 foreach (var friend in Friends)
994 {
995 conversationByFriend.TryGetValue(friend.Friend.User.Id, out var conversation);
996 values.Add(new ChatNavigationItem(conversation, friend.Friend));
997 if (conversation is not null) usedConversationIds.Add(conversation.Id);
998 }
999 foreach (var group in MyGroups)
1000 {
1001 conversationByGroup.TryGetValue(group.Group.Id, out var conversation);
1002 values.Add(new ChatNavigationItem(conversation, group.Group));
1003 if (conversation is not null) usedConversationIds.Add(conversation.Id);
1004 }
1005 values.AddRange(Conversations
1006 .Where(item => !usedConversationIds.Contains(item.Id))
1007 .Select(item => new ChatNavigationItem(item.Conversation)));
1008
1009 Replace(NavigationItems, values
1010 .OrderByDescending(item => item.LastMessageAtUtc.HasValue)
1011 .ThenByDescending(item => item.LastMessageAtUtc)
1012 .ThenBy(item => item.Title, StringComparer.CurrentCultureIgnoreCase));
1013
1014 SelectedNavigationItem = selectedKey is null
1015 ? NavigationItems.FirstOrDefault(item =>
1016 string.Equals(item.Conversation?.Id, SelectedConversation?.Id, StringComparison.Ordinal))
1017 : NavigationItems.FirstOrDefault(item => string.Equals(item.Key, selectedKey, StringComparison.Ordinal));
865 1018 }
866 1019
867 1020 private void AddOrReplaceMessage(ChatMessageInfo message)
@@ -897,6 +1050,7 @@ public partial class ChatPageViewModel : ObservableObject
897 1050 {
898 1051 messageLoadGeneration++;
899 1052 Conversations.Clear();
1053 NavigationItems.Clear();
900 1054 RecommendedGroups.Clear();
901 1055 MyGroups.Clear();
902 1056 Friends.Clear();
@@ -907,9 +1061,12 @@ public partial class ChatPageViewModel : ObservableObject
907 1061 InviteCandidates.Clear();
908 1062 ManagedGroupMembers.Clear();
909 1063 SelectedConversation = null;
1064 SelectedNavigationItem = null;
1065 IsLobbyOpen = false;
910 1066 LookupGroupResult = null;
911 1067 ManagedGroup = null;
912 1068 IsCreateGroupDialogOpen = false;
1069 IsContactManagementDialogOpen = false;
913 1070 IsGroupManagementDialogOpen = false;
914 1071 IsTransferring = false;
915 1072 TransferProgress = 0;
@@ -921,19 +1078,19 @@ public partial class ChatPageViewModel : ObservableObject
921 1078 private void ShowException(string title, Exception exception) =>
922 1079 ShowStatus(title, exception.Message, InfoBarSeverity.Error);
923 1080
924 private async void ShowStatus(
1081 private static void ShowStatus(
925 1082 string title,
926 1083 string message,
927 1084 InfoBarSeverity severity,
928 1085 bool autoClose = false)
929 1086 {
930 StatusTitle = title;
931 StatusMessage = message;
932 StatusSeverity = severity;
933 IsStatusOpen = true;
934 if (!autoClose) return;
935 await Task.Delay(2200);
936 if (StatusTitle == title && StatusMessage == message) IsStatusOpen = false;
1087 if (autoClose) return;
1088 DesktopNotificationService.Show(title, message, severity switch
1089 {
1090 InfoBarSeverity.Warning => DesktopNotificationLevel.Warning,
1091 InfoBarSeverity.Error => DesktopNotificationLevel.Error,
1092 _ => DesktopNotificationLevel.Information
1093 });
937 1094 }
938 1095
939 1096 private static void Replace<T>(ObservableCollection<T> target, IEnumerable<T> values)
Modified XFEToolBox/Views/Pages/ChatPage.xaml +128 -29
@@ -108,6 +108,8 @@
108 108 </StackPanel>
109 109 <StackPanel Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right">
110 110 <TextBlock Text="{Binding TimeText}" Foreground="{DynamicResource ToolTextDisabledBrush}" FontSize="8" HorizontalAlignment="Right"/>
111 <TextBlock Text="{Binding MutedText}" Foreground="{DynamicResource ToolTextDisabledBrush}" FontSize="8" HorizontalAlignment="Right" Margin="0,3,0,0"
112 Visibility="{Binding IsMuted, Converter={StaticResource BooleanToVisibilityConverter}}"/>
111 113 <Border Margin="0,4,0,0" Padding="5,1" CornerRadius="7" Background="{DynamicResource MainColor}"
112 114 Visibility="{Binding HasUnread, Converter={StaticResource BooleanToVisibilityConverter}}">
113 115 <TextBlock Text="{Binding UnreadText}" Foreground="White" FontSize="8"/>
@@ -125,7 +127,7 @@
125 127 <StackPanel Grid.Column="1" Margin="8,0,5,0" VerticalAlignment="Center">
126 128 <TextBlock Text="{Binding Name}" Foreground="{DynamicResource ToolTextPrimaryBrush}" FontWeight="SemiBold" FontSize="11" TextTrimming="CharacterEllipsis"/>
127 129 <TextBlock Style="{StaticResource HintStyle}" Margin="0,3,0,0">
128 <Run Text="{Binding GroupNumberText}"/><Run Text=" · "/><Run Text="{Binding MemberText}"/>
130 <Run Text="{Binding GroupNumberText, Mode=OneWay}"/><Run Text=" · "/><Run Text="{Binding MemberText, Mode=OneWay}"/>
129 131 </TextBlock>
130 132 </StackPanel>
131 133 <Button Grid.Column="2" Content="{Binding JoinActionText}" Style="{StaticResource InlineButtonStyle}"
@@ -220,7 +222,7 @@
220 222 </Border>
221 223 <StackPanel Grid.Column="1" Margin="7,0,7,0" VerticalAlignment="Center">
222 224 <TextBlock Text="{Binding AttachmentTitle}" Foreground="{DynamicResource ToolTextPrimaryBrush}" FontSize="10" FontWeight="SemiBold" TextTrimming="CharacterEllipsis"/>
223 <TextBlock Style="{StaticResource HintStyle}" Margin="0,2,0,0"><Run Text="{Binding AttachmentKindText}"/><Run Text=" · "/><Run Text="{Binding AttachmentSizeText}"/></TextBlock>
225 <TextBlock Style="{StaticResource HintStyle}" Margin="0,2,0,0"><Run Text="{Binding AttachmentKindText, Mode=OneWay}"/><Run Text=" · "/><Run Text="{Binding AttachmentSizeText, Mode=OneWay}"/></TextBlock>
224 226 </StackPanel>
225 227 <Button Grid.Column="2" Content="下载" Style="{StaticResource InlineButtonStyle}" Tag="{Binding Attachment}" Click="DownloadAttachment_Click"/>
226 228 </Grid>
@@ -271,22 +273,19 @@
271 273 </Border>
272 274
273 275 <Grid Visibility="{Binding IsLoggedIn, Converter={StaticResource BooleanToVisibilityConverter}}">
274 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
276 <Grid.RowDefinitions><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
275 277
276 <controls:InfoBar Margin="0,0,0,7" IsOpen="{Binding IsStatusOpen, Mode=TwoWay}" Title="{Binding StatusTitle}"
277 Message="{Binding StatusMessage}" Severity="{Binding StatusSeverity}"/>
278
279 <Grid x:Name="WorkspaceGrid" Grid.Row="1">
278 <Grid x:Name="WorkspaceGrid">
280 279 <Grid.ColumnDefinitions>
281 <ColumnDefinition x:Name="SectionColumn" Width="132"/>
282 <ColumnDefinition Width="8"/>
280 <ColumnDefinition x:Name="SectionColumn" Width="0"/>
281 <ColumnDefinition Width="0"/>
283 282 <ColumnDefinition x:Name="ListColumn" Width="272"/>
284 <ColumnDefinition Width="8"/>
285 <ColumnDefinition Width="*" MinWidth="245"/>
283 <ColumnDefinition x:Name="DetailGapColumn" Width="8"/>
284 <ColumnDefinition x:Name="DetailColumn" Width="*"/>
286 285 </Grid.ColumnDefinitions>
287 286
288 287 <!-- 大屏功能栏。 -->
289 <Border x:Name="SectionPane" Style="{StaticResource ChatPaneStyle}" Padding="10">
288 <Border x:Name="SectionPane" Style="{StaticResource ChatPaneStyle}" Padding="10" Visibility="Collapsed">
290 289 <Grid>
291 290 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
292 291 <StackPanel Margin="5,4,5,14">
@@ -304,7 +303,8 @@
304 303 </Border>
305 304
306 305 <!-- 中间列表栏。 -->
307 <Border Grid.Column="2" Style="{StaticResource ChatPaneStyle}" Padding="8">
306 <Border x:Name="ListPane" Grid.Column="2" Style="{StaticResource ChatPaneStyle}" Padding="8"
307 Visibility="{Binding ShowListPane, Converter={StaticResource BooleanToVisibilityConverter}}">
308 308 <Grid>
309 309 <Grid.RowDefinitions><RowDefinition x:Name="CompactTabsRow" Height="0"/><RowDefinition Height="*"/></Grid.RowDefinitions>
310 310 <StackPanel x:Name="CompactTabs" Orientation="Horizontal" Visibility="Collapsed" Margin="0,0,0,7">
@@ -314,18 +314,37 @@
314 314 <RadioButton Content="群聊" Style="{StaticResource CompactSectionButtonStyle}" IsChecked="{Binding IsGroupsSection, Mode=OneWay}" Command="{Binding SelectSectionCommand}" CommandParameter="{x:Static chat:ChatSection.Groups}"/>
315 315 </StackPanel>
316 316
317 <Grid Grid.Row="1" Visibility="{Binding IsConversationSection, Converter={StaticResource BooleanToVisibilityConverter}}">
318 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
317 <Grid Grid.Row="1">
318 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
319 319 <Grid Margin="4,3,4,8">
320 <StackPanel><TextBlock Text="最近会话" Style="{StaticResource HeadingStyle}"/><TextBlock Text="好友与群聊消息" Style="{StaticResource HintStyle}" Margin="0,3,0,0"/></StackPanel>
321 <Button Content="↻" Style="{StaticResource IconButtonStyle}" HorizontalAlignment="Right" Command="{Binding RefreshAllCommand}" ToolTip="刷新会话"/>
320 <StackPanel><TextBlock Text="聊天" Style="{StaticResource HeadingStyle}"/><TextBlock Text="好友与群聊统一按最新消息排序" Style="{StaticResource HintStyle}" Margin="0,3,0,0"/></StackPanel>
321 <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
322 <Button Content="人" Style="{StaticResource IconButtonStyle}" Command="{Binding OpenContactManagementDialogCommand}" ToolTip="好友申请与群邀请"/>
323 <Button Content="+" Style="{StaticResource IconButtonStyle}" Margin="5,0,0,0" Command="{Binding OpenCreateGroupDialogCommand}" ToolTip="创建群聊"/>
324 <Button Content="↻" Style="{StaticResource IconButtonStyle}" Margin="5,0,0,0" Command="{Binding RefreshAllCommand}" ToolTip="刷新"/>
325 </StackPanel>
322 326 </Grid>
323 <ListBox Grid.Row="1" ItemsSource="{Binding Conversations}" SelectedItem="{Binding SelectedConversation, Mode=OneWay}"
327 <Button Grid.Row="1" Height="54" Margin="1,0,1,7" Padding="10,0" HorizontalContentAlignment="Stretch"
328 Style="{StaticResource ToolBoxButtonStyle}" controls:ButtonAssist.CornerRadius="11"
329 Command="{Binding OpenLobbyCommand}">
330 <Grid>
331 <Grid.ColumnDefinitions><ColumnDefinition Width="38"/><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
332 <Border Width="32" Height="32" CornerRadius="10" Background="{DynamicResource ToolAccentSoftBrush}">
333 <TextBlock Text="⌂" Foreground="{DynamicResource MainColor}" FontSize="15" HorizontalAlignment="Center" VerticalAlignment="Center"/>
334 </Border>
335 <StackPanel Grid.Column="1" Margin="8,0,4,0" VerticalAlignment="Center">
336 <TextBlock Text="群聊大厅" Foreground="{DynamicResource ToolTextPrimaryBrush}" FontWeight="SemiBold" FontSize="11"/>
337 <TextBlock Text="公开群推荐与群号查找" Style="{StaticResource HintStyle}" Margin="0,3,0,0"/>
338 </StackPanel>
339 <TextBlock Grid.Column="2" Text="›" Foreground="{DynamicResource MainColor}" FontSize="18" VerticalAlignment="Center"/>
340 </Grid>
341 </Button>
342 <ListBox Grid.Row="2" ItemsSource="{Binding NavigationItems}" SelectedItem="{Binding SelectedNavigationItem, Mode=OneWay}"
324 343 ItemTemplate="{StaticResource ConversationTemplate}" ItemContainerStyle="{StaticResource ChatListItemStyle}"
325 344 SelectionChanged="ConversationList_SelectionChanged"/>
326 345 </Grid>
327 346
328 <Grid Grid.Row="1" Visibility="{Binding IsLobbySection, Converter={StaticResource BooleanToVisibilityConverter}}">
347 <Grid Grid.Row="1" Visibility="Collapsed">
329 348 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
330 349 <StackPanel Margin="4,3,4,8">
331 350 <TextBlock Text="公开群大厅" Style="{StaticResource HeadingStyle}"/>
@@ -354,7 +373,7 @@
354 373 </Grid>
355 374 </Grid>
356 375
357 <Grid Grid.Row="1" Visibility="{Binding IsFriendsSection, Converter={StaticResource BooleanToVisibilityConverter}}">
376 <Grid Grid.Row="1" Visibility="Collapsed">
358 377 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
359 378 <StackPanel Margin="4,3,4,8"><TextBlock Text="好友" Style="{StaticResource HeadingStyle}"/><TextBlock Text="查找用户、处理申请与管理好友" Style="{StaticResource HintStyle}" Margin="0,3,0,0"/></StackPanel>
360 379 <TabControl Grid.Row="1">
@@ -380,7 +399,7 @@
380 399 </TabControl>
381 400 </Grid>
382 401
383 <Grid Grid.Row="1" Visibility="{Binding IsGroupsSection, Converter={StaticResource BooleanToVisibilityConverter}}">
402 <Grid Grid.Row="1" Visibility="Collapsed">
384 403 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
385 404 <Grid Margin="4,3,4,8">
386 405 <StackPanel><TextBlock Text="我的群聊" Style="{StaticResource HeadingStyle}"/><TextBlock Text="管理已加入的群和邀请" Style="{StaticResource HintStyle}" Margin="0,3,0,0"/></StackPanel>
@@ -399,12 +418,47 @@
399 418 </Border>
400 419
401 420 <!-- 消息区。 -->
402 <Border Grid.Column="4" Style="{StaticResource ChatPaneStyle}" Padding="0">
421 <Border x:Name="MessagePane" Grid.Column="4" Style="{StaticResource ChatPaneStyle}" Padding="0"
422 Visibility="{Binding ShowDetailPane, Converter={StaticResource BooleanToVisibilityConverter}}">
403 423 <Grid>
424 <Grid Visibility="{Binding IsLobbyOpen, Converter={StaticResource BooleanToVisibilityConverter}}">
425 <Grid.RowDefinitions><RowDefinition Height="58"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
426 <Border Padding="14,0" BorderBrush="{DynamicResource ToolDividerBrush}" BorderThickness="0,0,0,1">
427 <Grid>
428 <Button Content="‹" Style="{StaticResource IconButtonStyle}" HorizontalAlignment="Left" Command="{Binding CloseDetailCommand}"
429 Visibility="{Binding ShowBackButton, Converter={StaticResource BooleanToVisibilityConverter}}" ToolTip="返回会话列表"/>
430 <StackPanel VerticalAlignment="Center" Margin="42,0,0,0">
431 <TextBlock Text="群聊大厅" Style="{StaticResource HeadingStyle}"/>
432 <TextBlock Text="发现公开群聊,或通过群号加入私密群" Style="{StaticResource HintStyle}" Margin="0,3,0,0"/>
433 </StackPanel>
434 <Button Content="↻" Style="{StaticResource IconButtonStyle}" HorizontalAlignment="Right" Command="{Binding RefreshRecommendedGroupsCommand}" ToolTip="刷新大厅"/>
435 </Grid>
436 </Border>
437 <Grid Grid.Row="1" Margin="12,10,12,8">
438 <Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
439 <controls:TextEditor Text="{Binding GroupSearchQuery, UpdateSourceTrigger=PropertyChanged}" Height="35" Padding="9,0" HintText="搜索公开群…" KeyDown="GroupSearchBox_KeyDown"/>
440 <Button Grid.Column="1" Content="搜索" Style="{StaticResource InlineButtonStyle}" Height="35" Margin="6,0,0,0" Command="{Binding RefreshRecommendedGroupsCommand}"/>
441 </Grid>
442 <ListBox Grid.Row="2" Margin="10,0" ItemsSource="{Binding RecommendedGroups}" ItemTemplate="{StaticResource GroupTemplate}" ItemContainerStyle="{StaticResource ChatListItemStyle}"/>
443 <Border Grid.Row="3" Margin="12,8,12,12" Padding="10" Background="{DynamicResource ToolControlBackgroundBrush}" CornerRadius="11" BorderBrush="{DynamicResource ToolDividerBrush}" BorderThickness="1">
444 <Grid>
445 <Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
446 <TextBlock Text="通过群号加入" Foreground="{DynamicResource ToolTextPrimaryBrush}" FontSize="10" FontWeight="SemiBold" VerticalAlignment="Center" Margin="0,0,10,0"/>
447 <controls:TextEditor Grid.Column="1" Text="{Binding GroupNumberQuery, UpdateSourceTrigger=PropertyChanged}" Height="32" Padding="8,0" HintText="输入私密群号" KeyDown="GroupNumberBox_KeyDown"/>
448 <Button Grid.Column="2" Content="查找" Style="{StaticResource InlineButtonStyle}" Height="32" Margin="6,0,0,0" Command="{Binding LookupGroupCommand}"/>
449 <ContentControl Grid.ColumnSpan="3" Margin="0,42,0,0" Content="{Binding LookupGroupResult}" ContentTemplate="{StaticResource GroupTemplate}"
450 Visibility="{Binding HasLookupGroupResult, Converter={StaticResource BooleanToVisibilityConverter}}"/>
451 </Grid>
452 </Border>
453 </Grid>
454
455 <Grid Visibility="{Binding IsMessageAreaOpen, Converter={StaticResource BooleanToVisibilityConverter}}">
404 456 <Grid.RowDefinitions><RowDefinition Height="58"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
405 457 <Border Padding="14,0" BorderBrush="{DynamicResource ToolDividerBrush}" BorderThickness="0,0,0,1">
406 458 <Grid>
407 <StackPanel VerticalAlignment="Center">
459 <Button Content="‹" Style="{StaticResource IconButtonStyle}" HorizontalAlignment="Left" Command="{Binding CloseDetailCommand}"
460 Visibility="{Binding ShowBackButton, Converter={StaticResource BooleanToVisibilityConverter}}" ToolTip="返回会话列表"/>
461 <StackPanel VerticalAlignment="Center" Margin="42,0,0,0">
408 462 <TextBlock Text="{Binding ConversationTitle}" Style="{StaticResource HeadingStyle}"/>
409 463 <TextBlock Text="{Binding ConversationSubtitle}" Style="{StaticResource HintStyle}" Margin="0,3,0,0"/>
410 464 </StackPanel>
@@ -412,7 +466,11 @@
412 466 <Button Content="↻" Style="{StaticResource IconButtonStyle}" ToolTip="刷新消息" Command="{Binding RefreshConversationCommand}" IsEnabled="{Binding HasSelectedConversation}"/>
413 467 <Button Content="设置" Style="{StaticResource InlineButtonStyle}" Height="34" Margin="6,0,0,0" ToolTip="群聊管理"
414 468 Command="{Binding OpenGroupManagementCommand}" Visibility="{Binding IsSelectedConversationGroup, Converter={StaticResource BooleanToVisibilityConverter}}"/>
415 <Button Content="☎" Style="{StaticResource IconButtonStyle}" Margin="6,0,0,0" ToolTip="发起语音通话" Command="{Binding StartCallCommand}" IsEnabled="{Binding HasSelectedConversation}"/>
469 <Button Style="{StaticResource IconButtonStyle}" Margin="6,0,0,0" ToolTip="发起语音通话" Command="{Binding StartCallCommand}" IsEnabled="{Binding HasSelectedConversation}">
470 <Viewbox Width="16" Height="16">
471 <Path Fill="{DynamicResource MainColor}" Data="M6.62,10.79 C8.06,13.62 10.38,15.94 13.21,17.38 L15.41,15.18 C15.69,14.9 16.08,14.81 16.43,14.93 C17.55,15.3 18.75,15.5 20,15.5 C20.55,15.5 21,15.95 21,16.5 L21,20 C21,20.55 20.55,21 20,21 C10.61,21 3,13.39 3,4 C3,3.45 3.45,3 4,3 L7.5,3 C8.05,3 8.5,3.45 8.5,4 C8.5,5.25 8.7,6.45 9.07,7.57 C9.18,7.92 9.1,8.31 8.82,8.59 Z"/>
472 </Viewbox>
473 </Button>
416 474 </StackPanel>
417 475 </Grid>
418 476 </Border>
@@ -447,11 +505,12 @@
447 505 <Button Grid.Column="2" Content="发送" Width="62" Height="38" VerticalAlignment="Bottom" controls:ButtonAssist.IsPrimary="True" Command="{Binding SendTextCommand}" IsEnabled="{Binding CanCompose}"/>
448 506 </Grid>
449 507 </Border>
508 </Grid>
450 509 </Grid>
451 510 </Border>
452 511 </Grid>
453 512
454 <Border Grid.Row="2" Margin="0,7,0,0" Padding="10,7" CornerRadius="10" Background="{DynamicResource ToolControlBackgroundBrush}"
513 <Border Grid.Row="1" Margin="0,7,0,0" Padding="10,7" CornerRadius="10" Background="{DynamicResource ToolControlBackgroundBrush}"
455 514 BorderBrush="{DynamicResource ToolDividerBrush}" BorderThickness="1"
456 515 Visibility="{Binding IsTransferring, Converter={StaticResource BooleanToVisibilityConverter}}">
457 516 <Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="160"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
@@ -462,6 +521,45 @@
462 521 </Border>
463 522 </Grid>
464 523
524 <!-- 好友、申请与群邀请统一管理浮层。 -->
525 <Grid Panel.ZIndex="29" Background="#680F0F20" Visibility="{Binding IsContactManagementDialogOpen, Converter={StaticResource BooleanToVisibilityConverter}}">
526 <Border Width="780" MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Page}}" MaxHeight="590" Margin="20" Padding="20"
527 HorizontalAlignment="Center" VerticalAlignment="Center" Style="{StaticResource ChatPaneStyle}">
528 <Grid>
529 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
530 <Grid>
531 <StackPanel><TextBlock Text="好友与邀请" Style="{StaticResource HeadingStyle}" FontSize="17"/><TextBlock Text="管理好友、好友申请和群聊邀请" Style="{StaticResource HintStyle}" Margin="0,3,0,0"/></StackPanel>
532 <Button Content="×" Style="{StaticResource IconButtonStyle}" HorizontalAlignment="Right" Command="{Binding CloseContactManagementDialogCommand}"/>
533 </Grid>
534 <Grid Grid.Row="1" Margin="0,16">
535 <Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="16"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
536 <Grid>
537 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
538 <TextBlock Text="好友" Style="{StaticResource HeadingStyle}" FontSize="12" Margin="2,0,0,7"/>
539 <ListBox Grid.Row="1" ItemsSource="{Binding Friends}" ItemTemplate="{StaticResource FriendTemplate}" ItemContainerStyle="{StaticResource ChatListItemStyle}" ContextMenuOpening="FriendList_ContextMenuOpening">
540 <ListBox.ContextMenu><ContextMenu><MenuItem Header="删除好友" Click="DeleteFriendMenuItem_Click"/></ContextMenu></ListBox.ContextMenu>
541 </ListBox>
542 <TextBlock Grid.Row="2" Text="好友申请" Style="{StaticResource HeadingStyle}" FontSize="12" Margin="2,12,0,7"/>
543 <ListBox Grid.Row="3" ItemsSource="{Binding FriendRequests}" ItemTemplate="{StaticResource FriendRequestTemplate}" ItemContainerStyle="{StaticResource ChatListItemStyle}"/>
544 </Grid>
545 <Grid Grid.Column="2">
546 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
547 <TextBlock Text="添加好友" Style="{StaticResource HeadingStyle}" FontSize="12" Margin="2,0,0,7"/>
548 <Grid Grid.Row="1"><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
549 <controls:TextEditor Text="{Binding UserSearchQuery, UpdateSourceTrigger=PropertyChanged}" Height="34" Padding="8,0" HintText="账号或昵称" KeyDown="UserSearchBox_KeyDown"/>
550 <Button Grid.Column="1" Content="搜索" Style="{StaticResource InlineButtonStyle}" Height="34" Margin="5,0,0,0" Command="{Binding SearchUsersCommand}"/>
551 </Grid>
552 <controls:TextEditor Grid.Row="2" Text="{Binding FriendRequestMessage}" Height="34" Padding="8,0" HintText="申请留言" Margin="0,6,0,6"/>
553 <ListBox Grid.Row="3" ItemsSource="{Binding UserSearchResults}" ItemTemplate="{StaticResource UserSearchTemplate}" ItemContainerStyle="{StaticResource ChatListItemStyle}"/>
554 <TextBlock Grid.Row="4" Text="群聊邀请" Style="{StaticResource HeadingStyle}" FontSize="12" Margin="2,12,0,7"/>
555 <ListBox Grid.Row="5" ItemsSource="{Binding GroupInvitations}" ItemTemplate="{StaticResource InvitationTemplate}" ItemContainerStyle="{StaticResource ChatListItemStyle}"/>
556 </Grid>
557 </Grid>
558 <Button Grid.Row="2" Content="完成" Width="78" Height="35" HorizontalAlignment="Right" Command="{Binding CloseContactManagementDialogCommand}"/>
559 </Grid>
560 </Border>
561 </Grid>
562
465 563 <!-- 创建群聊浮层。 -->
466 564 <Grid Panel.ZIndex="30" Background="#680F0F20" Visibility="{Binding IsCreateGroupDialogOpen, Converter={StaticResource BooleanToVisibilityConverter}}">
467 565 <Border Width="620" MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Page}}" MaxHeight="540" Margin="20" Padding="20"
@@ -504,9 +602,10 @@
504 602 <Grid.ColumnDefinitions><ColumnDefinition Width="240"/><ColumnDefinition Width="16"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
505 603 <StackPanel>
506 604 <TextBlock Text="群名称" Style="{StaticResource HintStyle}"/><controls:TextEditor Text="{Binding ManagedGroupName}" Height="37" Margin="0,5,0,11" IsEnabled="{Binding CanManageSelectedGroup}"/>
507 <TextBlock Text="群简介" Style="{StaticResource HintStyle}"/><controls:TextEditor Text="{Binding ManagedGroupDescription}" Height="96" Margin="0,5,0,11" AcceptsReturn="True" TextWrapping="Wrap" Padding="9,7" IsEnabled="{Binding CanManageSelectedGroup}"/>
508 <CheckBox Content="公开群聊" IsChecked="{Binding ManagedGroupIsPublic}" IsEnabled="{Binding CanManageSelectedGroup}"/>
509 <Button Content="保存群资料" Height="35" Margin="0,14,0,0" controls:ButtonAssist.IsPrimary="True" Command="{Binding UpdateManagedGroupCommand}" IsEnabled="{Binding CanManageSelectedGroup}"/>
605 <TextBlock Text="群简介" Style="{StaticResource HintStyle}"/><controls:TextEditor Text="{Binding ManagedGroupDescription}" Height="96" Margin="0,5,0,11" AcceptsReturn="True" TextWrapping="Wrap" Padding="9,7" IsEnabled="{Binding CanManageSelectedGroup}"/>
606 <CheckBox Content="公开群聊" IsChecked="{Binding ManagedGroupIsPublic}" IsEnabled="{Binding CanManageSelectedGroup}"/>
607 <CheckBox Content="消息免打扰" IsChecked="{Binding ManagedGroupIsMuted}" Margin="0,8,0,0" ToolTip="开启后不再显示该群的新消息 Windows 通知"/>
608 <Button Content="保存群资料" Height="35" Margin="0,14,0,0" controls:ButtonAssist.IsPrimary="True" Command="{Binding UpdateManagedGroupCommand}" IsEnabled="{Binding CanManageSelectedGroup}"/>
510 609 <Button Content="退出群聊" Height="35" Margin="0,8,0,0" Click="LeaveGroupButton_Click"/>
511 610 </StackPanel>
512 611 <Grid Grid.Column="2">
@@ -518,7 +617,7 @@
518 617 <Grid>
519 618 <Grid.ColumnDefinitions><ColumnDefinition Width="36"/><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
520 619 <controls:PersonPicture Width="30" Height="30" DisplayName="{Binding Name}" FontSize="10"/>
521 <StackPanel Grid.Column="1" Margin="7,0" VerticalAlignment="Center"><TextBlock Text="{Binding Name}" Foreground="{DynamicResource ToolTextPrimaryBrush}" FontSize="10.5"/><TextBlock Style="{StaticResource HintStyle}" Margin="0,2,0,0"><Run Text="{Binding AccountText}"/><Run Text=" · "/><Run Text="{Binding RoleText}"/></TextBlock></StackPanel>
620 <StackPanel Grid.Column="1" Margin="7,0" VerticalAlignment="Center"><TextBlock Text="{Binding Name}" Foreground="{DynamicResource ToolTextPrimaryBrush}" FontSize="10.5"/><TextBlock Style="{StaticResource HintStyle}" Margin="0,2,0,0"><Run Text="{Binding AccountText, Mode=OneWay}"/><Run Text=" · "/><Run Text="{Binding RoleText, Mode=OneWay}"/></TextBlock></StackPanel>
522 621 <StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
523 622 <Button Content="{Binding RoleActionText}" Style="{StaticResource InlineButtonStyle}" Margin="0,0,5,0" Command="{Binding DataContext.ToggleManagedGroupMemberRoleCommand, RelativeSource={RelativeSource AncestorType=Page}}" CommandParameter="{Binding}" IsEnabled="{Binding CanBeManaged}"/>
524 623 <Button Content="移除" Style="{StaticResource InlineButtonStyle}" Tag="{Binding}" Click="RemoveGroupMemberButton_Click" IsEnabled="{Binding CanBeManaged}"/>
Modified XFEToolBox/Views/Pages/ChatPage.xaml.cs +26 -12
@@ -31,10 +31,18 @@ public partial class ChatPage : Page
31 31 ChatRealtimeClient.Shared.EnvelopeReceived += ChatRealtimeClient_EnvelopeReceived;
32 32 ViewModel.MessagesChanged += ViewModel_MessagesChanged;
33 33 ViewModel.CallRequested += ViewModel_CallRequested;
34 ViewModel.PropertyChanged += (_, e) =>
35 {
36 if (e.PropertyName is nameof(ChatPageViewModel.IsSinglePaneMode)
37 or nameof(ChatPageViewModel.ShowListPane)
38 or nameof(ChatPageViewModel.ShowDetailPane))
39 UpdateAdaptiveLayout(ActualWidth);
40 };
34 41 }
35 42
36 43 private async void Page_Loaded(object sender, RoutedEventArgs e)
37 44 {
45 ViewModel.RefreshLayoutPreference();
38 46 UpdateAdaptiveLayout(ActualWidth);
39 47 if (!hasLoaded)
40 48 {
@@ -52,16 +60,22 @@ public partial class ChatPage : Page
52 60
53 61 private void UpdateAdaptiveLayout(double width)
54 62 {
55 var compact = width < 760;
56 SectionColumn.Width = compact ? new GridLength(0) : new GridLength(132);
57 SectionPane.Visibility = compact ? Visibility.Collapsed : Visibility.Visible;
58 CompactTabs.Visibility = compact ? Visibility.Visible : Visibility.Collapsed;
59 CompactTabsRow.Height = compact ? GridLength.Auto : new GridLength(0);
63 SectionColumn.Width = new GridLength(0);
64 SectionPane.Visibility = Visibility.Collapsed;
65 CompactTabs.Visibility = Visibility.Collapsed;
66 CompactTabsRow.Height = new GridLength(0);
67
68 if (ViewModel.IsSinglePaneMode)
69 {
70 ListColumn.Width = ViewModel.ShowListPane ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
71 DetailGapColumn.Width = new GridLength(0);
72 DetailColumn.Width = ViewModel.ShowDetailPane ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
73 return;
74 }
60 75
61 var listWidth = compact
62 ? Math.Clamp(width * 0.38, 205, 270)
63 : Math.Clamp(width * 0.30, 250, 300);
64 ListColumn.Width = new GridLength(listWidth);
76 ListColumn.Width = new GridLength(Math.Clamp(width * 0.31, 260, 330));
77 DetailGapColumn.Width = new GridLength(8);
78 DetailColumn.Width = new GridLength(1, GridUnitType.Star);
65 79 }
66 80
67 81 private async void ClientSession_SessionChanged(object? sender, EventArgs e)
@@ -110,11 +124,11 @@ public partial class ChatPage : Page
110 124
111 125 private async void ConversationList_SelectionChanged(object sender, SelectionChangedEventArgs e)
112 126 {
113 if (sender is not ListBox { SelectedItem: ChatConversationItem item } ||
114 ReferenceEquals(item, ViewModel.SelectedConversation))
127 if (sender is not ListBox { SelectedItem: ChatNavigationItem item } ||
128 ReferenceEquals(item, ViewModel.SelectedNavigationItem))
115 129 return;
116 130
117 await ViewModel.OpenConversationCommand.ExecuteAsync(item);
131 await ViewModel.OpenNavigationItemCommand.ExecuteAsync(item);
118 132 }
119 133
120 134 private async void GroupSearchBox_KeyDown(object sender, KeyEventArgs e)
Modified XFEToolBox/Views/Pages/SettingPage.xaml +6 -0
Modified XFEToolBox/XFEToolBox.Client.csproj +1 -1