LANCommander/LANCommander.UI/Components/Chat/ChatThread.razor
Pat Hartl 66abc6e59e Implement infinite loading with chat thread
- Chat messages are now loaded based on scroll position
- Read status is now updated and set by last message read
- Reworked script importing by using ScriptProvider
2026-01-11 14:38:24 -06:00

154 lines
No EOL
5.7 KiB
Text

@using System.Collections.Specialized
@using LANCommander.SDK.Models
@using LANCommander.SDK.Services
@inject IChatClient ChatClient
@namespace LANCommander.UI.Components
<Flex Direction="FlexDirection.Vertical" Class="chat-thread">
<Flex Direction="FlexDirection.Vertical" Class="chat-thread-messages">
@if (_thread != null)
{
<InfiniteLoader
T="SDK.Models.ChatMessageGroup"
KeySelector="g => g.Id.ToString()"
Loader="LoadMessages"
PageSize="10">
<ChatMessageGroup
UserId="context.UserId"
UserName="@context.UserName"
StartedOn="context.StartedOn.DateTime">
@foreach (var message in context.Messages)
{
<ChatMessage Id="message.Id" Content="@message.Content" SentOn="message.SentOn" />
}
</ChatMessageGroup>
</InfiniteLoader>
}
</Flex>
<ChatMessageInput ThreadId="@Id" Placeholder="@($"Message {_thread?.Title}")" />
</Flex>
@code {
[Parameter] public Guid? Id { get; set; }
SDK.Models.ChatThread? _thread;
private SDK.Models.ChatMessage? _lastCursor;
protected override async Task OnParametersSetAsync()
{
if ((_thread == null || _thread.Id != Id) && Id.HasValue)
{
_thread = await ChatClient.GetThreadAsync(Id.Value);
_thread.MessageGroups.CollectionChanged += MessageGroupsOnCollectionChanged;
await ChatClient.GetMessagesAsync(Id.Value, null, 10);
}
}
void MessageGroupsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
=> InvokeAsync(StateHasChanged);
async Task<InfiniteResponse<SDK.Models.ChatMessageGroup>> LoadMessages(SDK.Models.ChatMessageGroup? cursor, int pageSize)
{
// Use the oldest message from the cursor group, or the last cursor message
SDK.Models.ChatMessage? messageCursor = null;
if (cursor != null && cursor.Messages.Count > 0)
{
// Get the oldest message from the cursor group (first message in chronological order)
messageCursor = cursor.Messages.OrderBy(m => m.SentOn).First();
}
else if (_lastCursor != null)
{
messageCursor = _lastCursor;
}
var response = await ChatClient.GetMessagesAsync(Id.Value, messageCursor?.Id ?? null, pageSize);
// Filter out blank/whitespace messages
var validMessages = (response.Items ?? [])
.Where(m => !string.IsNullOrWhiteSpace(m.Content))
.ToList();
// Mark messages as read when they are loaded
if (Id.HasValue && validMessages.Count > 0)
{
await ChatClient.UpdatedReadStatus(Id.Value);
}
// Group messages by sender (messages are already in chronological order from the service)
var groups = GroupMessagesBySender(validMessages);
// Track the oldest message for next cursor (from the oldest group)
// Groups are in newest-first order (will be reversed by InfiniteLoader)
// So the oldest group is the last one
if (groups.Count > 0)
{
var oldestGroup = groups[groups.Count - 1];
if (oldestGroup.Messages.Count > 0)
{
// Get the oldest message from the oldest group
_lastCursor = oldestGroup.Messages.OrderBy(m => m.SentOn).First();
}
}
return new InfiniteResponse<SDK.Models.ChatMessageGroup>
{
Items = groups,
HasMore = response.HasMore
};
}
private List<SDK.Models.ChatMessageGroup> GroupMessagesBySender(IEnumerable<SDK.Models.ChatMessage> messages)
{
var groups = new List<SDK.Models.ChatMessageGroup>();
SDK.Models.ChatMessageGroup? currentGroup = null;
// Messages come from service in newest-first order (OrderByDescending)
// We'll process them in reverse to get oldest-first for grouping
var messagesList = messages.ToList();
// Reverse to get oldest-first order for proper grouping
messagesList.Reverse();
foreach (var message in messagesList)
{
// Start a new group if this is the first message, or if the sender changed
if (currentGroup == null || message.UserId != currentGroup.UserId)
{
if (currentGroup != null)
{
groups.Add(currentGroup);
}
currentGroup = new SDK.Models.ChatMessageGroup
{
Id = Guid.NewGuid(),
UserId = message.UserId,
UserName = message.UserName,
Messages = new List<SDK.Models.ChatMessage> { message }
};
}
else
{
// Add to current group (messages within group are in chronological order)
currentGroup.Messages.Add(message);
}
}
// Add the last group
if (currentGroup != null)
{
groups.Add(currentGroup);
}
// Groups are now in oldest-first order (first group is oldest)
// InfiniteLoader will reverse them, so we need to reverse again to maintain oldest-first
// Actually, wait - InfiniteLoader reverses, so if we return oldest-first, after reversal it's newest-first (wrong)
// So we should return newest-first, then InfiniteLoader reverses to oldest-first
groups.Reverse();
return groups;
}
}