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">
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file