返回提交历史
Added
XFEToolBox.Client.Wpf.Test/ChatNavigationTests.cs
+63
-0
Added
XFEToolBox.Client.Wpf.Test/ChatPageXamlTests.cs
+72
-0
Modified
XFEToolBox/App.xaml.cs
+6
-0
Modified
XFEToolBox/Models/Chat/ChatUiModels.cs
+62
-0
Modified
XFEToolBox/Profiles/CrossVersionProfiles/SystemProfile.cs
+10
-0
Added
XFEToolBox/Utilities/Chat/ChatDesktopNotificationCoordinator.cs
+137
-0
Added
XFEToolBox/Utilities/Chat/ChatNotificationPreferences.cs
+45
-0
Added
XFEToolBox/Utilities/DesktopNotificationService.cs
+25
-0
Modified
XFEToolBox/Utilities/TrayIconService.cs
+12
-0
Modified
XFEToolBox/ViewModel/Chat/ChatPageViewModel.cs
+166
-9
Modified
XFEToolBox/Views/Pages/ChatPage.xaml
+128
-29
Modified
XFEToolBox/Views/Pages/ChatPage.xaml.cs
+26
-12
Modified
XFEToolBox/Views/Pages/SettingPage.xaml
+6
-0
Modified
XFEToolBox/XFEToolBox.Client.csproj
+1
-1
XFEstudio/XFEToolBox
重构聊天模块:单栏模式、统一导航、通知优化
本次提交带来以下主要改进: - 新增“聊天单栏模式”,适配小屏/移动端体验,支持会话/联系人列表与消息详情分离展示。 - 会话列表重构为统一导航列表,整合好友、群聊、最近会话,支持未读数、免打扰、最后消息等信息,顶部新增“群聊大厅”入口。 - 支持群聊“消息免打扰”设置,配置持久化,免打扰群聊不再弹出 Windows 通知。 - 新增 `ChatDesktopNotificationCoordinator`,统一管理聊天消息、好友申请、群聊邀请等 Windows 通知,支持免打扰过滤。 - 移除 InfoBar 状态栏,所有状态提示统一通过 Windows 通知弹窗展示。 - 聊天页面 UI 结构优化,入口合并、弹窗统一管理、语音通话按钮更换为 Path 图标,文本绑定声明为 OneWay。 - 新增多项回归测试,确保统一导航、免打扰、通知、布局切换等功能的正确性。 - 辅助类如 `DesktopNotificationService`、`ChatNotificationPreferences` 实现通知和免打扰配置统一管理。 整体提升聊天模块的易用性、可维护性和多端适配能力。
89e8515
代码差异
14 个文件
+759
-51
@@ -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
}
@@ -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
}
@@ -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);
@@ -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;
@@ -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
/// 工具箱现在是否可以被关闭
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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;
@@ -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)
@@ -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}"/>
@@ -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)