mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 04:22:04 -04:00
Initial commit
This commit is contained in:
499
KfChatDotNetGui/Views/MainWindow.axaml.cs
Normal file
499
KfChatDotNetGui/Views/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,499 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using KfChatDotNetGui.Models;
|
||||
using KfChatDotNetGui.ViewModels;
|
||||
using KfChatDotNetWsClient;
|
||||
using KfChatDotNetWsClient.Models;
|
||||
using KfChatDotNetWsClient.Models.Events;
|
||||
using KfChatDotNetWsClient.Models.Json;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using Websocket.Client;
|
||||
|
||||
namespace KfChatDotNetGui.Views
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
// Using an empty config as we can update it later through the UpdateConfig method
|
||||
// Having this instance created early is handy for wiring up the events
|
||||
private ChatClient _chatClient = new(new ChatClientConfigModel());
|
||||
private SettingsModel _settings;
|
||||
private int _currentRoom;
|
||||
private ForumIdentityModel _forumIdentity;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_chatClient.OnMessages += OnMessages;
|
||||
_chatClient.OnUsersJoined += OnUsersJoined;
|
||||
_chatClient.OnUsersParted += OnUsersParted;
|
||||
_chatClient.OnWsReconnect += OnOnWsReconnect;
|
||||
_chatClient.OnWsDisconnection += OnOnWsDisconnection;
|
||||
_chatClient.OnDeleteMessages += OnDeleteMessages;
|
||||
_chatClient.OnFailedToJoinRoom += OnFailedToJoinRoom;
|
||||
}
|
||||
|
||||
private void OnFailedToJoinRoom(object sender, string message)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
UpdateStatus($"Failed to join room, room ID {_currentRoom} is probably invalid");
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDeleteMessages(object sender, List<int> messageIds)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
_logger.Info($"Received delete event for following message IDs: {string.Join(',', messageIds)}");
|
||||
// Gotta make a copy of all the messages (annoyingly) as we'll be deleting stuff and .NET has a very obvious limitation there
|
||||
var messages = (DataContext as MainWindowViewModel).Messages.ToList();
|
||||
foreach (var message in messages)
|
||||
{
|
||||
foreach (var innerMessage in message.Messages.Where(m => messageIds.Contains(m.MessageId)))
|
||||
{
|
||||
// Remove the parent message thingy as otherwise it just shows as a blank item
|
||||
if (message.Messages.Count == 1)
|
||||
{
|
||||
_logger.Info("Removing parent message box");
|
||||
(DataContext as MainWindowViewModel).Messages.Remove(message);
|
||||
}
|
||||
// Go scavenging if there are multiple messages and we don't want to lose the lot
|
||||
else
|
||||
{
|
||||
(DataContext as MainWindowViewModel)
|
||||
.Messages[(DataContext as MainWindowViewModel).Messages.IndexOf(message)].Messages
|
||||
.Remove(innerMessage);
|
||||
}
|
||||
|
||||
_logger.Info($"Removed {innerMessage.MessageId}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnOnWsDisconnection(object sender, DisconnectionInfo disconnectionInfo)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
UpdateStatus($"Disconnected from SneedChat due to {disconnectionInfo.Type}. Client should automatically attempt to reconnect");
|
||||
});
|
||||
}
|
||||
|
||||
private void OnOnWsReconnect(object sender, ReconnectionInfo reconnectionInfo)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
UpdateStatus("Reconnected to SneedChat. Reason was " + reconnectionInfo.Type);
|
||||
(DataContext as MainWindowViewModel).Messages.Clear();
|
||||
(DataContext as MainWindowViewModel).UserList.Clear();
|
||||
});
|
||||
|
||||
_chatClient.JoinRoom(_currentRoom);
|
||||
}
|
||||
|
||||
private void IdentitySettings_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var context = new IdentitySettingsWindowViewModel();
|
||||
if (File.Exists("settings.json"))
|
||||
{
|
||||
var settings = JsonConvert.DeserializeObject<SettingsModel>(File.ReadAllText("settings.json"));
|
||||
context.WsUri = settings.WsUri;
|
||||
context.XfSessionToken = settings.XfSessionToken;
|
||||
context.ReconnectTimeout = settings.ReconnectTimeout;
|
||||
context.AntiDdosPow = settings.AntiDdosPow;
|
||||
context.Username = settings.Username;
|
||||
}
|
||||
var identitySettingsWindow = new IdentitySettingsWindow
|
||||
{
|
||||
DataContext = context
|
||||
};
|
||||
identitySettingsWindow.ShowDialog(this);
|
||||
identitySettingsWindow.Closed += (o, args) =>
|
||||
{
|
||||
ReloadSettings();
|
||||
};
|
||||
}
|
||||
|
||||
private void ExitMenuItem_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
private async Task ConnectToSneedChat()
|
||||
{
|
||||
UpdateStatus("Connecting to SneedChat");
|
||||
if (!File.Exists("settings.json"))
|
||||
{
|
||||
_logger.Error("Cannot find settings.json and therefore unable to connect to SneedChat, notifying the user through the status bar");
|
||||
UpdateStatus("Unable to connect as client has not been configured (settings.json missing)");
|
||||
return;
|
||||
}
|
||||
ReloadSettings();
|
||||
UpdateStatus("Testing XenForo token validity");
|
||||
ForumIdentityModel forumIdentity;
|
||||
if (string.IsNullOrEmpty(_settings.Username))
|
||||
{
|
||||
try
|
||||
{
|
||||
forumIdentity = await Helpers.ForumIdentity.GetForumIdentity(_settings.XfSessionToken,
|
||||
new Uri($"https://{_settings.WsUri.Host}/test-chat"), _settings.AntiDdosPow);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex);
|
||||
UpdateStatus("Failed to test XenForo token, caught exception " + ex.Message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
forumIdentity = new ForumIdentityModel
|
||||
{
|
||||
Username = _settings.Username,
|
||||
Id = int.MaxValue
|
||||
};
|
||||
}
|
||||
|
||||
if (forumIdentity == null)
|
||||
{
|
||||
UpdateStatus("Failed to deserialize account info on SneedChat page");
|
||||
return;
|
||||
}
|
||||
|
||||
if (forumIdentity.Id == 0)
|
||||
{
|
||||
UpdateStatus("Token failed, SneedChat page returned Guest");
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateStatus("Token works! It belongs to " + forumIdentity.Username);
|
||||
_forumIdentity = forumIdentity;
|
||||
(DataContext as MainWindowViewModel).UserId = _forumIdentity.Id;
|
||||
var roomListControl = this.FindControl<ListBox>("RoomList");
|
||||
RoomSettingsModel.RoomList initialRoom;
|
||||
if (roomListControl.SelectedItem == null)
|
||||
{
|
||||
initialRoom = (DataContext as MainWindowViewModel).RoomList.First();
|
||||
}
|
||||
else
|
||||
{
|
||||
initialRoom = (RoomSettingsModel.RoomList) roomListControl.SelectedItem;
|
||||
}
|
||||
|
||||
_chatClient.UpdateConfig(new ChatClientConfigModel
|
||||
{
|
||||
CookieDomain = _settings.WsUri.Host,
|
||||
ReconnectTimeout = _settings.ReconnectTimeout,
|
||||
WsUri = _settings.WsUri,
|
||||
XfSessionToken = _settings.XfSessionToken
|
||||
});
|
||||
|
||||
await _chatClient.StartWsClient();
|
||||
_chatClient.JoinRoom(initialRoom.Id);
|
||||
_currentRoom = initialRoom.Id;
|
||||
UpdateStatus("Connected!");
|
||||
}
|
||||
|
||||
private void OnUsersJoined(object sender, List<UserModel> users, UsersJsonModel jsonPayload)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
foreach (var user in users)
|
||||
{
|
||||
if ((DataContext as MainWindowViewModel).UserList.FirstOrDefault(x => x.Id == user.Id) != null)
|
||||
{
|
||||
_logger.Info($"{user.Username} ({user.Id}) is already in the list but has joined again. New tab? Ignoring!");
|
||||
continue;
|
||||
}
|
||||
(DataContext as MainWindowViewModel).UserList.Add(new MainWindowViewModel.UserListViewModel
|
||||
{
|
||||
Id = user.Id,
|
||||
Name = user.Username
|
||||
});
|
||||
}
|
||||
UpdateUserTotalStatus();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnUsersParted(object sender, List<int> userIds)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
foreach (var id in userIds)
|
||||
{
|
||||
var row = (DataContext as MainWindowViewModel).UserList.FirstOrDefault(x => x.Id == id);
|
||||
if (row == null)
|
||||
{
|
||||
_logger.Info($"A user ({id}) who isn't in the list has parted, ignoring!");
|
||||
continue;
|
||||
}
|
||||
(DataContext as MainWindowViewModel).UserList.Remove(row);
|
||||
}
|
||||
UpdateUserTotalStatus();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnMessages(object sender, List<MessageModel> messages, MessagesJsonModel jsonPayload)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
var previousMessage = (DataContext as MainWindowViewModel).Messages.LastOrDefault();
|
||||
if (previousMessage == null)
|
||||
{
|
||||
previousMessage = new MainWindowViewModel.MessageViewModel {AuthorId = -1};
|
||||
}
|
||||
foreach (var message in messages)
|
||||
{
|
||||
_logger.Info("Received message, data payload next");
|
||||
_logger.Info(JsonConvert.SerializeObject(message, Formatting.Indented));
|
||||
if (message.RoomId != _currentRoom)
|
||||
{
|
||||
_logger.Info($"Message {message.MessageId} belongs to another room (we're in {_currentRoom}, this one was for {message.RoomId}), ignoring.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.MessageEditDate != null)
|
||||
{
|
||||
_logger.Info("Received an edit. Going to rewrite message if it already exists, " +
|
||||
"if it doesn't, nothing will happen as this would occur when loading historically modified messages.");
|
||||
foreach (var msg in (DataContext as MainWindowViewModel).Messages)
|
||||
{
|
||||
foreach (var innerMsg in msg.Messages.Where(m => m.MessageId == message.MessageId))
|
||||
{
|
||||
innerMsg.Message = WebUtility.HtmlDecode(message.MessageRaw);
|
||||
_logger.Info("Found the original message, text has been overwritten");
|
||||
return;
|
||||
}
|
||||
}
|
||||
_logger.Info("Never ended up finding the original message so this was probably historical");
|
||||
}
|
||||
|
||||
if (previousMessage.AuthorId == message.Author.Id)
|
||||
{
|
||||
_logger.Info("Found a message from the same author, merging");
|
||||
var lastMessage = (DataContext as MainWindowViewModel).Messages.Last();
|
||||
lastMessage.Messages.Add(new MainWindowViewModel.InnerMessageViewModel
|
||||
{
|
||||
Message = WebUtility.HtmlDecode(message.MessageRaw),
|
||||
MessageId = message.MessageId,
|
||||
OwnMessage = _forumIdentity.Username == message.Author.Username
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var viewMessage = new MainWindowViewModel.MessageViewModel
|
||||
{
|
||||
Author = message.Author.Username,
|
||||
Messages = new ObservableCollection<MainWindowViewModel.InnerMessageViewModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Message = WebUtility.HtmlDecode(message.MessageRaw),
|
||||
MessageId = message.MessageId,
|
||||
OwnMessage = _forumIdentity.Username == message.Author.Username
|
||||
}
|
||||
},
|
||||
PostedAt = message.MessageDate.LocalDateTime,
|
||||
AuthorId = message.Author.Id
|
||||
};
|
||||
(DataContext as MainWindowViewModel).Messages.Add(viewMessage);
|
||||
previousMessage = viewMessage;
|
||||
}
|
||||
var messagesControl = this.FindControl<ListBox>("ChatMessageList");
|
||||
messagesControl.ScrollIntoView((DataContext as MainWindowViewModel).Messages.Last());
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateStatus(string newStatus)
|
||||
{
|
||||
(DataContext as MainWindowViewModel)!.Status = newStatus;
|
||||
}
|
||||
|
||||
private void ConnectMenuItem_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => ConnectToSneedChat(), DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private void RoomSettingsMenuItem_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var context = new RoomSettingsWindowViewModel();
|
||||
if (File.Exists("rooms.json"))
|
||||
{
|
||||
var settings = JsonConvert.DeserializeObject<RoomSettingsModel>(File.ReadAllText("rooms.json"));
|
||||
context.RoomList.Clear();
|
||||
foreach (var room in settings.Rooms)
|
||||
{
|
||||
context.RoomList.Add(new RoomSettingsModel.RoomList
|
||||
{
|
||||
Id = room.Id,
|
||||
Name = room.Name
|
||||
});
|
||||
}
|
||||
}
|
||||
var roomSettingsWindow = new RoomSettingsWindow
|
||||
{
|
||||
DataContext = context
|
||||
};
|
||||
roomSettingsWindow.ShowDialog(this);
|
||||
roomSettingsWindow.Closed += (o, args) =>
|
||||
{
|
||||
ReloadRoomList();
|
||||
};
|
||||
}
|
||||
|
||||
private void ReloadSettings()
|
||||
{
|
||||
if (!File.Exists("settings.json"))
|
||||
{
|
||||
_logger.Error("Was asked to reload the settings but settings.json doesn't exist so I won't bother");
|
||||
return;
|
||||
}
|
||||
var settings = JsonConvert.DeserializeObject<SettingsModel>(File.ReadAllText("settings.json"));
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
private void ReloadRoomList()
|
||||
{
|
||||
if (!File.Exists("rooms.json"))
|
||||
{
|
||||
_logger.Error("Was asked to reload the room list but rooms.json doesn't exist so I won't bother");
|
||||
return;
|
||||
}
|
||||
var rooms = JsonConvert.DeserializeObject<RoomSettingsModel>(File.ReadAllText("rooms.json"));
|
||||
(DataContext as MainWindowViewModel)!.RoomList = rooms!.Rooms;
|
||||
}
|
||||
|
||||
private void RoomList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (!_chatClient.IsConnected())
|
||||
{
|
||||
UpdateStatus("Cannot join room as client is not connected");
|
||||
return;
|
||||
}
|
||||
var roomListControl = this.FindControl<ListBox>("RoomList");
|
||||
var room = (RoomSettingsModel.RoomList) roomListControl.SelectedItem!;
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
if (room == null)
|
||||
{
|
||||
_logger.Info("Got a selection change on room list with a null selected item. This seems to just happen sometimes, ignoring.");
|
||||
return;
|
||||
}
|
||||
UpdateStatus($"Connected! Changing to {room.Name}");
|
||||
(DataContext as MainWindowViewModel).Messages.Clear();
|
||||
(DataContext as MainWindowViewModel).UserList.Clear();
|
||||
_chatClient.JoinRoom(room.Id);
|
||||
_currentRoom = room.Id;
|
||||
}
|
||||
|
||||
private MainWindowViewModel.MessageViewModel? GetCurrentlySelectedMessage()
|
||||
{
|
||||
var messagesControl = this.FindControl<ListBox>("ChatMessageList");
|
||||
return messagesControl.SelectedItem as MainWindowViewModel.MessageViewModel;
|
||||
}
|
||||
|
||||
private void NewChatMessage_OnKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key is not (Key.Enter or Key.Return))
|
||||
{
|
||||
return;
|
||||
}
|
||||
TrySendMessageFromTextBox();
|
||||
}
|
||||
|
||||
private void NewChatMessageSubmitButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
TrySendMessageFromTextBox();
|
||||
}
|
||||
|
||||
private void TrySendMessageFromTextBox()
|
||||
{
|
||||
if (!_chatClient.IsConnected())
|
||||
{
|
||||
UpdateStatus("Cannot send a message while disconnected");
|
||||
return;
|
||||
}
|
||||
var newChatMessage = this.FindControl<TextBox>("NewChatMessage");
|
||||
_chatClient.SendMessage(newChatMessage.Text);
|
||||
newChatMessage.Clear();
|
||||
}
|
||||
|
||||
private void UpdateUserTotalStatus()
|
||||
{
|
||||
var userCount = (DataContext as MainWindowViewModel).UserList.Count;
|
||||
UpdateStatus($"Connected! {userCount} users in chat");
|
||||
}
|
||||
|
||||
private void MessageEditButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_logger.Info("Edit button clicked for " + ((e.Source as Button).DataContext as MainWindowViewModel.InnerMessageViewModel).MessageId);
|
||||
}
|
||||
|
||||
private void CopyButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var message = (e.Source as Button).DataContext as MainWindowViewModel.InnerMessageViewModel;
|
||||
if (message == null)
|
||||
{
|
||||
_logger.Info("Caught a null when trying to access the inner message model instance for the purposes of copying");
|
||||
return;
|
||||
}
|
||||
_logger.Info($"Copying {message.MessageId} to clipboard");
|
||||
GetTopLevel(e.Source as Button)?.Clipboard?.SetTextAsync(message.Message).Wait();
|
||||
}
|
||||
|
||||
private void InnerMessageRow_OnPointerEnter(object? sender, PointerEventArgs e)
|
||||
{
|
||||
((e.Source as ListBoxItem).DataContext as MainWindowViewModel.InnerMessageViewModel).IsHighlighted = true;
|
||||
}
|
||||
|
||||
private void InnerMessageRow_OnPointerLeave(object? sender, PointerEventArgs e)
|
||||
{
|
||||
((e.Source as ListBoxItem).DataContext as MainWindowViewModel.InnerMessageViewModel).IsHighlighted = false;
|
||||
}
|
||||
|
||||
private void OuterMessageRow_OnPointerEnter(object? sender, PointerEventArgs e)
|
||||
{
|
||||
var children = (e.Source as ListBox).GetLogicalChildren().Cast<ListBoxItem>();
|
||||
foreach (var child in children)
|
||||
{
|
||||
// Bit of a ghetto hack but it ensures that there's only ever one subscriber
|
||||
child.PointerEntered -= InnerMessageRow_OnPointerEnter;
|
||||
child.PointerExited -= InnerMessageRow_OnPointerLeave;
|
||||
child.PointerEntered += InnerMessageRow_OnPointerEnter;
|
||||
child.PointerExited += InnerMessageRow_OnPointerLeave;
|
||||
}
|
||||
}
|
||||
|
||||
private void MessageDeleteButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var newChatMessage = this.FindControl<TextBox>("NewChatMessage");
|
||||
var messageId = ((e.Source as Button).DataContext as MainWindowViewModel.InnerMessageViewModel).MessageId;
|
||||
newChatMessage.Text = "/delete " + messageId;
|
||||
}
|
||||
|
||||
private void AuthorNameButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var message = (e.Source as Button).DataContext as MainWindowViewModel.MessageViewModel;
|
||||
var newChatMessage = this.FindControl<TextBox>("NewChatMessage");
|
||||
newChatMessage.Text += $"@{message.Author}, ";
|
||||
newChatMessage.Focus();
|
||||
newChatMessage.SelectionStart = newChatMessage.Text.Length;
|
||||
newChatMessage.SelectionEnd = newChatMessage.Text.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user