Update: Mutli Channel Support
This commit is contained in:
@@ -5,26 +5,60 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
Title="Relay Client">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto" Padding="12" RowSpacing="10">
|
||||
<Grid RowDefinitions="Auto,*,Auto"
|
||||
ColumnDefinitions="220,*"
|
||||
Padding="12"
|
||||
RowSpacing="10"
|
||||
ColumnSpacing="10">
|
||||
|
||||
<Border Grid.Row="0" StrokeThickness="1" Padding="10">
|
||||
<!-- Header -->
|
||||
<Border Grid.Row="0"
|
||||
Grid.ColumnSpan="2"
|
||||
StrokeThickness="1"
|
||||
Padding="10">
|
||||
<VerticalStackLayout Spacing="4">
|
||||
<Label x:Name="UserLabel"
|
||||
Text="Logged in as: Unknown"
|
||||
FontAttributes="Bold"
|
||||
FontSize="18" />
|
||||
<Label Text="#general"
|
||||
<Label x:Name="ChannelLabel"
|
||||
Text="No channel selected"
|
||||
FontSize="14" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1" StrokeThickness="1" Padding="10">
|
||||
<ScrollView x:Name="MessagesScrollView">
|
||||
<VerticalStackLayout x:Name="MessagesLayout" Spacing="8" />
|
||||
<!-- Sidebar -->
|
||||
<Border Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
StrokeThickness="1"
|
||||
Padding="10">
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="Channels"
|
||||
FontAttributes="Bold"
|
||||
FontSize="16" />
|
||||
<VerticalStackLayout x:Name="SidebarList"
|
||||
Spacing="6" />
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="*,Auto" ColumnSpacing="10">
|
||||
<!-- Messages -->
|
||||
<Border Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
StrokeThickness="1"
|
||||
Padding="10">
|
||||
<ScrollView x:Name="MessagesScrollView">
|
||||
<VerticalStackLayout x:Name="MessagesLayout"
|
||||
Spacing="8" />
|
||||
</ScrollView>
|
||||
</Border>
|
||||
|
||||
<!-- Input -->
|
||||
<Grid Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="10">
|
||||
<Entry x:Name="MessageEntry"
|
||||
Grid.Column="0"
|
||||
Placeholder="Type a message..."
|
||||
|
||||
@@ -8,8 +8,13 @@ namespace RelayClient;
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
private readonly string _username;
|
||||
private readonly WebSocket wsc;
|
||||
private readonly WebSocket _wsc;
|
||||
private string? _serverPublicKey;
|
||||
private string? _currentChannelId;
|
||||
private string? _currentChannelName;
|
||||
|
||||
private readonly Dictionary<string, List<ChatMessage>> _messagesByChannel = new();
|
||||
private readonly List<ChannelItem> _channels = new();
|
||||
|
||||
public MainPage(string username)
|
||||
{
|
||||
@@ -25,15 +30,15 @@ public partial class MainPage : ContentPage
|
||||
KeyStorage.SavePublicKey(_username, keys.publicKey);
|
||||
}
|
||||
|
||||
wsc = new WebSocket("ws://localhost:1337/");
|
||||
_wsc = new WebSocket("ws://localhost:1337/");
|
||||
|
||||
wsc.OnMessage += WscOnMessage;
|
||||
wsc.Connect();
|
||||
_wsc.OnMessage += WscOnMessage;
|
||||
_wsc.Connect();
|
||||
|
||||
var publicKey = KeyStorage.LoadPublicKey(_username);
|
||||
wsc.Send($"REGISTER_KEY|{_username}|{publicKey}");
|
||||
wsc.Send("GET_SERVER_KEY");
|
||||
wsc.Send($"GET_HISTORY|{_username}");
|
||||
_wsc.Send($"REGISTER_KEY|{_username}|{publicKey}");
|
||||
_wsc.Send("GET_SERVER_KEY");
|
||||
_wsc.Send("GET_CHANNELS");
|
||||
}
|
||||
|
||||
private void SendButton_OnClicked(object? sender, EventArgs e)
|
||||
@@ -63,6 +68,7 @@ public partial class MainPage : ContentPage
|
||||
|
||||
var payload = new SocketEncryptedMessage
|
||||
{
|
||||
ChannelId = _currentChannelId!,
|
||||
Type = "client_encrypted_chat",
|
||||
SenderUsername = _username,
|
||||
CipherText = encrypted.CipherText,
|
||||
@@ -72,7 +78,7 @@ public partial class MainPage : ContentPage
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
wsc.Send(json);
|
||||
_wsc.Send(json);
|
||||
|
||||
Console.WriteLine($"[{_username}] sent encrypted message.");
|
||||
|
||||
@@ -100,6 +106,38 @@ public partial class MainPage : ContentPage
|
||||
|
||||
var type = typeElement.GetString();
|
||||
|
||||
if (type == "channel_list")
|
||||
{
|
||||
var channelList = JsonSerializer.Deserialize<SocketChannelList>(e.Data);
|
||||
if (channelList is null)
|
||||
return;
|
||||
|
||||
_channels.Clear();
|
||||
_channels.AddRange(channelList.Channels.OrderBy(c => c.CreatedAt));
|
||||
|
||||
var defaultChannel = _channels
|
||||
.Where(c => c.Name.Equals("welcome", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(c => c.CreatedAt)
|
||||
.FirstOrDefault()
|
||||
?? _channels.OrderBy(c => c.CreatedAt).FirstOrDefault();
|
||||
|
||||
if (defaultChannel is not null)
|
||||
{
|
||||
_currentChannelId = defaultChannel.ChannelId;
|
||||
_currentChannelName = defaultChannel.Name;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
ChannelLabel.Text = $"#{_currentChannelName}";
|
||||
RenderChannelList();
|
||||
});
|
||||
|
||||
_wsc.Send($"GET_HISTORY|{_username}|{_currentChannelId}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "server_public_key")
|
||||
{
|
||||
var serverKeyMessage = JsonSerializer.Deserialize<ServerPublicKeyMessage>(e.Data);
|
||||
@@ -146,48 +184,20 @@ public partial class MainPage : ContentPage
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(async () =>
|
||||
if (!_messagesByChannel.ContainsKey(payload.ChannelId))
|
||||
{
|
||||
bool isOwnMessage = message.SenderUsername == _username;
|
||||
_messagesByChannel[payload.ChannelId] = [];
|
||||
}
|
||||
|
||||
var bubble = new Border
|
||||
_messagesByChannel[payload.ChannelId].Add(message);
|
||||
|
||||
if (payload.ChannelId == _currentChannelId)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
StrokeThickness = 1,
|
||||
Padding = 10,
|
||||
Margin = isOwnMessage
|
||||
? new Thickness(40, 0, 0, 0)
|
||||
: new Thickness(0, 0, 40, 0),
|
||||
HorizontalOptions = isOwnMessage
|
||||
? LayoutOptions.End
|
||||
: LayoutOptions.Start,
|
||||
Content = new VerticalStackLayout
|
||||
{
|
||||
Spacing = 2,
|
||||
Children =
|
||||
{
|
||||
new Label
|
||||
{
|
||||
Text = message.SenderUsername,
|
||||
FontAttributes = FontAttributes.Bold,
|
||||
FontSize = 12
|
||||
},
|
||||
new Label
|
||||
{
|
||||
Text = message.Text,
|
||||
FontSize = 14
|
||||
},
|
||||
new Label
|
||||
{
|
||||
Text = message.Timestamp.ToString("h:mm tt"),
|
||||
FontSize = 10
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MessagesLayout.Children.Add(bubble);
|
||||
await MessagesScrollView.ScrollToAsync(MessagesLayout, ScrollToPosition.End, true);
|
||||
});
|
||||
RenderSingleMessage(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -197,8 +207,83 @@ public partial class MainPage : ContentPage
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
wsc.OnMessage -= WscOnMessage;
|
||||
wsc.Close();
|
||||
_wsc.OnMessage -= WscOnMessage;
|
||||
_wsc.Close();
|
||||
base.OnDisappearing();
|
||||
}
|
||||
|
||||
private void RenderChannelList()
|
||||
{
|
||||
SidebarList.Children.Clear();
|
||||
|
||||
foreach (var channel in _channels.OrderBy(c => c.CreatedAt))
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
Text = $"#{channel.Name}"
|
||||
};
|
||||
|
||||
button.Clicked += (_, _) =>
|
||||
{
|
||||
_currentChannelId = channel.ChannelId;
|
||||
_currentChannelName = channel.Name;
|
||||
|
||||
ChannelLabel.Text = $"#{_currentChannelName}";
|
||||
RenderCurrentChannelMessages();
|
||||
|
||||
if (!_messagesByChannel.ContainsKey(channel.ChannelId))
|
||||
{
|
||||
_wsc.Send($"GET_HISTORY|{_username}|{channel.ChannelId}");
|
||||
}
|
||||
};
|
||||
|
||||
SidebarList.Children.Add(button);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderCurrentChannelMessages()
|
||||
{
|
||||
MessagesLayout.Children.Clear();
|
||||
|
||||
if (_currentChannelId is null)
|
||||
return;
|
||||
|
||||
if (!_messagesByChannel.TryGetValue(_currentChannelId, out var messages))
|
||||
return;
|
||||
|
||||
foreach (var message in messages.OrderBy(m => m.Timestamp))
|
||||
{
|
||||
RenderSingleMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private async void RenderSingleMessage(ChatMessage message)
|
||||
{
|
||||
bool isOwnMessage = message.SenderUsername == _username;
|
||||
|
||||
var bubble = new Border
|
||||
{
|
||||
StrokeThickness = 1,
|
||||
Padding = 10,
|
||||
Margin = isOwnMessage
|
||||
? new Thickness(40, 0, 0, 0)
|
||||
: new Thickness(0, 0, 40, 0),
|
||||
HorizontalOptions = isOwnMessage
|
||||
? LayoutOptions.End
|
||||
: LayoutOptions.Start,
|
||||
Content = new VerticalStackLayout
|
||||
{
|
||||
Spacing = 2,
|
||||
Children =
|
||||
{
|
||||
new Label { Text = message.SenderUsername, FontAttributes = FontAttributes.Bold, FontSize = 12 },
|
||||
new Label { Text = message.Text, FontSize = 14 },
|
||||
new Label { Text = message.Timestamp.ToString("h:mm tt"), FontSize = 10 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MessagesLayout.Children.Add(bubble);
|
||||
await MessagesScrollView.ScrollToAsync(MessagesLayout, ScrollToPosition.End, true);
|
||||
}
|
||||
}
|
||||
8
RelayClient/Models/ChannelItem.cs
Normal file
8
RelayClient/Models/ChannelItem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace RelayClient.Models;
|
||||
|
||||
public class ChannelItem
|
||||
{
|
||||
public required string ChannelId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required DateTime CreatedAt { get; set; }
|
||||
}
|
||||
7
RelayClient/Models/SocketChannelList.cs
Normal file
7
RelayClient/Models/SocketChannelList.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace RelayClient.Models;
|
||||
|
||||
public class SocketChannelList
|
||||
{
|
||||
public required string Type { get; set; }
|
||||
public required List<ChannelItem> Channels { get; set; }
|
||||
}
|
||||
@@ -5,6 +5,7 @@ public class SocketEncryptedMessage
|
||||
public required string Type { get; set; }
|
||||
public required string SenderUsername { get; set; }
|
||||
public string? RecipientUsername { get; set; }
|
||||
public required string ChannelId { get; set; }
|
||||
public required string CipherText { get; set; }
|
||||
public required string Nonce { get; set; }
|
||||
public required string Tag { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user