014b5edd31
PR Checks / test-and-build (pull_request) Successful in 15m52s
Add format and location steps to the Telegram /newsession wizard, persist offline addresses in sessions.location_address, and render online links/offline addresses in schedule messages. Bump version to 3.10.0.
284 lines
16 KiB
C#
284 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using GmRelay.Shared.Domain;
|
|
|
|
namespace GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
|
|
|
/// <summary>
|
|
/// Produces a (text, list of <see cref="WizardAction"/>s) pair for each
|
|
/// wizard step. This is the "view builder" half of ADR-002: the same
|
|
/// builder is used by every platform messenger, and each messenger is
|
|
/// responsible for converting the action list into its native UI
|
|
/// (Telegram's <c>InlineKeyboardMarkup</c> today, Discord components
|
|
/// later).
|
|
/// </summary>
|
|
public static class WizardStepViewBuilder
|
|
{
|
|
public static (string Text, IReadOnlyList<WizardAction> Actions) Build(
|
|
WizardDraft draft,
|
|
WizardPayload payload,
|
|
IReadOnlyList<WizardClubOption>? clubs = null)
|
|
{
|
|
return draft.Step switch
|
|
{
|
|
WizardStepNames.Type => BuildType(),
|
|
WizardStepNames.Title => BuildTitle(),
|
|
WizardStepNames.Description => BuildDescription(),
|
|
WizardStepNames.Cover => BuildCover(),
|
|
WizardStepNames.System => BuildSystem(),
|
|
WizardStepNames.Duration => BuildDuration(),
|
|
WizardStepNames.DateTime => BuildDateTime(),
|
|
WizardStepNames.Capacity => BuildCapacity(),
|
|
WizardStepNames.Format => BuildFormat(),
|
|
WizardStepNames.Location => BuildLocation(payload),
|
|
WizardStepNames.Visibility => BuildVisibility(),
|
|
WizardStepNames.PickClub => BuildPickClub(clubs ?? Array.Empty<WizardClubOption>()),
|
|
WizardStepNames.Publish => BuildPublish(),
|
|
WizardStepNames.Confirm => BuildSingleConfirm(payload),
|
|
|
|
WizardStepNames.PoolSystemDuration => BuildPoolSystemDuration(),
|
|
WizardStepNames.PoolAddSlots => BuildPoolAddSlots(payload),
|
|
WizardStepNames.PoolSlotDateTime => BuildPoolSlotDateTime(),
|
|
WizardStepNames.PoolSlotCapacity => BuildPoolSlotCapacity(),
|
|
WizardStepNames.PoolConfirm => BuildPoolConfirm(payload),
|
|
|
|
_ => throw new InvalidOperationException($"Unknown wizard step: {draft.Step}"),
|
|
};
|
|
}
|
|
|
|
// ── Single-game views ──────────────────────────────────────────────
|
|
private static (string, IReadOnlyList<WizardAction>) BuildType() => (
|
|
"🎲 Создание новой игровой сессии\n\nЧто создаём?",
|
|
new[]
|
|
{
|
|
new WizardAction("🎯 Одну игру", WizardCallbackData.Choice(WizardStepNames.Type, "single"), WizardActionStyle.Primary),
|
|
new WizardAction("📅 Пул игр", WizardCallbackData.Choice(WizardStepNames.Type, "pool"), WizardActionStyle.Primary),
|
|
new WizardAction("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger),
|
|
});
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildTitle() => (
|
|
"📝 Введите название игры одним сообщением.",
|
|
BackCancel());
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildDescription() => (
|
|
"📄 Введите описание (или «-», чтобы пропустить).",
|
|
SkipBackCancel());
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildCover() => (
|
|
"🖼 Пришлите картинку как вложение или URL (или «-»).",
|
|
SkipBackCancel());
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildSystem() => (
|
|
"🎲 Выберите систему.",
|
|
new List<WizardAction>
|
|
{
|
|
new("D&D 5e", WizardCallbackData.Choice(WizardStepNames.System, "Dnd5e")),
|
|
new("Pathfinder 2e", WizardCallbackData.Choice(WizardStepNames.System, "Pathfinder2e")),
|
|
new("Call of Cthulhu",WizardCallbackData.Choice(WizardStepNames.System, "CallOfCthulhu7e")),
|
|
new("GURPS", WizardCallbackData.Choice(WizardStepNames.System, "GURPS")),
|
|
new("Fate", WizardCallbackData.Choice(WizardStepNames.System, "Fate")),
|
|
new("Другое… ✏️", WizardCallbackData.Choice(WizardStepNames.System, "_other")),
|
|
new("⏭ Пропустить", WizardCallbackData.Choice(WizardStepNames.System, "_skip")),
|
|
});
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildDuration() => (
|
|
"⏱ Выберите длительность.",
|
|
new List<WizardAction>
|
|
{
|
|
new("3 часа", WizardCallbackData.Choice(WizardStepNames.Duration, "180")),
|
|
new("4 часа", WizardCallbackData.Choice(WizardStepNames.Duration, "240")),
|
|
new("5 часов", WizardCallbackData.Choice(WizardStepNames.Duration, "300")),
|
|
new("6 часов", WizardCallbackData.Choice(WizardStepNames.Duration, "360")),
|
|
new("Другое… ✏️", WizardCallbackData.Choice(WizardStepNames.Duration, "_other")),
|
|
new("⏭ Пропустить", WizardCallbackData.Choice(WizardStepNames.Duration, "_skip")),
|
|
});
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildDateTime() => (
|
|
"📅 Введите дату и время в формате ДД.ММ.ГГГГ ЧЧ:ММ (Москва).",
|
|
BackCancel());
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildCapacity() => (
|
|
"👥 Введите лимит мест (1..50) одним числом.\nЗатем нажмите кнопку waitlist. Или сразу «♾ Без лимита».",
|
|
new List<WizardAction>
|
|
{
|
|
new("✅ Waitlist вкл", WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:on"), WizardActionStyle.Success),
|
|
new("❌ Без waitlist", WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:off"), WizardActionStyle.Danger),
|
|
new("♾ Без лимита", WizardCallbackData.Choice(WizardStepNames.Capacity, "no_limit"), WizardActionStyle.Primary),
|
|
});
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildFormat() => (
|
|
"🧭 Выберите формат игры.",
|
|
new List<WizardAction>
|
|
{
|
|
new("🌐 Online", WizardCallbackData.Choice(WizardStepNames.Format, "online"), WizardActionStyle.Primary),
|
|
new("📍 Offline", WizardCallbackData.Choice(WizardStepNames.Format, "offline"), WizardActionStyle.Primary),
|
|
new("⬅️ Назад", WizardCallbackData.Back()),
|
|
new("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger),
|
|
});
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildLocation(WizardPayload payload) => payload.Format switch
|
|
{
|
|
WizardSessionFormat.Offline => ("📍 Введите адрес места проведения.", BackCancel()),
|
|
_ => ("🔗 Введите ссылку для подключения к online-игре.", BackCancel()),
|
|
};
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildVisibility() => (
|
|
"🔒 Выберите видимость.",
|
|
new List<WizardAction>
|
|
{
|
|
new("🌐 Публичная в общем showcase", WizardCallbackData.Choice(WizardStepNames.Visibility, "public"), WizardActionStyle.Primary),
|
|
new("🏠 Публичная в витрине клуба", WizardCallbackData.Choice(WizardStepNames.Visibility, "club"), WizardActionStyle.Primary),
|
|
new("🔐 Только для членов клуба", WizardCallbackData.Choice(WizardStepNames.Visibility, "members")),
|
|
new("🏷 Выбрать клуб…", WizardCallbackData.Choice(WizardStepNames.Visibility, "pickclub")),
|
|
});
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildPickClub(IReadOnlyList<WizardClubOption> clubs)
|
|
{
|
|
if (clubs.Count == 0)
|
|
{
|
|
return (
|
|
"🏷 У вас нет клубов. Создайте клуб в Web dashboard и вернитесь.",
|
|
BackCancel());
|
|
}
|
|
var actions = new List<WizardAction>(clubs.Count);
|
|
foreach (var club in clubs)
|
|
{
|
|
actions.Add(new WizardAction(
|
|
club.Name,
|
|
WizardCallbackData.Choice(WizardStepNames.PickClub, club.ClubId.ToString())));
|
|
}
|
|
return ("🏷 Выберите клуб:", actions);
|
|
}
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildPublish() => (
|
|
"✨ Опубликовать в витрине сейчас?",
|
|
new List<WizardAction>
|
|
{
|
|
new("✅ Опубликовать", WizardCallbackData.Choice(WizardStepNames.Publish, "yes"), WizardActionStyle.Success),
|
|
new("📝 Только в чате", WizardCallbackData.Choice(WizardStepNames.Publish, "no")),
|
|
});
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildSingleConfirm(WizardPayload p)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("👀 Проверьте перед созданием:");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"🎲 {p.Title}");
|
|
if (!string.IsNullOrEmpty(p.Description)) sb.AppendLine($"📄 {p.Description}");
|
|
if (!string.IsNullOrEmpty(p.System)) sb.AppendLine($"🎲 Система: {p.System}");
|
|
if (p.DurationMinutes.HasValue) sb.AppendLine($"⏱ Длительность: {p.DurationMinutes / 60} ч");
|
|
AppendFormatLocation(sb, p);
|
|
if (p.Single?.ScheduledAt is { } at) sb.AppendLine($"📅 {at.FormatMoscow()} (МСК)");
|
|
if (p.Single?.MaxPlayers is { } mp) sb.AppendLine($"👥 Мест: {mp}, waitlist {(p.Waitlist == true ? "вкл" : "выкл")}");
|
|
sb.AppendLine($"🔒 Видимость: {RenderVisibilityText(p.Visibility)}");
|
|
return (
|
|
sb.ToString(),
|
|
new List<WizardAction>
|
|
{
|
|
new("✅ Создать", WizardCallbackData.Create(), WizardActionStyle.Success),
|
|
new("⬅️ Назад", WizardCallbackData.Back()),
|
|
new("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger),
|
|
});
|
|
}
|
|
|
|
// ── Pool views ─────────────────────────────────────────────────────
|
|
private static (string, IReadOnlyList<WizardAction>) BuildPoolSystemDuration() => (
|
|
"🎲 Выберите систему и длительность пула.",
|
|
new List<WizardAction>
|
|
{
|
|
new("D&D 5e · 4 ч", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "Dnd5e:240")),
|
|
new("Pathfinder 2e · 4 ч", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "Pathfinder2e:240")),
|
|
new("Call of Cthulhu · 3 ч",WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "CallOfCthulhu7e:180")),
|
|
new("GURPS · 4 ч", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "GURPS:240")),
|
|
new("Другое… ✏️", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "_custom")),
|
|
});
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildPoolAddSlots(WizardPayload p) => (
|
|
$"📅 Слоты пула «{p.Title}»\n\nДобавлено: {(p.Pool?.Slots.Count ?? 0)}",
|
|
new List<WizardAction>
|
|
{
|
|
new("➕ Добавить слот", WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "add"), WizardActionStyle.Primary),
|
|
new("✅ Готово, к превью", WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "done"), WizardActionStyle.Success),
|
|
});
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildPoolSlotDateTime() => (
|
|
"📅 Введите дату/время слота (ДД.ММ.ГГГГ ЧЧ:ММ).",
|
|
BackCancel());
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildPoolSlotCapacity() => (
|
|
"👥 Введите лимит мест (1..50) и выберите waitlist.",
|
|
new List<WizardAction>
|
|
{
|
|
new("✅ Waitlist вкл", WizardCallbackData.Choice(WizardStepNames.PoolSlotCapacity, "waitlist:on"), WizardActionStyle.Success),
|
|
new("❌ Без waitlist", WizardCallbackData.Choice(WizardStepNames.PoolSlotCapacity, "waitlist:off"), WizardActionStyle.Danger),
|
|
});
|
|
|
|
private static (string, IReadOnlyList<WizardAction>) BuildPoolConfirm(WizardPayload p)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("👀 Проверьте пул перед созданием:");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"📝 {p.Title}");
|
|
if (!string.IsNullOrEmpty(p.Description)) sb.AppendLine($"📄 {p.Description}");
|
|
if (!string.IsNullOrEmpty(p.System)) sb.AppendLine($"🎲 Система: {p.System}");
|
|
if (p.DurationMinutes.HasValue) sb.AppendLine($"⏱ Длительность: {p.DurationMinutes / 60} ч");
|
|
AppendFormatLocation(sb, p);
|
|
sb.AppendLine($"🔒 Видимость: {RenderVisibilityText(p.Visibility)}");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"Слоты ({p.Pool?.Slots.Count ?? 0}):");
|
|
if (p.Pool is not null)
|
|
{
|
|
foreach (var s in p.Pool.Slots)
|
|
{
|
|
sb.AppendLine($" • {s.ScheduledAt.FormatMoscow()} — мест {s.MaxPlayers}, waitlist {(s.Waitlist ? "вкл" : "выкл")}");
|
|
}
|
|
}
|
|
return (
|
|
sb.ToString(),
|
|
new List<WizardAction>
|
|
{
|
|
new("✅ Создать пул", WizardCallbackData.Create(), WizardActionStyle.Success),
|
|
new("⬅️ Назад", WizardCallbackData.Back()),
|
|
new("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger),
|
|
});
|
|
}
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
private static IReadOnlyList<WizardAction> BackCancel() => new[]
|
|
{
|
|
new WizardAction("⬅️ Назад", WizardCallbackData.Back()),
|
|
new WizardAction("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger),
|
|
};
|
|
|
|
private static IReadOnlyList<WizardAction> SkipBackCancel() => new[]
|
|
{
|
|
new WizardAction("⏭ Пропустить", WizardCallbackData.Choice("Skip", "1")),
|
|
new WizardAction("⬅️ Назад", WizardCallbackData.Back()),
|
|
new WizardAction("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger),
|
|
};
|
|
|
|
private static string RenderVisibilityText(WizardVisibility? v) => v switch
|
|
{
|
|
WizardVisibility.Public => "публичная в общем showcase",
|
|
WizardVisibility.Club => "публичная в витрине клуба",
|
|
WizardVisibility.Members => "только для членов клуба",
|
|
_ => "не задана",
|
|
};
|
|
|
|
private static void AppendFormatLocation(StringBuilder sb, WizardPayload p)
|
|
{
|
|
if (p.Format is null) return;
|
|
|
|
sb.AppendLine($"🧭 Формат: {p.Format}");
|
|
if (p.Format == WizardSessionFormat.Online && !string.IsNullOrWhiteSpace(p.JoinLink))
|
|
{
|
|
sb.AppendLine($"🔗 Ссылка: {p.JoinLink}");
|
|
}
|
|
else if (p.Format == WizardSessionFormat.Offline && !string.IsNullOrWhiteSpace(p.LocationAddress))
|
|
{
|
|
sb.AppendLine($"📍 Адрес: {p.LocationAddress}");
|
|
}
|
|
}
|
|
}
|