using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using Microsoft.Win32;
using XFEToolBox.Client.Model;
using XFEToolBox.Client.Models.Chat;
using XFEToolBox.Client.Utilities;
using XFEToolBox.Client.Utilities.Chat;
using XFEToolBox.Client.Utilities.Server;
using XFEToolBox.Client.ViewModel.Chat;
using XFEToolBox.Client.Views.Pages.Popups;
using XFEToolBox.Core.Chat;
namespace XFEToolBox.Client.Views.Pages;
public partial class ChatPage : Page
{
public static ChatPage Current { get; } = new();
public ChatPageViewModel ViewModel { get; } = new();
private bool hasLoaded;
public ChatPage()
{
InitializeComponent();
DataContext = ViewModel;
ClientSession.SessionChanged += ClientSession_SessionChanged;
ChatRealtimeClient.Shared.EnvelopeReceived += ChatRealtimeClient_EnvelopeReceived;
ViewModel.MessagesChanged += ViewModel_MessagesChanged;
ViewModel.CallRequested += ViewModel_CallRequested;
ViewModel.PropertyChanged += (_, e) =>
{
if (e.PropertyName is nameof(ChatPageViewModel.IsSinglePaneMode)
or nameof(ChatPageViewModel.ShowListPane)
or nameof(ChatPageViewModel.ShowDetailPane))
UpdateAdaptiveLayout(ActualWidth);
};
}
private async void Page_Loaded(object sender, RoutedEventArgs e)
{
ViewModel.RefreshLayoutPreference();
UpdateAdaptiveLayout(ActualWidth);
if (!hasLoaded)
{
hasLoaded = true;
await ViewModel.InitializeAsync();
}
else if (ClientSession.IsLoggedIn)
{
await ViewModel.RefreshAllCommand.ExecuteAsync(null);
}
if (ClientSession.IsLoggedIn) await VoiceCallService.Current.StartAsync();
}
private void Page_SizeChanged(object sender, SizeChangedEventArgs e) => UpdateAdaptiveLayout(e.NewSize.Width);
private void UpdateAdaptiveLayout(double width)
{
SectionColumn.Width = new GridLength(0);
SectionPane.Visibility = Visibility.Collapsed;
CompactTabs.Visibility = Visibility.Collapsed;
CompactTabsRow.Height = new GridLength(0);
if (ViewModel.IsSinglePaneMode)
{
ListColumn.Width = ViewModel.ShowListPane ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
DetailGapColumn.Width = new GridLength(0);
DetailColumn.Width = ViewModel.ShowDetailPane ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
return;
}
ListColumn.Width = new GridLength(Math.Clamp(width * 0.31, 260, 330));
DetailGapColumn.Width = new GridLength(8);
DetailColumn.Width = new GridLength(1, GridUnitType.Star);
}
private async void ClientSession_SessionChanged(object? sender, EventArgs e)
{
if (Dispatcher.CheckAccess())
{
await ViewModel.OnSessionChangedAsync();
return;
}
var operation = Dispatcher.InvokeAsync(ViewModel.OnSessionChangedAsync);
await await operation;
}
private void ViewModel_MessagesChanged(object? sender, bool scrollToEnd)
{
if (!scrollToEnd) return;
_ = Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
if (MessageList.Items.Count > 0) MessageList.ScrollIntoView(MessageList.Items[^1]);
}));
}
private async void ChatRealtimeClient_EnvelopeReceived(object? sender, ChatRealtimeEnvelopeEventArgs e)
{
if (Dispatcher.CheckAccess())
{
await ViewModel.HandleRealtimeEnvelopeAsync(e.Envelope);
return;
}
var operation = Dispatcher.InvokeAsync(() => ViewModel.HandleRealtimeEnvelopeAsync(e.Envelope));
await await operation;
}
private void OpenLoginButton_Click(object sender, RoutedEventArgs e)
{
PopupHelper.ShowDialog(new LoginPopupPage(), new PopupWindowOptions
{
Title = "登录 / 注册",
Subtitle = "登录后使用好友、群聊和通话功能",
Width = 420,
Height = 460,
Owner = Window.GetWindow(this),
ContentMargin = new Thickness(0)
});
}
private async void ConversationList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListBox { SelectedItem: ChatNavigationItem item } ||
ReferenceEquals(item, ViewModel.SelectedNavigationItem))
return;
await ViewModel.OpenNavigationItemCommand.ExecuteAsync(item);
}
private async void GroupSearchBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;
e.Handled = true;
await ViewModel.RefreshRecommendedGroupsCommand.ExecuteAsync(null);
}
private async void GroupNumberBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;
e.Handled = true;
await ViewModel.LookupGroupCommand.ExecuteAsync(null);
}
private async void UserSearchBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;
e.Handled = true;
await ViewModel.SearchUsersCommand.ExecuteAsync(null);
}
private async void ComposerBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter || Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) return;
e.Handled = true;
await ViewModel.SendTextCommand.ExecuteAsync(null);
}
private async void AttachFileButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
Title = "选择要发送的图片、视频或文件",
Filter = "所有支持的文件|*.png;*.jpg;*.jpeg;*.gif;*.webp;*.bmp;*.mp4;*.webm;*.mov;*.avi;*.mkv;*.pdf;*.zip;*.txt;*.md;*.*|图片|*.png;*.jpg;*.jpeg;*.gif;*.webp;*.bmp|视频|*.mp4;*.webm;*.mov;*.avi;*.mkv|所有文件|*.*",
Multiselect = true,
CheckFileExists = true
};
if (dialog.ShowDialog(Window.GetWindow(this)) != true) return;
foreach (var path in dialog.FileNames)
await ViewModel.SendFileAsync(path);
}
private async void DownloadAttachment_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: ChatAttachmentInfo attachment }) return;
var extension = Path.GetExtension(attachment.FileName);
var dialog = new SaveFileDialog
{
Title = "保存聊天文件",
FileName = Path.GetFileName(attachment.FileName),
DefaultExt = extension,
AddExtension = true,
OverwritePrompt = true,
Filter = string.IsNullOrWhiteSpace(extension)
? "所有文件|*.*"
: $"{extension.TrimStart('.').ToUpperInvariant()} 文件|*{extension}|所有文件|*.*"
};
if (dialog.ShowDialog(Window.GetWindow(this)) == true)
await ViewModel.DownloadAttachmentAsync(attachment, dialog.FileName);
}
private void FriendList_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (sender is ListBox list && list.SelectedItem is null) e.Handled = true;
}
private async void DeleteFriendMenuItem_Click(object sender, RoutedEventArgs e)
{
if (sender is not MenuItem menuItem ||
menuItem.Parent is not ContextMenu { PlacementTarget: ListBox { SelectedItem: ChatFriendItem friend } }) return;
var result = PopupHelper.ShowConfirmDialog(
$"确定删除好友“{friend.Name}”吗?历史消息仍会由服务器按策略保留。",
showCancelButton: true,
confirmText: "删除好友");
if (result == MessageBoxResult.OK) await ViewModel.DeleteFriendAsync(friend);
}
private async void LeaveGroupButton_Click(object sender, RoutedEventArgs e)
{
var group = ViewModel.ManagedGroup;
if (group is null) return;
var result = PopupHelper.ShowConfirmDialog(
$"确定退出群聊“{group.Name}”吗?退出后需要重新加入才能继续收发消息。",
showCancelButton: true,
confirmText: "退出群聊");
if (result == MessageBoxResult.OK) await ViewModel.LeaveManagedGroupAsync();
}
private async void RemoveGroupMemberButton_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: ChatGroupMemberItem member }) return;
var result = PopupHelper.ShowConfirmDialog(
$"确定将“{member.Name}”移出群聊吗?",
showCancelButton: true,
confirmText: "移出群聊");
if (result == MessageBoxResult.OK) await ViewModel.RemoveManagedGroupMemberAsync(member);
}
private async void TransferGroupOwnerButton_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: ChatGroupMemberItem member }) return;
var result = PopupHelper.ShowConfirmDialog(
$"确定将群主转让给“{member.Name}”吗?转让后你将失去群主权限。",
showCancelButton: true,
confirmText: "转让群主");
if (result == MessageBoxResult.OK) await ViewModel.TransferManagedGroupOwnershipAsync(member);
}
private async void ViewModel_CallRequested(object? sender, ChatConversationInfo conversation)
{
try
{
if (conversation.Kind == ChatConversationKind.Group)
{
var groupId = conversation.Group?.Id;
if (string.IsNullOrWhiteSpace(groupId)) throw new InvalidOperationException("群聊信息不完整。");
await VoiceCallService.Current.StartGroupCallAsync(groupId, conversation.Id);
}
else
{
var friendUserId = conversation.Friend?.Id;
if (string.IsNullOrWhiteSpace(friendUserId)) throw new InvalidOperationException("好友信息不完整。");
await VoiceCallService.Current.StartDirectCallAsync(friendUserId, conversation.Id);
}
}
catch (Exception exception)
{
PopupHelper.ShowConfirmDialog($"无法启动语音通话:{exception.GetBaseException().Message}", confirmText: "知道了");
}
}
}
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using Microsoft.Win32;
using XFEToolBox.Client.Model;
using XFEToolBox.Client.Models.Chat;
using XFEToolBox.Client.Utilities;
using XFEToolBox.Client.Utilities.Chat;
using XFEToolBox.Client.Utilities.Server;
using XFEToolBox.Client.ViewModel.Chat;
using XFEToolBox.Client.Views.Pages.Popups;
using XFEToolBox.Core.Chat;
namespace XFEToolBox.Client.Views.Pages;
public partial class ChatPage : Page
{
public static ChatPage Current { get; } = new();
public ChatPageViewModel ViewModel { get; } = new();
private bool hasLoaded;
public ChatPage()
{
InitializeComponent();
DataContext = ViewModel;
ClientSession.SessionChanged += ClientSession_SessionChanged;
ChatRealtimeClient.Shared.EnvelopeReceived += ChatRealtimeClient_EnvelopeReceived;
ViewModel.MessagesChanged += ViewModel_MessagesChanged;
ViewModel.CallRequested += ViewModel_CallRequested;
ViewModel.PropertyChanged += (_, e) =>
{
if (e.PropertyName is nameof(ChatPageViewModel.IsSinglePaneMode)
or nameof(ChatPageViewModel.ShowListPane)
or nameof(ChatPageViewModel.ShowDetailPane))
UpdateAdaptiveLayout(ActualWidth);
};
}
private async void Page_Loaded(object sender, RoutedEventArgs e)
{
ViewModel.RefreshLayoutPreference();
UpdateAdaptiveLayout(ActualWidth);
if (!hasLoaded)
{
hasLoaded = true;
await ViewModel.InitializeAsync();
}
else if (ClientSession.IsLoggedIn)
{
await ViewModel.RefreshAllCommand.ExecuteAsync(null);
}
if (ClientSession.IsLoggedIn) await VoiceCallService.Current.StartAsync();
}
private void Page_SizeChanged(object sender, SizeChangedEventArgs e) => UpdateAdaptiveLayout(e.NewSize.Width);
private void UpdateAdaptiveLayout(double width)
{
SectionColumn.Width = new GridLength(0);
SectionPane.Visibility = Visibility.Collapsed;
CompactTabs.Visibility = Visibility.Collapsed;
CompactTabsRow.Height = new GridLength(0);
if (ViewModel.IsSinglePaneMode)
{
ListColumn.Width = ViewModel.ShowListPane ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
DetailGapColumn.Width = new GridLength(0);
DetailColumn.Width = ViewModel.ShowDetailPane ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
return;
}
ListColumn.Width = new GridLength(Math.Clamp(width * 0.31, 260, 330));
DetailGapColumn.Width = new GridLength(8);
DetailColumn.Width = new GridLength(1, GridUnitType.Star);
}
private async void ClientSession_SessionChanged(object? sender, EventArgs e)
{
if (Dispatcher.CheckAccess())
{
await ViewModel.OnSessionChangedAsync();
return;
}
var operation = Dispatcher.InvokeAsync(ViewModel.OnSessionChangedAsync);
await await operation;
}
private void ViewModel_MessagesChanged(object? sender, bool scrollToEnd)
{
if (!scrollToEnd) return;
_ = Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
if (MessageList.Items.Count > 0) MessageList.ScrollIntoView(MessageList.Items[^1]);
}));
}
private async void ChatRealtimeClient_EnvelopeReceived(object? sender, ChatRealtimeEnvelopeEventArgs e)
{
if (Dispatcher.CheckAccess())
{
await ViewModel.HandleRealtimeEnvelopeAsync(e.Envelope);
return;
}
var operation = Dispatcher.InvokeAsync(() => ViewModel.HandleRealtimeEnvelopeAsync(e.Envelope));
await await operation;
}
private void OpenLoginButton_Click(object sender, RoutedEventArgs e)
{
PopupHelper.ShowDialog(new LoginPopupPage(), new PopupWindowOptions
{
Title = "登录 / 注册",
Subtitle = "登录后使用好友、群聊和通话功能",
Width = 420,
Height = 460,
Owner = Window.GetWindow(this),
ContentMargin = new Thickness(0)
});
}
private async void ConversationList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListBox { SelectedItem: ChatNavigationItem item } ||
ReferenceEquals(item, ViewModel.SelectedNavigationItem))
return;
await ViewModel.OpenNavigationItemCommand.ExecuteAsync(item);
}
private async void GroupSearchBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;
e.Handled = true;
await ViewModel.RefreshRecommendedGroupsCommand.ExecuteAsync(null);
}
private async void GroupNumberBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;
e.Handled = true;
await ViewModel.LookupGroupCommand.ExecuteAsync(null);
}
private async void UserSearchBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;
e.Handled = true;
await ViewModel.SearchUsersCommand.ExecuteAsync(null);
}
private async void ComposerBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter || Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) return;
e.Handled = true;
await ViewModel.SendTextCommand.ExecuteAsync(null);
}
private async void AttachFileButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
Title = "选择要发送的图片、视频或文件",
Filter = "所有支持的文件|*.png;*.jpg;*.jpeg;*.gif;*.webp;*.bmp;*.mp4;*.webm;*.mov;*.avi;*.mkv;*.pdf;*.zip;*.txt;*.md;*.*|图片|*.png;*.jpg;*.jpeg;*.gif;*.webp;*.bmp|视频|*.mp4;*.webm;*.mov;*.avi;*.mkv|所有文件|*.*",
Multiselect = true,
CheckFileExists = true
};
if (dialog.ShowDialog(Window.GetWindow(this)) != true) return;
foreach (var path in dialog.FileNames)
await ViewModel.SendFileAsync(path);
}
private async void DownloadAttachment_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: ChatAttachmentInfo attachment }) return;
var extension = Path.GetExtension(attachment.FileName);
var dialog = new SaveFileDialog
{
Title = "保存聊天文件",
FileName = Path.GetFileName(attachment.FileName),
DefaultExt = extension,
AddExtension = true,
OverwritePrompt = true,
Filter = string.IsNullOrWhiteSpace(extension)
? "所有文件|*.*"
: $"{extension.TrimStart('.').ToUpperInvariant()} 文件|*{extension}|所有文件|*.*"
};
if (dialog.ShowDialog(Window.GetWindow(this)) == true)
await ViewModel.DownloadAttachmentAsync(attachment, dialog.FileName);
}
private void FriendList_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (sender is ListBox list && list.SelectedItem is null) e.Handled = true;
}
private async void DeleteFriendMenuItem_Click(object sender, RoutedEventArgs e)
{
if (sender is not MenuItem menuItem ||
menuItem.Parent is not ContextMenu { PlacementTarget: ListBox { SelectedItem: ChatFriendItem friend } }) return;
var result = PopupHelper.ShowConfirmDialog(
$"确定删除好友“{friend.Name}”吗?历史消息仍会由服务器按策略保留。",
showCancelButton: true,
confirmText: "删除好友");
if (result == MessageBoxResult.OK) await ViewModel.DeleteFriendAsync(friend);
}
private async void LeaveGroupButton_Click(object sender, RoutedEventArgs e)
{
var group = ViewModel.ManagedGroup;
if (group is null) return;
var result = PopupHelper.ShowConfirmDialog(
$"确定退出群聊“{group.Name}”吗?退出后需要重新加入才能继续收发消息。",
showCancelButton: true,
confirmText: "退出群聊");
if (result == MessageBoxResult.OK) await ViewModel.LeaveManagedGroupAsync();
}
private async void RemoveGroupMemberButton_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: ChatGroupMemberItem member }) return;
var result = PopupHelper.ShowConfirmDialog(
$"确定将“{member.Name}”移出群聊吗?",
showCancelButton: true,
confirmText: "移出群聊");
if (result == MessageBoxResult.OK) await ViewModel.RemoveManagedGroupMemberAsync(member);
}
private async void TransferGroupOwnerButton_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: ChatGroupMemberItem member }) return;
var result = PopupHelper.ShowConfirmDialog(
$"确定将群主转让给“{member.Name}”吗?转让后你将失去群主权限。",
showCancelButton: true,
confirmText: "转让群主");
if (result == MessageBoxResult.OK) await ViewModel.TransferManagedGroupOwnershipAsync(member);
}
private async void ViewModel_CallRequested(object? sender, ChatConversationInfo conversation)
{
try
{
if (conversation.Kind == ChatConversationKind.Group)
{
var groupId = conversation.Group?.Id;
if (string.IsNullOrWhiteSpace(groupId)) throw new InvalidOperationException("群聊信息不完整。");
await VoiceCallService.Current.StartGroupCallAsync(groupId, conversation.Id);
}
else
{
var friendUserId = conversation.Friend?.Id;
if (string.IsNullOrWhiteSpace(friendUserId)) throw new InvalidOperationException("好友信息不完整。");
await VoiceCallService.Current.StartDirectCallAsync(friendUserId, conversation.Id);
}
}
catch (Exception exception)
{
PopupHelper.ShowConfirmDialog($"无法启动语音通话:{exception.GetBaseException().Message}", confirmText: "知道了");
}
}
}