< Summary

Information
Class: Chronicis.Client.Pages.SessionDetail
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/SessionDetail.razor
Line coverage
100%
Covered lines: 117
Uncovered lines: 0
Coverable lines: 117
Total lines: 1231
Line coverage: 100%
Branch coverage
100%
Covered branches: 44
Total branches: 44
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/SessionDetail.razor

#LineLine coverage
 1@page "/session/{SessionId:guid}"
 2@attribute [Authorize]
 3@implements IDisposable
 4@using System.ComponentModel
 5@using Chronicis.Client.Components.Articles
 6@using Chronicis.Client.Components.Maps
 7@using Chronicis.Client.Components.Shared
 8@using Chronicis.Client.Services
 9@using ArticleWikiLinkAutocompleteItem = Chronicis.Client.Components.Articles.WikiLinkAutocompleteItem
 10@using Chronicis.Shared.DTOs
 11@using Chronicis.Shared.Enums
 12@using Microsoft.AspNetCore.Components.Web
 13@using Microsoft.AspNetCore.Authorization
 14@using Microsoft.JSInterop
 15@inject SessionDetailViewModel ViewModel
 16@inject IDialogService DialogService
 17@inject IKeyboardShortcutService KeyboardShortcutService
 18@inject IJSRuntime JSRuntime
 19@inject NavigationManager Navigation
 20@inject ISnackbar Snackbar
 21@inject ILogger<SessionDetail> Logger
 22@inject ILinkApiService LinkApiService
 23@inject IExternalLinkApiService ExternalLinkApiService
 24@inject IMapApiService MapApi
 25@inject IWikiLinkService WikiLinkService
 26@inject IAppContextService AppContext
 27@inject IArticleCacheService ArticleCache
 28@inject IAISummaryApiService SummaryApi
 29@inject IWorldApiService WorldApi
 30@inject IDrawerCoordinator DrawerCoordinator
 31
 5432@if (ViewModel.IsLoading)
 33{
 34    <LoadingSkeleton />
 35}
 5336else if (ViewModel.Session == null)
 37{
 38    <NotFoundAlert EntityType="Session" />
 39}
 40else
 41{
 42    <MudPaper Elevation="2" Class="chronicis-article-card chronicis-fade-in">
 43        <ChroniclsBreadcrumbs Items="ViewModel.Breadcrumbs" Class="mb-2" />
 44
 45        <div class="d-flex align-center mb-3">
 46            <MudIcon Icon="@Icons.Material.Filled.EventNote"
 47                     Size="Size.Large"
 48                     Class="mr-3 generic-container-icon"
 49                     Style="color: var(--chronicis-beige-gold); font-size: 2.5rem;" />
 50            <div class="flex-grow-1">
 51                @if (ViewModel.CanManageSessionDetails)
 52                {
 53                    <MudTextField @bind-Value="ViewModel.EditName"
 54                                  Variant="Variant.Text"
 55                                  Placeholder="Untitled Session"
 56                                  Immediate="true"
 57                                  Underline="false"
 58                                  Class="flex-grow-1"
 59                                  Style="font-size: 2rem; font-family: var(--chronicis-font-heading);"
 60                                  @onkeydown="OnSessionTitleKeyDown" />
 61                    <div class="d-flex align-center flex-wrap">
 62                        <MudText Typo="Typo.body2" Color="Color.Secondary" Class="mr-2">
 63                            Session Date:
 64                        </MudText>
 65                        <MudDatePicker Date="@ViewModel.EditSessionDate"
 66                                       DateChanged="OnSessionDateChanged"
 67                                       DateFormat="MMMM d, yyyy"
 68                                       Variant="Variant.Text"
 69                                       Margin="Margin.Dense"
 70                                       Underline="false"
 71                                       Clearable="true"
 72                                       Editable="true"
 73                                       Class="mt-0" />
 74                    </div>
 75                }
 76                else
 77                {
 78                    <MudText Typo="Typo.h4" Style="font-family: var(--chronicis-font-heading);">
 79                        @ViewModel.Session.Name
 80                    </MudText>
 81                    <MudText Typo="Typo.body2" Color="Color.Secondary">
 82                        Session Date: @FormatSessionDate(ViewModel.Session.SessionDate)
 83                    </MudText>
 84                }
 85            </div>
 86            @if (ViewModel.CanManageSessionDetails)
 87            {
 88                <MudChip T="string" Color="Color.Warning" Variant="Variant.Outlined" Size="Size.Small">
 89                    GM
 90                </MudChip>
 91            }
 92        </div>
 93
 94        <div class="chronicis-rune-divider mb-4"></div>
 95
 96        <MudTabs Elevation="0"
 97                 Rounded="true"
 98                 ApplyEffectsToContainer="true"
 99                 KeepPanelsAlive="true"
 100                 PanelClass="pa-4 mb-2">
 101            <MudTabPanel Text="Notes" Icon="@Icons.Material.Filled.Public">
 102                <MudText Typo="Typo.h6" Class="mb-2" Style="color: var(--chronicis-beige-gold);">
 103                    <MudIcon Icon="@Icons.Material.Filled.Public" Size="Size.Small" Class="mr-2" />
 104                    Public Notes
 105                </MudText>
 106
 107                @if (ViewModel.CanManageSessionDetails)
 108                {
 109                    <MudText Typo="Typo.caption" Color="Color.Secondary" Class="mb-1">
 110                        Public Notes
 111                    </MudText>
 112                    <div id="@PublicEditorElementId" class="chronicis-editor-container mb-4"></div>
 113                }
 114                else if (!string.IsNullOrWhiteSpace(ViewModel.Session.PublicNotes))
 115                {
 116                    <MudPaper Outlined="true" Class="pa-4 mb-4">
 117                        @((MarkupString)ViewModel.Session.PublicNotes)
 118                    </MudPaper>
 119                }
 120                else
 121                {
 122                    <MudAlert Severity="Severity.Info" Class="mb-4">
 123                        No public notes yet.
 124                    </MudAlert>
 125                }
 126            </MudTabPanel>
 127
 128            @if (ViewModel.CanViewPrivateNotes)
 129            {
 130                <MudTabPanel Text="Private Notes" Icon="@Icons.Material.Filled.Lock">
 131                    <MudText Typo="Typo.h6" Class="mb-2" Style="color: var(--chronicis-beige-gold);">
 132                        <MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="mr-2" />
 133                        Private Notes
 134                    </MudText>
 135
 136                    @if (ViewModel.CanManageSessionDetails)
 137                    {
 138                        <MudText Typo="Typo.caption" Color="Color.Secondary" Class="mb-1">
 139                            Private Notes (World owner / GM only)
 140                        </MudText>
 141                        <div id="@PrivateEditorElementId" class="chronicis-editor-container mb-4"></div>
 142                    }
 143                    else
 144                    {
 145                        <MudAlert Severity="Severity.Info" Class="mb-4">
 146                            Private notes are visible to the world owner and GMs only.
 147                        </MudAlert>
 148                    }
 149                </MudTabPanel>
 150            }
 151        </MudTabs>
 152
 153        @if (ViewModel.CanManageSessionDetails)
 154        {
 155            <div class="chronicis-flex-between mb-4">
 156                <SaveStatusIndicator IsSaving="ViewModel.IsSavingNotes" HasUnsavedChanges="ViewModel.HasUnsavedChanges" 
 157
 158                <div class="d-flex align-center">
 159                    <MudButton Variant="Variant.Filled"
 160                               Color="Color.Primary"
 161                               OnClick="SaveSessionNotesAsync"
 162                               Disabled="@(ViewModel.IsSavingNotes || ViewModel.IsDeleting || !ViewModel.HasUnsavedChang
 163                               StartIcon="@Icons.Material.Filled.Save">
 164                        Save
 165                    </MudButton>
 166
 167                    <MudButton Variant="Variant.Outlined"
 168                               Color="Color.Error"
 169                               OnClick="OpenDeleteDialogAsync"
 170                               Disabled="@ViewModel.IsDeleting"
 171                               StartIcon="@Icons.Material.Filled.Delete"
 172                               Class="ml-2">
 173                        @(ViewModel.IsDeleting ? "Deleting..." : "Delete")
 174                    </MudButton>
 175                </div>
 176            </div>
 177        }
 178
 179        <MudDivider Class="my-4" />
 180
 181        <div class="d-flex align-center justify-space-between mb-2">
 182            <MudText Typo="Typo.h6" Style="color: var(--chronicis-beige-gold);">
 183                <MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Size="Size.Small" Class="mr-2" />
 184                AI Summary
 185            </MudText>
 186            <div class="d-flex align-center">
 187                @if (!string.IsNullOrWhiteSpace(ViewModel.Session.AiSummary))
 188                {
 189                    <MudTooltip Text="Refresh summary">
 190                        <MudIconButton Icon="@Icons.Material.Filled.Refresh"
 191                                       Color="Color.Secondary"
 192                                       Disabled="@(ViewModel.IsGeneratingSummary || ViewModel.IsDeletingSummary || ViewM
 193                                       OnClick="@ViewModel.GenerateAiSummaryAsync" />
 194                    </MudTooltip>
 195                    <MudTooltip Text="Delete summary">
 196                        <MudIconButton Icon="@Icons.Material.Filled.Delete"
 197                                       Color="Color.Error"
 198                                       Disabled="@(ViewModel.IsGeneratingSummary || ViewModel.IsDeletingSummary || ViewM
 199                                       OnClick="@ViewModel.ClearAiSummaryAsync" />
 200                    </MudTooltip>
 201                }
 202                else
 203                {
 204                    <MudButton Variant="Variant.Filled"
 205                               Color="Color.Secondary"
 206                               OnClick="@ViewModel.GenerateAiSummaryAsync"
 207                               Disabled="@(ViewModel.IsGeneratingSummary || ViewModel.IsDeleting)"
 208                               StartIcon="@Icons.Material.Filled.AutoAwesome">
 209                        @(ViewModel.IsGeneratingSummary ? "Generating..." : "Generate")
 210                    </MudButton>
 211                }
 212            </div>
 213        </div>
 214
 215        @if (ViewModel.IsGeneratingSummary || ViewModel.IsDeletingSummary)
 216        {
 217            <MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="mb-3" />
 218        }
 219
 220        @if (!string.IsNullOrWhiteSpace(ViewModel.Session.AiSummary))
 221        {
 222            <MudPaper Outlined="true" Class="pa-4 mb-2">
 223                <MudText Typo="Typo.body1" Style="white-space: pre-wrap;">@ViewModel.Session.AiSummary</MudText>
 224            </MudPaper>
 225            @if (ViewModel.Session.AiSummaryGeneratedAt.HasValue)
 226            {
 227                <MudText Typo="Typo.caption" Color="Color.Secondary" Class="mb-4">
 228                    Generated @ViewModel.Session.AiSummaryGeneratedAt.Value.ToLocalTime().ToString("g")
 229                </MudText>
 230            }
 231        }
 232        else
 233        {
 234            <MudAlert Severity="Severity.Info" Class="mb-4">
 235                No AI summary yet.
 236            </MudAlert>
 237        }
 238
 239        <MudDivider Class="my-4" />
 240
 241        <div class="d-flex align-center justify-space-between flex-wrap mb-2">
 242            <MudText Typo="Typo.h6" Style="color: var(--chronicis-beige-gold);">
 243                <MudIcon Icon="@Icons.Material.Filled.Description" Size="Size.Small" Class="mr-2" />
 244                Session Notes
 245            </MudText>
 246
 247            @if (ViewModel.CanCreateSessionNote)
 248            {
 249                <MudButton Variant="Variant.Outlined"
 250                           Color="Color.Primary"
 251                           Size="Size.Small"
 252                           OnClick="@ViewModel.CreateSessionNoteAsync"
 253                           Disabled="@(ViewModel.IsCreatingSessionNote || ViewModel.IsDeleting)"
 254                           StartIcon="@Icons.Material.Filled.NoteAdd">
 255                    @(ViewModel.IsCreatingSessionNote ? "Adding..." : "Add Session Note")
 256                </MudButton>
 257            }
 258        </div>
 259
 260        @if (ViewModel.SessionNotes.Any())
 261        {
 262            <MudList T="ArticleTreeDto" Dense="true">
 263                @foreach (var note in ViewModel.SessionNotes)
 264                {
 265                    <EntityListItem Icon="@Icons.Material.Filled.Description"
 266                                    Title="@(string.IsNullOrWhiteSpace(note.Title) ? "Untitled Session Note" : note.Titl
 267                                    OnClick="@(() => ViewModel.OpenSessionNoteAsync(note))">
 268                        <MudChip T="string"
 269                                 Size="Size.Small"
 270                                 Variant="Variant.Outlined"
 271                                 Color="@GetVisibilityColor(note.Visibility)">
 272                            @note.Visibility
 273                        </MudChip>
 274                    </EntityListItem>
 275                }
 276            </MudList>
 277        }
 278        else
 279        {
 280            <MudAlert Severity="Severity.Info">
 281                No attached SessionNote articles found.
 282            </MudAlert>
 283        }
 284
 285        @if (ViewModel.CanManageSessionDetails)
 286        {
 287            <MudDrawer @bind-Open="_externalPreviewOpen"
 288                       Anchor="Anchor.End"
 289                       Variant="@DrawerVariant.Temporary"
 290                       Elevation="9999"
 291                       Class="external-link-preview-drawer">
 292                <MudDrawerHeader>
 293                    <MudSpacer />
 294                    <MudIconButton Icon="@Icons.Material.Filled.Close" OnClick="CloseExternalPreview" />
 295                </MudDrawerHeader>
 296                <MudDrawerContainer>
 297                    <div class="external-link-preview-header">
 298                        @if (!string.IsNullOrWhiteSpace(_externalPreviewSource))
 299                        {
 300                            <span class="external-link-preview-source">@_externalPreviewSource.ToUpperInvariant()</span>
 301                        }
 302                        <MudText Typo="Typo.h6">@(_externalPreviewTitle ?? "External Link")</MudText>
 303                    </div>
 304                    @if (!string.IsNullOrWhiteSpace(_externalPreviewContent?.Kind))
 305                    {
 306                        <MudText Typo="Typo.caption" Style="color: var(--mud-palette-text-secondary);">
 307                            @_externalPreviewContent.Kind
 308                        </MudText>
 309                    }
 310                    @if (_externalPreviewLoading)
 311                    {
 312                        <div class="d-flex justify-center align-center" style="padding: 24px;">
 313                            <MudProgressCircular Indeterminate="true" />
 314                        </div>
 315                    }
 316                    else if (!string.IsNullOrWhiteSpace(_externalPreviewError))
 317                    {
 318                        <MudAlert Severity="Severity.Error">@_externalPreviewError</MudAlert>
 319                    }
 320                    else if (_externalPreviewContent != null)
 321                    {
 322                        <ExternalLinkDetailPanel Content="_externalPreviewContent" />
 323                    }
 324                </MudDrawerContainer>
 325            </MudDrawer>
 326
 327            @if (_isMapModalOpen)
 328            {
 329                <SessionMapViewerModal IsOpen="_isMapModalOpen"
 330                                       WorldId="CurrentWorldId"
 331                                       MapId="_selectedMapId"
 332                                       MapName="@_selectedMapName"
 333                                       OnClose="CloseMapModalAsync" />
 334            }
 335
 336            @if (_showAutocomplete)
 337            {
 338                <ArticleDetailWikiLinkAutocomplete Suggestions="@_autocompleteSuggestions"
 339                                                   Loading="@_autocompleteLoading"
 340                                                   SelectedIndex="@_autocompleteSelectedIndex"
 341                                                   SelectedIndexChanged="OnAutocompleteIndexChanged"
 342                                                   OnSelect="OnAutocompleteSelect"
 343                                                   OnCreate="OnAutocompleteCreate"
 344                                                   Position="@_autocompletePosition"
 345                                                   Query="@_autocompleteQuery"
 346                                                   IsExternalQuery="@_autocompleteIsExternalQuery" />
 347            }
 348        }
 349    </MudPaper>
 350}
 351
 352@code {
 1353    private static readonly DialogOptions _deleteDialogOptions = new()
 1354    {
 1355        CloseOnEscapeKey = true,
 1356        MaxWidth = MaxWidth.Small,
 1357        FullWidth = true,
 1358    };
 359
 360    [Parameter]
 361    public Guid SessionId { get; set; }
 362
 363    private enum SessionEditorKind
 364    {
 365        Public,
 366        Private
 367    }
 368
 369    private sealed class SessionEditorInteropBridge
 370    {
 371        private readonly SessionDetail _parent;
 372        private readonly SessionEditorKind _editorKind;
 373
 374        public SessionEditorInteropBridge(SessionDetail parent, SessionEditorKind editorKind)
 375        {
 32376            _parent = parent;
 32377            _editorKind = editorKind;
 32378        }
 379
 380        [JSInvokable]
 1381        public Task OnEditorUpdate(string html) => _parent.HandleEditorUpdateAsync(_editorKind, html);
 382
 383        [JSInvokable]
 384        public Task OnAutocompleteTriggered(string query, double x, double y)
 1385            => _parent.HandleAutocompleteTriggeredAsync(_editorKind, query, x, y);
 386
 387        [JSInvokable]
 1388        public Task OnAutocompleteHidden() => _parent.HandleAutocompleteHiddenAsync(_editorKind);
 389
 390        [JSInvokable]
 1391        public Task OnAutocompleteArrowDown() => _parent.HandleAutocompleteArrowDownAsync(_editorKind);
 392
 393        [JSInvokable]
 1394        public Task OnAutocompleteArrowUp() => _parent.HandleAutocompleteArrowUpAsync(_editorKind);
 395
 396        [JSInvokable]
 1397        public Task OnAutocompleteEnter() => _parent.HandleAutocompleteEnterAsync(_editorKind);
 398
 399        [JSInvokable]
 2400        public Task OnWikiLinkClicked(string targetArticleId) => _parent.OnWikiLinkClicked(targetArticleId);
 401
 402        [JSInvokable]
 1403        public Task OnBrokenLinkClicked(string targetArticleId) => _parent.OnBrokenLinkClicked(targetArticleId);
 404
 405        [JSInvokable]
 406        public Task OnExternalLinkClicked(string source, string id, string title)
 1407            => _parent.OnExternalLinkClicked(source, id, title);
 408
 409        [JSInvokable]
 410        public Task OnMapLinkClicked(string mapId, string? mapName)
 4411            => _parent.HandleMapLinkClickedAsync(mapId, mapName);
 412
 413        [JSInvokable]
 1414        public Task<string?> GetArticlePath(string targetArticleId) => _parent.GetArticlePath(targetArticleId);
 415
 416        [JSInvokable]
 417        public Task<object?> GetArticleSummaryPreview(string targetArticleId)
 1418            => _parent.GetArticleSummaryPreview(targetArticleId);
 419    }
 420
 421    private SessionEditorInteropBridge? _publicEditorBridge;
 422    private SessionEditorInteropBridge? _privateEditorBridge;
 423    private DotNetObjectReference<SessionEditorInteropBridge>? _publicEditorBridgeRef;
 424    private DotNetObjectReference<SessionEditorInteropBridge>? _privateEditorBridgeRef;
 425    private bool _publicEditorInitialized;
 426    private bool _privateEditorInitialized;
 427    private bool _editorsInitializing;
 428    private SessionEditorKind _activeEditorKind = SessionEditorKind.Public;
 429
 430    private bool _showAutocomplete;
 431    private bool _autocompleteLoading;
 16432    private List<ArticleWikiLinkAutocompleteItem> _autocompleteSuggestions = new();
 433    private int _autocompleteSelectedIndex;
 16434    private (double X, double Y) _autocompletePosition = (0, 0);
 16435    private string _autocompleteQuery = string.Empty;
 436    private bool _autocompleteIsExternalQuery;
 437    private string? _autocompleteExternalSourceKey;
 438
 439    private bool _externalPreviewOpen;
 440    private bool _externalPreviewLoading;
 441    private string? _externalPreviewError;
 442    private ExternalLinkContentDto? _externalPreviewContent;
 443    private string? _externalPreviewSource;
 444    private string? _externalPreviewTitle;
 16445    private readonly Dictionary<string, ExternalLinkContentDto> _externalLinkCache = new(StringComparer.OrdinalIgnoreCas
 446    private bool _isMapModalOpen;
 447    private Guid _selectedMapId;
 448    private string? _selectedMapName;
 449
 131450    private string PublicEditorElementId => $"session-public-editor-{SessionId}";
 130451    private string PrivateEditorElementId => $"session-private-editor-{SessionId}";
 6452    private Guid CurrentWorldId => ViewModel.World?.Id ?? AppContext.CurrentWorldId ?? Guid.Empty;
 453
 454    protected override void OnInitialized()
 455    {
 16456        KeyboardShortcutService.OnSaveRequested += HandleSaveShortcut;
 16457        DrawerCoordinator.OnChanged += HandleDrawerCoordinatorChanged;
 16458        _publicEditorBridge = new SessionEditorInteropBridge(this, SessionEditorKind.Public);
 16459        _privateEditorBridge = new SessionEditorInteropBridge(this, SessionEditorKind.Private);
 16460        _publicEditorBridgeRef = DotNetObjectReference.Create(_publicEditorBridge);
 16461        _privateEditorBridgeRef = DotNetObjectReference.Create(_privateEditorBridge);
 16462    }
 463
 464    protected override async Task OnParametersSetAsync()
 465    {
 466        await DestroyEditorsAsync();
 467        ViewModel.PropertyChanged -= OnViewModelChanged;
 468        ViewModel.PropertyChanged += OnViewModelChanged;
 469        await ViewModel.LoadAsync(SessionId);
 470
 471        if (ViewModel.World != null &&
 472            (AppContext.CurrentWorldId != ViewModel.World.Id || AppContext.CurrentCampaignId != ViewModel.Campaign?.Id))
 473        {
 474            await AppContext.SelectWorldAsync(ViewModel.World.Id, ViewModel.Campaign?.Id);
 475        }
 476    }
 477
 478    protected override async Task OnAfterRenderAsync(bool firstRender)
 479    {
 480        if (ViewModel.IsLoading || !ViewModel.CanManageSessionDetails || ViewModel.Session == null || ViewModel.Session.
 481        {
 482            return;
 483        }
 484
 485        if (_editorsInitializing || (_publicEditorInitialized && _privateEditorInitialized))
 486        {
 487            return;
 488        }
 489
 490        await InitializeEditorsAsync();
 491    }
 492
 493    private void OnViewModelChanged(object? sender, PropertyChangedEventArgs e)
 180494        => InvokeAsync(StateHasChanged);
 495
 496    private async Task OnSessionTitleKeyDown(KeyboardEventArgs e)
 497    {
 498        if (e.Key == "Enter" && ViewModel.CanManageSessionDetails)
 499        {
 500            await SaveSessionNotesAsync();
 501        }
 502    }
 503
 504    private async void HandleSaveShortcut()
 505    {
 506        if (!ViewModel.CanManageSessionDetails || ViewModel.Session == null)
 507        {
 508            return;
 509        }
 510
 511        await InvokeAsync(SaveSessionNotesAsync);
 512    }
 513
 514    private void OnSessionDateChanged(DateTime? value)
 515    {
 3516        ViewModel.EditSessionDate = value?.Date;
 3517    }
 518
 519    private async Task OpenDeleteDialogAsync()
 520    {
 521        if (!ViewModel.IsCurrentUserGM || ViewModel.Session == null || ViewModel.IsDeleting)
 522        {
 523            return;
 524        }
 525
 526        var parameters = new DialogParameters<DeleteSessionDialog>
 527        {
 528            { x => x.SessionName, ViewModel.Session.Name }
 529        };
 530
 531        var dialog = await DialogService.ShowAsync<DeleteSessionDialog>(
 532            $"Delete \"{ViewModel.Session.Name}\"", parameters, _deleteDialogOptions);
 533
 534        var result = await dialog.Result;
 535        if (result is { Canceled: false })
 536        {
 537            await ViewModel.DeleteSessionAsync();
 538        }
 539    }
 540
 541    private static string FormatSessionDate(DateTime? sessionDate)
 4542        => sessionDate?.ToLocalTime().ToString("MMMM d, yyyy") ?? "Not set";
 543
 4544    private static Color GetVisibilityColor(ArticleVisibility visibility) => visibility switch
 4545    {
 1546        ArticleVisibility.Public => Color.Success,
 1547        ArticleVisibility.MembersOnly => Color.Warning,
 1548        ArticleVisibility.Private => Color.Error,
 1549        _ => Color.Default
 4550    };
 551
 552    public void Dispose()
 553    {
 16554        KeyboardShortcutService.OnSaveRequested -= HandleSaveShortcut;
 16555        DrawerCoordinator.OnChanged -= HandleDrawerCoordinatorChanged;
 16556        ViewModel.PropertyChanged -= OnViewModelChanged;
 16557        _publicEditorBridgeRef?.Dispose();
 16558        _privateEditorBridgeRef?.Dispose();
 15559    }
 560
 561    private void HandleDrawerCoordinatorChanged()
 562    {
 3563        if (!_externalPreviewOpen || DrawerCoordinator.Current == DrawerType.None)
 564        {
 2565            return;
 566        }
 567
 1568        _externalPreviewOpen = false;
 1569        _ = InvokeAsync(StateHasChanged);
 1570    }
 571
 572    private async Task InitializeEditorsAsync()
 573    {
 574        if (ViewModel.Session == null || !ViewModel.CanManageSessionDetails || _publicEditorBridgeRef == null || _privat
 575        {
 576            return;
 577        }
 578
 579        _editorsInitializing = true;
 580
 581        try
 582        {
 583            await JSRuntime.InvokeVoidAsync("initializeTipTapEditor", PublicEditorElementId, ViewModel.EditPublicNotes, 
 584            await JSRuntime.InvokeVoidAsync("initializeWikiLinkAutocomplete", PublicEditorElementId, _publicEditorBridge
 585            await JSRuntime.InvokeVoidAsync("initializeImageUpload", PublicEditorElementId, _publicEditorBridgeRef);
 586            await JSRuntime.InvokeVoidAsync("resolveEditorImages", PublicEditorElementId, _publicEditorBridgeRef);
 587            _publicEditorInitialized = true;
 588
 589            await JSRuntime.InvokeVoidAsync("initializeTipTapEditor", PrivateEditorElementId, ViewModel.EditPrivateNotes
 590            await JSRuntime.InvokeVoidAsync("initializeWikiLinkAutocomplete", PrivateEditorElementId, _privateEditorBrid
 591            await JSRuntime.InvokeVoidAsync("initializeImageUpload", PrivateEditorElementId, _privateEditorBridgeRef);
 592            await JSRuntime.InvokeVoidAsync("resolveEditorImages", PrivateEditorElementId, _privateEditorBridgeRef);
 593            _privateEditorInitialized = true;
 594        }
 595        catch (JSDisconnectedException)
 596        {
 597            // Navigation/disposal race.
 598        }
 599        catch (Exception ex)
 600        {
 601            Logger.LogError(ex, "Failed to initialize session note editors for {SessionId}", SessionId);
 602            Snackbar.Add("Failed to initialize session editors", Severity.Warning);
 603        }
 604        finally
 605        {
 606            _editorsInitializing = false;
 607        }
 608    }
 609
 610    private async Task DestroyEditorsAsync()
 611    {
 612        try
 613        {
 614            if (_publicEditorInitialized)
 615            {
 616                await JSRuntime.InvokeVoidAsync("destroyTipTapEditor", PublicEditorElementId);
 617            }
 618
 619            if (_privateEditorInitialized)
 620            {
 621                await JSRuntime.InvokeVoidAsync("destroyTipTapEditor", PrivateEditorElementId);
 622            }
 623        }
 624        catch (JSDisconnectedException)
 625        {
 626            // Ignore during navigation/disposal.
 627        }
 628        catch (Exception ex)
 629        {
 630            Logger.LogDebug(ex, "Failed to destroy session editors");
 631        }
 632        finally
 633        {
 634            _publicEditorInitialized = false;
 635            _privateEditorInitialized = false;
 636            _showAutocomplete = false;
 637            _autocompleteSuggestions = new();
 638        }
 639    }
 640
 641    private async Task SaveSessionNotesAsync()
 642    {
 643        await SyncEditorsToViewModelAsync();
 644        await ViewModel.SaveNotesAsync();
 645    }
 646
 647    private async Task SyncEditorsToViewModelAsync()
 648    {
 649        if (!ViewModel.CanManageSessionDetails)
 650        {
 651            return;
 652        }
 653
 654        try
 655        {
 656            if (_publicEditorInitialized)
 657            {
 658                ViewModel.EditPublicNotes = await JSRuntime.InvokeAsync<string>("getTipTapContent", PublicEditorElementI
 659            }
 660
 661            if (_privateEditorInitialized)
 662            {
 663                ViewModel.EditPrivateNotes = await JSRuntime.InvokeAsync<string>("getTipTapContent", PrivateEditorElemen
 664            }
 665        }
 666        catch (JSDisconnectedException)
 667        {
 668            // Ignore during navigation/disposal.
 669        }
 670    }
 671
 672    private Task HandleEditorUpdateAsync(SessionEditorKind editorKind, string html)
 673    {
 4674        if (!ViewModel.CanManageSessionDetails)
 675        {
 1676            return Task.CompletedTask;
 677        }
 678
 3679        if (editorKind == SessionEditorKind.Public)
 680        {
 2681            ViewModel.EditPublicNotes = html;
 682        }
 683        else
 684        {
 1685            ViewModel.EditPrivateNotes = html;
 686        }
 687
 3688        return Task.CompletedTask;
 689    }
 690
 691    private async Task HandleAutocompleteTriggeredAsync(SessionEditorKind editorKind, string query, double x, double y)
 692    {
 693        var normalizedQuery = query?.TrimStart() ?? string.Empty;
 694        _activeEditorKind = editorKind;
 695        _autocompletePosition = (x, y);
 696        _showAutocomplete = true;
 697        _autocompleteSelectedIndex = 0;
 698        var isMapAutocomplete = normalizedQuery.StartsWith("maps/", StringComparison.OrdinalIgnoreCase);
 699        string? mapSearch = null;
 700
 701        if (isMapAutocomplete)
 702        {
 703            var remainder = normalizedQuery.Substring("maps/".Length);
 704            mapSearch = string.IsNullOrWhiteSpace(remainder) ? null : remainder.Trim();
 705            _autocompleteIsExternalQuery = false;
 706            _autocompleteExternalSourceKey = null;
 707            _autocompleteQuery = mapSearch ?? string.Empty;
 708        }
 709        else
 710        {
 711            _autocompleteIsExternalQuery = TryParseExternalAutocompleteQuery(normalizedQuery, out var sourceKey, out var
 712            var mapsExternalSource = _autocompleteIsExternalQuery
 713                && string.Equals(sourceKey, "maps", StringComparison.OrdinalIgnoreCase);
 714
 715            if (mapsExternalSource)
 716            {
 717                isMapAutocomplete = true;
 718                mapSearch = string.IsNullOrWhiteSpace(remainder) ? null : remainder.Trim();
 719                _autocompleteIsExternalQuery = false;
 720                _autocompleteExternalSourceKey = null;
 721                _autocompleteQuery = mapSearch ?? string.Empty;
 722            }
 723            else
 724            {
 725                _autocompleteExternalSourceKey = _autocompleteIsExternalQuery ? sourceKey : null;
 726                _autocompleteQuery = _autocompleteIsExternalQuery ? remainder : normalizedQuery;
 727            }
 728
 729            var minLength = _autocompleteIsExternalQuery ? 0 : 3;
 730            if (!isMapAutocomplete && _autocompleteQuery.Length < minLength)
 731            {
 732                _autocompleteSuggestions = new();
 733                await InvokeAsync(StateHasChanged);
 734                return;
 735            }
 736        }
 737
 738        _autocompleteLoading = true;
 739        await InvokeAsync(StateHasChanged);
 740
 741        try
 742        {
 743            var worldId = ViewModel.World?.Id ?? AppContext.CurrentWorldId ?? Guid.Empty;
 744
 745            if (isMapAutocomplete)
 746            {
 747                var mapSuggestions = await MapApi.GetMapAutocompleteAsync(worldId, mapSearch);
 748                _autocompleteSuggestions = mapSuggestions
 749                    .Select(ArticleWikiLinkAutocompleteItem.FromMapAutocomplete)
 750                    .ToList();
 751            }
 752            else if (_autocompleteIsExternalQuery)
 753            {
 754                var externalSuggestions = await ExternalLinkApiService.GetSuggestionsAsync(
 755                    worldId,
 756                    _autocompleteExternalSourceKey ?? string.Empty,
 757                    _autocompleteQuery,
 758                    CancellationToken.None);
 759
 760                _autocompleteSuggestions = externalSuggestions.Select(ArticleWikiLinkAutocompleteItem.FromExternal).ToLi
 761            }
 762            else
 763            {
 764                var internalSuggestions = await LinkApiService.GetSuggestionsAsync(worldId, _autocompleteQuery);
 765                _autocompleteSuggestions = internalSuggestions.Select(ArticleWikiLinkAutocompleteItem.FromInternal).ToLi
 766            }
 767        }
 768        catch (Exception ex)
 769        {
 770            Logger.LogError(ex, "Error getting session editor autocomplete suggestions");
 771            _autocompleteSuggestions = new();
 772        }
 773        finally
 774        {
 775            _autocompleteLoading = false;
 776            await InvokeAsync(StateHasChanged);
 777        }
 778    }
 779
 780    private Task HandleAutocompleteHiddenAsync(SessionEditorKind editorKind)
 781    {
 3782        if (_activeEditorKind != editorKind)
 783        {
 1784            return Task.CompletedTask;
 785        }
 786
 2787        _showAutocomplete = false;
 2788        _autocompleteSuggestions = new();
 2789        _autocompleteIsExternalQuery = false;
 2790        _autocompleteExternalSourceKey = null;
 2791        return InvokeAsync(StateHasChanged);
 792    }
 793
 794    private Task HandleAutocompleteArrowDownAsync(SessionEditorKind editorKind)
 795    {
 3796        _activeEditorKind = editorKind;
 797
 3798        if (_autocompleteSuggestions.Any())
 799        {
 1800            _autocompleteSelectedIndex = (_autocompleteSelectedIndex + 1) % _autocompleteSuggestions.Count;
 1801            return InvokeAsync(StateHasChanged);
 802        }
 803
 2804        return Task.CompletedTask;
 805    }
 806
 807    private Task HandleAutocompleteArrowUpAsync(SessionEditorKind editorKind)
 808    {
 3809        _activeEditorKind = editorKind;
 810
 3811        if (_autocompleteSuggestions.Any())
 812        {
 1813            _autocompleteSelectedIndex = (_autocompleteSelectedIndex - 1 + _autocompleteSuggestions.Count) % _autocomple
 1814            return InvokeAsync(StateHasChanged);
 815        }
 816
 2817        return Task.CompletedTask;
 818    }
 819
 820    private async Task HandleAutocompleteEnterAsync(SessionEditorKind editorKind)
 821    {
 822        _activeEditorKind = editorKind;
 823
 824        if (_autocompleteSuggestions.Any() && _autocompleteSelectedIndex < _autocompleteSuggestions.Count)
 825        {
 826            await OnAutocompleteSelect(_autocompleteSuggestions[_autocompleteSelectedIndex]);
 827        }
 828    }
 829
 830    private Task OnAutocompleteIndexChanged(int index)
 831    {
 1832        _autocompleteSelectedIndex = index;
 1833        StateHasChanged();
 1834        return Task.CompletedTask;
 835    }
 836
 837    private async Task OnAutocompleteSelect(ArticleWikiLinkAutocompleteItem suggestion)
 838    {
 839        if (!ViewModel.CanManageSessionDetails)
 840        {
 841            return;
 842        }
 843
 844        var editorElementId = _activeEditorKind == SessionEditorKind.Public ? PublicEditorElementId : PrivateEditorEleme
 845
 846        try
 847        {
 848            if (suggestion.IsCategory && !string.IsNullOrEmpty(suggestion.CategoryKey))
 849            {
 850                await JSRuntime.InvokeVoidAsync("updateAutocompleteText", editorElementId, $"{suggestion.Source}/{sugges
 851                return;
 852            }
 853
 854            if (suggestion.MapId.HasValue)
 855            {
 856                await JSRuntime.InvokeVoidAsync(
 857                    "insertMapLinkToken",
 858                    editorElementId,
 859                    suggestion.MapId.Value.ToString(),
 860                    suggestion.Title);
 861            }
 862
 863            else if (suggestion.IsExternal)
 864            {
 865                if (string.IsNullOrWhiteSpace(suggestion.Source) || string.IsNullOrWhiteSpace(suggestion.ExternalId))
 866                {
 867                    Logger.LogWarning("External suggestion missing source or id");
 868                    return;
 869                }
 870
 871                await JSRuntime.InvokeVoidAsync(
 872                    "insertExternalLinkToken",
 873                    editorElementId,
 874                    suggestion.Source,
 875                    suggestion.ExternalId,
 876                    suggestion.Title);
 877            }
 878            else
 879            {
 880                if (!suggestion.ArticleId.HasValue)
 881                {
 882                    Logger.LogWarning("Internal suggestion missing article id");
 883                    return;
 884                }
 885
 886                var displayText = !string.IsNullOrWhiteSpace(suggestion.MatchedAlias)
 887                    ? $"{suggestion.MatchedAlias} â†’ {suggestion.Title}"
 888                    : suggestion.Title;
 889
 890                await JSRuntime.InvokeVoidAsync("insertWikiLink", editorElementId, suggestion.ArticleId.Value.ToString()
 891            }
 892
 893            _showAutocomplete = false;
 894            _autocompleteSuggestions = new();
 895            StateHasChanged();
 896        }
 897        catch (Exception ex)
 898        {
 899            Logger.LogError(ex, "Error inserting session editor link");
 900            Snackbar.Add("Failed to insert link", Severity.Error);
 901        }
 902    }
 903
 904    private async Task OnAutocompleteCreate(string articleName)
 905    {
 906        if (_autocompleteIsExternalQuery || string.IsNullOrWhiteSpace(articleName))
 907        {
 908            return;
 909        }
 910
 911        var worldId = AppContext.CurrentWorldId ?? ViewModel.World?.Id ?? Guid.Empty;
 912        if (worldId == Guid.Empty)
 913        {
 914            Snackbar.Add("Unable to determine world for new article", Severity.Warning);
 915            return;
 916        }
 917
 918        var editorElementId = _activeEditorKind == SessionEditorKind.Public ? PublicEditorElementId : PrivateEditorEleme
 919
 920        try
 921        {
 922            var created = await WikiLinkService.CreateArticleFromAutocompleteAsync(articleName, worldId);
 923            if (created == null)
 924            {
 925                Snackbar.Add("Failed to create article", Severity.Error);
 926                return;
 927            }
 928
 929            await JSRuntime.InvokeVoidAsync("insertWikiLink", editorElementId, created.Id.ToString(), created.Title);
 930            _showAutocomplete = false;
 931            _autocompleteSuggestions = new();
 932            StateHasChanged();
 933            Snackbar.Add($"Created and linked '{articleName}'", Severity.Success);
 934        }
 935        catch (Exception ex)
 936        {
 937            Logger.LogError(ex, "Error creating article from session editor autocomplete");
 938            Snackbar.Add($"Failed to create article: {ex.Message}", Severity.Error);
 939        }
 940    }
 941
 942    private static bool TryParseExternalAutocompleteQuery(string query, out string sourceKey, out string remainder)
 943    {
 8944        sourceKey = string.Empty;
 8945        remainder = string.Empty;
 946
 8947        if (string.IsNullOrWhiteSpace(query))
 948        {
 1949            return false;
 950        }
 951
 7952        var slashIndex = query.IndexOf('/');
 7953        if (slashIndex <= 0)
 954        {
 4955            return false;
 956        }
 957
 3958        sourceKey = query.Substring(0, slashIndex).Trim().ToLowerInvariant();
 3959        remainder = query.Substring(slashIndex + 1);
 3960        return !string.IsNullOrWhiteSpace(sourceKey);
 961    }
 962
 963    private Task HandleMapLinkClickedAsync(string mapId, string? mapName)
 964    {
 4965        if (!Guid.TryParse(mapId, out var parsedMapId))
 966        {
 1967            return Task.CompletedTask;
 968        }
 969
 3970        _selectedMapId = parsedMapId;
 3971        _selectedMapName = string.IsNullOrWhiteSpace(mapName) ? null : mapName.Trim();
 3972        _isMapModalOpen = true;
 973
 3974        return InvokeAsync(StateHasChanged);
 975    }
 976
 977    private Task CloseMapModalAsync()
 978    {
 1979        _isMapModalOpen = false;
 1980        _selectedMapId = Guid.Empty;
 1981        _selectedMapName = null;
 982
 1983        return InvokeAsync(StateHasChanged);
 984    }
 985
 986    private async Task OnWikiLinkClicked(string targetArticleId)
 987    {
 988        if (!Guid.TryParse(targetArticleId, out var articleId))
 989        {
 990            return;
 991        }
 992
 993        try
 994        {
 995            var path = await ArticleCache.GetNavigationPathAsync(articleId);
 996            if (!string.IsNullOrWhiteSpace(path))
 997            {
 998                Navigation.NavigateTo($"/article/{path}");
 999            }
 1000            else
 1001            {
 1002                Snackbar.Add("Article not found", Severity.Warning);
 1003            }
 1004        }
 1005        catch (Exception ex)
 1006        {
 1007            Logger.LogError(ex, "Error navigating to wiki link {ArticleId}", targetArticleId);
 1008            Snackbar.Add("Failed to navigate to article", Severity.Error);
 1009        }
 1010    }
 1011
 1012    private Task OnBrokenLinkClicked(string targetArticleId)
 1013    {
 21014        Snackbar.Add("This link points to a missing article", Severity.Warning);
 21015        return Task.CompletedTask;
 1016    }
 1017
 1018    private async Task<string?> GetArticlePath(string targetArticleId)
 1019    {
 1020        if (!Guid.TryParse(targetArticleId, out var articleId))
 1021        {
 1022            return null;
 1023        }
 1024
 1025        try
 1026        {
 1027            return await ArticleCache.GetArticlePathAsync(articleId);
 1028        }
 1029        catch (Exception ex)
 1030        {
 1031            Logger.LogError(ex, "Error getting article path {ArticleId}", targetArticleId);
 1032            return null;
 1033        }
 1034    }
 1035
 1036    private async Task<object?> GetArticleSummaryPreview(string targetArticleId)
 1037    {
 1038        if (!Guid.TryParse(targetArticleId, out var articleId))
 1039        {
 1040            return null;
 1041        }
 1042
 1043        try
 1044        {
 1045            var preview = await SummaryApi.GetSummaryPreviewAsync(articleId);
 1046            if (preview == null || !preview.HasSummary)
 1047            {
 1048                return null;
 1049            }
 1050
 1051            return new
 1052            {
 1053                title = preview.Title,
 1054                summary = preview.Summary,
 1055                templateName = preview.TemplateName
 1056            };
 1057        }
 1058        catch (Exception ex)
 1059        {
 1060            Logger.LogError(ex, "Error getting article summary preview {ArticleId}", targetArticleId);
 1061            return null;
 1062        }
 1063    }
 1064
 1065    private async Task OnExternalLinkClicked(string source, string id, string title)
 1066    {
 1067        if (string.IsNullOrWhiteSpace(source) || string.IsNullOrWhiteSpace(id))
 1068        {
 1069            return;
 1070        }
 1071
 1072        DrawerCoordinator.Close();
 1073        if (DrawerCoordinator.Current != DrawerType.None)
 1074        {
 1075            return;
 1076        }
 1077
 1078        _externalPreviewOpen = true;
 1079        _externalPreviewLoading = true;
 1080        _externalPreviewError = null;
 1081        _externalPreviewSource = source;
 1082        _externalPreviewTitle = string.IsNullOrWhiteSpace(title) ? "External Link" : title;
 1083        _externalPreviewContent = null;
 1084        StateHasChanged();
 1085
 1086        var cacheKey = $"{source}:{id}".ToLowerInvariant();
 1087        if (_externalLinkCache.TryGetValue(cacheKey, out var cached))
 1088        {
 1089            _externalPreviewContent = cached;
 1090            _externalPreviewLoading = false;
 1091            StateHasChanged();
 1092            return;
 1093        }
 1094
 1095        try
 1096        {
 1097            var content = await ExternalLinkApiService.GetContentAsync(source, id, CancellationToken.None);
 1098
 1099            if (content == null || string.IsNullOrWhiteSpace(content.Markdown))
 1100            {
 1101                _externalPreviewError = "No content available.";
 1102            }
 1103            else
 1104            {
 1105                _externalPreviewContent = content;
 1106                _externalLinkCache[cacheKey] = content;
 1107            }
 1108        }
 1109        catch (Exception ex)
 1110        {
 1111            Logger.LogError(ex, "Error loading external link preview for {Source} {Id}", source, id);
 1112            _externalPreviewError = "Failed to load external content.";
 1113        }
 1114        finally
 1115        {
 1116            _externalPreviewLoading = false;
 1117            StateHasChanged();
 1118        }
 1119    }
 1120
 1121    private void CloseExternalPreview()
 1122    {
 11123        _externalPreviewOpen = false;
 11124        StateHasChanged();
 11125    }
 1126
 1127    private async Task InsertPrivateImageFromToolbarAsync()
 1128    {
 1129        if (!_privateEditorInitialized || _privateEditorBridgeRef == null)
 1130        {
 1131            return;
 1132        }
 1133
 1134        await JSRuntime.InvokeVoidAsync("triggerImageUpload", PrivateEditorElementId, _privateEditorBridgeRef);
 1135    }
 1136
 1137    [JSInvokable]
 1138    public async Task<object?> OnImageUploadRequested(string fileName, string contentType, long fileSize)
 1139    {
 1140        var worldId = ViewModel.World?.Id ?? AppContext.CurrentWorldId ?? Guid.Empty;
 1141        if (worldId == Guid.Empty)
 1142        {
 1143            return null;
 1144        }
 1145
 1146        try
 1147        {
 1148            var request = new WorldDocumentUploadRequestDto
 1149            {
 1150                FileName = fileName,
 1151                ContentType = contentType,
 1152                FileSizeBytes = fileSize,
 1153                ArticleId = null,
 1154                Description = $"Inline image for session private notes: {ViewModel.Session?.Name ?? "Session"}"
 1155            };
 1156
 1157            var response = await WorldApi.RequestDocumentUploadAsync(worldId, request);
 1158            if (response == null)
 1159            {
 1160                return null;
 1161            }
 1162
 1163            return new
 1164            {
 1165                uploadUrl = response.UploadUrl,
 1166                documentId = response.DocumentId.ToString()
 1167            };
 1168        }
 1169        catch (Exception ex)
 1170        {
 1171            Logger.LogError(ex, "Error requesting session private note image upload for world {WorldId}", worldId);
 1172            return null;
 1173        }
 1174    }
 1175
 1176    [JSInvokable]
 1177    public async Task OnImageUploadConfirmed(string documentIdStr)
 1178    {
 1179        var worldId = ViewModel.World?.Id ?? AppContext.CurrentWorldId ?? Guid.Empty;
 1180        if (worldId == Guid.Empty || !Guid.TryParse(documentIdStr, out var documentId))
 1181        {
 1182            return;
 1183        }
 1184
 1185        try
 1186        {
 1187            await WorldApi.ConfirmDocumentUploadAsync(worldId, documentId);
 1188        }
 1189        catch (Exception ex)
 1190        {
 1191            Logger.LogError(ex, "Error confirming session private note image upload {DocumentId}", documentId);
 1192        }
 1193    }
 1194
 1195    [JSInvokable]
 11196    public string GetImageProxyUrl(string documentIdStr) => $"chronicis-image:{documentIdStr}";
 1197
 1198    [JSInvokable]
 1199    public async Task<string?> ResolveImageUrl(string documentIdStr)
 1200    {
 1201        if (!Guid.TryParse(documentIdStr, out var documentId))
 1202        {
 1203            return null;
 1204        }
 1205
 1206        try
 1207        {
 1208            var result = await WorldApi.DownloadDocumentAsync(documentId);
 1209            return result?.DownloadUrl;
 1210        }
 1211        catch (Exception ex)
 1212        {
 1213            Logger.LogError(ex, "Error resolving session private note image {DocumentId}", documentId);
 1214            return null;
 1215        }
 1216    }
 1217
 1218    [JSInvokable]
 1219    public void OnImageUploadStarted(string fileName)
 1220    {
 11221        Snackbar.Add($"Uploading {fileName}...", Severity.Info);
 11222        StateHasChanged();
 11223    }
 1224
 1225    [JSInvokable]
 1226    public void OnImageUploadError(string message)
 1227    {
 11228        Snackbar.Add(message, Severity.Error);
 11229        StateHasChanged();
 11230    }
 1231}

Methods/Properties

BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder)
.cctor()
.ctor(Chronicis.Client.Pages.SessionDetail,Chronicis.Client.Pages.SessionDetail/SessionEditorKind)
OnEditorUpdate(System.String)
OnAutocompleteTriggered(System.String,System.Double,System.Double)
OnAutocompleteHidden()
OnAutocompleteArrowDown()
OnAutocompleteArrowUp()
OnAutocompleteEnter()
OnWikiLinkClicked(System.String)
OnBrokenLinkClicked(System.String)
OnExternalLinkClicked(System.String,System.String,System.String)
OnMapLinkClicked(System.String,System.String)
GetArticlePath(System.String)
GetArticleSummaryPreview(System.String)
.ctor()
get_PublicEditorElementId()
get_PrivateEditorElementId()
get_CurrentWorldId()
OnInitialized()
OnViewModelChanged(System.Object,System.ComponentModel.PropertyChangedEventArgs)
OnSessionDateChanged(System.Nullable`1<System.DateTime>)
FormatSessionDate(System.Nullable`1<System.DateTime>)
GetVisibilityColor(Chronicis.Shared.Enums.ArticleVisibility)
Dispose()
HandleDrawerCoordinatorChanged()
HandleEditorUpdateAsync(Chronicis.Client.Pages.SessionDetail/SessionEditorKind,System.String)
HandleAutocompleteHiddenAsync(Chronicis.Client.Pages.SessionDetail/SessionEditorKind)
HandleAutocompleteArrowDownAsync(Chronicis.Client.Pages.SessionDetail/SessionEditorKind)
HandleAutocompleteArrowUpAsync(Chronicis.Client.Pages.SessionDetail/SessionEditorKind)
OnAutocompleteIndexChanged(System.Int32)
TryParseExternalAutocompleteQuery(System.String,System.String&,System.String&)
HandleMapLinkClickedAsync(System.String,System.String)
CloseMapModalAsync()
OnBrokenLinkClicked(System.String)
CloseExternalPreview()
GetImageProxyUrl(System.String)
OnImageUploadStarted(System.String)
OnImageUploadError(System.String)