feat: add preferences and better backlog ordering

This commit is contained in:
Anna 2020-11-05 19:48:21 -05:00
parent 98cdf7916e
commit fa5980c61b
6 changed files with 180 additions and 25 deletions

View File

@ -1,9 +1,10 @@
using Newtonsoft.Json;
using Sodium;
using System;
using System.Collections.Concurrent;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
@ -100,7 +101,7 @@ namespace XIVChat_Desktop {
}
[JsonObject]
public class Tab : INotifyPropertyChanged {
public class Tab : IEnumerable<ServerMessage>, INotifyCollectionChanged {
private string name;
public Tab(string name) {
@ -111,25 +112,53 @@ namespace XIVChat_Desktop {
get => this.name;
set {
this.name = value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Name)));
}
}
public Filter Filter { get; set; } = new Filter();
[JsonIgnore]
public ObservableCollection<ServerMessage> Messages { get; } = new ObservableCollection<ServerMessage>();
public List<ServerMessage> Messages { get; } = new List<ServerMessage>();
public event PropertyChangedEventHandler? PropertyChanged;
private void NotifyReset() {
this.CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void RepopulateMessages(ConcurrentStack<ServerMessage> mainMessages) {
private void NotifyAdd(ServerMessage message) {
this.CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, message));
}
private void NotifyAddItemsAt(IList messages, int index) {
this.CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, messages, index));
}
public void RepopulateMessages(IEnumerable<ServerMessage> mainMessages) {
this.Messages.Clear();
foreach (var message in mainMessages.Where(msg => this.Filter.Allowed(msg)).Reverse()) {
// add messages from newest to oldest
foreach (var message in mainMessages.Where(msg => this.Filter.Allowed(msg))) {
this.Messages.Add(message);
}
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Messages)));
this.NotifyReset();
}
private int lastSequence = -1;
private int insertAt;
public void AddReversedChunk(ServerMessage[] messages, int sequence) {
if (sequence != this.lastSequence) {
this.lastSequence = sequence;
this.insertAt = this.Messages.Count;
}
var filtered = messages
.Where(msg => msg.Channel == 0 || this.Filter.Allowed(msg))
.ToList();
this.Messages.InsertRange(this.insertAt, filtered);
this.NotifyAddItemsAt(filtered, this.insertAt);
}
public void AddMessage(ServerMessage message) {
@ -138,13 +167,12 @@ namespace XIVChat_Desktop {
}
this.Messages.Add(message);
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Messages)));
this.NotifyAdd(message);
}
public void ClearMessages() {
this.Messages.Clear();
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Messages)));
this.NotifyReset();
}
public static Filter GeneralFilter() {
@ -177,6 +205,16 @@ namespace XIVChat_Desktop {
},
};
}
public IEnumerator<ServerMessage> GetEnumerator() {
return this.Messages.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return this.GetEnumerator();
}
public event NotifyCollectionChangedEventHandler? CollectionChanged;
}
[JsonObject]

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Sockets;
@ -7,6 +8,7 @@ using System.Threading.Channels;
using System.Threading.Tasks;
using System.Windows;
using XIVChatCommon;
using DispatcherPriority = System.Windows.Threading.DispatcherPriority;
namespace XIVChat_Desktop {
public class Connection : INotifyPropertyChanged {
@ -85,6 +87,16 @@ namespace XIVChat_Desktop {
this.app.Window.AddSystemMessage("Connected");
});
// tell the server our preferences
var preferences = new ClientPreferences {
Preferences = new Dictionary<ClientPreference, object> {
{
ClientPreference.BacklogNewestMessagesFirst, true
},
},
};
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, preferences, this.cancel.Token);
// check if backlog or catch-up is needed
if (sameHost) {
// catch-up
@ -235,9 +247,14 @@ namespace XIVChat_Desktop {
case ServerOperation.Backlog:
var backlog = ServerBacklog.Decode(payload);
foreach (var msg in backlog.messages) {
this.Dispatch(() => {
this.app.Window.AddMessage(msg);
this.backlogSequence += 1;
var seq = this.backlogSequence;
foreach (var msg in backlog.messages.ToList().Chunks(100)) {
msg.Reverse();
var array = msg.ToArray();
this.Dispatch(DispatcherPriority.Background, () => {
this.app.Window.AddReversedChunk(array, seq);
});
}
@ -249,6 +266,8 @@ namespace XIVChat_Desktop {
}
}
private int backlogSequence = -1;
private void SetPlayerData(PlayerData? playerData) {
var visibility = playerData == null ? Visibility.Collapsed : Visibility.Visible;
@ -271,7 +290,11 @@ namespace XIVChat_Desktop {
}
private void Dispatch(Action action) {
this.app.Dispatcher.BeginInvoke(action);
this.Dispatch(DispatcherPriority.Normal, action);
}
private void Dispatch(DispatcherPriority priority, Action action) {
this.app.Dispatcher.BeginInvoke(priority, action);
}
}
}

View File

@ -63,7 +63,7 @@
<ItemsControl Background="#333"
x:Name="items"
Padding="4"
ItemsSource="{Binding Messages, UpdateSourceTrigger=PropertyChanged}">
ItemsSource="{Binding .}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel IsVirtualizing="True"
@ -110,4 +110,4 @@
Visibility="Collapsed" />
</StatusBar>
</Grid>
</Window>
</Window>

View File

@ -15,7 +15,7 @@ namespace XIVChat_Desktop {
public partial class MainWindow {
public App App => (App)Application.Current;
public ConcurrentStack<ServerMessage> Messages { get; } = new ConcurrentStack<ServerMessage>();
public List<ServerMessage> Messages { get; } = new List<ServerMessage>();
public MainWindow() {
this.InitializeComponent();
@ -67,13 +67,39 @@ namespace XIVChat_Desktop {
this.AddMessage(message);
}
private int lastSequence = -1;
private int insertAt;
public void AddReversedChunk(ServerMessage[] messages, int sequence) {
if (sequence != this.lastSequence) {
this.lastSequence = sequence;
this.insertAt = this.Messages.Count;
}
// detect if scroller is at the bottom
var scroller = this.FindElementByName<ScrollViewer>(this.Tabs, "scroller");
var wasAtBottom = Math.Abs(scroller!.VerticalOffset - scroller.ScrollableHeight) < .0001;
// add messages to main list
this.Messages.InsertRange(this.insertAt, messages);
// add message to each tab if the filter allows for it
foreach (var tab in this.App.Config.Tabs) {
tab.AddReversedChunk(messages, sequence);
}
// scroll to the bottom if previously at the bottom
if (wasAtBottom) {
scroller.ScrollToBottom();
}
}
public void AddMessage(ServerMessage message) {
// detect if scroller is at the bottom
var scroller = this.FindElementByName<ScrollViewer>(this.Tabs, "scroller");
var wasAtBottom = Math.Abs(scroller!.VerticalOffset - scroller.ScrollableHeight) < .0001;
// add message to main list
this.Messages.Push(message);
this.Messages.Add(message);
// add message to each tab if the filter allows for it
foreach (var tab in this.App.Config.Tabs) {
tab.AddMessage(message);

View File

@ -2,6 +2,8 @@
using MessagePack.Formatters;
using System;
using System.Collections.Generic;
using System.Reflection;
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable ClassNeverInstantiated.Global
@ -36,7 +38,7 @@ namespace XIVChatCommon {
[IgnoreMember]
protected override byte Code => (byte)ServerOperation.Message;
public ServerMessage(DateTime timestamp, ChatType channel, byte[] sender, byte[] content, List<Chunk> chunks) {
this.Timestamp = timestamp;
this.Channel = channel;
@ -76,7 +78,7 @@ namespace XIVChatCommon {
[Key(4)]
public string Content { get; set; }
public TextChunk(string content) {
this.Content = content;
}
@ -601,7 +603,7 @@ namespace XIVChatCommon {
[IgnoreMember]
protected override byte Code => (byte)ClientOperation.Message;
public ClientMessage(string content) {
this.Content = content;
}
@ -839,7 +841,7 @@ namespace XIVChatCommon {
public ClientCatchUp(DateTime after) {
this.After = after;
}
public static ClientCatchUp Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<ClientCatchUp>(bytes);
}
@ -858,7 +860,7 @@ namespace XIVChatCommon {
public Player[] Players { get; set; }
protected override byte Code => (byte)ServerOperation.PlayerList;
public ServerPlayerList(PlayerListType type, Player[] players) {
this.Type = type;
this.Players = players;
@ -944,6 +946,57 @@ namespace XIVChatCommon {
public byte MainLanguage { get; set; }
}
[MessagePackObject]
public class ClientPreferences : IEncodable {
[Key(0)]
public Dictionary<ClientPreference, object> Preferences { get; set; } = new Dictionary<ClientPreference, object>();
protected override byte Code => (byte)ClientOperation.Preferences;
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
public static ClientPreferences Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<ClientPreferences>(bytes);
}
}
public enum ClientPreference {
[Preference(typeof(bool))]
BacklogNewestMessagesFirst,
}
public class PreferenceAttribute : Attribute {
public Type ValueType { get; }
public PreferenceAttribute(Type valueType) {
this.ValueType = valueType;
}
}
public static class ClientPreferencesExtension {
public static bool TryGetValue<T>(this ClientPreferences prefs, ClientPreference pref, out T value) {
value = default!;
if (!prefs.Preferences.TryGetValue(pref, out var obj)) {
return false;
}
var attr = pref
.GetType()
.GetField(pref.ToString())
?.GetCustomAttribute<PreferenceAttribute>(false);
if (obj.GetType() != typeof(T) || obj.GetType() != attr?.ValueType) {
return false;
}
value = (T)obj;
return true;
}
}
public abstract class IEncodable {
protected abstract byte Code { get; }
protected abstract byte[] PayloadEncode();
@ -981,6 +1034,7 @@ namespace XIVChatCommon {
CatchUp = 5,
PlayerList = 6,
LinkshellList = 7,
Preferences = 8,
}
public class MillisecondsDateTimeFormatter : IMessagePackFormatter<DateTime> {

View File

@ -377,7 +377,10 @@ namespace XIVChatPlugin {
node = node.Previous;
}
backlogMessages.Reverse();
bool bNewestFirst = false;
if (client.Preferences?.TryGetValue(ClientPreference.BacklogNewestMessagesFirst, out bNewestFirst) == true && !bNewestFirst) {
backlogMessages.Reverse();
}
await SendBacklogs(backlogMessages.ToArray(), stream, client);
break;
@ -387,6 +390,11 @@ namespace XIVChatPlugin {
var after = catchUp.After.AddMilliseconds(1);
var msgs = this.MessagesAfter(after);
bool cNewestFirst = false;
if (client.Preferences?.TryGetValue(ClientPreference.BacklogNewestMessagesFirst, out cNewestFirst) == true && cNewestFirst) {
msgs = msgs.Reverse();
}
await SendBacklogs(msgs, stream, client);
break;
case ClientOperation.PlayerList:
@ -400,6 +408,11 @@ namespace XIVChatPlugin {
}
}
break;
case ClientOperation.Preferences:
var preferences = ClientPreferences.Decode(payload);
client.Preferences = preferences;
break;
}
}
@ -789,6 +802,7 @@ namespace XIVChatPlugin {
public bool Connected { get; set; }
public TcpClient Conn { get; }
public HandshakeInfo? Handshake { get; set; }
public ClientPreferences? Preferences { get; set; }
public CancellationTokenSource TokenSource { get; } = new CancellationTokenSource();
public Channel<ServerMessage> Queue { get; } = Channel.CreateUnbounded<ServerMessage>();