export type MeetupMode = "offline" | "online" | "hybrid"; export type MeetupAccessLevel = "public" | "members" | "vip" | "hosts"; export interface MeetupFeatureOption { key: string; zh: string; en: string; } export interface MeetupLike { id: string; city?: string; date?: string; time?: string; meetingUrl?: string; mirotalkRoom?: string; loungeChannel?: string; mode?: MeetupMode | string; accessLevel?: MeetupAccessLevel | string; } export interface MeetupViewer { loaded?: boolean; user?: { id?: string; email?: string; name?: string; vip?: boolean; role?: string; memberRole?: string; siteRole?: string; } | null; vip?: boolean; } export interface MeetupSession { canJoin: boolean; lockReason?: string; displayName: string; videoUrl: string; chatUrl: string; replayUrl?: string; loungeChannel: string; mirotalkRoom: string; meetingProvider: string; user?: MeetupViewer["user"]; } export const DEFAULT_MIROTALK_URL = "https://mirotalk.nomadro.cn"; export const MIROTALK_PROVIDER_LABEL = "MiroTalk P2P"; export const DEFAULT_LOUNGE_URL = "https://lounge.nomadro.cn"; export const WEB_CHAT_PROVIDER_LABEL = "The Lounge + Ergo IRC"; export const MEETUP_FEATURE_OPTIONS: MeetupFeatureOption[] = [ { key: "group_video", zh: "多人视频", en: "Group video" }, { key: "screen_share", zh: "屏幕共享", en: "Screen share" }, { key: "whiteboard", zh: "白板协作", en: "Whiteboard" }, { key: "breakout", zh: "分组讨论", en: "Breakout rooms" }, { key: "recording", zh: "录制回放", en: "Recording" }, { key: "host_controls", zh: "主持人控场", en: "Host controls" }, ]; export const DEFAULT_ONLINE_FEATURES = ["group_video", "screen_share", "whiteboard", "host_controls"]; export function normalizeMiroTalkRoom(value: string): string { return value .trim() .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 80); } export function normalizeLoungeChannel(value: string): string { const channel = value.trim(); if (!channel) return ""; return channel.startsWith("#") ? channel : `#${channel}`; } export function createMiroTalkRoomSlug(meetup: MeetupLike): string { const datePart = String(meetup.date || "").slice(0, 10).replace(/[^0-9]/g, ""); const timePart = String(meetup.time || "").slice(0, 5).replace(/[^0-9]/g, ""); const cityPart = normalizeMiroTalkRoom(meetup.city || "online"); return normalizeMiroTalkRoom(["nomadcna", cityPart, datePart, timePart, meetup.id].filter(Boolean).join("-")); } export function createLoungeChannel(meetup: MeetupLike): string { const explicit = normalizeLoungeChannel(meetup.loungeChannel || ""); if (explicit) return explicit; const room = normalizeMiroTalkRoom(meetup.mirotalkRoom || ""); if (room) return `#${room}`; return `#${createMiroTalkRoomSlug(meetup)}`; } export function getMiroTalkBaseUrl(): string { return ( process.env.NEXT_PUBLIC_MIROTALK_URL || process.env.NEXT_PUBLIC_MIROTALK_P2P_URL || process.env.NEXT_PUBLIC_MIROTALK_SFU_URL || DEFAULT_MIROTALK_URL ).replace(/\/$/, ""); } export function getMiroTalkServerLabel(): string { const baseUrl = getMiroTalkBaseUrl(); try { return new URL(baseUrl).host; } catch { return baseUrl.replace(/^https?:\/\//i, ""); } } export function buildMiroTalkJoinUrl(meetup: MeetupLike, displayName = "NomadCNA"): string { if (meetup.meetingUrl && /^https?:\/\//i.test(meetup.meetingUrl)) { return meetup.meetingUrl; } const room = normalizeMiroTalkRoom(meetup.mirotalkRoom || createMiroTalkRoomSlug(meetup)); const name = displayName.trim() || "NomadCNA"; const params = new URLSearchParams({ room, name }); return `${getMiroTalkBaseUrl()}/join?${params.toString()}`; } export function getLoungeBaseUrl(): string { return (process.env.NEXT_PUBLIC_LOUNGE_URL || DEFAULT_LOUNGE_URL).replace(/\/$/, ""); } export function getLoungeServerLabel(): string { const baseUrl = getLoungeBaseUrl(); try { return new URL(baseUrl).host; } catch { return baseUrl.replace(/^https?:\/\//i, ""); } } export function buildMeetupChatUrl(meetup: MeetupLike): string { const channel = createLoungeChannel(meetup); return `${getLoungeBaseUrl()}/#/chan-${channel}`; } export function buildMeetupLivePath(meetupId: string): string { return `/meetups/${meetupId}/live`; } export function viewerDisplayName(viewer: MeetupViewer): string { const name = String(viewer.user?.name || "").trim(); if (name) return name; const email = String(viewer.user?.email || "").trim(); return email ? email.split("@")[0] : "NomadCNA"; } export function isHostViewer(viewer: MeetupViewer): boolean { const role = String( viewer.user?.role || viewer.user?.memberRole || viewer.user?.siteRole || "" ).toLowerCase(); return ["admin", "owner", "host", "organizer", "moderator"].includes(role); } export function canAccessMeetup(meetup: Pick, viewer: MeetupViewer): boolean { const access = String(meetup.accessLevel || "public").toLowerCase(); if (access === "public") return true; if (access === "members") return Boolean(viewer.user); if (access === "vip") return Boolean(viewer.vip || viewer.user?.vip); return isHostViewer(viewer); } export function featureLabels(keys: string[], locale: string): string[] { const selected = new Set(keys); return MEETUP_FEATURE_OPTIONS.filter((feature) => selected.has(feature.key)).map((feature) => locale === "zh" ? feature.zh : feature.en ); } export function isLiveMeetup(meetup: Pick): boolean { return meetup.mode === "online" || meetup.mode === "hybrid"; }