공개: alpha.11 검색과 전환 개선 반영
This commit is contained in:
parent
f75dcb49c2
commit
b54eca25f8
31 changed files with 1975 additions and 364 deletions
|
|
@ -4,4 +4,7 @@ public sealed record DesktopWorkspaceLayout(
|
|||
bool IsCompactDensity,
|
||||
bool IsInspectorVisible,
|
||||
bool IsConversationPaneCollapsed,
|
||||
double ConversationPaneWidth = 348);
|
||||
double ConversationPaneWidth = 304,
|
||||
double? WindowWidth = null,
|
||||
double? WindowHeight = null,
|
||||
bool IsWindowMaximized = false);
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
<Product>KoTalk</Product>
|
||||
<Description>한국어 중심의 차분한 메시징 경험을 다시 설계하는 Windows-first 메신저</Description>
|
||||
<AssemblyTitle>KoTalk</AssemblyTitle>
|
||||
<AssemblyVersion>0.1.0.6</AssemblyVersion>
|
||||
<FileVersion>0.1.0.6</FileVersion>
|
||||
<Version>0.1.0-alpha.6</Version>
|
||||
<InformationalVersion>0.1.0-alpha.6</InformationalVersion>
|
||||
<AssemblyVersion>0.1.0.11</AssemblyVersion>
|
||||
<FileVersion>0.1.0.11</FileVersion>
|
||||
<Version>0.1.0-alpha.11</Version>
|
||||
<InformationalVersion>0.1.0-alpha.11</InformationalVersion>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,28 @@ namespace PhysOn.Desktop.ViewModels;
|
|||
public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
||||
{
|
||||
private const string DefaultApiBaseUrl = "https://vstalk.phy.kr";
|
||||
private const string DefaultPublicAlphaKey = "ALPHA-OPEN-2026";
|
||||
private const double DefaultWorkspaceWindowWidth = 1180;
|
||||
private const double DefaultWorkspaceWindowHeight = 760;
|
||||
private const double DefaultOnboardingWindowWidth = 420;
|
||||
private const double DefaultOnboardingWindowHeight = 508;
|
||||
private const double MinWorkspaceWindowWidth = 840;
|
||||
private const double MinWorkspaceWindowHeight = 560;
|
||||
private const double MinOnboardingWindowWidth = 404;
|
||||
private const double MinOnboardingWindowHeight = 468;
|
||||
private const double DefaultConversationPaneWidth = 304;
|
||||
private const double MinConversationPaneWidth = 248;
|
||||
private const double MaxConversationPaneWidth = 360;
|
||||
private readonly PhysOnApiClient _apiClient = new();
|
||||
private readonly SessionStore _sessionStore = new();
|
||||
private readonly PhysOnRealtimeClient _realtimeClient = new();
|
||||
private readonly WorkspaceLayoutStore _workspaceLayoutStore;
|
||||
private readonly IConversationWindowManager _conversationWindowManager;
|
||||
private readonly Dictionary<string, List<MessageRowViewModel>> _messageCache = new(StringComparer.Ordinal);
|
||||
|
||||
private DesktopSession? _session;
|
||||
private string? _currentUserId;
|
||||
private bool _isSampleWorkspace;
|
||||
|
||||
public MainWindowViewModel()
|
||||
: this(new ConversationWindowManager(), new WorkspaceLayoutStore())
|
||||
|
|
@ -116,8 +130,12 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
[ObservableProperty] private bool isCompactDensity = true;
|
||||
[ObservableProperty] private bool isInspectorVisible;
|
||||
[ObservableProperty] private bool isConversationPaneCollapsed;
|
||||
[ObservableProperty] private double conversationPaneWidthValue = 348;
|
||||
[ObservableProperty] private bool isConversationLoading;
|
||||
[ObservableProperty] private double conversationPaneWidthValue = DefaultConversationPaneWidth;
|
||||
[ObservableProperty] private int detachedWindowCount;
|
||||
[ObservableProperty] private double? windowWidth;
|
||||
[ObservableProperty] private double? windowHeight;
|
||||
[ObservableProperty] private bool isWindowMaximized;
|
||||
|
||||
public bool ShowOnboarding => !IsAuthenticated;
|
||||
public bool ShowShell => IsAuthenticated;
|
||||
|
|
@ -128,7 +146,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
public int UnreadConversationCount => Conversations.Count(item => item.UnreadCount > 0);
|
||||
public int PinnedConversationCount => Conversations.Count(item => item.IsPinned);
|
||||
public bool ShowConversationEmptyState => !HasFilteredConversations;
|
||||
public string AdvancedSettingsButtonText => ShowAdvancedSettings ? "기본" : "고급";
|
||||
public string AdvancedSettingsButtonText => ShowAdvancedSettings ? "옵션 닫기" : "옵션";
|
||||
public string CurrentUserMonogram =>
|
||||
string.IsNullOrWhiteSpace(CurrentUserDisplayName) ? "KO" : CurrentUserDisplayName.Trim()[..Math.Min(2, CurrentUserDisplayName.Trim().Length)];
|
||||
public string AllFilterButtonText => "◎";
|
||||
|
|
@ -162,12 +180,14 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
public bool IsConversationPaneExpanded => !IsConversationPaneCollapsed;
|
||||
public double ConversationPaneWidth => IsConversationPaneCollapsed ? 0 : ConversationPaneWidthValue;
|
||||
public double InspectorPaneWidth => IsInspectorVisible ? (IsCompactDensity ? 92 : 108) : 0;
|
||||
public bool HasPersistedWindowBounds => WindowWidth is > 0 && WindowHeight is > 0;
|
||||
public Thickness ConversationRowPadding => IsCompactDensity ? new Thickness(6, 5) : new Thickness(8, 6);
|
||||
public Thickness MessageBubblePadding => IsCompactDensity ? new Thickness(10, 7) : new Thickness(12, 9);
|
||||
public double ConversationAvatarSize => IsCompactDensity ? 28 : 32;
|
||||
public double ComposerMinHeight => IsCompactDensity ? 48 : 58;
|
||||
public string ComposerCounterText => $"{ComposerText.Trim().Length}";
|
||||
public string SearchWatermark => "검색";
|
||||
public bool HasConversationSearchText => !string.IsNullOrWhiteSpace(ConversationSearchText);
|
||||
public string InspectorStatusText => HasDetachedWindows
|
||||
? $"{RealtimeStatusGlyph} {DetachedWindowBadgeText}"
|
||||
: RealtimeStatusGlyph;
|
||||
|
|
@ -175,12 +195,32 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
public string StatusSummaryText => string.IsNullOrWhiteSpace(StatusLine) ? RealtimeStatusText : StatusLine;
|
||||
public string ComposerPlaceholderText => HasSelectedConversation ? "메시지" : "대화 선택";
|
||||
public string ComposerActionText => Messages.Count == 0 ? "시작" : "보내기";
|
||||
public bool ShowMessageEmptyState => Messages.Count == 0;
|
||||
public bool ShowMessageEmptyState => Messages.Count == 0 && !IsConversationLoading;
|
||||
public bool ShowConversationLoadingState => HasSelectedConversation && IsConversationLoading;
|
||||
public string MessageEmptyStateTitle => HasSelectedConversation ? "첫 메시지" : "대화 선택";
|
||||
public string MessageEmptyStateText => HasSelectedConversation
|
||||
? "짧게 남기세요."
|
||||
: "목록에서 선택";
|
||||
|
||||
public (double Width, double Height, bool IsMaximized) GetSuggestedWindowLayout()
|
||||
{
|
||||
if (HasPersistedWindowBounds)
|
||||
{
|
||||
return (WindowWidth!.Value, WindowHeight!.Value, IsWindowMaximized);
|
||||
}
|
||||
|
||||
return ShowOnboarding
|
||||
? (DefaultOnboardingWindowWidth, DefaultOnboardingWindowHeight, false)
|
||||
: (DefaultWorkspaceWindowWidth, DefaultWorkspaceWindowHeight, false);
|
||||
}
|
||||
|
||||
public (double MinWidth, double MinHeight) GetSuggestedWindowConstraints()
|
||||
{
|
||||
return ShowOnboarding
|
||||
? (MinOnboardingWindowWidth, MinOnboardingWindowHeight)
|
||||
: (MinWorkspaceWindowWidth, MinWorkspaceWindowHeight);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("KOTALK_DESKTOP_SAMPLE_MODE"), "1", StringComparison.Ordinal))
|
||||
|
|
@ -222,6 +262,37 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public void ClearSearch()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ConversationSearchText))
|
||||
{
|
||||
ConversationSearchText = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ActivateSearchResultAsync(bool detach)
|
||||
{
|
||||
if (!HasConversationSearchText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = FilteredConversations.FirstOrDefault();
|
||||
if (target is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (detach)
|
||||
{
|
||||
await DetachConversationRowAsync(target);
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedConversation = target;
|
||||
ClearSearch();
|
||||
}
|
||||
|
||||
partial void OnIsAuthenticatedChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(ShowOnboarding));
|
||||
|
|
@ -236,7 +307,11 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
partial void OnDisplayNameChanged(string value) => SignInCommand.NotifyCanExecuteChanged();
|
||||
partial void OnInviteCodeChanged(string value) => SignInCommand.NotifyCanExecuteChanged();
|
||||
partial void OnShowAdvancedSettingsChanged(bool value) => OnPropertyChanged(nameof(AdvancedSettingsButtonText));
|
||||
partial void OnConversationSearchTextChanged(string value) => RefreshConversationFilter();
|
||||
partial void OnConversationSearchTextChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasConversationSearchText));
|
||||
RefreshConversationFilter();
|
||||
}
|
||||
partial void OnSelectedListFilterChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsAllFilterSelected));
|
||||
|
|
@ -299,6 +374,11 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
OnPropertyChanged(nameof(InspectorStatusText));
|
||||
OnPropertyChanged(nameof(WorkspaceModeText));
|
||||
}
|
||||
partial void OnIsConversationLoadingChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(ShowConversationLoadingState));
|
||||
NotifyMessageStateChanged();
|
||||
}
|
||||
|
||||
partial void OnSelectedConversationChanged(ConversationRowViewModel? value)
|
||||
{
|
||||
|
|
@ -324,12 +404,12 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
var apiBaseUrl = ResolveApiBaseUrl();
|
||||
var request = new RegisterAlphaQuickRequest(
|
||||
DisplayName.Trim(),
|
||||
InviteCode.Trim(),
|
||||
InviteCode.Trim() is { Length: > 0 } inviteCode ? inviteCode : DefaultPublicAlphaKey,
|
||||
new DeviceRegistrationDto(
|
||||
$"desktop-{Environment.MachineName.ToLowerInvariant()}",
|
||||
"windows",
|
||||
Environment.MachineName,
|
||||
"0.1.0-alpha.6"));
|
||||
"0.1.0-alpha.11"));
|
||||
|
||||
var response = await _apiClient.RegisterAlphaQuickAsync(apiBaseUrl, request, CancellationToken.None);
|
||||
ApiBaseUrl = apiBaseUrl;
|
||||
|
|
@ -371,7 +451,10 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
Conversations.Clear();
|
||||
FilteredConversations.Clear();
|
||||
Messages.Clear();
|
||||
_messageCache.Clear();
|
||||
IsAuthenticated = false;
|
||||
IsConversationLoading = false;
|
||||
_isSampleWorkspace = false;
|
||||
CurrentUserDisplayName = "KO";
|
||||
StatusLine = string.Empty;
|
||||
RealtimeState = RealtimeConnectionState.Idle;
|
||||
|
|
@ -390,53 +473,83 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
if (!IsAuthenticated || value is null || _session is null)
|
||||
{
|
||||
IsConversationLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Messages.Clear();
|
||||
NotifyMessageStateChanged();
|
||||
|
||||
await RunBusyAsync(async () =>
|
||||
var conversationId = value.ConversationId;
|
||||
if (_isSampleWorkspace)
|
||||
{
|
||||
var items = await _apiClient.GetMessagesAsync(_session.ApiBaseUrl, _session.AccessToken, value.ConversationId, CancellationToken.None);
|
||||
|
||||
if (!string.Equals(SelectedConversation?.ConversationId, value.ConversationId, StringComparison.Ordinal))
|
||||
if (_messageCache.TryGetValue(conversationId, out var sampleItems))
|
||||
{
|
||||
return;
|
||||
ReplaceMessages(sampleItems);
|
||||
}
|
||||
|
||||
IsConversationLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_messageCache.TryGetValue(conversationId, out var cachedItems))
|
||||
{
|
||||
ReplaceMessages(cachedItems);
|
||||
}
|
||||
else
|
||||
{
|
||||
Messages.Clear();
|
||||
foreach (var item in items.Items)
|
||||
{
|
||||
Messages.Add(MapMessage(item));
|
||||
}
|
||||
|
||||
if (Messages.Count > 0)
|
||||
{
|
||||
var lastSequence = Messages[^1].ServerSequence;
|
||||
value.LastReadSequence = lastSequence;
|
||||
value.UnreadCount = 0;
|
||||
await _apiClient.UpdateReadCursorAsync(
|
||||
_session.ApiBaseUrl,
|
||||
_session.AccessToken,
|
||||
value.ConversationId,
|
||||
new UpdateReadCursorRequest(lastSequence),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
NotifyConversationMetricsChanged();
|
||||
NotifyMessageStateChanged();
|
||||
RefreshConversationFilter(value.ConversationId);
|
||||
}
|
||||
|
||||
if (_session is not null)
|
||||
IsConversationLoading = true;
|
||||
|
||||
try
|
||||
{
|
||||
await RunBusyAsync(async () =>
|
||||
{
|
||||
_session = _session with { LastConversationId = value.ConversationId };
|
||||
if (RememberSession)
|
||||
var items = await _apiClient.GetMessagesAsync(_session.ApiBaseUrl, _session.AccessToken, conversationId, CancellationToken.None);
|
||||
var mappedItems = items.Items.Select(MapMessage).ToList();
|
||||
|
||||
if (!string.Equals(SelectedConversation?.ConversationId, conversationId, StringComparison.Ordinal))
|
||||
{
|
||||
await _sessionStore.SaveAsync(_session);
|
||||
return;
|
||||
}
|
||||
|
||||
ReplaceMessages(mappedItems);
|
||||
_messageCache[conversationId] = CloneMessages(mappedItems);
|
||||
|
||||
if (Messages.Count > 0)
|
||||
{
|
||||
var lastSequence = Messages[^1].ServerSequence;
|
||||
value.LastReadSequence = lastSequence;
|
||||
value.UnreadCount = 0;
|
||||
await _apiClient.UpdateReadCursorAsync(
|
||||
_session.ApiBaseUrl,
|
||||
_session.AccessToken,
|
||||
conversationId,
|
||||
new UpdateReadCursorRequest(lastSequence),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
NotifyConversationMetricsChanged();
|
||||
NotifyMessageStateChanged();
|
||||
RefreshConversationFilter(conversationId);
|
||||
|
||||
if (_session is not null)
|
||||
{
|
||||
_session = _session with { LastConversationId = conversationId };
|
||||
if (RememberSession)
|
||||
{
|
||||
await _sessionStore.SaveAsync(_session);
|
||||
}
|
||||
}
|
||||
}, clearMessages: false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (string.Equals(SelectedConversation?.ConversationId, conversationId, StringComparison.Ordinal))
|
||||
{
|
||||
IsConversationLoading = false;
|
||||
}
|
||||
}, clearMessages: false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendMessageAsync()
|
||||
|
|
@ -529,8 +642,12 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
|
||||
private void ApplyBootstrap(BootstrapResponse bootstrap, string displayName, string? preferredConversationId)
|
||||
{
|
||||
_isSampleWorkspace = false;
|
||||
_currentUserId = bootstrap.Me.UserId;
|
||||
CurrentUserDisplayName = displayName;
|
||||
_messageCache.Clear();
|
||||
Messages.Clear();
|
||||
IsConversationLoading = false;
|
||||
Conversations.Clear();
|
||||
|
||||
foreach (var item in bootstrap.Conversations.Items)
|
||||
|
|
@ -568,8 +685,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
|
||||
private bool CanSignIn() =>
|
||||
!IsBusy &&
|
||||
!string.IsNullOrWhiteSpace(DisplayName) &&
|
||||
!string.IsNullOrWhiteSpace(InviteCode);
|
||||
!string.IsNullOrWhiteSpace(DisplayName);
|
||||
|
||||
private bool CanSendMessage() =>
|
||||
!IsBusy &&
|
||||
|
|
@ -612,9 +728,10 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
private void RefreshConversationFilter(string? preferredConversationId = null)
|
||||
{
|
||||
var search = ConversationSearchText.Trim();
|
||||
var hasSearch = !string.IsNullOrWhiteSpace(search);
|
||||
var filtered = Conversations
|
||||
.Where(PassesSelectedFilter)
|
||||
.Where(item => string.IsNullOrWhiteSpace(search) || MatchesConversationSearch(item, search))
|
||||
.Where(item => !hasSearch || MatchesConversationSearch(item, search))
|
||||
.ToList();
|
||||
|
||||
FilteredConversations.Clear();
|
||||
|
|
@ -624,7 +741,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
}
|
||||
|
||||
HasFilteredConversations = filtered.Count > 0;
|
||||
ConversationEmptyStateText = string.IsNullOrWhiteSpace(search)
|
||||
ConversationEmptyStateText = !hasSearch
|
||||
? (SelectedListFilter switch
|
||||
{
|
||||
"unread" => "안읽음 대화가 없습니다.",
|
||||
|
|
@ -640,7 +757,16 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
|
||||
if (target is null)
|
||||
{
|
||||
target = FilteredConversations.FirstOrDefault();
|
||||
target = hasSearch
|
||||
? null
|
||||
: FilteredConversations.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (hasSearch && preferredConversationId is null && target is null)
|
||||
{
|
||||
UpdateSelectedConversationState(SelectedConversation?.ConversationId);
|
||||
NotifyMessageStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(SelectedConversation, target))
|
||||
|
|
@ -664,11 +790,19 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
};
|
||||
}
|
||||
|
||||
private static bool MatchesConversationSearch(ConversationRowViewModel item, string search)
|
||||
private bool MatchesConversationSearch(ConversationRowViewModel item, string search)
|
||||
{
|
||||
return item.Title.Contains(search, StringComparison.CurrentCultureIgnoreCase) ||
|
||||
item.LastMessageText.Contains(search, StringComparison.CurrentCultureIgnoreCase) ||
|
||||
item.Subtitle.Contains(search, StringComparison.CurrentCultureIgnoreCase);
|
||||
if (item.Title.Contains(search, StringComparison.CurrentCultureIgnoreCase) ||
|
||||
item.LastMessageText.Contains(search, StringComparison.CurrentCultureIgnoreCase) ||
|
||||
item.Subtitle.Contains(search, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _messageCache.TryGetValue(item.ConversationId, out var cachedItems) &&
|
||||
cachedItems.Any(message =>
|
||||
message.Text.Contains(search, StringComparison.CurrentCultureIgnoreCase) ||
|
||||
message.SenderName.Contains(search, StringComparison.CurrentCultureIgnoreCase));
|
||||
}
|
||||
|
||||
private void UpdateSelectedConversationState(string? conversationId)
|
||||
|
|
@ -694,13 +828,29 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
var clamped = Math.Clamp(Math.Round(width), 280, 480);
|
||||
var clamped = Math.Clamp(Math.Round(width), MinConversationPaneWidth, MaxConversationPaneWidth);
|
||||
if (Math.Abs(clamped - ConversationPaneWidthValue) > 1)
|
||||
{
|
||||
ConversationPaneWidthValue = clamped;
|
||||
}
|
||||
}
|
||||
|
||||
public void CaptureWindowLayout(double width, double height, bool maximized)
|
||||
{
|
||||
if (width > 0)
|
||||
{
|
||||
WindowWidth = Math.Round(width);
|
||||
}
|
||||
|
||||
if (height > 0)
|
||||
{
|
||||
WindowHeight = Math.Round(height);
|
||||
}
|
||||
|
||||
IsWindowMaximized = maximized;
|
||||
_ = PersistWorkspaceLayoutAsync();
|
||||
}
|
||||
|
||||
private void ApplyQuickDraft(string template)
|
||||
{
|
||||
ComposerText = string.IsNullOrWhiteSpace(ComposerText)
|
||||
|
|
@ -710,6 +860,8 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
|
||||
private void LoadSampleWorkspace()
|
||||
{
|
||||
_isSampleWorkspace = true;
|
||||
_messageCache.Clear();
|
||||
Conversations.Clear();
|
||||
FilteredConversations.Clear();
|
||||
Messages.Clear();
|
||||
|
|
@ -908,6 +1060,8 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
Messages.Add(item);
|
||||
}
|
||||
|
||||
_messageCache["sample-ops"] = CloneMessages(Messages);
|
||||
|
||||
OnPropertyChanged(nameof(CurrentUserMonogram));
|
||||
NotifyMessageStateChanged();
|
||||
}
|
||||
|
|
@ -920,6 +1074,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
private void NotifyMessageStateChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(ShowMessageEmptyState));
|
||||
OnPropertyChanged(nameof(ShowConversationLoadingState));
|
||||
OnPropertyChanged(nameof(MessageEmptyStateTitle));
|
||||
OnPropertyChanged(nameof(MessageEmptyStateText));
|
||||
OnPropertyChanged(nameof(ComposerPlaceholderText));
|
||||
|
|
@ -1107,6 +1262,39 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
{
|
||||
Messages.Add(item);
|
||||
}
|
||||
|
||||
if (SelectedConversation is not null)
|
||||
{
|
||||
_messageCache[SelectedConversation.ConversationId] = CloneMessages(ordered);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReplaceMessages(IEnumerable<MessageRowViewModel> items)
|
||||
{
|
||||
Messages.Clear();
|
||||
foreach (var item in items)
|
||||
{
|
||||
Messages.Add(CloneMessage(item));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<MessageRowViewModel> CloneMessages(IEnumerable<MessageRowViewModel> items) =>
|
||||
items.Select(CloneMessage).ToList();
|
||||
|
||||
private static MessageRowViewModel CloneMessage(MessageRowViewModel item)
|
||||
{
|
||||
return new MessageRowViewModel
|
||||
{
|
||||
MessageId = item.MessageId,
|
||||
ClientMessageId = item.ClientMessageId,
|
||||
Text = item.Text,
|
||||
SenderName = item.SenderName,
|
||||
MetaText = item.MetaText,
|
||||
IsMine = item.IsMine,
|
||||
IsPending = item.IsPending,
|
||||
IsFailed = item.IsFailed,
|
||||
ServerSequence = item.ServerSequence
|
||||
};
|
||||
}
|
||||
|
||||
private void ReorderConversations(string? selectedConversationId)
|
||||
|
|
@ -1130,7 +1318,10 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
IsCompactDensity = true;
|
||||
IsInspectorVisible = false;
|
||||
IsConversationPaneCollapsed = false;
|
||||
ConversationPaneWidthValue = 348;
|
||||
ConversationPaneWidthValue = DefaultConversationPaneWidth;
|
||||
WindowWidth = null;
|
||||
WindowHeight = null;
|
||||
IsWindowMaximized = false;
|
||||
StatusLine = "초기화";
|
||||
}
|
||||
|
||||
|
|
@ -1139,7 +1330,10 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
IsCompactDensity = layout.IsCompactDensity;
|
||||
IsInspectorVisible = layout.IsInspectorVisible;
|
||||
IsConversationPaneCollapsed = layout.IsConversationPaneCollapsed;
|
||||
ConversationPaneWidthValue = Math.Clamp(layout.ConversationPaneWidth, 280, 480);
|
||||
ConversationPaneWidthValue = Math.Clamp(layout.ConversationPaneWidth, MinConversationPaneWidth, MaxConversationPaneWidth);
|
||||
WindowWidth = layout.WindowWidth;
|
||||
WindowHeight = layout.WindowHeight;
|
||||
IsWindowMaximized = layout.IsWindowMaximized;
|
||||
}
|
||||
|
||||
private Task PersistWorkspaceLayoutAsync()
|
||||
|
|
@ -1148,7 +1342,10 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
|||
IsCompactDensity,
|
||||
IsInspectorVisible,
|
||||
IsConversationPaneCollapsed,
|
||||
ConversationPaneWidthValue));
|
||||
ConversationPaneWidthValue,
|
||||
WindowWidth,
|
||||
WindowHeight,
|
||||
IsWindowMaximized));
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
xmlns:vm="using:PhysOn.Desktop.ViewModels"
|
||||
x:Class="PhysOn.Desktop.Views.ConversationWindow"
|
||||
x:DataType="vm:ConversationWindowViewModel"
|
||||
Width="404"
|
||||
Height="748"
|
||||
MinWidth="340"
|
||||
MinHeight="520"
|
||||
Width="560"
|
||||
Height="720"
|
||||
MinWidth="420"
|
||||
MinHeight="500"
|
||||
Background="#F7F3EE"
|
||||
Title="{Binding ConversationTitle}">
|
||||
|
||||
|
|
@ -59,8 +59,8 @@
|
|||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
<Grid Margin="10" RowDefinitions="Auto,Auto,*,Auto" RowSpacing="8">
|
||||
<Border Classes="surface" Padding="10">
|
||||
<Grid Margin="6" RowDefinitions="Auto,Auto,*,Auto" RowSpacing="6">
|
||||
<Border Classes="surface" Padding="8">
|
||||
<Grid ColumnDefinitions="40,*,Auto,Auto" ColumnSpacing="10">
|
||||
<Border Width="40" Height="40" Classes="muted">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
|
||||
<Border Grid.Row="1"
|
||||
Classes="surface"
|
||||
Padding="9"
|
||||
Padding="8"
|
||||
IsVisible="{Binding HasErrorText}">
|
||||
<TextBlock Text="{Binding ErrorText}"
|
||||
Foreground="#C9573C"
|
||||
|
|
@ -102,8 +102,8 @@
|
|||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="2" Classes="surface" Padding="8">
|
||||
<ScrollViewer Name="MessagesScrollViewer" MaxWidth="360" HorizontalAlignment="Center">
|
||||
<Border Grid.Row="2" Classes="surface" Padding="6">
|
||||
<ScrollViewer Name="MessagesScrollViewer" HorizontalAlignment="Stretch">
|
||||
<ItemsControl ItemsSource="{Binding Messages}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:MessageRowViewModel">
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="3" Classes="muted" Padding="8" MaxWidth="360" HorizontalAlignment="Center">
|
||||
<Border Grid.Row="3" Classes="muted" Padding="6" HorizontalAlignment="Stretch">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="10">
|
||||
<TextBox Name="ComposerTextBox"
|
||||
PlaceholderText="메시지"
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
x:Name="RootWindow"
|
||||
x:Class="PhysOn.Desktop.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
d:DesignWidth="1440"
|
||||
d:DesignHeight="900"
|
||||
d:DesignWidth="1180"
|
||||
d:DesignHeight="760"
|
||||
Title="KoTalk"
|
||||
Width="1440"
|
||||
Height="900"
|
||||
MinWidth="980"
|
||||
MinHeight="640"
|
||||
Width="1180"
|
||||
Height="760"
|
||||
MinWidth="404"
|
||||
MinHeight="468"
|
||||
Background="#F7F3EE">
|
||||
|
||||
<Design.DataContext>
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
<Setter Property="Background" Value="#FBF7F2" />
|
||||
<Setter Property="BorderBrush" Value="#E6D8CC" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="MaxWidth" Value="560" />
|
||||
<Setter Property="MaxWidth" Value="680" />
|
||||
<Setter Property="Margin" Value="0,0,0,6" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
</Style>
|
||||
|
|
@ -174,15 +174,15 @@
|
|||
<Setter Property="BorderBrush" Value="#394350" />
|
||||
</Style>
|
||||
<Style Selector="Button.rail-button">
|
||||
<Setter Property="Width" Value="38" />
|
||||
<Setter Property="Height" Value="38" />
|
||||
<Setter Property="Width" Value="30" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="CornerRadius" Value="2" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Background" Value="#FFFFFF" />
|
||||
<Setter Property="Foreground" Value="#394350" />
|
||||
<Setter Property="BorderBrush" Value="#E8DDD2" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="FontSize" Value="11.5" />
|
||||
</Style>
|
||||
<Style Selector="Button.rail-button.active">
|
||||
<Setter Property="Background" Value="#394350" />
|
||||
|
|
@ -206,17 +206,17 @@
|
|||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
<Grid Margin="12">
|
||||
<Grid Margin="4">
|
||||
<Grid IsVisible="{Binding ShowOnboarding}"
|
||||
HorizontalAlignment="Center"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
MaxWidth="380">
|
||||
<Border Classes="surface" Padding="22">
|
||||
<StackPanel Spacing="14">
|
||||
<StackPanel Spacing="8">
|
||||
MaxWidth="404">
|
||||
<Border Classes="surface" Padding="16">
|
||||
<StackPanel Spacing="12">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="KO · TALK" Classes="eyebrow" />
|
||||
<Border Width="50" Height="50" Classes="surface-muted" HorizontalAlignment="Left" Padding="7">
|
||||
<Image Source="avares://PhysOn.Desktop/Assets/kotalk-mark-128.png"
|
||||
<Border Width="46" Height="46" Classes="surface-muted" HorizontalAlignment="Left" Padding="7">
|
||||
<Image Source="avares://KoTalk/Assets/kotalk-mark-128.png"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
<TextBlock Text="KoTalk" Classes="display-title" />
|
||||
|
|
@ -226,23 +226,22 @@
|
|||
PlaceholderText="표시 이름"
|
||||
Text="{Binding DisplayName}" />
|
||||
|
||||
<TextBox Classes="input"
|
||||
PlaceholderText="참여 키"
|
||||
Text="{Binding InviteCode}" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="10">
|
||||
<Button Classes="secondary-button"
|
||||
Command="{Binding ToggleAdvancedSettingsCommand}"
|
||||
Content="{Binding AdvancedSettingsButtonText}" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="input"
|
||||
IsVisible="{Binding ShowAdvancedSettings}"
|
||||
PlaceholderText="서버를 바꿀 때만 입력"
|
||||
Text="{Binding ApiBaseUrl}" />
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="8"
|
||||
IsVisible="{Binding ShowAdvancedSettings}">
|
||||
<TextBox Classes="input"
|
||||
PlaceholderText="참여 키"
|
||||
Text="{Binding InviteCode}" />
|
||||
<TextBox Classes="input"
|
||||
PlaceholderText="서버 주소"
|
||||
Text="{Binding ApiBaseUrl}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<CheckBox Content="유지" IsChecked="{Binding RememberSession}" />
|
||||
|
||||
<Border Classes="inline-alert" IsVisible="{Binding HasErrorText}">
|
||||
<TextBlock Text="{Binding ErrorText}"
|
||||
Classes="caption"
|
||||
|
|
@ -260,17 +259,17 @@
|
|||
|
||||
<Grid IsVisible="{Binding ShowShell}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="56" />
|
||||
<ColumnDefinition Width="44" />
|
||||
<ColumnDefinition Width="{Binding ConversationPaneWidth}" />
|
||||
<ColumnDefinition Width="6" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" Classes="rail-surface" Padding="8">
|
||||
<Border Grid.Column="0" Classes="rail-surface" Padding="6">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<StackPanel Spacing="8">
|
||||
<Border Width="38" Height="38" Classes="surface-muted" Padding="6">
|
||||
<Image Source="avares://PhysOn.Desktop/Assets/kotalk-mark-128.png"
|
||||
<Border Width="30" Height="30" Classes="surface-muted" Padding="5">
|
||||
<Image Source="avares://KoTalk/Assets/kotalk-mark-128.png"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
|
||||
|
|
@ -280,10 +279,11 @@
|
|||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="2" Spacing="8">
|
||||
<Border Width="38" Height="38" Classes="surface-muted">
|
||||
<Border Width="30" Height="30" Classes="surface-muted">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding CurrentUserMonogram}"
|
||||
FontSize="11.5"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#394350" />
|
||||
</Border>
|
||||
|
|
@ -299,19 +299,21 @@
|
|||
<Border x:Name="ConversationPaneHost"
|
||||
Grid.Column="1"
|
||||
Classes="surface"
|
||||
Padding="10"
|
||||
Padding="8"
|
||||
IsVisible="{Binding IsConversationPaneExpanded}"
|
||||
SizeChanged="ConversationPaneHost_OnSizeChanged">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*" RowSpacing="8">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*" RowSpacing="6">
|
||||
<Grid ColumnDefinitions="*" ColumnSpacing="10">
|
||||
<TextBlock Text="받은함" Classes="section-title" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
|
||||
<TextBox Grid.Column="0"
|
||||
<TextBox x:Name="ConversationSearchTextBox"
|
||||
Grid.Column="0"
|
||||
Classes="search-input"
|
||||
PlaceholderText="{Binding SearchWatermark}"
|
||||
Text="{Binding ConversationSearchText}" />
|
||||
Text="{Binding ConversationSearchText}"
|
||||
KeyDown="ConversationSearchTextBox_OnKeyDown" />
|
||||
<Button Grid.Column="1"
|
||||
Classes="icon-button"
|
||||
ToolTip.Tip="밀도 전환"
|
||||
|
|
@ -353,7 +355,7 @@
|
|||
|
||||
<Grid Grid.Row="3">
|
||||
<Border Classes="surface-muted"
|
||||
Padding="18"
|
||||
Padding="12"
|
||||
IsVisible="{Binding ShowConversationEmptyState}">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="{Binding ConversationEmptyStateText}" Classes="section-title" />
|
||||
|
|
@ -372,60 +374,72 @@
|
|||
<ItemsControl ItemsSource="{Binding FilteredConversations}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ConversationRowViewModel">
|
||||
<Button Classes="row-button"
|
||||
Margin="0,0,0,6"
|
||||
Command="{Binding $parent[Window].DataContext.SelectConversationCommand}"
|
||||
CommandParameter="{Binding .}">
|
||||
<Border Classes="row-card"
|
||||
Classes.active="{Binding IsSelected}"
|
||||
Padding="{Binding $parent[Window].DataContext.ConversationRowPadding}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
RowDefinitions="Auto,Auto"
|
||||
ColumnSpacing="9"
|
||||
RowSpacing="2">
|
||||
<Border Width="{Binding $parent[Window].DataContext.ConversationAvatarSize}"
|
||||
Height="{Binding $parent[Window].DataContext.ConversationAvatarSize}"
|
||||
Classes="surface-muted">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding AvatarText}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#20242B" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Title}"
|
||||
Classes="body"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding MetaText}"
|
||||
Classes="caption" />
|
||||
|
||||
<TextBlock Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Text="{Binding LastMessageText}"
|
||||
Classes="caption"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="6"
|
||||
HorizontalAlignment="Right">
|
||||
<TextBlock Text="★"
|
||||
Classes="caption"
|
||||
IsVisible="{Binding IsPinned}" />
|
||||
<Border Classes="unread-badge"
|
||||
IsVisible="{Binding HasUnread}">
|
||||
<TextBlock Text="{Binding UnreadBadgeText}"
|
||||
FontSize="10.5"
|
||||
Foreground="#FFFFFF" />
|
||||
<Grid Margin="0,0,0,6"
|
||||
ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="6">
|
||||
<Button Grid.Column="0"
|
||||
Classes="row-button"
|
||||
Command="{Binding $parent[Window].DataContext.SelectConversationCommand}"
|
||||
CommandParameter="{Binding .}">
|
||||
<Border Classes="row-card"
|
||||
Classes.active="{Binding IsSelected}"
|
||||
Padding="{Binding $parent[Window].DataContext.ConversationRowPadding}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
RowDefinitions="Auto,Auto"
|
||||
ColumnSpacing="9"
|
||||
RowSpacing="2">
|
||||
<Border Width="{Binding $parent[Window].DataContext.ConversationAvatarSize}"
|
||||
Height="{Binding $parent[Window].DataContext.ConversationAvatarSize}"
|
||||
Classes="surface-muted">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding AvatarText}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#20242B" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Button>
|
||||
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Title}"
|
||||
Classes="body"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding MetaText}"
|
||||
Classes="caption" />
|
||||
|
||||
<TextBlock Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Text="{Binding LastMessageText}"
|
||||
Classes="caption"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="6"
|
||||
HorizontalAlignment="Right">
|
||||
<TextBlock Text="★"
|
||||
Classes="caption"
|
||||
IsVisible="{Binding IsPinned}" />
|
||||
<Border Classes="unread-badge"
|
||||
IsVisible="{Binding HasUnread}">
|
||||
<TextBlock Text="{Binding UnreadBadgeText}"
|
||||
FontSize="10.5"
|
||||
Foreground="#FFFFFF" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Button>
|
||||
|
||||
<Button Grid.Column="1"
|
||||
Classes="icon-button compact"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip.Tip="분리"
|
||||
Command="{Binding $parent[Window].DataContext.DetachConversationRowCommand}"
|
||||
CommandParameter="{Binding .}"
|
||||
Content="↗" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
|
@ -441,8 +455,8 @@
|
|||
ResizeDirection="Columns"
|
||||
ShowsPreview="True" />
|
||||
|
||||
<Border Grid.Column="3" Classes="surface" Padding="10">
|
||||
<Grid RowDefinitions="Auto,*,Auto" RowSpacing="10">
|
||||
<Border Grid.Column="3" Classes="surface" Padding="8">
|
||||
<Grid RowDefinitions="Auto,*,Auto" RowSpacing="8">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="8">
|
||||
<Border Width="38" Height="38" Classes="surface-muted">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
|
|
@ -501,9 +515,8 @@
|
|||
</Border>
|
||||
|
||||
<ScrollViewer Name="MessagesScrollViewer"
|
||||
Margin="0,12,0,0"
|
||||
MaxWidth="860"
|
||||
HorizontalAlignment="Center">
|
||||
Margin="0,6,0,0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ItemsControl ItemsSource="{Binding Messages}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:MessageRowViewModel">
|
||||
|
|
@ -530,10 +543,10 @@
|
|||
</ScrollViewer>
|
||||
|
||||
<Border Classes="surface-muted"
|
||||
Width="280"
|
||||
HorizontalAlignment="Center"
|
||||
MaxWidth="220"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Padding="14"
|
||||
Padding="10"
|
||||
IsVisible="{Binding ShowMessageEmptyState}">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="{Binding MessageEmptyStateTitle}" Classes="section-title" />
|
||||
|
|
@ -548,13 +561,26 @@
|
|||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="surface-muted"
|
||||
MaxWidth="220"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Padding="10"
|
||||
IsVisible="{Binding ShowConversationLoadingState}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="불러오는 중" Classes="section-title" />
|
||||
<TextBlock Text="현재 대화를 맞추고 있습니다."
|
||||
Classes="caption"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="2"
|
||||
Classes="surface-muted"
|
||||
Padding="8"
|
||||
MaxWidth="860"
|
||||
HorizontalAlignment="Center">
|
||||
Padding="6"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Grid RowDefinitions="Auto,Auto" RowSpacing="6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="quick-button"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
|
|
@ -9,12 +10,14 @@ namespace PhysOn.Desktop.Views;
|
|||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private bool _initialLayoutApplied;
|
||||
private MainWindowViewModel? _boundViewModel;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
Opened += OnOpened;
|
||||
}
|
||||
|
||||
private async void ComposerTextBox_OnKeyDown(object? sender, KeyEventArgs e)
|
||||
|
|
@ -33,6 +36,15 @@ public partial class MainWindow : Window
|
|||
{
|
||||
base.OnKeyDown(e);
|
||||
|
||||
if (e.Key == Key.K &&
|
||||
e.KeyModifiers.HasFlag(KeyModifiers.Control) &&
|
||||
DataContext is MainWindowViewModel)
|
||||
{
|
||||
FocusConversationSearch(selectAll: true);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.O &&
|
||||
e.KeyModifiers.HasFlag(KeyModifiers.Control) &&
|
||||
e.KeyModifiers.HasFlag(KeyModifiers.Shift) &&
|
||||
|
|
@ -43,11 +55,33 @@ public partial class MainWindow : Window
|
|||
}
|
||||
}
|
||||
|
||||
private async void ConversationSearchTextBox_OnKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel viewModel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
await viewModel.ActivateSearchResultAsync(detach: e.KeyModifiers.HasFlag(KeyModifiers.Control));
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
viewModel.ClearSearch();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_boundViewModel is not null)
|
||||
{
|
||||
_boundViewModel.Messages.CollectionChanged -= Messages_OnCollectionChanged;
|
||||
_boundViewModel.PropertyChanged -= ViewModel_OnPropertyChanged;
|
||||
}
|
||||
|
||||
_boundViewModel = DataContext as MainWindowViewModel;
|
||||
|
|
@ -55,6 +89,20 @@ public partial class MainWindow : Window
|
|||
if (_boundViewModel is not null)
|
||||
{
|
||||
_boundViewModel.Messages.CollectionChanged += Messages_OnCollectionChanged;
|
||||
_boundViewModel.PropertyChanged += ViewModel_OnPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpened(object? sender, EventArgs e)
|
||||
{
|
||||
ApplySuggestedWindowLayout(force: true);
|
||||
}
|
||||
|
||||
private void ViewModel_OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(MainWindowViewModel.ShowOnboarding) or nameof(MainWindowViewModel.ShowShell))
|
||||
{
|
||||
ApplySuggestedWindowLayout(force: !_initialLayoutApplied || !(_boundViewModel?.HasPersistedWindowBounds ?? true));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +126,9 @@ public partial class MainWindow : Window
|
|||
{
|
||||
if (_boundViewModel is not null)
|
||||
{
|
||||
_boundViewModel.CaptureWindowLayout(Width, Height, WindowState == WindowState.Maximized);
|
||||
_boundViewModel.Messages.CollectionChanged -= Messages_OnCollectionChanged;
|
||||
_boundViewModel.PropertyChanged -= ViewModel_OnPropertyChanged;
|
||||
_ = _boundViewModel.DisposeAsync();
|
||||
}
|
||||
|
||||
|
|
@ -92,4 +142,41 @@ public partial class MainWindow : Window
|
|||
scrollViewer.Offset = new Vector(scrollViewer.Offset.X, scrollViewer.Extent.Height);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySuggestedWindowLayout(bool force)
|
||||
{
|
||||
if (_boundViewModel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_initialLayoutApplied && !force)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (minWidth, minHeight) = _boundViewModel.GetSuggestedWindowConstraints();
|
||||
MinWidth = minWidth;
|
||||
MinHeight = minHeight;
|
||||
|
||||
var (suggestedWidth, suggestedHeight, maximized) = _boundViewModel.GetSuggestedWindowLayout();
|
||||
Width = suggestedWidth;
|
||||
Height = suggestedHeight;
|
||||
WindowState = maximized ? WindowState.Maximized : WindowState.Normal;
|
||||
_initialLayoutApplied = true;
|
||||
}
|
||||
|
||||
private void FocusConversationSearch(bool selectAll)
|
||||
{
|
||||
if (this.FindControl<TextBox>("ConversationSearchTextBox") is not { } searchBox)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
searchBox.Focus();
|
||||
if (selectAll)
|
||||
{
|
||||
searchBox.SelectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue