177 lines
5.6 KiB
TypeScript
177 lines
5.6 KiB
TypeScript
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<MeetupLike, "accessLevel">, 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<MeetupLike, "mode">): boolean {
|
|
return meetup.mode === "online" || meetup.mode === "hybrid";
|
|
}
|