Files
Relay/RelayClient/MainPage.xaml.cs

347 lines
9.9 KiB
C#

using System.Text.Json.Serialization;
using RelayClient.Crypto;
using RelayClient.Services;
using RelayShared.Rtc;
using RelayShared.Services;
namespace RelayClient;
public partial class MainPage : ContentPage
{
public static string _username;
private readonly RelaySocketClient _socket;
private readonly RtcBridgeService _rtc;
public static string? _userToken;
private string? _currentChannelId;
private string? _currentChannelName;
private readonly Dictionary<string, List<ChatMessage>> _messagesByChannel = new();
private readonly List<ChannelItem> _channels = [];
public MainPage(string username)
{
InitializeComponent();
_username = username;
UserLabel.Text = $"Logged in as: {_username}";
if (!KeyStorage.HasKeys(_username))
{
var keys = E2EeHelper.GenerateRsaKeyPair();
KeyStorage.SavePrivateKey(_username, keys.privateKey);
KeyStorage.SavePublicKey(_username, keys.publicKey);
}
var waitFor = ServerAPI.setupClient();
_socket = new RelaySocketClient(_username);
_rtc = new RtcBridgeService(
_username,
_socket,
hybridWebView,
() => _currentChannelId,
SafeSendRawToWebView
);
hybridWebView.SetInvokeJavaScriptTarget(_rtc);
_socket.Log += Console.WriteLine;
_socket.ChannelListReceived += HandleChannelList;
_socket.EncryptedChatReceived += HandleEncryptedChat;
_socket.EncryptedRtcSignalReceived += payload =>
{
MainThread.BeginInvokeOnMainThread(async () =>
{
await _rtc.HandleIncomingRtcSignalAsync(payload);
});
};
// while(!waitFor.IsCompleted){}
_socket.Connect();
}
private void SendButton_OnClicked(object? sender, EventArgs e)
{
SendMessage();
}
private void MessageEntry_OnCompleted(object? sender, EventArgs e)
{
SendMessage();
}
private void SendMessage()
{
var text = MessageEntry.Text?.Trim();
if (string.IsNullOrWhiteSpace(text))
return;
if (string.IsNullOrWhiteSpace(_socket.ServerPublicKey))
{
Console.WriteLine("Server public key not loaded yet.");
return;
}
if (string.IsNullOrWhiteSpace(_currentChannelId))
{
Console.WriteLine("No channel selected yet.");
return;
}
var encrypted = E2EeHelper.EncryptForRecipient(text, _socket.ServerPublicKey);
var payload = new SocketEncryptedMessage
{
ChannelId = _currentChannelId!,
Type = SignalType.ClientEncryptedChat,
SenderUsername = _username,
CipherText = encrypted.CipherText,
Nonce = encrypted.Nonce,
Tag = encrypted.Tag,
EncryptedKey = encrypted.EncryptedKey
};
_socket.SendJson(payload);
Console.WriteLine($"[{_username}] sent encrypted message.");
MessageEntry.Text = string.Empty;
MessageEntry.Focus();
}
private void HandleChannelList(SocketChannelList channelList)
{
_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 null) return;
_currentChannelId = defaultChannel.ChannelId;
_currentChannelName = defaultChannel.Name;
MainThread.BeginInvokeOnMainThread(async () =>
{
ChannelLabel.Text = $"#{_currentChannelName}";
RenderChannelList();
await _rtc.PushRtcContextToJsAsync();
});
_socket.SendRaw($"GET_HISTORY|{_username}|{_currentChannelId}");
}
private void HandleEncryptedChat(SocketEncryptedMessage payload) {
if (payload.RecipientUsername != _username)
return;
string decryptedText;
try
{
var privateKey = KeyStorage.LoadPrivateKey(_username);
decryptedText = E2EeHelper.DecryptForRecipient(
new EncryptedPayload
{
CipherText = payload.CipherText,
Nonce = payload.Nonce,
Tag = payload.Tag,
EncryptedKey = payload.EncryptedKey
},
privateKey
);
}
catch (Exception ex)
{
Console.WriteLine($"[{_username}] failed to decrypt chat: {ex.Message}");
return;
}
var message = new ChatMessage
{
SenderUsername = payload.SenderUsername,
Text = decryptedText,
Timestamp = DateTime.Now
};
if (!_messagesByChannel.ContainsKey(payload.ChannelId))
_messagesByChannel[payload.ChannelId] = [];
_messagesByChannel[payload.ChannelId].Add(message);
if (payload.ChannelId == _currentChannelId)
{
MainThread.BeginInvokeOnMainThread(() =>
{
RenderSingleMessage(message);
});
}
}
protected override void OnDisappearing()
{
_socket.Disconnect();
base.OnDisappearing();
}
private void RenderChannelList()
{
SidebarList.Children.Clear();
foreach (var channel in _channels.OrderBy(c => c.CreatedAt))
{
var button = new ChannelButton
{
Text = $"#{channel.Name}",
Type = channel.Type,
Group = channel.Group
};
button.Clicked += (_, _) =>
{
_currentChannelId = channel.ChannelId;
_currentChannelName = channel.Name;
MainThread.BeginInvokeOnMainThread(async () =>
{
await _rtc.PushRtcContextToJsAsync();
if (channel.Type == ChannelType.Voice)
{
if (!RtcView.IsVisible)
SwapView();
_ = _rtc.JoinRtcChannel();
}
});
ChannelLabel.Text = $"#{_currentChannelName}";
RenderCurrentChannelMessages();
if (!_messagesByChannel.ContainsKey(channel.ChannelId))
_socket.SendRaw($"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)
{
var 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);
}
private void SwapView()
{
if (RtcView.IsVisible)
{
MessagesScrollView.IsVisible = true;
RtcView.IsVisible = false;
ViewSwapped.Text = "Swap to Web View";
}
else
{
MessagesScrollView.IsVisible = false;
RtcView.IsVisible = true;
ViewSwapped.Text = "Swap to Message View";
}
}
private void SwapView_OnClicked(object? sender, EventArgs e)
{
SwapView();
}
private async void OnHybridWebViewRawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e)
{
if (e.Message == "rtc_page_ready")
{
await _rtc.PushRtcContextToJsAsync();
return;
}
SafeSendRawToWebView($"JS RAW -> C#: {e.Message}");
}
private void SafeSendRawToWebView(string message)
{
MainThread.BeginInvokeOnMainThread(() =>
{
try
{
hybridWebView.SendRawMessage(message);
}
catch (Exception ex)
{
Console.WriteLine($"[{_username}] failed to send raw message to HybridWebView: {ex.Message}");
}
});
}
public class ChannelButton : Button
{
public ChannelType Type { get; set; }
public string Group { get; set; } = string.Empty;
}
[JsonSourceGenerationOptions(WriteIndented = false)]
[JsonSerializable(typeof(RtcDescription))]
[JsonSerializable(typeof(List<RtcSignalMessage>))]
[JsonSerializable(typeof(IceCandidate))]
[JsonSerializable(typeof(List<IceCandidate>))]
[JsonSerializable(typeof(string))]
internal partial class HybridJSType : JsonSerializerContext
{
}
}