feat(desktop): add configurable notifications

This commit is contained in:
Anna 2020-11-23 00:00:04 -05:00
parent 21af7395d8
commit caf6f456c6
13 changed files with 506 additions and 29 deletions

View File

@ -1,33 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="XIVChat for Windows" Language="1033" Version="1.0.0.0" Manufacturer="XIVChat" UpgradeCode="82f30f39-99e8-4040-9bc0-32c1eac159df">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<Product Id="*" Name="XIVChat for Windows" Language="1033" Version="1.0.0.0" Manufacturer="XIVChat"
UpgradeCode="82f30f39-99e8-4040-9bc0-32c1eac159df">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine"/>
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<MediaTemplate EmbedCab="yes" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed."/>
<MediaTemplate EmbedCab="yes"/>
<Feature Id="ProductFeature" Title="XIVChat for Windows" Level="1">
<ComponentGroupRef Id="XIVChat_Desktop" />
<ComponentRef Id="App_Start_Menu_Shortcut" />
</Feature>
</Product>
<Feature Id="ProductFeature" Title="XIVChat for Windows" Level="1">
<ComponentGroupRef Id="XIVChat_Desktop"/>
<ComponentRef Id="App_Start_Menu_Shortcut"/>
</Feature>
</Product>
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="XIVChat for Windows" />
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="XIVChat for Windows"/>
</Directory>
</Directory>
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="XIVChat for Windows"/>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="XIVChat for Windows"/>
</Directory>
</Directory>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="App_Start_Menu_Shortcut" Guid="6EF30C67-2EAA-4988-8470-E1149D5C1B50">
<Shortcut Id="ApplicationStartMenuShortcut" Name="XIVChat for Windows" Target="[INSTALLFOLDER]XIVChat Desktop.exe" WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="RemoveApplicationProgramsFolder" Directory="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\XIVChat for Windows" Name="installed" Type="integer" Value="1" KeyPath="yes" />
</Component>
</DirectoryRef>
</Fragment>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="App_Start_Menu_Shortcut" Guid="6EF30C67-2EAA-4988-8470-E1149D5C1B50">
<Shortcut Id="ApplicationStartMenuShortcut" Name="XIVChat for Windows"
Target="[INSTALLFOLDER]XIVChat Desktop.exe" WorkingDirectory="INSTALLFOLDER">
<!-- AUMID -->
<ShortcutProperty Key="System.AppUserModel.ID" Value="XIVChat.XIVChat_Desktop"/>
<!-- COM CLSID -->
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID"
Value="{F12BCC85-6FEE-4A9A-BBB8-08DFAA7BE1A8}"/>
</Shortcut>
<RemoveFolder Id="RemoveApplicationProgramsFolder" Directory="ApplicationProgramsFolder"
On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\XIVChat for Windows" Name="installed" Type="integer" Value="1"
KeyPath="yes"/>
</Component>
</DirectoryRef>
</Fragment>
</Wix>

View File

@ -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 {
/// <summary>
@ -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);
}
}
}

View File

@ -84,5 +84,41 @@
</Button>
</Grid>
</TabItem>
<TabItem Header="Notifications">
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ListView Grid.Column="0"
x:Name="Notifications"
ItemsSource="{Binding Config.Notifications}"
MouseDoubleClick="Notifications_DoubleClick">
<ListView.View>
<GridView>
<GridViewColumn Header="Name"
DisplayMemberBinding="{Binding Name}" />
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Column="1"
Margin="8,0,0,0"
Orientation="Vertical">
<Button Margin="0,0,0,4"
HorizontalAlignment="Stretch"
Content="Add"
Click="Notifications_Add_Click" />
<Button Margin="0,0,0,4"
HorizontalAlignment="Stretch"
Content="Edit"
Click="Notifications_Edit_Click" />
<Button HorizontalAlignment="Stretch"
Content="Delete"
Click="Notifications_Delete_Click" />
</StackPanel>
</Grid>
</TabItem>
</TabControl>
</local:XivChatWindow>

View File

@ -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();
}
}
}

View File

@ -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<Notification> Notifications { get; set; } = new ObservableCollection<Notification>();
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<ChatType> Channels { get; set; } = new List<ChatType>();
public List<string> Substrings { get; set; } = new List<string>();
private IReadOnlyCollection<String> regexes = new List<string>();
public IReadOnlyCollection<string> Regexes {
get => this.regexes;
set {
this.regexes = value;
this.ResetRegexes();
}
}
[JsonIgnore]
public Lazy<List<Regex>> ParsedRegexes { get; private set; } = null!;
public Notification(string name) {
this.Name = name;
this.ResetRegexes();
}
private void ResetRegexes() {
this.ParsedRegexes = new Lazy<List<Regex>>(
() => {
try {
return this.ParseRegexes();
} catch (ArgumentException) {
return new List<Regex>();
}
}
);
}
private List<Regex> 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;
}
}
}

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -0,0 +1,103 @@
<local:XivChatWindow x:Class="XIVChat_Desktop.ManageNotification"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:XIVChat_Desktop"
xmlns:ui="http://schemas.modernwpf.com/2019"
ui:WindowHelper.UseModernWindowStyle="True"
Closed="ManageNotification_OnClosed"
mc:Ignorable="d"
Title="Manage notification"
Height="800"
Width="450"
d:DataContext="{d:DesignInstance local:ManageNotification}">
<local:XivChatWindow.CommandBindings>
<CommandBinding Command="local:ManageNotification.AddEmpty"
Executed="AddEmpty_Execute"
CanExecute="AddEmpty_CanExecute" />
</local:XivChatWindow.CommandBindings>
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBox Grid.Row="0"
Margin="0,0,0,8"
ui:ControlHelper.PlaceholderText="Name"
ui:ControlHelper.Header="Name"
Text="{Binding Notification.Name}" />
<Label Grid.Row="1"
Margin="0,0,0,8"
Content="Regular expressions" />
<ScrollViewer Height="100"
Grid.Row="2">
<ItemsControl ItemsSource="{Binding Regexes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Margin="0,0,0,8">
<TextBox.Text>
<Binding Path="Value">
<Binding.ValidationRules>
<local:RegexValidator />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Button Grid.Row="3"
Margin="0,0,0,8"
Content="Add"
Command="local:ManageNotification.AddEmpty"
CommandParameter="{Binding Regexes}" />
<Label Grid.Row="4"
Margin="0,0,0,8"
Content="Substrings" />
<ScrollViewer Height="100"
Grid.Row="5">
<ItemsControl ItemsSource="{Binding Substrings}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Margin="0,0,0,8"
Text="{Binding Value}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Button Grid.Row="6"
Margin="0,0,0,8"
Content="Add"
Command="local:ManageNotification.AddEmpty"
CommandParameter="{Binding Substrings}" />
<Label Grid.Row="7"
Margin="0,0,0,8"
Content="Channels" />
<ScrollViewer Grid.Row="8">
<WrapPanel x:Name="Channels"
Orientation="Vertical" />
</ScrollViewer>
</Grid>
</local:XivChatWindow>

View File

@ -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<XivChatNotificationActivator>("XIVChat.XIVChat_Desktop");
DesktopNotificationManagerCompat.RegisterActivator<XivChatNotificationActivator>();
}
}
[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
}
}
}

View File

@ -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));
}
}
}

View File

@ -49,6 +49,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="6.1.1" />
<PackageReference Include="ModernWpfUI" Version="0.9.2" />
<PackageReference Include="ModernWpfUI.MahApps" Version="0.9.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />

View File

@ -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,