feat: enhance user presence and channel interaction features

- Fix userlist not refreshing after creating a new channel.
- Implement unmute timer with a background job to automatically unmute users.
- Improve userlist display by filtering invisible users and ensuring proper transitions between statuses.
- Add clickable usernames, @mentions, and #channels for easier navigation.
- Embed theme colors from source sites for a more cohesive UI.
- Introduce a stateful userlist that updates incrementally via SignalR events.
- Restrict auto-opening of files to safe types only, enhancing security.
- Refactor user management into a dedicated service to reduce code duplication.
- Add a MuteExpirationService to handle timed mutes.
- Update documentation with Mermaid diagrams for major flows.
This commit is contained in:
HueByte
2026-02-24 20:56:40 +01:00
parent 8647b05c12
commit 0c16f44db6
21 changed files with 451 additions and 49 deletions
@@ -108,9 +108,47 @@ public partial class LinkEmbedService
siteName = siteName is not null ? WebUtility.HtmlDecode(siteName) : null;
description = description is not null ? WebUtility.HtmlDecode(description) : null;
return new EmbedDto(siteName, title, description, null, url);
// Extract theme-color meta tag for embed border color
var themeColor = ParseThemeColor(html);
return new EmbedDto(siteName, title, description, null, url, themeColor);
}
private static string? ParseThemeColor(string html)
{
// ThemeColorRegex: group 3 = color value
var match = ThemeColorRegex().Match(html);
var color = match.Success ? match.Groups[3].Value.Trim() : null;
if (color is null)
{
// ThemeColorReversedRegex: group 2 = color value
match = ThemeColorReversedRegex().Match(html);
color = match.Success ? match.Groups[2].Value.Trim() : null;
}
if (color is null)
return null;
if (color.Length == 4 && color[0] == '#'
&& IsHexDigit(color[1]) && IsHexDigit(color[2]) && IsHexDigit(color[3]))
{
// Expand #RGB to #RRGGBB
return $"#{color[1]}{color[1]}{color[2]}{color[2]}{color[3]}{color[3]}";
}
if (color.Length == 7 && color[0] == '#'
&& color[1..].All(IsHexDigit))
{
return color;
}
return null;
}
private static bool IsHexDigit(char c) =>
c is (>= '0' and <= '9') or (>= 'a' and <= 'f') or (>= 'A' and <= 'F');
private static List<string> ExtractUrls(string content)
{
var urls = new List<string>();
@@ -213,4 +251,14 @@ public partial class LinkEmbedService
[GeneratedRegex(@"<title[^>]*>([^<]+)</title>", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex TitleTagRegex();
// <meta name="theme-color" content="#hex">
[GeneratedRegex(@"<meta\s+[^>]*?name\s*=\s*([""'])theme-color\1[^>]*?content\s*=\s*([""'])(.*?)\2[^>]*/?>",
RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)]
private static partial Regex ThemeColorRegex();
// <meta content="#hex" name="theme-color">
[GeneratedRegex(@"<meta\s+[^>]*?content\s*=\s*([""'])(.*?)\1[^>]*?name\s*=\s*([""'])theme-color\3[^>]*/?>",
RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)]
private static partial Regex ThemeColorReversedRegex();
}