From a391c51761995116d2a4a3610f1dd4b2d13987c6 Mon Sep 17 00:00:00 2001 From: Toutsu Date: Mon, 15 Jun 2026 15:05:12 +0300 Subject: [PATCH] feat(listsessions): add join/leave buttons for players MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For non-managers /listsessions now shows player-friendly actions: - ✅ Записаться when not registered - ✖️ Выйти when already active - ✖️ Выйти из ожидания when waitlisted Extend SessionListItemDto and the shared SQL query with IsUserActive and IsUserWaitlisted flags so the renderer can choose the right button. Update tests to cover all three player states. --- .../SessionListMessageRenderer.cs | 36 ++++++++- .../ListSessions/ListSessionsHandler.cs | 34 ++++++++- .../SessionListMessageRendererTests.cs | 73 +++++++++++++++++-- 3 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/GmRelay.Bot/Features/Sessions/ListSessions/SessionListMessageRenderer.cs b/src/GmRelay.Bot/Features/Sessions/ListSessions/SessionListMessageRenderer.cs index de3fe78..48d892b 100644 --- a/src/GmRelay.Bot/Features/Sessions/ListSessions/SessionListMessageRenderer.cs +++ b/src/GmRelay.Bot/Features/Sessions/ListSessions/SessionListMessageRenderer.cs @@ -23,11 +23,18 @@ internal static class SessionListMessageRenderer public static IReadOnlyList RenderActions(IReadOnlyList sessions) { - if (sessions.Count == 0 || !sessions.First().CanManage) + if (sessions.Count == 0) { return []; } + return sessions.First().CanManage + ? RenderManagerActions(sessions) + : RenderPlayerActions(sessions); + } + + private static IReadOnlyList RenderManagerActions(IReadOnlyList sessions) + { var actions = new List(); foreach (var session in sessions) @@ -60,4 +67,31 @@ internal static class SessionListMessageRenderer return actions; } + + private static IReadOnlyList RenderPlayerActions(IReadOnlyList sessions) + { + var actions = new List(); + + foreach (var session in sessions) + { + var dateTitle = session.ScheduledAt.FormatMoscowShort(); + + if (session.IsUserActive || session.IsUserWaitlisted) + { + actions.Add(new PlatformMessageAction( + $"leave_session:{session.Id}", + session.IsUserWaitlisted ? $"✖️ Выйти из ожидания {dateTitle}" : $"✖️ Выйти {dateTitle}", + $"leave_session:{session.Id}")); + } + else + { + actions.Add(new PlatformMessageAction( + $"join_session:{session.Id}", + $"✅ Записаться {dateTitle}", + $"join_session:{session.Id}")); + } + } + + return actions; + } } diff --git a/src/GmRelay.Shared/Features/Sessions/ListSessions/ListSessionsHandler.cs b/src/GmRelay.Shared/Features/Sessions/ListSessions/ListSessionsHandler.cs index 25d047a..38fb65e 100644 --- a/src/GmRelay.Shared/Features/Sessions/ListSessions/ListSessionsHandler.cs +++ b/src/GmRelay.Shared/Features/Sessions/ListSessions/ListSessionsHandler.cs @@ -5,7 +5,17 @@ using Npgsql; namespace GmRelay.Shared.Features.Sessions.ListSessions; -public sealed record SessionListItemDto(Guid Id, string Title, DateTime ScheduledAt, string Status, int? MaxPlayers, int PlayerCount, int WaitlistCount, bool CanManage); +public sealed record SessionListItemDto( + Guid Id, + string Title, + DateTime ScheduledAt, + string Status, + int? MaxPlayers, + int PlayerCount, + int WaitlistCount, + bool CanManage, + bool IsUserActive, + bool IsUserWaitlisted); public sealed record SessionListResult( IReadOnlyList Sessions, @@ -29,7 +39,27 @@ public sealed class ListSessionsHandler( WHERE gm.group_id = s.group_id AND manager_player.platform = @Platform AND manager_player.external_user_id = @ExternalUserId - ) AS CanManage + ) AS CanManage, + EXISTS ( + SELECT 1 + FROM session_participants user_sp + JOIN players user_p ON user_p.id = user_sp.player_id + WHERE user_sp.session_id = s.id + AND user_sp.is_gm = false + AND user_sp.registration_status = @Active + AND user_p.platform = @Platform + AND user_p.external_user_id = @ExternalUserId + ) AS IsUserActive, + EXISTS ( + SELECT 1 + FROM session_participants user_sp + JOIN players user_p ON user_p.id = user_sp.player_id + WHERE user_sp.session_id = s.id + AND user_sp.is_gm = false + AND user_sp.registration_status = @Waitlisted + AND user_p.platform = @Platform + AND user_p.external_user_id = @ExternalUserId + ) AS IsUserWaitlisted FROM sessions s JOIN game_groups g ON s.group_id = g.id LEFT JOIN session_participants sp ON s.id = sp.session_id diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/ListSessions/SessionListMessageRendererTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/ListSessions/SessionListMessageRendererTests.cs index cca7d80..e4368f0 100644 --- a/tests/GmRelay.Bot.Tests/Features/Sessions/ListSessions/SessionListMessageRendererTests.cs +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/ListSessions/SessionListMessageRendererTests.cs @@ -20,7 +20,9 @@ public sealed class SessionListMessageRendererTests 4, 3, 1, - true) + true, + false, + false) }; var text = SessionListMessageRenderer.RenderText(sessions); @@ -41,22 +43,83 @@ public sealed class SessionListMessageRendererTests } [Fact] - public void Render_ShouldHideManagerActions_WhenUserCannotManage() + public void Render_ShouldIncludeJoinAction_WhenPlayerIsNotRegistered() { + var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionListItemDto( - Guid.NewGuid(), + sessionId, + "Ravenloft", + new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc), + SessionStatus.Planned, + 4, + 3, + 0, + false, + false, + false) + }; + + var actions = SessionListMessageRenderer.RenderActions(sessions); + var shortDate = new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc).FormatMoscowShort(); + + Assert.Single(actions); + Assert.Contains(actions, a => a.Payload == $"join_session:{sessionId}"); + Assert.Contains(actions, a => a.Label == $"✅ Записаться {shortDate}"); + } + + [Fact] + public void Render_ShouldIncludeLeaveAction_WhenPlayerIsActive() + { + var sessionId = Guid.NewGuid(); + var sessions = new[] + { + new SessionListItemDto( + sessionId, + "Ravenloft", + new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc), + SessionStatus.Planned, + 4, + 3, + 0, + false, + true, + false) + }; + + var actions = SessionListMessageRenderer.RenderActions(sessions); + var shortDate = new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc).FormatMoscowShort(); + + Assert.Single(actions); + Assert.Contains(actions, a => a.Payload == $"leave_session:{sessionId}"); + Assert.Contains(actions, a => a.Label == $"✖️ Выйти {shortDate}"); + } + + [Fact] + public void Render_ShouldIncludeLeaveWaitlistAction_WhenPlayerIsWaitlisted() + { + var sessionId = Guid.NewGuid(); + var sessions = new[] + { + new SessionListItemDto( + sessionId, "Ravenloft", new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc), SessionStatus.Planned, 4, 3, 1, - false) + false, + false, + true) }; var actions = SessionListMessageRenderer.RenderActions(sessions); - Assert.Empty(actions); + var shortDate = new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc).FormatMoscowShort(); + + Assert.Single(actions); + Assert.Contains(actions, a => a.Payload == $"leave_session:{sessionId}"); + Assert.Contains(actions, a => a.Label == $"✖️ Выйти из ожидания {shortDate}"); } }