diff --git a/src/NuSocial/AppShell.xaml b/src/NuSocial/AppShell.xaml index 9bd0517..d38a265 100644 --- a/src/NuSocial/AppShell.xaml +++ b/src/NuSocial/AppShell.xaml @@ -10,7 +10,7 @@ xmlns:viewModels="clr-namespace:NuSocial.ViewModels" xmlns:views="clr-namespace:NuSocial.Views" x:DataType="viewModels:ShellViewModel" - FlyoutBackdrop="{DynamicResource Gray500}" + FlyoutBackdrop="{DynamicResource Gray500Translucent}" FlyoutBackgroundColor="{DynamicResource Primary}" FlyoutIsPresented="{Binding IsPresented, Mode=TwoWay}" Shell.BackgroundColor="{DynamicResource Primary}"> @@ -40,28 +40,14 @@ Title="{loc:Translate Home}" Icon="{x:Static fa:FontAwesomeIcons.House}" Route="main"> - - + - - + diff --git a/src/NuSocial/Converters/ContentStringToMarkdownHtmlConverter.cs b/src/NuSocial/Converters/ContentStringToMarkdownHtmlConverter.cs new file mode 100644 index 0000000..db08edf --- /dev/null +++ b/src/NuSocial/Converters/ContentStringToMarkdownHtmlConverter.cs @@ -0,0 +1,34 @@ +using Markdig; +using Markdig.Renderers.Roundtrip; +using Markdig.Syntax; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace NuSocial.Converters; + +public class ContentStringToMarkdownHtmlConverter : IValueConverter +{ + private static MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().UseEmphasisExtras().UseGridTables().UsePipeTables().UseTaskLists().UseAutoLinks().Build(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + try + { + return Markdown.Parse((string)value, true).ToHtml(Pipeline); + } + catch (Exception) + { + return (string)value; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + diff --git a/src/NuSocial/GlobalSetting.cs b/src/NuSocial/GlobalSetting.cs new file mode 100644 index 0000000..ab9bb13 --- /dev/null +++ b/src/NuSocial/GlobalSetting.cs @@ -0,0 +1,43 @@ +using CommunityToolkit.Mvvm.Messaging; +using Nostr.Client.Keys; +using NuSocial.Messages; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NuSocial +{ + public class GlobalSetting + { + private User? _user; + + public static GlobalSetting Instance { get; } = new GlobalSetting(); + + public User CurrentUser + { + get => _user; + set + { + if (value?.PublicKey != null) + { + _user = value; + UpdateUser(_user.PublicKey, _user.PrivateKey); + } + } + } + + private static void UpdateUser(NostrPublicKey publicKey, NostrPrivateKey? privateKey) + { + if (privateKey != null) + { + WeakReferenceMessenger.Default.Send(new((publicKey.Hex, privateKey.Hex))); + } + else + { + WeakReferenceMessenger.Default.Send(new((publicKey.Hex, string.Empty))); + } + } + } +} diff --git a/src/NuSocial/MauiProgram.cs b/src/NuSocial/MauiProgram.cs index 8fd18e4..92243f0 100644 --- a/src/NuSocial/MauiProgram.cs +++ b/src/NuSocial/MauiProgram.cs @@ -5,6 +5,9 @@ using Microsoft.Extensions.Logging; using Mopups.Hosting; using NuSocial.Core.Threading; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.FastConsole; using SkiaSharp.Views.Maui.Controls.Hosting; using System.Reflection; using Volo.Abp; @@ -16,9 +19,10 @@ public static class MauiProgram { public static MauiApp CreateMauiApp() { - var builder = MauiApp.CreateBuilder(); - builder - .UseMauiApp() + var builder = MauiApp.CreateBuilder(); + SetupSerilog(); + builder + .UseMauiApp() .UseMauiCommunityToolkit(options => { options.SetShouldSuppressExceptionsInConverters(true); @@ -46,15 +50,29 @@ public static MauiApp CreateMauiApp() options.Services.ReplaceConfiguration(builder.Configuration); }); - AddDebugLogging(builder.Logging); + AddDebugLogging(builder.Logging); var app = builder.Build(); app.Services.GetRequiredService().Initialize(app.Services); return app; - } - + } + + private static void SetupSerilog() + { + var flushInterval = new TimeSpan(0, 0, 1); + var file = Path.Combine(FileSystem.AppDataDirectory, "NuSocial.log"); + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .Enrich.FromLogContext() + //.WriteTo.File(file, flushToDiskInterval: flushInterval, encoding: System.Text.Encoding.UTF8, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 22) + .WriteTo.FastConsole() + .CreateLogger(); + } + [Conditional("DEBUG")] private static void AddDebugLogging(ILoggingBuilder logging) { @@ -87,12 +105,15 @@ private static void ConfigureFromConfigurationOptions(MauiAppBuilder builder) } private static ContainerBuilder GetAutofacContainerBuilder(IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddLocalization(); - - var builder = new Autofac.ContainerBuilder(); + { + var db = new LocalStorage(); + services.AddSingleton(db); + services.AddSingleton(); + services.AddSingleton(new NostrService(db)); + services.AddLocalization(); + services.AddLogging(logging => logging.AddSerilog()); + + var builder = new Autofac.ContainerBuilder(); SetupAutofacDebug(builder); diff --git a/src/NuSocial/Messages/LogoutMessage.cs b/src/NuSocial/Messages/LogoutMessage.cs index 94d84e1..97df2b4 100644 --- a/src/NuSocial/Messages/LogoutMessage.cs +++ b/src/NuSocial/Messages/LogoutMessage.cs @@ -1,24 +1,24 @@ -using CommunityToolkit.Mvvm.Messaging.Messages; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuSocial.Messages -{ - public class LogoutMessage : ValueChangedMessage - { - public LogoutMessage(bool? value = null) : base(value) - { - } - } - - public class ResetNavMessage : ValueChangedMessage - { - public ResetNavMessage(string? route = null) : base(route) - { - } - } -} - +using CommunityToolkit.Mvvm.Messaging.Messages; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NuSocial.Messages +{ + public class LogoutMessage : ValueChangedMessage + { + public LogoutMessage(bool? value = null) : base(value) + { + } + } + + public class ResetNavMessage : ValueChangedMessage + { + public ResetNavMessage(string? route = null) : base(route) + { + } + } +} + diff --git a/src/NuSocial/Messages/NostrMessages.cs b/src/NuSocial/Messages/NostrMessages.cs new file mode 100644 index 0000000..5ec1c80 --- /dev/null +++ b/src/NuSocial/Messages/NostrMessages.cs @@ -0,0 +1,20 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; +using Nostr.Client.Keys; + +namespace NuSocial.Messages +{ + public class NostrUserChangedMessage : ValueChangedMessage<(string pubKey, string privKey)> + { + public NostrUserChangedMessage((string pubKey, string privKey) value) : base(value) + { + } + } + + public class NostrPostMessage : ValueChangedMessage + { + public NostrPostMessage(string? value = null) : base(value) + { + } + } +} + diff --git a/src/NuSocial/Models/Relay.cs b/src/NuSocial/Models/Relay.cs index e2b10ff..aea9e58 100644 --- a/src/NuSocial/Models/Relay.cs +++ b/src/NuSocial/Models/Relay.cs @@ -56,7 +56,13 @@ public Uri? Uri public class Post { + [Ignore] + public Contact Contact { get; set; } + + public string ContactId => Contact.PublicKey; + public string Content { get; set; } + public DateTime CreatedAt { get; set; } [PrimaryKey] public string Hash { get; set; } diff --git a/src/NuSocial/Models/User.cs b/src/NuSocial/Models/User.cs index e53f0cf..88157a4 100644 --- a/src/NuSocial/Models/User.cs +++ b/src/NuSocial/Models/User.cs @@ -1,45 +1,101 @@ -using Nostr.Client.Keys; -using SQLite; - -namespace NuSocial.Models; - -public class User -{ - [Ignore] - public NostrPublicKey? PublicKey { get; set; } - - [Ignore] +using Nostr.Client.Keys; +using SQLite; +using System.Text.Json.Serialization; +using System.Xml.Linq; + +namespace NuSocial.Models; + +public class User +{ + [Ignore] + public NostrPublicKey? PublicKey { get; set; } + + [Ignore] public NostrPrivateKey? PrivateKey { get; set; } public string? ProfileImageBlurHash { get; set; } - - [PrimaryKey] - public string PublicKeyString - { - get => PublicKey?.Bech32 ?? string.Empty; - set => PublicKey = NostrPublicKey.FromBech32(value); - } - - public string PrivateKeyString - { - get => PrivateKey?.Bech32 ?? string.Empty; - set => PrivateKey = NostrPrivateKey.FromBech32(value); - } - - public bool IsReadOnly => PublicKey != null && PrivateKey == null; -} - -public class AuthenticateResult -{ - public NostrKeyPair KeyPair { get; set; } -} - -public class AuthenticateRequest -{ - -} - -public class UserConfiguration -{ - -} \ No newline at end of file + + [PrimaryKey] + public string PublicKeyString + { + get => PublicKey?.Bech32 ?? string.Empty; + set => PublicKey = NostrPublicKey.FromBech32(value); + } + + public string PrivateKeyString + { + get => PrivateKey?.Bech32 ?? string.Empty; + set => PrivateKey = NostrPrivateKey.FromBech32(value); + } + + public bool IsReadOnly => PublicKey != null && PrivateKey == null; +} + +public class AuthenticateResult +{ + public NostrKeyPair KeyPair { get; set; } +} + +public class AuthenticateRequest +{ + +} + +public class UserConfiguration +{ + +} + +/// +/// A simple model for a contact. +/// +/// Gets the name of the contact. +/// Gets the email of the contact. +/// Gets the picture of the contact. +public sealed record Contact +{ + [JsonPropertyName("name")] + public Name Name { get; set; } + + [JsonIgnore] + public string PetName { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonIgnore] + public string? Nip05 { get; set; } + + [JsonIgnore] + public string PublicKey { get; set; } + + [JsonPropertyName("picture")] + public Picture Picture { get; set; } + + public override string ToString() + { + return Name?.ToString() ?? PublicKey; + } +} + +/// +/// A simple model for the name of a contact. +/// +/// The first name of the contact. +/// The last name of the contact. +public sealed record Name( + [property: JsonPropertyName("first")] string First, + [property: JsonPropertyName("last")] string Last) +{ + /// + public override string ToString() + { + return $"{First} {Last}"; + } +} + +/// +/// A simple model for the picture of a contact. +/// +/// The URL of the picture. +public sealed record Picture([property: JsonPropertyName("thumbnail")] Uri Url); \ No newline at end of file diff --git a/src/NuSocial/NuSocial.csproj b/src/NuSocial/NuSocial.csproj index 8c8f951..7cff041 100644 --- a/src/NuSocial/NuSocial.csproj +++ b/src/NuSocial/NuSocial.csproj @@ -68,6 +68,10 @@ + + + + diff --git a/src/NuSocial/Resources/Styles/Colors.xaml b/src/NuSocial/Resources/Styles/Colors.xaml index ba44a51..cb7e13c 100644 --- a/src/NuSocial/Resources/Styles/Colors.xaml +++ b/src/NuSocial/Resources/Styles/Colors.xaml @@ -1,51 +1,52 @@ - - - - - #FF373063 - #FFDFD8F7 - #FF2B0B98 - White - Black - #FFF8F8F8 - #FFE1E1E1 - #FFC8C8C8 - #FFACACAC - #FF919191 - #FF6E6E6E - #FF404040 - #FF212121 - #FF141414 - - #F7B548 - - - - - - - - - - - - - - - - #FFF7B548 - #FFFFD590 - #FFFFE5B9 - #FF28C2D1 - #FF7BDDEF - #FFC3F2F4 - #FF3E8EED - #FF72ACF1 - #FFA7CBF6 - - - - - - - + + + + + #FF373063 + #FFDFD8F7 + #FF2B0B98 + White + Black + #FFF8F8F8 + #FFE1E1E1 + #FFC8C8C8 + #FFACACAC + #FF919191 + #FF6E6E6E + #996E6E6E + #FF404040 + #FF212121 + #FF141414 + + #F7B548 + + + + + + + + + + + + + + + + #FFF7B548 + #FFFFD590 + #FFFFE5B9 + #FF28C2D1 + #FF7BDDEF + #FFC3F2F4 + #FF3E8EED + #FF72ACF1 + #FFA7CBF6 + + + + + + + diff --git a/src/NuSocial/Services/NostrService.cs b/src/NuSocial/Services/NostrService.cs index b6205c7..cda9c3e 100644 --- a/src/NuSocial/Services/NostrService.cs +++ b/src/NuSocial/Services/NostrService.cs @@ -1,16 +1,299 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuSocial.Services -{ - public interface INostrService - { - - } - public class NostrService : INostrService - { - } -} +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Extensions.Logging.Abstractions; +using Nostr.Client.Client; +using Nostr.Client.Communicator; +using Nostr.Client.Keys; +using Nostr.Client.Requests; +using Serilog; +using Nostr.Client.Responses; +using NuSocial.Messages; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Nostr.Client.Messages.Metadata; +using Nostr.Client.Messages; +using Websocket.Client.Models; +using System.Security.Cryptography.X509Certificates; + +namespace NuSocial.Services +{ + public interface INostrService + { + NostrClientStreams? Streams { get; } + + void Dispose(); + void RegisterFilter(string subscription, NostrFilter filter); + void StartNostr(); + void StopNostr(); + } + public class NostrService : INostrService, IDisposable + { + private readonly IDatabase _db; + private NostrMultiWebsocketClient? _client; + private INostrCommunicator[]? _communicators; + private bool _isDisposed; + + private readonly Dictionary _subscriptionToFilter = new(); + + public NostrService(IDatabase db) + { + _db = db; + WeakReferenceMessenger.Default.Register(this, (r, m) => + { + Receive(m); + }); + } + + public async void Receive(NostrUserChangedMessage message) + { + if (message is null) + { + throw new ArgumentNullException(nameof(message)); + } + + if (message.Value is (string pubKey, string privKey)) + { + if (!string.IsNullOrEmpty(privKey)) + { + var keyPair = NostrKeyPair.From(NostrPrivateKey.FromHex(privKey)); + await Setup(keyPair); + } + else if (!string.IsNullOrEmpty(pubKey)) + { + + } + else + { + // nothing usable + } + } + } + + public NostrClientStreams? Streams => _client?.Streams; + + private async Task Setup(NostrKeyPair keyPair) + { + try + { + _communicators = await CreateCommunicatorsAsync(); + + _client = new NostrMultiWebsocketClient(NullLogger.Instance, _communicators); + + _client.Streams.EventStream.Subscribe(HandleEvent); + + RegisterFilter(keyPair.PublicKey.Hex, new NostrFilter() + { + Kinds = new[] + { + NostrKind.ShortTextNote + }, + Limit = 0 + }); + + StartNostr(); + } + catch (Exception) + { + throw; + } + } + + public void RegisterFilter(string subscription, NostrFilter filter) + { + _subscriptionToFilter[subscription] = filter; + } + + public void StartNostr() + { + if (_communicators != null) + { + foreach (var comm in _communicators) + { + // fire and forget + _ = comm.Start(); + } + } + } + + public void StopNostr() + { + if (_communicators != null) + { + foreach (var comm in _communicators) + { + // fire and forget + _ = comm.Stop(WebSocketCloseStatus.NormalClosure, string.Empty); + } + } + } + + private async Task CreateCommunicatorsAsync() + { + var relays = await _db.GetRelaysAsync(); + + if (!relays.Any()) + { + // make sure there's at least one + relays.Add(new Relay("wss://relay.damus.io")); + await _db.UpdateRelaysAsync(relays); + } + + return relays.Where(x => x.Uri != null).Select(x => CreateCommunicator(x.Uri!)).ToArray(); + } + + private INostrCommunicator CreateCommunicator(Uri uri) + { + var comm = new NostrWebsocketCommunicator(uri, () => + { + var client = new ClientWebSocket(); + client.Options.SetRequestHeader("Origin", "http://localhost"); + return client; + }); + + comm.Name = uri.Host; + comm.ReconnectTimeout = null; //TimeSpan.FromSeconds(30); + comm.ErrorReconnectTimeout = TimeSpan.FromSeconds(60); + + comm.ReconnectionHappened.Subscribe(info => OnCommunicatorReconnection(info, comm.Name)); + comm.DisconnectionHappened.Subscribe(info => + Log.Information("[{relay}] Disconnected, type: {type}, reason: {reason}", comm.Name, info.Type, info.CloseStatus)); + return comm; + } + + private void OnCommunicatorReconnection(ReconnectionInfo info, string communicatorName) + { + try + { + if (_client == null) return; + + Log.Information("[{relay}] Reconnected, sending Nostr filters ({filterCount})", communicatorName, _subscriptionToFilter.Count); + + var client = _client.FindClient(communicatorName); + if (client == null) + { + Log.Warning("[{relay}] Cannot find client", communicatorName); + return; + } + + foreach (var (sub, filter) in _subscriptionToFilter) + { + client.Send(new NostrRequest(sub, filter)); + } + } + catch (Exception e) + { + Log.Error(e, "[{relay}] Failed to process reconnection, error: {error}", communicatorName, e.Message); + } + } + + private void HandleEvent(NostrEventResponse response) + { + if (response is null) + { + throw new ArgumentNullException(nameof(response)); + } + var ev = response.Event; + Log.Information("{kind}: {content}", ev?.Kind, ev?.Content); + + if (ev is NostrMetadataEvent evm) + { + Log.Information("Name: {name}, about: {about}", evm.Metadata?.Name, evm.Metadata?.About); + } + + if (response.Event != null && response.Event.IsSignatureValid()) + { + switch (response.Event.Kind) + { + case NostrKind.Metadata: + break; + case NostrKind.ShortTextNote: + WeakReferenceMessenger.Default.Send(new(ev?.Content)); + break; + case NostrKind.RecommendRelay: + break; + case NostrKind.Contacts: + break; + case NostrKind.EncryptedDm: + break; + case NostrKind.EventDeletion: + break; + case NostrKind.Reserved: + break; + case NostrKind.Reaction: + break; + case NostrKind.BadgeAward: + break; + case NostrKind.ChannelCreation: + break; + case NostrKind.ChannelMetadata: + break; + case NostrKind.ChannelMessage: + break; + case NostrKind.ChannelHideMessage: + break; + case NostrKind.ChanelMuteUser: + break; + case NostrKind.Reporting: + break; + case NostrKind.ZapRequest: + break; + case NostrKind.Zap: + break; + case NostrKind.RelayListMetadata: + break; + case NostrKind.ClientAuthentication: + break; + case NostrKind.NostrConnect: + break; + case NostrKind.ProfileBadges: + break; + case NostrKind.BadgeDefinition: + break; + case NostrKind.LongFormContent: + break; + case NostrKind.ApplicationSpecificData: + break; + default: + break; + } + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _client?.Dispose(); + if (_communicators != null) + { + foreach (var comm in _communicators) + { + comm.Dispose(); + } + } + } + + _isDisposed = true; + } + } + + ~NostrService() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/NuSocial/ViewModels/LoginViewModel.cs b/src/NuSocial/ViewModels/LoginViewModel.cs index 7acca32..f908f9d 100644 --- a/src/NuSocial/ViewModels/LoginViewModel.cs +++ b/src/NuSocial/ViewModels/LoginViewModel.cs @@ -1,67 +1,68 @@ -using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.Configuration; -using Nostr.Client.Keys; -using NuSocial.Core.Validation; -using NuSocial.Core.ViewModel; -using NuSocial.Messages; -using System.ComponentModel.DataAnnotations; -using Volo.Abp.DependencyInjection; - -namespace NuSocial.ViewModels; - -public partial class LoginViewModel : BaseFormModel, ITransientDependency +using Nostr.Client.Keys; +using NuSocial.Core.Validation; +using NuSocial.Core.ViewModel; +using NuSocial.Messages; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.DependencyInjection; + +namespace NuSocial.ViewModels; + +public partial class LoginViewModel : BaseFormModel, ITransientDependency { private readonly IConfiguration _configuration; - private readonly IDatabase _db; - - [Required] - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsAccountKeyValid))] - [NostrKeyValid] - private string _accountKey = string.Empty; - + private readonly IDatabase _db; + + [Required] + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsAccountKeyValid))] + [NostrKeyValid] + private string _accountKey = string.Empty; + public LoginViewModel(IDialogService dialogService, INavigationService navigationService, IConfiguration configuration, - IDatabase db) : base(dialogService, navigationService) - { + IDatabase db) : base(dialogService, navigationService) + { Title = L["Login"]; _configuration = configuration; - _db = db; - } - - public bool IsAccountKeyValid => !string.IsNullOrEmpty(AccountKey); - - [RelayCommand(CanExecute = nameof(IsNotBusy))] - private Task LoginAsync() - { - return SetBusyAsync(() => - { - return ValidateAsync(async (isValid) => - { - if (isValid) - { - if (AccountKey.StartsWith("npub", StringComparison.OrdinalIgnoreCase)) - { - var pubKey = NostrPublicKey.FromBech32(AccountKey); - - var user = new User() { PublicKey = pubKey }; - await _db.UpdateUsersAsync(new ObservableCollection { user }); - await Navigation.NavigateTo("//main", user); - } - else - { - var privKey = NostrPrivateKey.FromBech32(AccountKey); - var pubKeyEc = privKey.Ec.CreateXOnlyPubKey(); - var pubKey = NostrPublicKey.FromEc(pubKeyEc); - - var user = new User() { PublicKey = pubKey, PrivateKey = privKey }; - await _db.UpdateUsersAsync(new ObservableCollection { user }); - await Navigation.NavigateTo("//main", user); - } - } - }, async () => await ShowErrorsCommand.ExecuteAsync(null)); - }); + _db = db; + } + + public bool IsAccountKeyValid => !string.IsNullOrEmpty(AccountKey); + + [RelayCommand(CanExecute = nameof(IsNotBusy))] + private Task LoginAsync() + { + return SetBusyAsync(() => + { + return ValidateAsync(async (isValid) => + { + if (isValid) + { + if (AccountKey.StartsWith("npub", StringComparison.OrdinalIgnoreCase)) + { + var pubKey = NostrPublicKey.FromBech32(AccountKey); + + var user = new User() { PublicKey = pubKey }; + await _db.UpdateUsersAsync(new ObservableCollection { user }); + await Navigation.NavigateTo("//main", user); + } + else + { + var privKey = NostrPrivateKey.FromBech32(AccountKey); + var pubKeyEc = privKey.Ec.CreateXOnlyPubKey(); + var pubKey = NostrPublicKey.FromEc(pubKeyEc); + + var user = new User() { PublicKey = pubKey, PrivateKey = privKey }; + await _db.UpdateUsersAsync(new ObservableCollection { user }); + GlobalSetting.Instance.CurrentUser = user; + await Navigation.NavigateTo("//main", user); + } + } + }, async () => await ShowErrorsCommand.ExecuteAsync(null)); + }); } public override Task OnFirstAppear() diff --git a/src/NuSocial/ViewModels/MainViewModel.cs b/src/NuSocial/ViewModels/MainViewModel.cs index 1a80ce2..18d6feb 100644 --- a/src/NuSocial/ViewModels/MainViewModel.cs +++ b/src/NuSocial/ViewModels/MainViewModel.cs @@ -1,35 +1,124 @@ using CommunityToolkit.Mvvm.Messaging; using NuSocial.Core.ViewModel; using NuSocial.Messages; +using System.Collections.Immutable; using Volo.Abp.DependencyInjection; namespace NuSocial.ViewModels; public partial class MainViewModel : BaseViewModel, ITransientDependency { - [ObservableProperty] - private User? _user; - + [ObservableProperty] + private ObservableCollection _posts; + + private List _postsWaiting = new(); + + [ObservableProperty] + private User? _user; + public MainViewModel(IDialogService dialogService, INavigationService navigationService) : base(dialogService, navigationService) - { - } - + { + } + + public string UnreadLabel => $"{L["Unread"]} ({_postsWaiting.Count})"; + public override Task OnFirstAppear() { - WeakReferenceMessenger.Default.Send(new()); + WeakReferenceMessenger.Default.Send(new()); + WeakReferenceMessenger.Default.Register(this, (r, m) => + { + UpdateUser(); + }); + Posts = new ObservableCollection(); + WeakReferenceMessenger.Default.Register(this, (r, m) => + { + ReceivePost(m.Value); + }); return Task.CompletedTask; - } - + } + public override Task OnParameterSet() { return SetBusyAsync(() => { if (NavigationParameter is User user) { - User = user; + GlobalSetting.Instance.CurrentUser = user; } return Task.CompletedTask; }); - } + } + + [RelayCommand(CanExecute = nameof(IsNotBusy))] + private Task BoostPostAsync() + { + return SetBusyAsync(() => + { + return Task.CompletedTask; + }); + } + + [RelayCommand(CanExecute = nameof(IsNotBusy))] + private Task GoToDetailsAsync(Post item) + { + return SetBusyAsync(() => + { + return Task.CompletedTask; + }); + } + + [RelayCommand(CanExecute = nameof(IsNotBusy))] + private Task ReactToPostAsync() + { + return SetBusyAsync(() => + { + return Task.CompletedTask; + }); + } + + private void ReceivePost(string? value) + { + _postsWaiting.Add(new Post() { Content = value ?? string.Empty }); + OnPropertyChanged(nameof(UnreadLabel)); + } + + [RelayCommand(CanExecute = nameof(IsNotBusy))] + private Task RefreshAsync() + { + return SetBusyAsync(() => + { + return Task.CompletedTask; + }); + } + + [RelayCommand(CanExecute = nameof(IsNotBusy))] + private Task ReplyToPostAsync() + { + return SetBusyAsync(() => + { + return Task.CompletedTask; + }); + } + + [RelayCommand(CanExecute = nameof(IsNotBusy))] + private Task UpdatePostsAsync() + { + return SetBusyAsync(() => + { + var posts = _postsWaiting.ToImmutableList(); + _postsWaiting.Clear(); + OnPropertyChanged(nameof(UnreadLabel)); + foreach (var post in posts) + { + Posts.AddFirst(post); + } + return Task.CompletedTask; + }); + } + + private void UpdateUser() + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/NuSocial/ViewModels/MessageViewModel.cs b/src/NuSocial/ViewModels/MessageViewModel.cs deleted file mode 100644 index 7cee911..0000000 --- a/src/NuSocial/ViewModels/MessageViewModel.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NuSocial.Core.ViewModel; -using Volo.Abp.DependencyInjection; - -namespace NuSocial.ViewModels; - -[QueryProperty(nameof(MessageContentProperty), "set")] -public partial class MessagesViewModel : BaseViewModel, ITransientDependency -{ - public string? MessageContentProperty { get; set; } - - public MessagesViewModel(IDialogService dialogService, INavigationService navigationService) : base(dialogService, navigationService) - { - } - - public override Task OnFirstAppear() - { - return base.OnFirstAppear(); - } - - public override Task OnParameterSet() - { - return SetBusyAsync(() => - { - if (NavigationParameter is User user) - { - //User = user; - } - - return Task.CompletedTask; - }); - } -} \ No newline at end of file diff --git a/src/NuSocial/ViewModels/MessagesViewModel.cs b/src/NuSocial/ViewModels/MessagesViewModel.cs new file mode 100644 index 0000000..37ab1fa --- /dev/null +++ b/src/NuSocial/ViewModels/MessagesViewModel.cs @@ -0,0 +1,13 @@ +using NuSocial.Core.ViewModel; +using Volo.Abp.DependencyInjection; + +namespace NuSocial.ViewModels; + +public partial class MessagesViewModel : BaseViewModel, ITransientDependency +{ + public string? MessageContentProperty { get; set; } + + public MessagesViewModel(IDialogService dialogService, INavigationService navigationService) : base(dialogService, navigationService) + { + } +} \ No newline at end of file diff --git a/src/NuSocial/Views/MainView.xaml b/src/NuSocial/Views/MainView.xaml index 0439552..2817ab5 100644 --- a/src/NuSocial/Views/MainView.xaml +++ b/src/NuSocial/Views/MainView.xaml @@ -4,9 +4,13 @@ xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:base="clr-namespace:NuSocial.Core.View" + xmlns:converters="clr-namespace:NuSocial.Converters" + xmlns:fa="clr-namespace:FontAwesome" xmlns:loc="clr-namespace:NuSocial.Helpers" + xmlns:models="clr-namespace:NuSocial.Models" xmlns:viewModels="clr-namespace:NuSocial.ViewModels" Title="{Binding Title}" + Padding="20" x:DataType="viewModels:MainViewModel" x:TypeArguments="viewModels:MainViewModel" Shell.FlyoutBehavior="Flyout" @@ -14,10 +18,159 @@ Shell.TabBarBackgroundColor="{StaticResource Primary}" Shell.TabBarForegroundColor="{StaticResource White}" Shell.TabBarIsVisible="True"> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +