mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-06 07:36:01 +02:00
feat: add IsPublic property to channels and enhance channel creation with visibility options
This commit is contained in:
@@ -212,6 +212,8 @@ public sealed class AppOrchestrator : IDisposable
|
||||
var history = await _connection!.JoinChannelAsync(channelName);
|
||||
InvokeUI(() =>
|
||||
{
|
||||
// Add to channel list if not already there (e.g. private channels)
|
||||
_mainWindow.EnsureChannelInList(channelName);
|
||||
_mainWindow.SwitchToChannel(channelName);
|
||||
if (history.Count > 0)
|
||||
_mainWindow.LoadHistory(channelName, history);
|
||||
@@ -704,17 +706,18 @@ public sealed class AppOrchestrator : IDisposable
|
||||
|
||||
RunAsync(async () =>
|
||||
{
|
||||
var channel = await _apiClient!.CreateChannelAsync(result.Name, result.Topic);
|
||||
var channel = await _apiClient!.CreateChannelAsync(result.Name, result.Topic, result.IsPublic);
|
||||
if (channel is null) return;
|
||||
|
||||
_joinedChannels.Add(channel.Name);
|
||||
var history = await _connection!.JoinChannelAsync(channel.Name);
|
||||
|
||||
// Refresh the channel list
|
||||
// Refresh the channel list and ensure private channels show up
|
||||
var channels = await _apiClient.GetChannelsAsync();
|
||||
InvokeUI(() =>
|
||||
{
|
||||
_mainWindow.SetChannels(channels);
|
||||
_mainWindow.EnsureChannelInList(channel.Name);
|
||||
_mainWindow.SwitchToChannel(channel.Name);
|
||||
if (history.Count > 0)
|
||||
_mainWindow.LoadHistory(channel.Name, history);
|
||||
|
||||
@@ -4,6 +4,8 @@ using EchoHub.Client.Themes;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Serilog;
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Drawing;
|
||||
|
||||
|
||||
var appSettingsPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
|
||||
if (!File.Exists(appSettingsPath))
|
||||
|
||||
@@ -186,10 +186,10 @@ public sealed class ApiClient : IDisposable
|
||||
return await response.Content.ReadFromJsonAsync<MessageDto>();
|
||||
}
|
||||
|
||||
public async Task<ChannelDto?> CreateChannelAsync(string name, string? topic = null)
|
||||
public async Task<ChannelDto?> CreateChannelAsync(string name, string? topic = null, bool isPublic = true)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
var request = new CreateChannelRequest(name, topic);
|
||||
var request = new CreateChannelRequest(name, topic, isPublic);
|
||||
var response = await AuthenticatedRequestAsync(() =>
|
||||
_http.PostAsJsonAsync("/api/channels", request));
|
||||
await EnsureSuccessAsync(response);
|
||||
|
||||
@@ -90,13 +90,23 @@ public partial class ChatLine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a string containing ANSI 24-bit color escape codes into colored segments.
|
||||
/// Supports foreground (\x1b[38;2;R;G;Bm), background (\x1b[48;2;R;G;Bm), and reset (\x1b[0m).
|
||||
/// Returns true if a line contains color tags (new format or legacy ANSI).
|
||||
/// </summary>
|
||||
public static ChatLine FromAnsi(string ansiText, Attribute? defaultAttr = null)
|
||||
public static bool HasColorTags(string text) =>
|
||||
text.Contains("{F:") || text.Contains("{B:") || text.Contains("{X}") || text.Contains('\x1b');
|
||||
|
||||
/// <summary>
|
||||
/// Parse a string containing color tags into colored segments.
|
||||
/// Supports the new printable format ({F:RRGGBB}, {B:RRGGBB}, {X})
|
||||
/// and legacy ANSI format (\x1b[38;2;R;G;Bm, \x1b[48;2;R;G;Bm, \x1b[0m).
|
||||
/// </summary>
|
||||
public static ChatLine FromColoredText(string text, Attribute? defaultAttr = null)
|
||||
{
|
||||
// Detect which format is used and pick the right regex
|
||||
var regex = text.Contains('\x1b') ? AnsiColorRegex() : ColorTagRegex();
|
||||
bool isAnsi = text.Contains('\x1b');
|
||||
|
||||
var segments = new List<ChatSegment>();
|
||||
var regex = AnsiColorRegex();
|
||||
int lastIndex = 0;
|
||||
Color? currentFg = null;
|
||||
Color? currentBg = null;
|
||||
@@ -111,52 +121,76 @@ public partial class ChatLine
|
||||
return new Attribute(fg, bg);
|
||||
}
|
||||
|
||||
foreach (Match match in regex.Matches(ansiText))
|
||||
foreach (Match match in regex.Matches(text))
|
||||
{
|
||||
// Add any text before this escape sequence
|
||||
if (match.Index > lastIndex)
|
||||
{
|
||||
var text = ansiText[lastIndex..match.Index];
|
||||
if (text.Length > 0)
|
||||
segments.Add(new ChatSegment(text, BuildAttr()));
|
||||
var t = text[lastIndex..match.Index];
|
||||
if (t.Length > 0)
|
||||
segments.Add(new ChatSegment(t, BuildAttr()));
|
||||
}
|
||||
|
||||
// Parse the escape sequence
|
||||
if (match.Groups[1].Value == "0")
|
||||
if (isAnsi)
|
||||
{
|
||||
// Reset
|
||||
currentFg = null;
|
||||
currentBg = null;
|
||||
// Legacy ANSI format
|
||||
if (match.Groups[1].Value == "0")
|
||||
{
|
||||
currentFg = null;
|
||||
currentBg = null;
|
||||
}
|
||||
else if (match.Groups[2].Success)
|
||||
{
|
||||
var r = int.Parse(match.Groups[3].Value);
|
||||
var g = int.Parse(match.Groups[4].Value);
|
||||
var b = int.Parse(match.Groups[5].Value);
|
||||
if (match.Groups[2].Value == "38;2")
|
||||
currentFg = new Color(r, g, b);
|
||||
else
|
||||
currentBg = new Color(r, g, b);
|
||||
}
|
||||
}
|
||||
else if (match.Groups[2].Success)
|
||||
else
|
||||
{
|
||||
var r = int.Parse(match.Groups[3].Value);
|
||||
var g = int.Parse(match.Groups[4].Value);
|
||||
var b = int.Parse(match.Groups[5].Value);
|
||||
|
||||
if (match.Groups[2].Value == "38;2")
|
||||
currentFg = new Color(r, g, b);
|
||||
else // 48;2
|
||||
currentBg = new Color(r, g, b);
|
||||
// New printable tag format: {F:RRGGBB}, {B:RRGGBB}, {X}
|
||||
if (match.Groups[6].Success)
|
||||
{
|
||||
// Reset {X}
|
||||
currentFg = null;
|
||||
currentBg = null;
|
||||
}
|
||||
else if (match.Groups[7].Success)
|
||||
{
|
||||
var hex = match.Groups[8].Value;
|
||||
var r = Convert.ToInt32(hex[..2], 16);
|
||||
var g = Convert.ToInt32(hex[2..4], 16);
|
||||
var b = Convert.ToInt32(hex[4..6], 16);
|
||||
if (match.Groups[7].Value == "F")
|
||||
currentFg = new Color(r, g, b);
|
||||
else
|
||||
currentBg = new Color(r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
lastIndex = match.Index + match.Length;
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < ansiText.Length)
|
||||
if (lastIndex < text.Length)
|
||||
{
|
||||
var text = ansiText[lastIndex..];
|
||||
if (text.Length > 0)
|
||||
segments.Add(new ChatSegment(text, BuildAttr()));
|
||||
var t = text[lastIndex..];
|
||||
if (t.Length > 0)
|
||||
segments.Add(new ChatSegment(t, BuildAttr()));
|
||||
}
|
||||
|
||||
return segments.Count > 0 ? new ChatLine(segments) : new ChatLine("");
|
||||
}
|
||||
|
||||
// Matches: \x1b[0m (reset), \x1b[38;2;R;G;Bm (fg), or \x1b[48;2;R;G;Bm (bg)
|
||||
// Legacy: \x1b[0m, \x1b[38;2;R;G;Bm, \x1b[48;2;R;G;Bm
|
||||
[GeneratedRegex(@"\x1b\[(?:(0)|(?:(38;2|48;2);(\d{1,3});(\d{1,3});(\d{1,3})))m")]
|
||||
private static partial Regex AnsiColorRegex();
|
||||
|
||||
// New: {X} (reset), {F:RRGGBB} (foreground), {B:RRGGBB} (background)
|
||||
[GeneratedRegex(@"\{(?:(X)|(?:(F|B):([0-9A-Fa-f]{6})))\}")]
|
||||
private static partial Regex ColorTagRegex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,7 +4,7 @@ using Terminal.Gui.ViewBase;
|
||||
|
||||
namespace EchoHub.Client.UI;
|
||||
|
||||
public record CreateChannelResult(string Name, string? Topic);
|
||||
public record CreateChannelResult(string Name, string? Topic, bool IsPublic);
|
||||
|
||||
public sealed class CreateChannelDialog
|
||||
{
|
||||
@@ -12,7 +12,7 @@ public sealed class CreateChannelDialog
|
||||
{
|
||||
CreateChannelResult? result = null;
|
||||
|
||||
var dialog = new Dialog { Title = "Create Channel", Width = 50, Height = 12 };
|
||||
var dialog = new Dialog { Title = "Create Channel", Width = 50, Height = 14 };
|
||||
|
||||
var nameLabel = new Label { Text = "Name:", X = 1, Y = 1 };
|
||||
var nameField = new TextField { X = 10, Y = 1, Width = Dim.Fill(2) };
|
||||
@@ -20,11 +20,19 @@ public sealed class CreateChannelDialog
|
||||
var topicLabel = new Label { Text = "Topic:", X = 1, Y = 3 };
|
||||
var topicField = new TextField { X = 10, Y = 3, Width = Dim.Fill(2) };
|
||||
|
||||
var publicCheckbox = new CheckBox
|
||||
{
|
||||
Text = "Public (visible to all users)",
|
||||
X = 1,
|
||||
Y = 5,
|
||||
Value = CheckState.Checked
|
||||
};
|
||||
|
||||
var hintLabel = new Label
|
||||
{
|
||||
Text = "Lowercase letters, digits, hyphens, underscores (2-100 chars)",
|
||||
X = 1,
|
||||
Y = 5,
|
||||
Y = 7,
|
||||
};
|
||||
|
||||
var createButton = new Button
|
||||
@@ -32,14 +40,14 @@ public sealed class CreateChannelDialog
|
||||
Text = "Create",
|
||||
IsDefault = true,
|
||||
X = Pos.Center() - 10,
|
||||
Y = 7
|
||||
Y = 9
|
||||
};
|
||||
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
X = Pos.Center() + 5,
|
||||
Y = 7
|
||||
Y = 9
|
||||
};
|
||||
|
||||
createButton.Accepting += (s, e) =>
|
||||
@@ -55,7 +63,8 @@ public sealed class CreateChannelDialog
|
||||
if (string.IsNullOrWhiteSpace(topic))
|
||||
topic = null;
|
||||
|
||||
result = new CreateChannelResult(name, topic);
|
||||
var isPublic = publicCheckbox.Value == CheckState.Checked;
|
||||
result = new CreateChannelResult(name, topic, isPublic);
|
||||
e.Handled = true;
|
||||
app.RequestStop();
|
||||
};
|
||||
@@ -67,7 +76,7 @@ public sealed class CreateChannelDialog
|
||||
app.RequestStop();
|
||||
};
|
||||
|
||||
dialog.Add(nameLabel, nameField, topicLabel, topicField, hintLabel, createButton, cancelButton);
|
||||
dialog.Add(nameLabel, nameField, topicLabel, topicField, publicCheckbox, hintLabel, createButton, cancelButton);
|
||||
|
||||
nameField.SetFocus();
|
||||
app.Run(dialog);
|
||||
|
||||
@@ -582,6 +582,20 @@ public sealed class MainWindow : Runnable
|
||||
RefreshChannelList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure a channel exists in the left panel list (used for private channels joined via /join).
|
||||
/// </summary>
|
||||
public void EnsureChannelInList(string channelName)
|
||||
{
|
||||
if (_channelNames.Contains(channelName))
|
||||
return;
|
||||
|
||||
_channelNames.Add(channelName);
|
||||
if (!_channelMessages.ContainsKey(channelName))
|
||||
_channelMessages[channelName] = [];
|
||||
RefreshChannelList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the topic for a specific channel.
|
||||
/// </summary>
|
||||
@@ -838,10 +852,10 @@ public sealed class MainWindow : Runnable
|
||||
{
|
||||
foreach (var artLine in message.Content.Split('\n'))
|
||||
{
|
||||
// Parse ANSI color codes from colored ASCII art
|
||||
// Parse color tags from colored ASCII art
|
||||
var trimmed = artLine.TrimEnd('\r');
|
||||
if (trimmed.Contains('\x1b'))
|
||||
lines.Add(ChatLine.FromAnsi(" " + trimmed));
|
||||
if (ChatLine.HasColorTags(trimmed))
|
||||
lines.Add(ChatLine.FromColoredText(" " + trimmed));
|
||||
else
|
||||
lines.Add(new ChatLine($" {trimmed}"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user