diff --git a/XIVChat Desktop Installer/Product.wxs b/XIVChat Desktop Installer/Product.wxs
index a5e2e9f..2e54342 100644
--- a/XIVChat Desktop Installer/Product.wxs
+++ b/XIVChat Desktop Installer/Product.wxs
@@ -1,33 +1,44 @@
-
-
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/XIVChat Desktop/App.xaml.cs b/XIVChat Desktop/App.xaml.cs
index 4e5f202..6ee07e6 100644
--- a/XIVChat Desktop/App.xaml.cs
+++ b/XIVChat Desktop/App.xaml.cs
@@ -1,12 +1,18 @@
using System;
using System.ComponentModel;
using System.Globalization;
+using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Markup;
+using Windows.UI.Notifications;
+using Microsoft.Toolkit.Uwp.Notifications;
using ModernWpf;
+using XIVChatCommon.Message;
+using XIVChatCommon.Message.Server;
-// TODO: key word notification, notifications on message type, targeted message (like emote targeting you)
+// TODO: search messages
+// TODO: notifications for targeted messages (like emote targeting you)
namespace XIVChat_Desktop {
///
@@ -34,6 +40,8 @@ namespace XIVChat_Desktop {
public event PropertyChangedEventHandler? PropertyChanged;
private async void Application_Startup(object sender, StartupEventArgs e) {
+ Notifications.Initialise();
+
try {
this.Config = Configuration.Load() ?? new Configuration();
} catch (Exception ex) {
@@ -121,6 +129,7 @@ namespace XIVChat_Desktop {
}
this.Connection = new Connection(this, host, port);
+ this.Connection.ReceiveMessage += this.OnReceiveMessage;
Task.Run(this.Connection.Connect);
}
@@ -132,5 +141,37 @@ namespace XIVChat_Desktop {
this.Connection?.Disconnect();
this.Connection = null;
}
+
+ private void OnReceiveMessage(ServerMessage message) {
+ if (!this.Config.Notifications.Any(notif => notif.Matches(message))) {
+ return;
+ }
+
+ var sender = message.GetSenderPlayer();
+
+ var builder = new ToastContentBuilder();
+
+ if (sender != null) {
+ var name = sender.Name;
+
+ if (sender.Server != 0) {
+ name += $" ({Util.WorldName(sender.Server)})";
+ }
+
+ builder.AddText(name);
+ } else {
+ builder.AddText("Notification");
+ }
+
+ builder
+ .AddText(message.ContentText)
+ .AddAttributionText(message.Channel.Name());
+
+ var content = builder.GetToastContent();
+
+ var toast = new ToastNotification(content.GetXml());
+
+ DesktopNotificationManagerCompat.CreateToastNotifier().Show(toast);
+ }
}
}
diff --git a/XIVChat Desktop/ConfigWindow.xaml b/XIVChat Desktop/ConfigWindow.xaml
index a5caf7e..71ff8e4 100644
--- a/XIVChat Desktop/ConfigWindow.xaml
+++ b/XIVChat Desktop/ConfigWindow.xaml
@@ -84,5 +84,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XIVChat Desktop/ConfigWindow.xaml.cs b/XIVChat Desktop/ConfigWindow.xaml.cs
index 1465b5e..71fa4ac 100644
--- a/XIVChat Desktop/ConfigWindow.xaml.cs
+++ b/XIVChat Desktop/ConfigWindow.xaml.cs
@@ -49,5 +49,35 @@ namespace XIVChat_Desktop {
var allDigits = e.Text.All(c => char.IsDigit(c));
e.Handled = !allDigits;
}
+
+ private void Notifications_DoubleClick(object sender, MouseButtonEventArgs e) {
+ var context = ((FrameworkElement)e.OriginalSource).DataContext;
+ if (!(context is Notification notification)) {
+ return;
+ }
+
+ new ManageNotification(this, notification).Show();
+ }
+
+ private void Notifications_Add_Click(object sender, RoutedEventArgs e) {
+ new ManageNotification(this, null).Show();
+ }
+
+ private void Notifications_Edit_Click(object sender, RoutedEventArgs e) {
+ if (!(this.Notifications.SelectedItem is Notification notif)) {
+ return;
+ }
+
+ new ManageNotification(this, notif).Show();
+ }
+
+ private void Notifications_Delete_Click(object sender, RoutedEventArgs e) {
+ if (!(this.Notifications.SelectedItem is Notification notif)) {
+ return;
+ }
+
+ this.Config.Notifications.Remove(notif);
+ this.Config.Save();
+ }
}
}
diff --git a/XIVChat Desktop/Configuration.cs b/XIVChat Desktop/Configuration.cs
index c3219b0..d5c3736 100644
--- a/XIVChat Desktop/Configuration.cs
+++ b/XIVChat Desktop/Configuration.cs
@@ -10,6 +10,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
using XIVChatCommon.Message;
using XIVChatCommon.Message.Server;
@@ -73,6 +74,8 @@ namespace XIVChat_Desktop {
}
}
+ public ObservableCollection Notifications { get; set; } = new ObservableCollection();
+
private void OnPropertyChanged(string propName) {
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
@@ -357,4 +360,70 @@ namespace XIVChat_Desktop {
return this.Types.Any(type => type.Allowed(code));
}
}
+
+ [JsonObject]
+ public class Notification {
+ public string Name { get; set; }
+ public List Channels { get; set; } = new List();
+ public List Substrings { get; set; } = new List();
+
+ private IReadOnlyCollection regexes = new List();
+
+ public IReadOnlyCollection Regexes {
+ get => this.regexes;
+ set {
+ this.regexes = value;
+ this.ResetRegexes();
+ }
+ }
+
+ [JsonIgnore]
+ public Lazy> ParsedRegexes { get; private set; } = null!;
+
+ public Notification(string name) {
+ this.Name = name;
+ this.ResetRegexes();
+ }
+
+ private void ResetRegexes() {
+ this.ParsedRegexes = new Lazy>(
+ () => {
+ try {
+ return this.ParseRegexes();
+ } catch (ArgumentException) {
+ return new List();
+ }
+ }
+ );
+ }
+
+ private List ParseRegexes() {
+ return this.Regexes
+ .Select(regex => new Regex(regex, RegexOptions.Compiled))
+ .ToList();
+ }
+
+ [SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
+ public bool Matches(ServerMessage message) {
+ if (!this.Channels.Contains(message.Channel)) {
+ return false;
+ }
+
+ if (this.Substrings.Count == 0 && this.Regexes.Count == 0) {
+ return false;
+ }
+
+ var text = message.ContentText;
+
+ if (this.Substrings.Any(substring => text.ContainsIgnoreCase(substring))) {
+ return true;
+ }
+
+ if (this.ParsedRegexes.Value.Any(regex => regex.IsMatch(text))) {
+ return true;
+ }
+
+ return false;
+ }
+ }
}
diff --git a/XIVChat Desktop/Connection.cs b/XIVChat Desktop/Connection.cs
index 4b6db57..b5f736a 100644
--- a/XIVChat Desktop/Connection.cs
+++ b/XIVChat Desktop/Connection.cs
@@ -30,6 +30,10 @@ namespace XIVChat_Desktop {
public readonly CancellationTokenSource cancel = new CancellationTokenSource();
+ public delegate void ReceiveMessageDelegate(ServerMessage message);
+
+ public event ReceiveMessageDelegate? ReceiveMessage;
+
public event PropertyChangedEventHandler? PropertyChanged;
public string? CurrentChannel { get; private set; }
@@ -269,6 +273,7 @@ namespace XIVChat_Desktop {
var message = ServerMessage.Decode(payload);
this.app.Dispatch(() => {
+ this.ReceiveMessage?.Invoke(message);
this.app.Window.AddMessage(message);
});
break;
diff --git a/XIVChat Desktop/Controls/MessageTextBlock.cs b/XIVChat Desktop/Controls/MessageTextBlock.cs
index 2a6425a..138947a 100644
--- a/XIVChat Desktop/Controls/MessageTextBlock.cs
+++ b/XIVChat Desktop/Controls/MessageTextBlock.cs
@@ -1,5 +1,7 @@
-using System.Windows;
+using System.Linq;
+using System.Windows;
using System.Windows.Data;
+using System.Windows.Media;
using XIVChatCommon.Message.Server;
namespace XIVChat_Desktop.Controls {
@@ -54,11 +56,15 @@ namespace XIVChat_Desktop.Controls {
}
var message = textBlock.Message;
-
if (message == null) {
return;
}
+ var config = ((App)Application.Current).Config;
+ if (config.Notifications.Any(notif => notif.Matches(message))) {
+ textBlock.Background = new SolidColorBrush(Color.FromArgb(128, 200, 100, 100));
+ }
+
textBlock.ClearValue(TextProperty);
textBlock.Inlines.Clear();
diff --git a/XIVChat Desktop/Export.xaml.cs b/XIVChat Desktop/Export.xaml.cs
index 7fa83de..bff24f7 100644
--- a/XIVChat Desktop/Export.xaml.cs
+++ b/XIVChat Desktop/Export.xaml.cs
@@ -2,6 +2,7 @@
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -165,6 +166,7 @@ namespace XIVChat_Desktop {
this.Senders.Add(sender);
}
+ [SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
public bool AllowedMinusSenders(ServerMessage message) {
if (!base.Allowed(message)) {
return false;
@@ -181,6 +183,7 @@ namespace XIVChat_Desktop {
return true;
}
+ [SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
public override bool Allowed(ServerMessage message) {
if (!this.AllowedMinusSenders(message)) {
return false;
diff --git a/XIVChat Desktop/ManageNotification.xaml b/XIVChat Desktop/ManageNotification.xaml
new file mode 100644
index 0000000..67387ef
--- /dev/null
+++ b/XIVChat Desktop/ManageNotification.xaml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XIVChat Desktop/Notifications.cs b/XIVChat Desktop/Notifications.cs
new file mode 100644
index 0000000..b0c3527
--- /dev/null
+++ b/XIVChat Desktop/Notifications.cs
@@ -0,0 +1,21 @@
+using System.Runtime.InteropServices;
+using Microsoft.Toolkit.Uwp.Notifications;
+
+namespace XIVChat_Desktop {
+ public class Notifications {
+ public static void Initialise() {
+ DesktopNotificationManagerCompat.RegisterAumidAndComServer("XIVChat.XIVChat_Desktop");
+ DesktopNotificationManagerCompat.RegisterActivator();
+ }
+ }
+
+
+ [ClassInterface(ClassInterfaceType.None)]
+ [ComSourceInterfaces(typeof(INotificationActivationCallback))]
+ [Guid("F12BCC85-6FEE-4A9A-BBB8-08DFAA7BE1A8"), ComVisible(true)]
+ public class XivChatNotificationActivator : NotificationActivator {
+ public override void OnActivated(string invokedArgs, NotificationUserInput userInput, string appUserModelId) {
+ // TODO: Handle activation
+ }
+ }
+}
diff --git a/XIVChat Desktop/Util.cs b/XIVChat Desktop/Util.cs
index e695418..c53a544 100644
--- a/XIVChat Desktop/Util.cs
+++ b/XIVChat Desktop/Util.cs
@@ -1,5 +1,10 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using System.Windows.Controls;
using System.Windows.Threading;
namespace XIVChat_Desktop {
@@ -673,5 +678,63 @@ namespace XIVChat_Desktop {
_ => null,
};
}
+
+ public static bool ContainsIgnoreCase(this string haystack, string needle) {
+ return CultureInfo.InvariantCulture.CompareInfo.IndexOf(haystack, needle, CompareOptions.IgnoreCase) >= 0;
+ }
+
+ public static bool IsValidRegex(this string regex) {
+ try {
+ _ = new Regex(regex);
+ return true;
+ } catch (ArgumentException) {
+ return false;
+ }
+ }
+
+ public static bool IsValidRegex(this string regex, out ArgumentException exception) {
+ try {
+ _ = new Regex(regex);
+ exception = default!;
+ return true;
+ } catch (ArgumentException ex) {
+ exception = ex;
+ return false;
+ }
+ }
+ }
+
+ public class RegexValidator : ValidationRule {
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
+ if (!(value is string text)) {
+ return new ValidationResult(false, "Value is not text.");
+ }
+
+ return text.IsValidRegex(out var ex)
+ ? ValidationResult.ValidResult
+ : new ValidationResult(false, $"Invalid regular expression: {ex.Message}");
+ }
+ }
+
+ public class StringWrapper : INotifyPropertyChanged {
+ private string value;
+
+ public String Value {
+ get => this.value;
+ set {
+ this.value = value;
+ this.OnPropertyChanged(nameof(this.Value));
+ }
+ }
+
+ public StringWrapper(string value) {
+ this.value = value;
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ private void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
+ this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
}
}
diff --git a/XIVChat Desktop/XIVChat Desktop.csproj b/XIVChat Desktop/XIVChat Desktop.csproj
index 7e7735d..6f06933 100644
--- a/XIVChat Desktop/XIVChat Desktop.csproj
+++ b/XIVChat Desktop/XIVChat Desktop.csproj
@@ -49,6 +49,7 @@
+
diff --git a/XIVChatCommon/Message/Message.cs b/XIVChatCommon/Message/Message.cs
index bc275cd..44d20aa 100644
--- a/XIVChatCommon/Message/Message.cs
+++ b/XIVChatCommon/Message/Message.cs
@@ -538,6 +538,94 @@ namespace XIVChatCommon.Message {
CrossLinkshell8 = 107,
}
+ public static class ChatTypeExt {
+ public static string? Name(this ChatType type) {
+ return type switch {
+ ChatType.Debug => "Debug",
+ ChatType.Urgent => "Urgent",
+ ChatType.Notice => "Notice",
+ ChatType.Say => "Say",
+ ChatType.Shout => "Shout",
+ ChatType.TellOutgoing => "Tell (Outgoing)",
+ ChatType.TellIncoming => "Tell (Incoming)",
+ ChatType.Party => "Party",
+ ChatType.Alliance => "Alliance",
+ ChatType.Linkshell1 => "Linkshell [1]",
+ ChatType.Linkshell2 => "Linkshell [2]",
+ ChatType.Linkshell3 => "Linkshell [3]",
+ ChatType.Linkshell4 => "Linkshell [4]",
+ ChatType.Linkshell5 => "Linkshell [5]",
+ ChatType.Linkshell6 => "Linkshell [6]",
+ ChatType.Linkshell7 => "Linkshell [7]",
+ ChatType.Linkshell8 => "Linkshell [8]",
+ ChatType.FreeCompany => "Free Company",
+ ChatType.NoviceNetwork => "Novice Network",
+ ChatType.CustomEmote => "Custom Emotes",
+ ChatType.StandardEmote => "Standard Emotes",
+ ChatType.Yell => "Yell",
+ ChatType.CrossParty => "Cross-world Party",
+ ChatType.PvpTeam => "PvP Team",
+ ChatType.CrossLinkshell1 => "Cross-world Linkshell [1]",
+ ChatType.Damage => "Damage dealt",
+ ChatType.Miss => "Failed attacks",
+ ChatType.Action => "Actions used",
+ ChatType.Item => "Items used",
+ ChatType.Healing => "Healing",
+ ChatType.GainBuff => "Beneficial effects granted",
+ ChatType.GainDebuff => "Detrimental effects inflicted",
+ ChatType.LoseBuff => "Beneficial effects lost",
+ ChatType.LoseDebuff => "Detrimental effects cured",
+ ChatType.Alarm => "Alarm Notifications",
+ ChatType.Echo => "Echo",
+ ChatType.System => "System Messages",
+ ChatType.BattleSystem => "Battle System Messages",
+ ChatType.GatheringSystem => "Gathering System Messages",
+ ChatType.Error => "Error Messages",
+ ChatType.NpcDialogue => "NPC Dialogue",
+ ChatType.LootNotice => "Loot Notices",
+ ChatType.Progress => "Progression Messages",
+ ChatType.LootRoll => "Loot Messages",
+ ChatType.Crafting => "Synthesis Messages",
+ ChatType.Gathering => "Gathering Messages",
+ ChatType.NpcAnnouncement => "NPC Dialogue (Announcements)",
+ ChatType.FreeCompanyAnnouncement => "Free Company Announcements",
+ ChatType.FreeCompanyLoginLogout => "Free Company Member Login Notifications",
+ ChatType.RetainerSale => "Retainer Sale Notifications",
+ ChatType.PeriodicRecruitmentNotification => "Periodic Recruitment Notifications",
+ ChatType.Sign => "Sign Messages for PC Targets",
+ ChatType.RandomNumber => "Random Number Messages",
+ ChatType.NoviceNetworkSystem => "Novice Network Notifications",
+ ChatType.Orchestrion => "Current Orchestrion Track Messages",
+ ChatType.PvpTeamAnnouncement => "PvP Team Announcements",
+ ChatType.PvpTeamLoginLogout => "PvP Team Member Login Notifications",
+ ChatType.MessageBook => "Message Book Alert",
+ ChatType.GmTell => "Tell (GM)",
+ ChatType.GmSay => "Say (GM)",
+ ChatType.GmShout => "Shout (GM)",
+ ChatType.GmYell => "Yell (GM)",
+ ChatType.GmParty => "Party (GM)",
+ ChatType.GmFreeCompany => "Free Company (GM)",
+ ChatType.GmLinkshell1 => "Linkshell [1] (GM)",
+ ChatType.GmLinkshell2 => "Linkshell [2] (GM)",
+ ChatType.GmLinkshell3 => "Linkshell [3] (GM)",
+ ChatType.GmLinkshell4 => "Linkshell [4] (GM)",
+ ChatType.GmLinkshell5 => "Linkshell [5] (GM)",
+ ChatType.GmLinkshell6 => "Linkshell [6] (GM)",
+ ChatType.GmLinkshell7 => "Linkshell [7] (GM)",
+ ChatType.GmLinkshell8 => "Linkshell [8] (GM)",
+ ChatType.GmNoviceNetwork => "Novice Network (GM)",
+ ChatType.CrossLinkshell2 => "Cross-world Linkshell [2]",
+ ChatType.CrossLinkshell3 => "Cross-world Linkshell [3]",
+ ChatType.CrossLinkshell4 => "Cross-world Linkshell [4]",
+ ChatType.CrossLinkshell5 => "Cross-world Linkshell [5]",
+ ChatType.CrossLinkshell6 => "Cross-world Linkshell [6]",
+ ChatType.CrossLinkshell7 => "Cross-world Linkshell [7]",
+ ChatType.CrossLinkshell8 => "Cross-world Linkshell [8]",
+ _ => type.ToString(),
+ };
+ }
+ }
+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1028:Enum Storage should be Int32")]
public enum ChatSource : ushort {
Self = 2,