diff --git a/src/Decho/Views/ImageViewerWindow.axaml b/src/Decho/Views/ImageViewerWindow.axaml
index abed49e..7c27870 100644
--- a/src/Decho/Views/ImageViewerWindow.axaml
+++ b/src/Decho/Views/ImageViewerWindow.axaml
@@ -14,16 +14,18 @@
Background="{DynamicResource UiTheme03}">
-
-
+
+
+
+
+
diff --git a/src/Decho/Views/ImageViewerWindow.axaml.cs b/src/Decho/Views/ImageViewerWindow.axaml.cs
index b0031e5..3ff92f1 100644
--- a/src/Decho/Views/ImageViewerWindow.axaml.cs
+++ b/src/Decho/Views/ImageViewerWindow.axaml.cs
@@ -1,20 +1,25 @@
using Avalonia.Controls;
using Avalonia.Media.Imaging;
+using Avalonia.Platform.Storage;
namespace Decho.Views;
public sealed partial class ImageViewerWindow : Window
{
+ private readonly Func>? _downloadAsync;
+
public ImageViewerWindow()
{
InitializeComponent();
}
- public ImageViewerWindow(Bitmap bitmap, string fileName)
+ public ImageViewerWindow(Bitmap bitmap, string fileName, Func>? downloadAsync = null)
{
InitializeComponent();
Title = fileName;
ImageView.Source = bitmap;
+ _downloadAsync = downloadAsync;
+ DownloadButton.IsVisible = downloadAsync is not null;
Opened += (_, _) =>
{
@@ -27,6 +32,33 @@ public sealed partial class ImageViewerWindow : Window
};
}
+ private async void OnDownloadClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ if (_downloadAsync is null) return;
+
+ string? tempPath = await _downloadAsync();
+ if (tempPath is null) return;
+
+ IStorageFile? file = await this.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
+ {
+ SuggestedFileName = Title,
+ });
+
+ if (file?.TryGetLocalPath() is string savePath)
+ {
+ File.Copy(tempPath, savePath, overwrite: true);
+ }
+
+ try
+ {
+ File.Delete(tempPath);
+ }
+ catch
+ {
+ // Ignore if temp file cannot be deleted
+ }
+ }
+
private void OnCloseClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Close();
diff --git a/src/Decho/Views/MessageItemView.axaml.cs b/src/Decho/Views/MessageItemView.axaml.cs
index dddcc61..752367a 100644
--- a/src/Decho/Views/MessageItemView.axaml.cs
+++ b/src/Decho/Views/MessageItemView.axaml.cs
@@ -14,6 +14,7 @@ using EchoHub.Core.Models;
using Romzetron.Avalonia;
+using System.Diagnostics;
using System.Text.RegularExpressions;
namespace Decho.Views;
@@ -22,6 +23,7 @@ public partial class MessageItemView : UserControl
{
private static readonly Regex MentionRegex = new(@"@(\w+)", RegexOptions.Compiled);
private static readonly Regex ChannelRegex = new(@"(? TopLevel.GetTopLevel(this) as Window;
+
+ private async Task GetOrDownloadImageAsync(MessageViewModel msg, AttachmentDto att)
+ {
+ if (msg.ImageCache.TryGetValue(att.Url, out Bitmap? cached))
+ {
+ return cached;
+ }
+
+ MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
+ if (mainVm is null)
+ {
+ return null;
+ }
+
+ byte[]? bytes = await mainVm.ConnectionService.DownloadImageBytesAsync(
+ msg.ServerUrl ?? "", msg.Model.ChannelName, att.Url);
+
+ if (bytes is null || bytes.Length == 0)
+ {
+ return null;
+ }
+
+ using MemoryStream stream = new MemoryStream(bytes);
+ Bitmap bitmap = new Bitmap(stream);
+ msg.ImageCache[att.Url] = bitmap;
+ return bitmap;
+ }
+
+ private async Task OpenProfileAsync(string username, string serverUrl)
+ {
+ MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
+ Window? parent = GetParentWindow();
+ if (mainVm is null || parent is null)
+ {
+ return;
+ }
+
+ UserProfileDto? profile = await mainVm.ConnectionService.GetUserProfileAsync(serverUrl, username);
+ if (profile is null)
+ {
+ return;
+ }
+
+ await new ProfileWindow(profile).ShowDialog(parent);
+ }
+
private void BuildMessageInlines(string content)
{
TextBlock? tb = MessageContent;
@@ -61,7 +122,8 @@ public partial class MessageItemView : UserControl
IEnumerable mentionMatches = MentionRegex.Matches(content).Cast();
IEnumerable channelMatches = ChannelRegex.Matches(content).Cast();
- List allMatches = mentionMatches.Concat(channelMatches)
+ IEnumerable urlMatches = UrlRegex.Matches(content).Cast();
+ List allMatches = mentionMatches.Concat(channelMatches).Concat(urlMatches)
.OrderBy(m => m.Index)
.ToList();
@@ -92,15 +154,23 @@ public partial class MessageItemView : UserControl
string username = match.Groups[1].Value;
label.Margin = new Thickness(0, 0, 0, -2);
label.Foreground = ColorPalette.Yellow05;
- label.PointerPressed += (_, args) => OnMentionPointerPressed(username, args);
+ string serverUrl = ResolveServerUrl();
+ label.PointerPressed += (_, args) => _ = OpenProfileAsync(username, serverUrl);
}
- else
+ else if (match.Value[0] == '#')
{
string channelName = match.Groups[1].Value;
label.Margin = new Thickness(0, 0, 0, -2);
label.Foreground = ColorPalette.Blue05;
label.PointerPressed += (_, args) => OnChannelPointerPressed(channelName, args);
}
+ else
+ {
+ string url = match.Value;
+ label.Foreground = ColorPalette.Blue05;
+ label.TextDecorations = TextDecorations.Underline;
+ label.PointerPressed += (_, args) => OpenUrl(url);
+ }
container.Child = label;
tb.Inlines?.Add(container);
@@ -114,32 +184,17 @@ public partial class MessageItemView : UserControl
}
}
- private async void OnMentionPointerPressed(string username, PointerPressedEventArgs e)
+ private string ResolveServerUrl()
{
- if (DataContext is not MessageViewModel msg)
+ if (DataContext is MessageViewModel msg)
{
- return;
- }
-
- MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
- if (mainVm is null)
- {
- return;
- }
-
- string serverUrl = msg.ServerUrl ?? mainVm.Chat.CurrentServerUrl;
- UserProfileDto? profile = await mainVm.ConnectionService.GetUserProfileAsync(serverUrl, username);
- if (profile is null)
- {
- return;
- }
-
- ProfileWindow dialog = new ProfileWindow(profile);
-
- if (TopLevel.GetTopLevel(this) is Window parent)
- {
- await dialog.ShowDialog(parent);
+ MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
+ if (mainVm is not null)
+ {
+ return msg.ServerUrl ?? mainVm.Chat.CurrentServerUrl;
+ }
}
+ return string.Empty;
}
private void OnChannelPointerPressed(string channelName, PointerPressedEventArgs e)
@@ -194,40 +249,19 @@ public partial class MessageItemView : UserControl
return;
}
- CancellationTokenSource? cts = _loadCts;
- if (cts is null)
+ if (_loadCts is null)
{
return;
}
- if (msg.ImageCache.TryGetValue(att.Url, out Bitmap? cached))
- {
- image.Source = cached;
- return;
- }
-
try
{
- MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
- if (mainVm is null)
+ Bitmap? bitmap = await GetOrDownloadImageAsync(msg, att);
+ _loadCts.Token.ThrowIfCancellationRequested();
+ if (bitmap is not null)
{
- return;
+ image.Source = bitmap;
}
-
- byte[]? bytes = await mainVm.ConnectionService.DownloadImageBytesAsync(
- msg.ServerUrl ?? "", msg.Model.ChannelName, att.Url);
-
- cts.Token.ThrowIfCancellationRequested();
- if (bytes is null || bytes.Length == 0)
- {
- return;
- }
-
- using MemoryStream stream = new MemoryStream(bytes);
- Bitmap bitmap = new Bitmap(stream);
- cts.Token.ThrowIfCancellationRequested();
- msg.ImageCache[att.Url] = bitmap;
- image.Source = bitmap;
}
catch
{
@@ -237,32 +271,13 @@ public partial class MessageItemView : UserControl
private async void OnAuthorNamePointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
{
- MessageViewModel? msg = this.GetDataContext();
- if (msg is null)
+ if (DataContext is not MessageViewModel msg)
{
return;
}
- MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
- if (mainVm is null)
- {
- return;
- }
-
- string serverUrl = msg.ServerUrl ?? mainVm.Chat.CurrentServerUrl;
- string username = msg.Model.Author.Id;
- UserProfileDto? profile = await mainVm.ConnectionService.GetUserProfileAsync(serverUrl, username);
- if (profile is null)
- {
- return;
- }
-
- ProfileWindow dialog = new ProfileWindow(profile);
-
- if (TopLevel.GetTopLevel(this) is Window parent)
- {
- await dialog.ShowDialog(parent);
- }
+ string serverUrl = ResolveServerUrl();
+ await OpenProfileAsync(msg.Model.Author.Id, serverUrl);
}
private async void OnAttachmentDownloadClicked(object? sender, RoutedEventArgs e)
@@ -344,38 +359,32 @@ public partial class MessageItemView : UserControl
return;
}
- if (msg.ImageCache.TryGetValue(att.Url, out Bitmap? cached))
+ Bitmap? bitmap = await GetOrDownloadImageAsync(msg, att);
+ if (bitmap is null)
{
- ImageViewerWindow viewer = new ImageViewerWindow(cached, att.FileName);
- if (TopLevel.GetTopLevel(this) is Window parent)
+ return;
+ }
+
+ Window? parent = GetParentWindow();
+ if (parent is null)
+ {
+ return;
+ }
+
+ string serverUrl = msg.ServerUrl ?? "";
+ string channelName = msg.Model.ChannelName;
+ Func> downloadAsync = async () =>
+ {
+ MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
+ if (mainVm is null)
{
- await viewer.ShowDialog(parent);
+ return null;
}
- return;
- }
+ return await mainVm.ConnectionService.DownloadAttachmentAsync(
+ serverUrl, channelName, att.Url, att.FileName);
+ };
- MainWindowViewModel? mainVm = this.GetMainWindowViewModel();
- if (mainVm is null)
- {
- return;
- }
-
- byte[]? bytes = await mainVm.ConnectionService.DownloadImageBytesAsync(
- msg.ServerUrl ?? "", msg.Model.ChannelName, att.Url);
-
- if (bytes is null || bytes.Length == 0)
- {
- return;
- }
-
- using MemoryStream stream = new MemoryStream(bytes);
- Bitmap bitmap = new Bitmap(stream);
- msg.ImageCache[att.Url] = bitmap;
-
- ImageViewerWindow viewerWindow = new ImageViewerWindow(bitmap, att.FileName);
- if (TopLevel.GetTopLevel(this) is Window parentWindow)
- {
- await viewerWindow.ShowDialog(parentWindow);
- }
+ ImageViewerWindow viewer = new ImageViewerWindow(bitmap, att.FileName, downloadAsync);
+ await viewer.ShowDialog(parent);
}
}
\ No newline at end of file