8000 Add Messages feature by miladsoft · Pull Request #343 · block-core/angor · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add Messages feature #343

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 49 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c61fc0c
Add Messages feature
miladsoft Apr 23, 2025
2a40467
Refactor messaging system and UI updates
miladsoft Apr 24, 2025
076c936
Refactor MessageService for improved readability and maintainability
miladsoft Apr 24, 2025
7ce3f6d
Delete bitcoin.svg
miladsoft Apr 24, 2025
d184871
Refactor Messages component to integrate message service and improve …
miladsoft Apr 24, 2025
a50f74d
Update Icon component usage to specify Height and Width attributes fo…
miladsoft Apr 24, 2025
ac86a75
Enhance modal styling in PasswordComponent for improved layout
miladsoft Apr 24, 2025
1abab70
Delete Messages.razor file to remove the Messages component from the …
miladsoft Apr 24, 2025
d869405
Merge branch 'main' into message
miladsoft May 6, 2025
317f9ba
Add message and investor npub columns, improve UI for displaying npub…
miladsoft May 6, 2025
33761eb
Update button label to clarify viewing of private keys (nsec)
miladsoft May 6, 2025
53ee6cd
Remove MessageAppUrl parameter and related chat functionality from mu…
miladsoft May 8, 2025
a072e63
Rename messaging methods for clarity and consistency across components
miladsoft May 8, 2025
2775d9f
Merge branch 'main' into message
miladsoft May 8, 2025
b3fec65
Remove unused Investor Npub and Chat columns from signatures table
8000 miladsoft May 8, 2025
c4ca290
Update chat background colors for improved visibility
miladsoft May 8, 2025
5720e2e
Reorder and enhance signature table columns for better clarity and us…
miladsoft May 8, 2025
f442807
Add loading spinner to refresh button and improve error handling on m…
miladsoft May 9, 2025
1e8a12d
Refactor message handling to improve thread safety and prevent duplic…
miladsoft May 9, 2025
e65a29a
Add auto-refresh functionality for messages with timer management
miladsoft May 9, 2025
965e858
Remove OpenChatApp method to streamline chat functionality
miladsoft May 9, 2025
ef72988
Enhance display of Additional NPUB with copy functionality and improv…
miladsoft May 9, 2025
1002423
Filter displayed messages based on sender and recipient public keys f…
miladsoft May 9, 2025
0617455
Add null checks and recipientPubkey handling in message processing
miladsoft May 9, 2025
0447314
Refactor message filtering logic and ensure consistency of contact keys
miladsoft May 9, 2025
1259c4b
Refactor message handling by simplifying filtering logic and removing…
miladsoft May 9, 2025
915e9e5
Implement asynchronous loading of new messages with error handling an…
miladsoft May 9, 2025
abd6fa3
Enhance message refresh logic with local loading state management
miladsoft May 9, 2025
ca13fe3
Add shortened Npub display in message modal title
miladsoft May 9, 2025
e56d4dd
Add shortened NPUB display for signatures in the Signatures page
miladsoft May 9, 2025
e1f90f8
Add rel attribute to external link for security enhancement
miladsoft May 9, 2025
7fcab22
Add rel attribute to project explorer link for security enhancement
miladsoft May 9, 2025
38f66f7
formating
dangershony May 9, 2025
c0dd2e2
Add NsecValue parameter to MessageComponent and update Signatures pag…
miladsoft May 9, 2025
c1d463a
Refactor NsecValue handling in MessageComponent for consistency
miladsoft May 11, 2025
9315219
Remove unnecessary scroll flag update in RefreshMessagesAsync method
miladsoft May 11, 2025
4011f0b
Update loader styles for improved appearance and consistency
miladsoft May 11, 2025
bc5e42c
Update navbar and sidebar styles for improved layout and spacing
miladsoft May 11, 2025
85dca82
Enhance real-time messaging features and improve subscription handling
miladsoft May 12, 2025
170365c
Refactor signatures display to use accordion layout for improved read…
miladsoft May 12, 2025
fbf5052
Refactor MessageService to use a dictionary for direct messages and i…
miladsoft May 12, 2025
a4d4e76
Add the ability to keep a subscription open to continuously receive m…
dangershony May 12, 2025
4a6dd7f
Remove unused NsecValue parameter from MessageComponent and clean up …
miladsoft May 13, 2025
d30ed84
Refactor MessageComponent to use derived properties for contact keys …
miladsoft May 13, 2025
e5a53e0
null the private key when modal is closed
dangershony May 13, 2025
56a09b3
Rename to OnStateChange
dangershony May 13, 2025
78dc1f6
Fix the handling of keys
dangershony May 13, 2025
7262f7e
fix usings
dangershony May 13, 2025
d087164
check the delegate is not null
dangershony May 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
363 changes: 363 additions & 0 deletions src/Angor/Client/Components/MessageComponent.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
@using Angor.Client.Models
@using Angor.Shared
@using Angor.Shared.Services
@using Angor.Client.Services
@using Angor.Shared.Utilities
@using Nostr.Client.Messages
@using System.Reactive.Linq
@using System.Security.Cryptography
@using Blockcore.NBitcoin.DataEncoders
@using Nostr.Client.Keys
@implements IDisposable
@implements IAsyncDisposable

@inject IJSRuntime JS
@inject ILogger<MessageComponent> Logger
@inject IMessageService MessageService
@inject NostrConversionHelper NostrHelper

<div class="modal-wrapper">
<div class="modal fade show d-block" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content modern-modal animate-fade-in">
<div class="modal-header border-0 pb-0">
<div class="d-flex align-items-center">
<Icon IconName="chat" Height="32" Width="32" class="me-2" />
<h5 class="modal-title" title="@OtherUserNpub">@NostrHelper.GetShortenedNpub(OtherUserNpub)</h5>
</div>
<div class="d-flex align-items-center">
<div class="status-indicator me-2">
<div class="@(MessageService.IsSubscriptionActive ? "connected" : "disconnected")"
title="@(MessageService.IsSubscriptionActive ? "Connected - Real-time messages enabled" : "Disconnected - Messages may be delayed")">
</div>
</div>
<button class="btn-menu-custom" @ disabled="@(_localIsRefreshing || MessageService.IsRefreshing)">
@if (_localIsRefreshing || MessageService.IsRefreshing)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
}
else
{
<Icon IconName="refresh" Height="24" Width="24" />
}
</button>
<div class="dropdown me-2 position-relative">
<button class="btn-menu-custom" @>
<Icon IconName="menu" Height="24" Width="24" />
</button>
@if (showActionsMenu)
{
<div class="card card-body position-absolute end-0 mt-1 p-1 border rounded shadow-sm" style="z-index: 1000; min-width: 200px;">
<button class="dropdown-item d-flex align-items-center px-3 py-2" @ disabled="@MessageService.IsRefreshing">
<Icon IconName="refresh" Height="16" Width="16" class="me-2" />
<span>Refresh Messages</span>
</button>
<button class="dropdown-item d-flex align-items-center px-3 py-2" @ => CopyToClipboard(OtherUserNpub)">
<Icon IconName="copy" Height="16" Width="16" class="me-2" />
<span>Copy Contact NPUB</span>
</button>
<button class="dropdown-item d-flex align-items-center px-3 py-2" @ => CopyToClipboard(CurrentUserNpub)">
<Icon IconName="copy" Height="16" Width="16" class="me-2" />
<span>Copy Your NPUB</span>
</button>
<button class="dropdown-item d-flex align-items-center px-3 py-2" @>
<Icon IconName="key" Height="16" Width="16" class="me-2" />
<span>Copy Private Key</span>
</button>
</div>
}
</div>
<button class="btn-close-custom" @>
<Icon IconName="close-circle" Height="24" Width="24" />
</button>
</div>
</div>

<div class="modal-body py-4">
<div class="d-flex flex-column">
<!-- Messages Section -->
<div class="info-card chat-container-modal">
<div class="chat-messages" @ref="messagesContainerRef" style="height: 300px; overflow-y: auto;">
@if (MessageService.IsLoadingMessages)
{
<div class="loading-messages d-flex justify-content-center align-items-center h-100">
<div>
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading messages...</p>
</div>
</div>
}
else if (MessageService.DirectMessages == null || !MessageService.DirectMessages.Any())
{
<div class="empty-messages d-flex flex-column justify-content-center align-items-center h-100">
<Icon IconName="chat" Width="32" Height="32" />
<p class="mt-2">No messages yet. Send a message!</p>
</div>
}
else
{
<div class="messages-list p-2">
@foreach (var message in MessageService.DirectMessages.Where(m => m != null))
{
<div class="message-item @(message.IsFromCurrentUser ? "outgoing" : "incoming")">
<div class="message-bubble">
<div class="message-content">@message.Content</div>
<div class="message-timestamp">@message.Timestamp.ToLocalTime().ToString("HH:mm")</div>
</div>
</div>
}
</div>
}
</div>

<!-- Input -->
<div class="chat-input mt-3">
<div class="input-group">
<input type="text" class="form-control"
placeholder="Type a message..."
@bind="newMessage"
@ e => { if(e.Key is "Enter" or "NumpadEnter") await SendMessageAsync(); })"
disabled="@MessageService.IsSendingMessage" />
<button class="btn btn-border-success"
@>
disabled="@MessageService.IsSendingMessage">
@if (MessageService.IsSendingMessage)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
}
else
{
<Icon IconName="send" Height="20" Width="20" />
}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

@code {
public string OtherUserNpub { get; set; }
public string CurrentUserNpub { get; set; }
[Parameter] public string OtherUserPubkeyHex { get; set; }
[Parameter] public string CurrentUserPrvKeyHex { get; set; }
[Parameter] public string MessageTitle { get; set; } = "Investor";

[Parameter] public EventCallback OnClose { get; set; }
[Parameter] public EventCallback<bool> OnNsecRequest { get; set; }
[Parameter] public EventCallback<string> OnNotification { get; set; }

private ElementReference messagesContainerRef;
private string newMessage = "";
private bool showNsec = false;
private string nsecValue = "";
private bool showActionsMenu = false;
private bool _needsScroll = false;
private int _messageCount = 0;
private bool _localIsRefreshing = false;

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();

MessageService.OnStateChange += HandleMessageServiceChange;
}

protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();

if (string.IsNullOrEmpty(CurrentUserNpub) && !string.IsNullOrEmpty(CurrentUserPrvKeyHex))
{
OtherUserNpub = NostrHelper.ConvertHexToNpub(OtherUserPubkeyHex)!;
var privateKey = NostrPrivateKey.FromHex(CurrentUserPrvKeyHex);
nsecValue = privateKey.Bech32;
CurrentUserNpub = privateKey.DerivePublicKey().Bech32;
}

if (!string.IsNullOrEmpty(CurrentUserNpub) && !string.IsNullOrEmpty(OtherUserNpub))
{
await ShowMessage();
}
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_needsScroll)
{
await JS.InvokeVoidAsync("scrollToBottom", messagesContainerRef);
_needsScroll = false;
}
}

private async Task ShowMessage()
{
if (string.IsNullOrEmpty(CurrentUserNpub) ||
string.IsNullOrEmpty(OtherUserNpub))
{
await OnNotification.InvokeAsync("Missing required message information (keys/contact).");
return;
}

try
{
if (!MessageService.IsSubscriptionActive)
{
await MessageService.InitializeAsync(CurrentUserPrvKeyHex, OtherUserPubkeyHex);
_needsScroll = true;

if (MessageService.IsSubscriptionActive)
{
await OnNotification.InvokeAsync("Real-time message connection established");
}
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to initialize messaging service.");
await OnNotification.InvokeAsync($"Error initializing messages: {ex.Message}");
}
}

private async Task SendMessageAsync()
{
if (string.IsNullOrWhiteSpace(newMessage) || MessageService.IsSendingMessage) return;

string messageToSend = newMessage;
newMessage = "";

try
{
await MessageService.SendMessageAsync(messageToSend);
_needsScroll = true;
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to send message.");
await OnNotification.InvokeAsync($"Failed to send message: {ex.Message}");
newMessage = messageToSend;
}
}

private async Task RefreshMessagesAsync()
{
if (MessageService.IsRefreshing || _localIsRefreshing) return;

try
{
_localIsRefreshing = true;
await InvokeAsync(StateHasChanged);

await Task.Delay(50);

await MessageService.LoadMessagesAsync();

if (MessageService.IsSubscriptionActive)
{
await OnNotification.InvokeAsync("Real-time message connection active");
}
else
{
await Task.Delay(100);
await MessageService.LoadMessagesAsync();
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to refresh messages.");
await OnNotification.InvokeAsync($"Failed to refresh messages: {ex.Message}");
}
finally
{
_localIsRefreshing = false;
await InvokeAsync(StateHasChanged);
}
}

private async Task CopyToClipboard(string text)
{
if (string.IsNullOrEmpty(text)) return;
try
{
await JS.InvokeVoidAsync("navigator.clipboard.writeText", text);
await OnNotification.InvokeAsync("Copied to clipboard.");
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to copy text to clipboard.");
await OnNotification.InvokeAsync("Failed to copy to clipboard.");
}
showActionsMenu = false;
StateHasChanged();
}

private async Task CopyNsec()
{
if (string.IsNullOrEmpty(nsecValue))
{
await OnNotification.InvokeAsync("Private key not available.");
return;
}

await CopyToClipboard(nsecValue);
}

private void ToggleActionsMenu()
{
showActionsMenu = !showActionsMenu;
StateHasChanged();
}

private void CloseModal()
{
CurrentUserPrvKeyHex = string.Empty;
nsecValue = string.Empty;
MessageService.DisconnectSubscriptions();
OnClose.InvokeAsync();
}

private string GetMaskedNsec(string nsec)
{
if (string.IsNullOrEmpty(nsec) || nsec.Length <= 20) return nsec;
return $"{nsec.Substring(0, 10)}...{nsec.Substring(nsec.Length - 10)}";
}

private void HandleMessageServiceChange()
{
int previousCount = _messageCount;
_messageCount = MessageService.DirectMessages.Count;
if (_messageCount > previousCount)
{
_needsScroll = true;
InvokeAsync(async () =>
{
await Task.Yield();
StateHasChanged();

await JS.InvokeVoidAsync("scrollToBottom", messagesContainerRef);
});
}
else
{
InvokeAsync(StateHasChanged);
}
}

public void Dispose()
{
MessageService.OnStateChange -= HandleMessageServiceChange;
}

public async ValueTask DisposeAsync()
{
MessageService.OnStateChange -= HandleMessageServiceChange;
await ValueTask.CompletedTask;
}
}

14 changes: 14 additions & 0 deletions src/Angor/Client/Models/DirectMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;

namespace Angor.Client.Models
{
public class DirectMessage
{
public string Id { get; set; }
public string Content { get; set; }
public string SenderPubkey { get; set; }
public string RecipientPubkey { get; set; }
public DateTime Timestamp { get; set; }
public bool IsFromCurrentUser { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this as part of the class? Shouldn't we filter out any message that is not by the investor or founder?

}
}
Loading
0