224 lines
7.2 KiB
C#
224 lines
7.2 KiB
C#
using System.Collections.ObjectModel;
|
||
using Avalonia.Threading;
|
||
using CommunityToolkit.Mvvm.ComponentModel;
|
||
using CommunityToolkit.Mvvm.Input;
|
||
using PhysOn.Contracts.Conversations;
|
||
using PhysOn.Contracts.Realtime;
|
||
using PhysOn.Desktop.Services;
|
||
|
||
namespace PhysOn.Desktop.ViewModels;
|
||
|
||
public partial class ConversationWindowViewModel : ViewModelBase, IAsyncDisposable
|
||
{
|
||
private readonly PhysOnApiClient _apiClient = new();
|
||
private readonly PhysOnRealtimeClient _realtimeClient = new();
|
||
private readonly ConversationWindowLaunch _launchContext;
|
||
|
||
public ConversationWindowViewModel(ConversationWindowLaunch launchContext)
|
||
{
|
||
_launchContext = launchContext;
|
||
ConversationTitle = launchContext.ConversationTitle;
|
||
ConversationSubtitle = launchContext.ConversationSubtitle;
|
||
SendMessageCommand = new AsyncRelayCommand(SendMessageAsync, CanSendMessage);
|
||
ReloadCommand = new AsyncRelayCommand(LoadMessagesAsync, () => !IsBusy);
|
||
|
||
_realtimeClient.ConnectionStateChanged += HandleRealtimeConnectionStateChanged;
|
||
_realtimeClient.MessageCreated += HandleMessageCreated;
|
||
}
|
||
|
||
public ObservableCollection<MessageRowViewModel> Messages { get; } = [];
|
||
|
||
public IAsyncRelayCommand SendMessageCommand { get; }
|
||
public IAsyncRelayCommand ReloadCommand { get; }
|
||
|
||
[ObservableProperty] private string conversationTitle = string.Empty;
|
||
[ObservableProperty] private string conversationSubtitle = string.Empty;
|
||
[ObservableProperty] private string composerText = string.Empty;
|
||
[ObservableProperty] private string statusText = "·";
|
||
[ObservableProperty] private bool isBusy;
|
||
[ObservableProperty] private string? errorText;
|
||
|
||
public string ConversationGlyph =>
|
||
string.IsNullOrWhiteSpace(ConversationTitle) ? "PO" : ConversationTitle.Trim()[..Math.Min(2, ConversationTitle.Trim().Length)];
|
||
|
||
public bool HasErrorText => !string.IsNullOrWhiteSpace(ErrorText);
|
||
|
||
public async Task InitializeAsync()
|
||
{
|
||
await LoadMessagesAsync();
|
||
|
||
try
|
||
{
|
||
var bootstrap = await _apiClient.GetBootstrapAsync(
|
||
_launchContext.ApiBaseUrl,
|
||
_launchContext.AccessToken,
|
||
CancellationToken.None);
|
||
await _realtimeClient.ConnectAsync(bootstrap.Ws.Url, _launchContext.AccessToken, CancellationToken.None);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
ErrorText = exception.Message;
|
||
}
|
||
}
|
||
|
||
public async Task SendMessageFromShortcutAsync()
|
||
{
|
||
if (SendMessageCommand.CanExecute(null))
|
||
{
|
||
await SendMessageCommand.ExecuteAsync(null);
|
||
}
|
||
}
|
||
|
||
public async ValueTask DisposeAsync()
|
||
{
|
||
await _realtimeClient.DisposeAsync();
|
||
}
|
||
|
||
partial void OnComposerTextChanged(string value) => SendMessageCommand.NotifyCanExecuteChanged();
|
||
partial void OnErrorTextChanged(string? value) => OnPropertyChanged(nameof(HasErrorText));
|
||
partial void OnConversationTitleChanged(string value) => OnPropertyChanged(nameof(ConversationGlyph));
|
||
|
||
private async Task LoadMessagesAsync()
|
||
{
|
||
if (IsBusy)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
IsBusy = true;
|
||
ErrorText = null;
|
||
StatusText = "◌";
|
||
|
||
var items = await _apiClient.GetMessagesAsync(
|
||
_launchContext.ApiBaseUrl,
|
||
_launchContext.AccessToken,
|
||
_launchContext.ConversationId,
|
||
CancellationToken.None);
|
||
|
||
Messages.Clear();
|
||
foreach (var item in items.Items.OrderBy(message => message.ServerSequence))
|
||
{
|
||
Messages.Add(MapMessage(item));
|
||
}
|
||
|
||
StatusText = "●";
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
ErrorText = exception.Message;
|
||
StatusText = "×";
|
||
}
|
||
finally
|
||
{
|
||
IsBusy = false;
|
||
ReloadCommand.NotifyCanExecuteChanged();
|
||
SendMessageCommand.NotifyCanExecuteChanged();
|
||
}
|
||
}
|
||
|
||
private async Task SendMessageAsync()
|
||
{
|
||
if (!CanSendMessage())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var draft = ComposerText.Trim();
|
||
var clientMessageId = Guid.NewGuid();
|
||
ComposerText = string.Empty;
|
||
|
||
var pendingMessage = new MessageRowViewModel
|
||
{
|
||
MessageId = $"pending-{Guid.NewGuid():N}",
|
||
ClientMessageId = clientMessageId,
|
||
Text = draft,
|
||
SenderName = _launchContext.DisplayName,
|
||
MetaText = "보내는 중",
|
||
IsMine = true,
|
||
IsPending = true,
|
||
ServerSequence = Messages.Count == 0 ? 1 : Messages[^1].ServerSequence + 1
|
||
};
|
||
|
||
Messages.Add(pendingMessage);
|
||
|
||
try
|
||
{
|
||
var committed = await _apiClient.SendTextMessageAsync(
|
||
_launchContext.ApiBaseUrl,
|
||
_launchContext.AccessToken,
|
||
_launchContext.ConversationId,
|
||
new PostTextMessageRequest(clientMessageId, draft),
|
||
CancellationToken.None);
|
||
|
||
Messages.Remove(pendingMessage);
|
||
UpsertMessage(MapMessage(committed));
|
||
StatusText = "●";
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
pendingMessage.IsPending = false;
|
||
pendingMessage.IsFailed = true;
|
||
pendingMessage.MetaText = "전송 실패";
|
||
ErrorText = exception.Message;
|
||
}
|
||
}
|
||
|
||
private bool CanSendMessage() => !IsBusy && !string.IsNullOrWhiteSpace(ComposerText);
|
||
|
||
private void HandleRealtimeConnectionStateChanged(RealtimeConnectionState state)
|
||
{
|
||
Dispatcher.UIThread.Post(() =>
|
||
{
|
||
StatusText = state switch
|
||
{
|
||
RealtimeConnectionState.Connected => "●",
|
||
RealtimeConnectionState.Reconnecting => "◔",
|
||
RealtimeConnectionState.Disconnected => "○",
|
||
RealtimeConnectionState.Connecting => "◌",
|
||
_ => StatusText
|
||
};
|
||
});
|
||
}
|
||
|
||
private void HandleMessageCreated(MessageItemDto payload)
|
||
{
|
||
if (!string.Equals(payload.ConversationId, _launchContext.ConversationId, StringComparison.Ordinal))
|
||
{
|
||
return;
|
||
}
|
||
|
||
Dispatcher.UIThread.Post(() => UpsertMessage(MapMessage(payload)));
|
||
}
|
||
|
||
private static MessageRowViewModel MapMessage(MessageItemDto item)
|
||
{
|
||
return new MessageRowViewModel
|
||
{
|
||
MessageId = item.MessageId,
|
||
ClientMessageId = item.ClientMessageId,
|
||
Text = item.Text,
|
||
SenderName = item.Sender.DisplayName,
|
||
MetaText = item.CreatedAt.LocalDateTime.ToString("HH:mm"),
|
||
IsMine = item.IsMine,
|
||
ServerSequence = item.ServerSequence
|
||
};
|
||
}
|
||
|
||
private void UpsertMessage(MessageRowViewModel next)
|
||
{
|
||
var existing = Messages.FirstOrDefault(item =>
|
||
string.Equals(item.MessageId, next.MessageId, StringComparison.Ordinal) ||
|
||
(next.ClientMessageId != Guid.Empty && item.ClientMessageId == next.ClientMessageId));
|
||
|
||
if (existing is not null)
|
||
{
|
||
var index = Messages.IndexOf(existing);
|
||
Messages[index] = next;
|
||
return;
|
||
}
|
||
|
||
Messages.Add(next);
|
||
}
|
||
}
|