refactor(guild_chat): extract chat-user lookup to viewer repo; fix NEW-direction exclusivity
Add ChatUserProfile record and IViewerRepository.LoadChatProfilesAsync (AsNoTracking, projects Name/EmblemId/CountryCode/Rank/DegreeId from Viewer+Info nav refs). GuildChatController.Messages now injects IViewerRepository instead of SVSimDbContext; calls LoadChatProfilesAsync to populate users[] (removes direct db access from controller). GuildChatMessageRepository direction=2 (NEW): change >= start to > start (exclusive). GuildChatPollTests updated to assert the start message is NOT returned by direction=NEW. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,9 +102,9 @@ public sealed class GuildChatMessageRepository : IGuildChatMessageRepository
|
||||
.OrderBy(m => m.MessageId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
case 2: // NEW — messages newer than (and including) start
|
||||
case 2: // NEW: strictly newer than start (exclusive)
|
||||
return await baseQ
|
||||
.Where(m => m.MessageId >= start)
|
||||
.Where(m => m.MessageId > start)
|
||||
.OrderBy(m => m.MessageId)
|
||||
.Take(limit)
|
||||
.ToListAsync(ct);
|
||||
|
||||
@@ -2,6 +2,17 @@ using SVSim.Database.Enums;
|
||||
|
||||
namespace SVSim.Database.Repositories.Viewer;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight profile shape used by chat surfaces (guild_chat, gathering_chat).
|
||||
/// Fields match the ChatUserDto wire shape.
|
||||
/// </summary>
|
||||
public record ChatUserProfile(
|
||||
string Name,
|
||||
long EmblemId,
|
||||
string CountryCode,
|
||||
int Rank,
|
||||
int DegreeId);
|
||||
|
||||
public interface IViewerRepository
|
||||
{
|
||||
Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId);
|
||||
@@ -39,4 +50,11 @@ public interface IViewerRepository
|
||||
/// ids with no matching row are absent from the result. Read-only (AsNoTracking).
|
||||
/// </summary>
|
||||
Task<Dictionary<long, string>> LoadDisplayNamesAsync(IReadOnlyCollection<long> viewerIds, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Batch-loads the <see cref="ChatUserProfile"/> fields for a set of viewer ids. Used by chat
|
||||
/// surfaces (guild_chat, gathering_chat) to populate <c>users[]</c> without a direct DB context
|
||||
/// in the controller. Ids with no matching row are absent from the result. Read-only (AsNoTracking).
|
||||
/// </summary>
|
||||
Task<IReadOnlyDictionary<long, ChatUserProfile>> LoadChatProfilesAsync(IReadOnlyCollection<long> viewerIds, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -296,6 +296,33 @@ public class ViewerRepository : IViewerRepository
|
||||
.ToDictionaryAsync(v => v.Id, v => v.DisplayName, ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<long, ChatUserProfile>> LoadChatProfilesAsync(IReadOnlyCollection<long> viewerIds, CancellationToken ct = default)
|
||||
{
|
||||
if (viewerIds.Count == 0) return new Dictionary<long, ChatUserProfile>();
|
||||
var rows = await _dbContext.Set<Models.Viewer>()
|
||||
.AsNoTracking()
|
||||
.Include(v => v.Info.SelectedEmblem)
|
||||
.Include(v => v.Info.SelectedDegree)
|
||||
.Where(v => viewerIds.Contains(v.Id))
|
||||
.Select(v => new
|
||||
{
|
||||
v.Id,
|
||||
v.DisplayName,
|
||||
EmblemId = v.Info.SelectedEmblem != null ? v.Info.SelectedEmblem.Id : 100_000_000L,
|
||||
CountryCode = v.Info.CountryCode ?? "",
|
||||
DegreeId = v.Info.SelectedDegree != null ? (int)v.Info.SelectedDegree.Id : 0,
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
return rows.ToDictionary(
|
||||
r => r.Id,
|
||||
r => new ChatUserProfile(
|
||||
Name: r.DisplayName,
|
||||
EmblemId: r.EmblemId,
|
||||
CountryCode: r.CountryCode,
|
||||
Rank: 1, // TODO: real rank when rank tracking lands
|
||||
DegreeId: r.DegreeId));
|
||||
}
|
||||
|
||||
private async Task<Models.Viewer> BuildDefaultViewer(string displayName, int initialTutorialState = 1)
|
||||
{
|
||||
Models.Viewer viewer = new Models.Viewer
|
||||
|
||||
Reference in New Issue
Block a user