Skip to content

Commit 7813f7f

Browse files
committed
PDChat improvements
1 parent 4db5f28 commit 7813f7f

8 files changed

Lines changed: 161 additions & 25 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using Microsoft.AspNetCore.Components;
2+
using Microsoft.JSInterop;
3+
4+
namespace PanoramicData.Blazor;
5+
6+
/// <summary>
7+
/// Base class for Blazor components that need to load JavaScript modules.
8+
/// Provides common functionality for loading and disposing JavaScript modules.
9+
/// </summary>
10+
public abstract class JSModuleComponentBase : ComponentBase, IAsyncDisposable
11+
{
12+
[Inject] protected IJSRuntime JSRuntime { get; set; } = null!;
13+
14+
protected IJSObjectReference? Module { get; private set; }
15+
16+
/// <summary>
17+
/// Gets the JavaScript module path for this component.
18+
/// </summary>
19+
protected abstract string ModulePath { get; }
20+
21+
/// <summary>
22+
/// Called after the JavaScript module is successfully loaded.
23+
/// Override this method to perform initialization that requires the module.
24+
/// </summary>
25+
/// <param name="firstRender">True if this is the first render of the component.</param>
26+
protected virtual Task OnModuleLoadedAsync(bool firstRender) => Task.CompletedTask;
27+
28+
/// <summary>
29+
/// Called after every render when the module is available.
30+
/// Override this method to perform actions that should happen after each render.
31+
/// </summary>
32+
/// <param name="firstRender">True if this is the first render of the component.</param>
33+
protected virtual Task OnAfterRenderWithModuleAsync(bool firstRender) => Task.CompletedTask;
34+
35+
protected override async Task OnAfterRenderAsync(bool firstRender)
36+
{
37+
if (firstRender && JSRuntime is not null)
38+
{
39+
try
40+
{
41+
Module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", ModulePath).ConfigureAwait(true);
42+
await OnModuleLoadedAsync(firstRender);
43+
}
44+
catch
45+
{
46+
// BC-40 - fast page switching in Server Side blazor can lead to OnAfterRender call after page / objects disposed
47+
}
48+
}
49+
50+
if (Module is not null)
51+
{
52+
try
53+
{
54+
await OnAfterRenderWithModuleAsync(firstRender);
55+
}
56+
catch
57+
{
58+
// Ignore JavaScript errors if the element is not available
59+
}
60+
}
61+
}
62+
63+
public virtual async ValueTask DisposeAsync()
64+
{
65+
try
66+
{
67+
if (Module is not null)
68+
{
69+
await Module.DisposeAsync();
70+
Module = null;
71+
}
72+
}
73+
catch
74+
{
75+
// Ignore disposal errors
76+
}
77+
78+
GC.SuppressFinalize(this);
79+
}
80+
}

PanoramicData.Blazor/Models/PDChatDockMode.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
public enum PDChatDockMode
44
{
5+
Minimized,
56
BottomRight,
67
TopRight,
78
BottomLeft,

PanoramicData.Blazor/PDChat.razor

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
@using static PanoramicData.Blazor.PDResizePane
22
@inherits ComponentBase
33

4-
<div class="pdchat-container @GetDockModeClasses() @( _isOpen ? "open" : "" )">
5-
@if (_isOpen)
4+
<div class="pdchat-container @GetDockModeClasses() @( DockMode == PDChatDockMode.Minimized ? "open" : "" )">
5+
@if (DockMode != PDChatDockMode.Minimized)
66
{
7-
<div class="pdchat-window@(IsFullScreen ? " fullscreen" : string.Empty)">
7+
<div class="pdchat-window@(DockMode == PDChatDockMode.FullScreen ? " fullscreen" : string.Empty)">
88
<div class="pdchat-header">
99
<span class="pdchat-title">@Title@(ChatService.IsLive ? string.Empty : " (Offline)")</span>
1010
@if (IsClearPermitted && _messages.Count > 0)
@@ -18,14 +18,14 @@
1818
</button>
1919
@if (IsMaximizePermitted)
2020
{
21-
<button class="pdchat-header-btn" @onclick="ToggleFullScreenAsync" title="@(IsFullScreen ? "Restore" : "Fullscreen")">
22-
<span>@(IsFullScreen ? "🗗" : "🗖")</span>
21+
<button class="pdchat-header-btn" @onclick="ToggleFullScreenAsync" title="@(DockMode == PDChatDockMode.FullScreen ? "Restore" : "Fullscreen")">
22+
<span>@(DockMode == PDChatDockMode.FullScreen ? "🗗" : "🗖")</span>
2323
</button>
2424
}
2525
<button class="pdchat-close" @onclick="ToggleChat" title="Minimize">🗕</button>
2626
</div>
2727

28-
@if (IsFullScreen && IsCanvasUsePermitted)
28+
@if (DockMode == PDChatDockMode.FullScreen && IsCanvasUsePermitted)
2929
{
3030
<PDSplitter Direction="@GetSplitDirection()" GutterSize="6" CssClass="pdchat-splitter">
3131
@if (IsChatFirstPanel())
@@ -93,14 +93,14 @@
9393
{
9494
@if(ChatService.IsLive)
9595
{
96-
<button class="pdchat-toggle-collapsed" @onclick="ToggleChat">
97-
<span>@(_isOpen ? "×" : "💬")</span>
96+
<button class="pdchat-toggle-collapsed @(_unreadMessages ? "pulsate" : "")" @onclick="ToggleChat">
97+
<span>@CollapsedIcon</span>
9898
</button>
9999
}
100100
else
101101
{
102102
<button class="pdchat-toggle-collapsed pdchat-not-live" @onclick="ToggleChat" title="Offline">
103-
<span>💬</span>
103+
<span>@CollapsedIcon</span>
104104
</button>
105105
}
106106
}

PanoramicData.Blazor/PDChat.razor.cs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ public partial class PDChat
4444
public bool IsClearPermitted { get; set; } = true;
4545

4646
private IJSObjectReference? _module;
47-
private bool _isOpen;
4847
private bool _isMuted;
4948
private bool _unreadMessages;
5049
private string _currentInput = "";
@@ -54,14 +53,11 @@ public partial class PDChat
5453
private PDTabSet? _tabSetRef;
5554
private PDMessages? _pdMessages;
5655

57-
// Helper property to check if in fullscreen mode
58-
private bool IsFullScreen => DockMode == PDChatDockMode.FullScreen;
59-
6056
protected override Task OnInitializedAsync()
6157
{
6258
// Initialize previous dock mode with current value
6359
_previousDockMode = DockMode;
64-
60+
6561
ChatService.OnMessageReceived += OnMessageReceived;
6662
ChatService.OnLiveStatusChanged += OnLiveStatusChanged;
6763
ChatService.Initialize();
@@ -73,7 +69,7 @@ private void OnLiveStatusChanged(bool obj)
7369
StateHasChanged();
7470
}
7571

76-
private void OnMessageReceived(ChatMessage message)
72+
private async void OnMessageReceived(ChatMessage message)
7773
{
7874
var existing = _messages.FirstOrDefault(m => m.Id == message.Id);
7975
if (existing != null)
@@ -90,7 +86,7 @@ private void OnMessageReceived(ChatMessage message)
9086
_messages.Add(message);
9187
}
9288

93-
if (!_isOpen)
89+
if (DockMode == PDChatDockMode.Minimized)
9490
{
9591
_unreadMessages = true;
9692
}
@@ -104,7 +100,7 @@ private void OnMessageReceived(ChatMessage message)
104100
_module?.InvokeVoidAsync("playSound", soundUrlString);
105101
}
106102

107-
InvokeAsync(StateHasChanged);
103+
await InvokeAsync(StateHasChanged);
108104
}
109105

110106
public void ShowToast(ChatMessage message)
@@ -115,11 +111,15 @@ public void ShowToast(ChatMessage message)
115111

116112
private void ToggleChat()
117113
{
118-
_isOpen = !_isOpen;
119-
120-
if (_isOpen)
114+
if (DockMode == PDChatDockMode.Minimized)
121115
{
122-
_unreadMessages = false;
116+
DockMode = _previousDockMode;
117+
_unreadMessages = false; // Clear unread messages when chat is opened
118+
}
119+
else
120+
{
121+
_previousDockMode = DockMode;
122+
DockMode = PDChatDockMode.Minimized;
123123
}
124124
}
125125

@@ -137,7 +137,7 @@ private async Task ToggleFullScreenAsync()
137137
}
138138
else
139139
{
140-
// Store current dock mode before going fullscreen
140+
// Store current dock mode before going full screen
141141
_previousDockMode = DockMode;
142142
await SetDockModeAsync(PDChatDockMode.FullScreen);
143143
}
@@ -209,6 +209,11 @@ private string GetDockModeClasses()
209209
PDChatDockMode.BottomLeft => "dock-bottom-left",
210210
PDChatDockMode.TopLeft => "dock-top-left",
211211
PDChatDockMode.FullScreen => "dock-fullscreen",
212+
PDChatDockMode.Left => "dock-left",
213+
PDChatDockMode.Right => "dock-right",
214+
PDChatDockMode.Top => "dock-top",
215+
PDChatDockMode.Bottom => "dock-bottom",
216+
PDChatDockMode.Minimized => "dock-minimized",
212217
_ => "dock-bottom-right" // Default fallback
213218
};
214219
}

PanoramicData.Blazor/PDChat.razor.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,34 @@
2525
left: 20px;
2626
}
2727

28+
.pdchat-container.dock-left {
29+
top: 50%;
30+
left: 20px;
31+
transform: translateY(-50%);
32+
}
33+
34+
.pdchat-container.dock-right {
35+
top: 50%;
36+
right: 20px;
37+
transform: translateY(-50%);
38+
}
39+
40+
.pdchat-container.dock-top {
41+
top: 20px;
42+
left: 50%;
43+
transform: translateX(-50%);
44+
}
45+
46+
.pdchat-container.dock-bottom {
47+
bottom: 20px;
48+
left: 50%;
49+
transform: translateX(-50%);
50+
}
51+
52+
.pdchat-container.dock-minimized {
53+
/* Minimized state inherits positioning from previous dock mode */
54+
}
55+
2856
.pdchat-container.dock-fullscreen {
2957
top: 0;
3058
left: 0;

PanoramicData.Blazor/PDMessages.razor.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public partial class PDMessages
2424

2525
protected override async Task OnParametersSetAsync()
2626
{
27+
// Always try to scroll to bottom when parameters change (new messages)
2728
await ScrollToBottomAsync();
2829
}
2930

@@ -34,6 +35,9 @@ protected async override Task OnAfterRenderAsync(bool firstRender)
3435
_module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/PanoramicData.Blazor/PDMessages.razor.js").ConfigureAwait(true);
3536
}
3637

38+
await ScrollToBottomAsync();
39+
40+
3741
if (_inputRef.Context != null)
3842
{
3943
await _inputRef.FocusAsync();
@@ -47,9 +51,17 @@ private async Task ScrollToBottomAsync()
4751
return;
4852
}
4953

50-
StateHasChanged();
54+
// Small delay to ensure DOM is updated
55+
await Task.Delay(10);
5156

52-
await _module.InvokeVoidAsync("scrollToBottom", _messagesContainer);
57+
try
58+
{
59+
await _module.InvokeVoidAsync("scrollToBottom", _messagesContainer);
60+
}
61+
catch (Exception)
62+
{
63+
// Ignore JavaScript errors if the element is not available
64+
}
5365
}
5466

5567
private async Task OnInputChanged(ChangeEventArgs e)

PanoramicData.Blazor/PDMessages.razor.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
background: #f9f9f9;
1919
min-height: 0;
2020
max-height: none;
21+
scroll-behavior: smooth;
2122
}
2223

2324
/* Maximized mode support for messages and input */
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
export function scrollToBottom(element) {
22
if (element) {
3-
element.scrollTop = element.scrollHeight;
3+
// Use smooth scrolling for better UX
4+
element.scrollTo({
5+
top: element.scrollHeight,
6+
behavior: 'smooth'
7+
});
8+
9+
// Fallback for browsers that don't support smooth scrolling
10+
if (element.scrollTop < element.scrollHeight - element.clientHeight - 10) {
11+
element.scrollTop = element.scrollHeight;
12+
}
413
}
514
}

0 commit comments

Comments
 (0)