fix: make community joins self-healing and keep /c in the app
This commit is contained in:
parent
56c718d216
commit
89e6438ee1
32 changed files with 435 additions and 29 deletions
|
|
@ -23,8 +23,10 @@
|
|||
Cache-Control = "public, max-age=31536000, immutable"
|
||||
|
||||
# Human-friendly short links.
|
||||
# /community -> the app's Community page (HashRouter route).
|
||||
# /c -> the Cordn room reference (short link for sharing).
|
||||
# /community and /c -> the app's Community page (HashRouter route).
|
||||
# /cordn is deliberately the raw Cordn room reference for Staircase/Cordn
|
||||
# clients; normal users should use /c, which stays inside the app's own native
|
||||
# MLS flow (Cordn web currently stalls after NIP-42).
|
||||
# Keep these BEFORE the SPA fallback.
|
||||
[[redirects]]
|
||||
from = "/community"
|
||||
|
|
@ -34,7 +36,7 @@
|
|||
|
||||
[[redirects]]
|
||||
from = "/c"
|
||||
to = "https://cordn.net/chat/cordn1qgdhwumn8ghj7un9d3shjtnnddshgefdwdcx7arn9ehhyecpyqpgz6s9k82wwznal8r39mm0wfe8wsnf6eda3ghjvt8qxvxs3ptfxqq5wd4kzar9wdcx7arn8f3k7mtdw4hxjare8hqt74"
|
||||
to = "/#/community"
|
||||
status = 302
|
||||
force = true
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
* COMMUNITY_RELAY coordinator relay (default wss://relay.skate-spots.org)
|
||||
* COMMUNITY_POLL_MS poll interval (default 15000)
|
||||
* COMMUNITY_AUTO_APPROVE=0 leave requests for admins instead of adding
|
||||
* COMMUNITY_MAX_MEMBERS stop auto-approving at this group size (default 250)
|
||||
* COMMUNITY_MAX_DEVICES_PER_PUBKEY per-account device cap (default 5)
|
||||
* COMMUNITY_ONCE=1 one cycle then exit (for a dry run)
|
||||
*/
|
||||
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
||||
|
|
@ -42,6 +44,14 @@ const STATE_PATH =
|
|||
process.env.COMMUNITY_STATE ?? '/var/lib/skatespots-community/state.json';
|
||||
const POLL_MS = Number(process.env.COMMUNITY_POLL_MS ?? 15_000);
|
||||
const AUTO_APPROVE = process.env.COMMUNITY_AUTO_APPROVE !== '0';
|
||||
const parsedMaxMembers = Number(process.env.COMMUNITY_MAX_MEMBERS ?? 250);
|
||||
const MAX_MEMBERS = Number.isFinite(parsedMaxMembers) && parsedMaxMembers >= 2
|
||||
? Math.floor(parsedMaxMembers)
|
||||
: 250;
|
||||
const parsedMaxDevices = Number(process.env.COMMUNITY_MAX_DEVICES_PER_PUBKEY ?? 5);
|
||||
const MAX_DEVICES_PER_PUBKEY = Number.isFinite(parsedMaxDevices) && parsedMaxDevices >= 1
|
||||
? Math.floor(parsedMaxDevices)
|
||||
: 5;
|
||||
const CREATE = process.env.COMMUNITY_CREATE === '1';
|
||||
const ONCE = process.env.COMMUNITY_ONCE === '1';
|
||||
|
||||
|
|
@ -119,10 +129,17 @@ async function joinRoom(): Promise<boolean> {
|
|||
return false;
|
||||
}
|
||||
|
||||
const approved = new Set<string>();
|
||||
// Keyed by the requester's key-package ref, not just their pubkey: one account
|
||||
// can legitimately join from a second device, and a consumed key package must
|
||||
// not be retried forever. Entries expire so a re-request after a long gap works.
|
||||
const APPROVAL_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const approved = new Map<string, number>();
|
||||
const lastAttempt = new Map<string, number>();
|
||||
let lastJoinRequestAt = 0;
|
||||
let lastError = '';
|
||||
let lastPending = 0;
|
||||
let capLogged = false;
|
||||
const deviceCapLogged = new Set<string>();
|
||||
|
||||
async function cycle(): Promise<void> {
|
||||
try {
|
||||
|
|
@ -149,23 +166,104 @@ async function cycle(): Promise<void> {
|
|||
|
||||
if (!AUTO_APPROVE) return;
|
||||
|
||||
const requests = await session.fetchJoinRequests(GID);
|
||||
const room = session.listGroups().find((group) => group.gid === GID);
|
||||
const memberCount = room?.members.length ?? 0;
|
||||
const requests = (await session.fetchJoinRequests(GID))
|
||||
.filter((request) => request.pk !== session.pubkey); // our own boot request
|
||||
if (requests.length !== lastPending && requests.length > 0) {
|
||||
log('pending join requests', requests.length);
|
||||
}
|
||||
lastPending = requests.length;
|
||||
|
||||
// A public room can be flooded by sybil key packages; MLS also gets more
|
||||
// expensive for every member. Stop auto-adding at a sane cap and leave the
|
||||
// queue for a human admin to review (raise the cap via env if needed).
|
||||
if (memberCount >= MAX_MEMBERS) {
|
||||
if (!capLogged) {
|
||||
capLogged = true;
|
||||
log('member cap reached', memberCount, '/', MAX_MEMBERS, '— leaving requests for admins');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (capLogged) {
|
||||
capLogged = false;
|
||||
log('member cap cleared; auto-approval resumed');
|
||||
}
|
||||
|
||||
const handled: Array<{ pk: string; at: number }> = [];
|
||||
for (const request of requests.slice(0, 10)) {
|
||||
if (request.pk === session.pubkey) continue; // our own boot request
|
||||
if (approved.has(request.pk)) continue;
|
||||
const last = lastAttempt.get(request.pk) ?? 0;
|
||||
if (!/^[0-9a-f]{64}$/i.test(request.pk)) {
|
||||
log('skipping malformed join request');
|
||||
continue;
|
||||
}
|
||||
const currentRoom = session.listGroups().find((group) => group.gid === GID);
|
||||
if ((currentRoom?.members.length ?? 0) >= MAX_MEMBERS) {
|
||||
if (!capLogged) {
|
||||
capLogged = true;
|
||||
log('member cap reached', currentRoom?.members.length ?? 0, '/', MAX_MEMBERS, '— stopping this cycle');
|
||||
}
|
||||
break;
|
||||
}
|
||||
const devices = currentRoom?.members.filter((member) => member.pubkey === request.pk).length ?? 0;
|
||||
if (devices >= MAX_DEVICES_PER_PUBKEY) {
|
||||
if (!deviceCapLogged.has(request.pk)) {
|
||||
deviceCapLogged.add(request.pk);
|
||||
log('device cap reached for', short(request.pk), devices, '/', MAX_DEVICES_PER_PUBKEY);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const approvalKey = request.kp_ref || request.pk;
|
||||
const approvedAt = approved.get(approvalKey) ?? 0;
|
||||
if (Date.now() - approvedAt < APPROVAL_TTL_MS) continue;
|
||||
const last = lastAttempt.get(approvalKey) ?? 0;
|
||||
if (Date.now() - last < 5 * 60_000) continue;
|
||||
try {
|
||||
await session.addMemberByIdentifier(GID, request.pk);
|
||||
approved.add(request.pk);
|
||||
// Prefer the exact key package the requester published. Falling back to
|
||||
// the pubkey keeps older coordinator responses working.
|
||||
try {
|
||||
await session.addMemberByIdentifier(GID, approvalKey);
|
||||
} catch (error) {
|
||||
if (request.kp_ref && request.kp_ref !== request.pk) {
|
||||
await session.addMemberByIdentifier(GID, request.pk);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
approved.set(approvalKey, Date.now());
|
||||
handled.push({ pk: request.pk, at: request.at });
|
||||
log('approved member', short(request.pk));
|
||||
await sleep(1500); // be gentle with the coordinator
|
||||
} catch (error) {
|
||||
lastAttempt.set(request.pk, Date.now());
|
||||
log('could not approve', short(request.pk), error instanceof Error ? error.message : String(error));
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
// A stale request for an account already in the group cannot be added
|
||||
// again. Retire it instead of retrying forever (and let the user use
|
||||
// the Link-device flow for a second device).
|
||||
if (/already in the group/i.test(message)) {
|
||||
approved.set(approvalKey, Date.now());
|
||||
handled.push({ pk: request.pk, at: request.at });
|
||||
log('request already satisfied', short(request.pk));
|
||||
continue;
|
||||
}
|
||||
lastAttempt.set(approvalKey, Date.now());
|
||||
log('could not approve', short(request.pk), message);
|
||||
await sleep(1500);
|
||||
}
|
||||
}
|
||||
|
||||
if (handled.length > 0) {
|
||||
try {
|
||||
await session.fetchJoinRequests(GID, handled);
|
||||
log('retired handled requests', handled.length);
|
||||
} catch (error) {
|
||||
log('could not retire handled requests', error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the in-memory approval memory bounded.
|
||||
const cutoff = Date.now() - APPROVAL_TTL_MS;
|
||||
for (const [key, at] of approved) {
|
||||
if (at < cutoff) approved.delete(key);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message !== lastError) {
|
||||
|
|
@ -195,7 +293,12 @@ async function main(): Promise<void> {
|
|||
privateKeyHex: BOT_KEY,
|
||||
store: new FileCordnStore(STATE_PATH),
|
||||
});
|
||||
log('bot pubkey', session.pubkey, 'room', GID);
|
||||
log(
|
||||
'bot pubkey', session.pubkey, 'room', GID,
|
||||
'auto-approve', AUTO_APPROVE ? 'on' : 'off',
|
||||
'max', MAX_MEMBERS,
|
||||
'devices/account', MAX_DEVICES_PER_PUBKEY,
|
||||
);
|
||||
|
||||
do {
|
||||
await cycle();
|
||||
|
|
|
|||
|
|
@ -112,6 +112,12 @@ export function CordnNativePanel({
|
|||
const queryClient = useQueryClient();
|
||||
const [roleBusy, setRoleBusy] = useState<string | null>(null);
|
||||
const isCommunity = scope === 'community';
|
||||
const waitingApprovalKey = isCommunity ? 'communityUi.waitingApproval' : 'cordnUi.waitingApproval';
|
||||
const waitingApprovalManualKey = isCommunity
|
||||
? 'communityUi.waitingApprovalManual'
|
||||
: 'cordnUi.waitingApprovalManual';
|
||||
const noInviteKey = isCommunity ? 'communityUi.noInvite' : 'cordnUi.noInvite';
|
||||
|
||||
const sourceCrew = crew as SkateCrew;
|
||||
const canChangeRoles = !isCommunity && user?.pubkey === crew.pubkey && !!sourceCrew.event;
|
||||
const copy = {
|
||||
|
|
@ -451,8 +457,8 @@ export function CordnNativePanel({
|
|||
setAwaiting(false);
|
||||
return;
|
||||
}
|
||||
if (!awaiting) setNotice(t('cordnUi.noInvite'));
|
||||
}, [checkNow, awaiting, t, native.busy, user?.pubkey, native.groupId]);
|
||||
if (!awaiting) setNotice(t(noInviteKey));
|
||||
}, [checkNow, awaiting, t, noInviteKey, native.busy, user?.pubkey, native.groupId]);
|
||||
|
||||
// Keep the latest checker in a ref: the poll must not restart just because a
|
||||
// parent re-render gave the hook callbacks new identities.
|
||||
|
|
@ -621,6 +627,12 @@ export function CordnNativePanel({
|
|||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{isCommunity && native.group && botPubkey && !native.group.members.some((member) => member.pubkey === botPubkey) && (
|
||||
<div role="status" className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
|
||||
{t('communityUi.botMissing')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{native.error && (
|
||||
<div className="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-200">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
|
|
@ -726,16 +738,33 @@ export function CordnNativePanel({
|
|||
</>
|
||||
) : native.interactiveHintVisible ? (
|
||||
<>
|
||||
<RefreshCw className="h-3.5 w-3.5" /> {t('cordnUi.waitingApprovalManual')}
|
||||
<RefreshCw className="h-3.5 w-3.5" /> {t(waitingApprovalManualKey)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" /> {t('cordnUi.waitingApproval')}
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" /> {t(waitingApprovalKey)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{awaiting && !deviceLinkPending && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!native.ready || native.busy}
|
||||
onClick={() => void handleRequestJoin()}
|
||||
className="mx-auto block text-[11px] text-teal-300 underline underline-offset-2 hover:text-teal-200 disabled:opacity-50"
|
||||
>
|
||||
{t('cordnUi.requestAgain')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isCommunity && (
|
||||
<p className="mx-auto max-w-sm text-center text-[11px] text-gray-500">
|
||||
{t('communityUi.secondDeviceHint')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{notice && <p className="text-center text-xs text-teal-300">{notice}</p>}
|
||||
|
||||
<AdvancedPanel
|
||||
|
|
@ -1070,7 +1099,10 @@ export function CordnNativePanel({
|
|||
/>
|
||||
|
||||
{canManage && (
|
||||
<div className="space-y-2 rounded-md border border-white/10 bg-black/20 p-2">
|
||||
<div
|
||||
id={isCommunity ? 'community-requests' : undefined}
|
||||
className="scroll-mt-24 space-y-2 rounded-md border border-white/10 bg-black/20 p-2"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-300">
|
||||
{t('cordnUi.requests')}
|
||||
|
|
@ -1097,7 +1129,7 @@ export function CordnNativePanel({
|
|||
key={`${req.pk}-${req.kp_ref}`}
|
||||
pubkey={req.pk}
|
||||
disabled={native.busy}
|
||||
onApprove={() => void native.approve(req.pk)}
|
||||
onApprove={() => void native.approve(req.kp_ref || req.pk, req.pk, { pk: req.pk, at: req.at })}
|
||||
botPubkey={botPubkey}
|
||||
botName={copy.botName}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -83,8 +83,12 @@ export interface UseCordnNativeResult {
|
|||
loadRequests: () => Promise<void>;
|
||||
/** Background check that never toggles `busy` (for a tab/request badge). */
|
||||
loadRequestsQuiet: () => Promise<void>;
|
||||
/** Founder/admin: add a requester (by pubkey) and deliver a welcome. */
|
||||
approve: (identifier: string) => Promise<boolean>;
|
||||
/** Founder/admin: add a requester by key-package ref, falling back to pubkey. */
|
||||
approve: (
|
||||
identifier: string,
|
||||
fallbackIdentifier?: string,
|
||||
handled?: { pk: string; at: number },
|
||||
) => Promise<boolean>;
|
||||
/** Founder/admin: remove a member leaf (device) from the group. */
|
||||
removeMember: (leafIndex: number) => Promise<boolean>;
|
||||
/** New device: publish a key package and return the QR payload. */
|
||||
|
|
@ -484,12 +488,30 @@ export function useCordnNative({
|
|||
);
|
||||
|
||||
const approve = useCallback(
|
||||
async (identifier: string) => {
|
||||
async (identifier: string, fallbackIdentifier?: string, handled?: { pk: string; at: number }) => {
|
||||
const session = sessionRef.current;
|
||||
if (!session || !group) return false;
|
||||
const approved = await run(async () => {
|
||||
await session.addMemberByIdentifier(group.gid, identifier);
|
||||
const pending = await session.fetchJoinRequests(group.gid);
|
||||
// Prefer the requester's exact key package; older coordinators may only
|
||||
// resolve the pubkey, so keep that as a fallback.
|
||||
try {
|
||||
await session.addMemberByIdentifier(group.gid, identifier);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
// A stale request for an account already in the group: retire it
|
||||
// instead of leaving it in the queue forever.
|
||||
if (/already in the group/i.test(message)) {
|
||||
if (handled) await session.fetchJoinRequests(group.gid, [handled]);
|
||||
setRequests((current) => current.filter((request) => request.pk !== handled?.pk));
|
||||
return true;
|
||||
}
|
||||
if (!fallbackIdentifier || fallbackIdentifier === identifier) throw error;
|
||||
await session.addMemberByIdentifier(group.gid, fallbackIdentifier);
|
||||
}
|
||||
const pending = await session.fetchJoinRequests(
|
||||
group.gid,
|
||||
handled ? [handled] : [],
|
||||
);
|
||||
setRequests(pending);
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -918,6 +918,7 @@ const batch1 = {
|
|||
"noGroupMember": "Encara no ets al xat privat d'aquesta colla.",
|
||||
"create": "Crea el xat privat",
|
||||
"requestJoin": "Demana d'unir-t'hi",
|
||||
"requestAgain": "Request again",
|
||||
"checkInvite": "Comprova si hi ha invitació",
|
||||
"requested": "Sol·licitud enviada: un administrador de la colla l'aprovarà.",
|
||||
"noInvite": "Encara no hi ha cap invitació. Demana a un administrador que aprovi la teva sol·licitud.",
|
||||
|
|
@ -1012,6 +1013,13 @@ const batch1 = {
|
|||
"notLoggedIn": "Inicia sessió per entrar al xat de la comunitat.",
|
||||
"noGroupAdmin": "Encara no s'ha creat la sala de la comunitat.",
|
||||
"noGroupMember": "Encara no ets a la sala de la comunitat.",
|
||||
"waitingApproval": "Esperant que el bot de la comunitat o un administrador ho aprovi…",
|
||||
"waitingApprovalManual": "Mode manual. Toca «Comprova si hi ha invitació» quan estigui aprovat.",
|
||||
"noInvite": "Encara no hi ha invitació. El bot normalment aprova en un minut; després toca «Comprova si hi ha invitació».",
|
||||
"pendingRequests": "{{count}} sol·licituds d'entrada en espera",
|
||||
"pendingRequestsDesc": "El bot normalment aprova automàticament. Si s'ha encallat, obre la sala de sota i toca Comprova les sol·licituds.",
|
||||
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
"create": "Crea la sala de la comunitat",
|
||||
"setupDeviceBody": "Això dóna a aquest dispositiu una clau per a la sala de la comunitat. Un administrador o el bot l'aprova una vegada.",
|
||||
"onboardingFounder1": "Crea la sala de la comunitat.",
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ const drift = {
|
|||
"noGroupMember": "V soukromém chatu této crew zatím nejsi.",
|
||||
"create": "Vytvořit soukromý chat",
|
||||
"requestJoin": "Požádat o připojení",
|
||||
"requestAgain": "Request again",
|
||||
"checkInvite": "Zkontrolovat pozvánku",
|
||||
"requested": "Žádost odeslána — administrátor crew ji schválí.",
|
||||
"noInvite": "Zatím nečeká žádná pozvánka. Požádej administrátora o schválení žádosti.",
|
||||
|
|
@ -398,6 +399,13 @@ const drift = {
|
|||
"notLoggedIn": "Přihlas se, abys mohl vstoupit do komunitního chatu.",
|
||||
"noGroupAdmin": "Komunitní místnost ještě nebyla vytvořena.",
|
||||
"noGroupMember": "V komunitní místnosti zatím nejsi.",
|
||||
"waitingApproval": "Čeká se na schválení komunitním botem nebo administrátorem…",
|
||||
"waitingApprovalManual": "Ruční režim. Až bude schváleno, klepni na „Zkontrolovat pozvánku“.",
|
||||
"noInvite": "Zatím žádná pozvánka. Bot obvykle schválí do minuty — potom klepni na „Zkontrolovat pozvánku“.",
|
||||
"pendingRequests": "{{count}} čekajících žádostí o připojení",
|
||||
"pendingRequestsDesc": "Bot obvykle schvaluje automaticky. Pokud se zasekl, otevři níže místnost a klepni na Zkontrolovat žádosti.",
|
||||
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
"create": "Vytvořit komunitní místnost",
|
||||
"setupDeviceBody": "Toto zařízení tím získá klíč do komunitní místnosti. Schválí ho jednou administrátor nebo bot.",
|
||||
"onboardingFounder1": "Vytvoř komunitní místnost.",
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ const drift = {
|
|||
noGroupMember: "Du bist noch nicht im privaten Chat dieser Crew.",
|
||||
create: "Privaten Chat erstellen",
|
||||
requestJoin: "Um Beitritt bitten",
|
||||
requestAgain: "Request again",
|
||||
checkInvite: "Nach Einladung suchen",
|
||||
requested: "Anfrage gesendet – ein Crew-Admin wird sie genehmigen.",
|
||||
noInvite: "Noch keine Einladung. Bitte einen Admin, deine Anfrage zu genehmigen.",
|
||||
|
|
@ -400,6 +401,13 @@ const drift = {
|
|||
notLoggedIn: "Melde dich an, um am Community-Chat teilzunehmen.",
|
||||
noGroupAdmin: "Der Community-Raum wurde noch nicht erstellt.",
|
||||
noGroupMember: "Du bist noch nicht im Community-Raum.",
|
||||
waitingApproval: "Warte auf die Freigabe durch den Community-Bot oder einen Admin…",
|
||||
waitingApprovalManual: "Manueller Modus. Tippe auf „Nach Einladung suchen“, sobald es freigegeben ist.",
|
||||
noInvite: "Noch keine Einladung. Der Bot genehmigt normalerweise innerhalb einer Minute – tippe danach auf „Nach Einladung suchen“.",
|
||||
pendingRequests: "{{count}} offene Beitrittsanfragen",
|
||||
pendingRequestsDesc: "Der Bot genehmigt normalerweise automatisch. Wenn es hängt, öffne unten den Raum und tippe auf Anfragen prüfen.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Community-Raum erstellen",
|
||||
setupDeviceBody: "Damit erhält dieses Gerät einen Schlüssel für den Community-Raum. Ein Admin oder der Bot bestätigt ihn einmal.",
|
||||
onboardingFounder1: "Erstelle den Community-Raum.",
|
||||
|
|
|
|||
|
|
@ -311,6 +311,7 @@ const drift = {
|
|||
noGroupMember: "Δεν είσαι ακόμη στην ιδιωτική συνομιλία αυτού του crew.",
|
||||
create: "Δημιουργία ιδιωτικής συνομιλίας",
|
||||
requestJoin: "Ζήτα συμμετοχή",
|
||||
requestAgain: "Request again",
|
||||
checkInvite: "Έλεγχος πρόσκλησης",
|
||||
requested: "Το αίτημα στάλθηκε — ένας διαχειριστής του crew θα το εγκρίνει.",
|
||||
noInvite: "Δεν περιμένει ακόμη πρόσκληση. Ζήτα από έναν διαχειριστή να εγκρίνει το αίτημά σου.",
|
||||
|
|
@ -402,6 +403,13 @@ const drift = {
|
|||
notLoggedIn: "Συνδέσου για να μπεις στη συνομιλία της κοινότητας.",
|
||||
noGroupAdmin: "Το δωμάτιο της κοινότητας δεν έχει δημιουργηθεί ακόμη.",
|
||||
noGroupMember: "Δεν είσαι ακόμη στο δωμάτιο της κοινότητας.",
|
||||
waitingApproval: "Αναμονή έγκρισης από το bot της κοινότητας ή έναν διαχειριστή…",
|
||||
waitingApprovalManual: "Χειροκίνητη λειτουργία. Πάτησε «Έλεγχος πρόσκλησης» μόλις εγκριθεί.",
|
||||
noInvite: "Δεν υπάρχει ακόμη πρόσκληση. Το bot εγκρίνει συνήθως μέσα σε ένα λεπτό — μετά πάτησε «Έλεγχος πρόσκλησης».",
|
||||
pendingRequests: "{{count}} αιτήματα συμμετοχής σε αναμονή",
|
||||
pendingRequestsDesc: "Το bot εγκρίνει συνήθως αυτόματα. Αν έχει κολλήσει, άνοιξε το δωμάτιο παρακάτω και πάτησε Έλεγχος αιτημάτων.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Δημιουργία δωματίου κοινότητας",
|
||||
setupDeviceBody: "Αυτό δίνει σε αυτή τη συσκευή ένα κλειδί για το δωμάτιο της κοινότητας. Ένας διαχειριστής ή το bot το εγκρίνει μία φορά.",
|
||||
onboardingFounder1: "Δημιούργησε το δωμάτιο της κοινότητας.",
|
||||
|
|
|
|||
|
|
@ -918,6 +918,7 @@ const batch1 = {
|
|||
noGroupMember: "Oraindik ez zaude talde honen txat pribatuan.",
|
||||
create: "Sortu txat pribatua",
|
||||
requestJoin: "Eskatu sartzea",
|
||||
requestAgain: "Request again",
|
||||
checkInvite: "Egiaztatu gonbidapena",
|
||||
requested: "Eskaera bidali da — taldeko administratzaile batek onartuko du.",
|
||||
noInvite: "Oraindik ez dago gonbidapenik zain. Eskatu administratzaile bati zure eskaera onartzeko.",
|
||||
|
|
@ -1012,6 +1013,13 @@ const batch1 = {
|
|||
notLoggedIn: "Hasi saioa komunitate txatean sartzeko.",
|
||||
noGroupAdmin: "Komunitate gela oraindik ez da sortu.",
|
||||
noGroupMember: "Oraindik ez zaude komunitate gelan.",
|
||||
waitingApproval: "Komunitate botaren edo administratzaile baten onarpenaren zain…",
|
||||
waitingApprovalManual: "Modu manuala. Onartutakoan, sakatu «Egiaztatu gonbidapena».",
|
||||
noInvite: "Oraindik ez dago gonbidapenik. Botak normalean minutu batean onartzen du — gero sakatu «Egiaztatu gonbidapena».",
|
||||
pendingRequests: "{{count}} sartzeko eskaera zain",
|
||||
pendingRequestsDesc: "Botak normalean automatikoki onartzen du. Blokeatuta badago, ireki beheko gela eta sakatu Egiaztatu eskaerak.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Sortu komunitate gela",
|
||||
setupDeviceBody: "Honek gailu honi komunitate gelarako gako bat ematen dio. Administratzaile batek edo botak behin onartzen du.",
|
||||
onboardingFounder1: "Sortu komunitate gela.",
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ const drift = {
|
|||
noGroupMember: "Vous n'êtes pas encore dans le chat privé de cette Crew.",
|
||||
create: "Créer le chat privé",
|
||||
requestJoin: "Demander à rejoindre",
|
||||
requestAgain: "Request again",
|
||||
checkInvite: "Vérifier l'invitation",
|
||||
requested: "Demande envoyée — un admin de la Crew l'approuvera.",
|
||||
noInvite: "Aucune invitation en attente. Demandez à un admin d'approuver votre demande.",
|
||||
|
|
@ -400,6 +401,13 @@ const drift = {
|
|||
notLoggedIn: "Connecte-toi pour rejoindre le chat de la communauté.",
|
||||
noGroupAdmin: "Le salon de la communauté n’a pas encore été créé.",
|
||||
noGroupMember: "Tu n’es pas encore dans le salon de la communauté.",
|
||||
waitingApproval: "En attente de l’approbation du bot de la communauté ou d’un admin…",
|
||||
waitingApprovalManual: "Mode manuel. Appuie sur « Vérifier l’invitation » une fois approuvé.",
|
||||
noInvite: "Pas encore d’invitation. Le bot approuve généralement en moins d’une minute — appuie ensuite sur « Vérifier l’invitation ».",
|
||||
pendingRequests: "{{count}} demande(s) d’adhésion en attente",
|
||||
pendingRequestsDesc: "Le bot approuve généralement automatiquement. S’il est bloqué, ouvre le salon ci-dessous et appuie sur Vérifier les demandes.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Créer le salon de la communauté",
|
||||
setupDeviceBody: "Cela donne à cet appareil une clé pour le salon de la communauté. Un admin ou le bot l’approuve une fois.",
|
||||
onboardingFounder1: "Crée le salon de la communauté.",
|
||||
|
|
|
|||
|
|
@ -918,6 +918,7 @@ const batch1 = {
|
|||
noGroupMember: "Aínda non estás no chat privado desta crew.",
|
||||
create: "Crear o chat privado",
|
||||
requestJoin: "Pedir acceso",
|
||||
requestAgain: "Request again",
|
||||
checkInvite: "Comprobar se hai invitación",
|
||||
requested: "Solicitude enviada: un administrador da crew aprobaraa.",
|
||||
noInvite: "Aínda non hai ningunha invitación. Pídele a un administrador que aprobe a túa solicitude.",
|
||||
|
|
@ -1012,6 +1013,13 @@ const batch1 = {
|
|||
notLoggedIn: "Inicia sesión para entrar no chat da comunidade.",
|
||||
noGroupAdmin: "A sala da comunidade aínda non se creou.",
|
||||
noGroupMember: "Aínda non estás na sala da comunidade.",
|
||||
waitingApproval: "Agardando a aprobación do bot da comunidade ou dun administrador…",
|
||||
waitingApprovalManual: "Modo manual. Toca «Comprobar se hai invitación» cando se aprobe.",
|
||||
noInvite: "Aínda non hai invitación. O bot adoita aprobar nun minuto; despois toca «Comprobar se hai invitación».",
|
||||
pendingRequests: "{{count}} solicitudes de entrada en espera",
|
||||
pendingRequestsDesc: "O bot adoita aprobar automaticamente. Se se atascou, abre a sala de abaixo e toca Comprobar solicitudes.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Crear a sala da comunidade",
|
||||
setupDeviceBody: "Isto dálle a este dispositivo unha chave para a sala da comunidade. Un administrador ou o bot apruxaa unha vez.",
|
||||
onboardingFounder1: "Crea a sala da comunidade.",
|
||||
|
|
|
|||
|
|
@ -918,6 +918,7 @@ const batch1 = {
|
|||
"noGroupMember": "आप अभी इस क्रू की निजी चैट में नहीं हैं।",
|
||||
"create": "निजी चैट बनाएँ",
|
||||
"requestJoin": "शामिल होने का अनुरोध करें",
|
||||
"requestAgain": "Request again",
|
||||
"checkInvite": "निमंत्रण जाँचें",
|
||||
"requested": "अनुरोध भेजा गया — क्रू एडमिन इसे स्वीकृत करेगा।",
|
||||
"noInvite": "अभी कोई निमंत्रण प्रतीक्षा में नहीं। एडमिन से अपना अनुरोध स्वीकृत करने को कहें।",
|
||||
|
|
@ -1012,6 +1013,13 @@ const batch1 = {
|
|||
"notLoggedIn": "कम्युनिटी चैट में शामिल होने के लिए लॉग इन करें।",
|
||||
"noGroupAdmin": "कम्युनिटी रूम अभी नहीं बनाया गया है।",
|
||||
"noGroupMember": "आप अभी कम्युनिटी रूम में नहीं हैं।",
|
||||
"waitingApproval": "कम्युनिटी बॉट या एडमिन की मंज़ूरी का इंतज़ार है…",
|
||||
"waitingApprovalManual": "मैन्युअल मोड। मंज़ूरी मिलने पर “निमंत्रण जाँचें” टैप करें।",
|
||||
"noInvite": "अभी कोई निमंत्रण नहीं। बॉट आमतौर पर एक मिनट में मंज़ूरी दे देता है — उसके बाद “निमंत्रण जाँचें” टैप करें।",
|
||||
"pendingRequests": "{{count}} शामिल होने के अनुरोध इंतज़ार में",
|
||||
"pendingRequestsDesc": "बॉट आमतौर पर अपने आप मंज़ूरी देता है। अगर अटका हुआ है, तो नीचे रूम खोलें और “अनुरोध जाँचें” टैप करें।",
|
||||
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
"create": "कम्युनिटी रूम बनाएँ",
|
||||
"setupDeviceBody": "इससे इस डिवाइस को कम्युनिटी रूम की एक कुंजी मिलती है। कोई एडमिन या बॉट इसे एक बार मंज़ूर करता है।",
|
||||
"onboardingFounder1": "कम्युनिटी रूम बनाएँ।",
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ const driftStrings = {
|
|||
noGroupMember: 'Kamu belum ada di chat privat crew ini.',
|
||||
create: 'Buat chat privat',
|
||||
requestJoin: 'Minta gabung',
|
||||
requestAgain: "Request again",
|
||||
checkInvite: 'Periksa undangan',
|
||||
requested: 'Permintaan terkirim — admin crew akan menyetujuinya.',
|
||||
noInvite: 'Belum ada undangan menunggu. Minta admin menyetujui permintaanmu.',
|
||||
|
|
@ -394,6 +395,13 @@ const driftStrings = {
|
|||
notLoggedIn: "Masuk untuk bergabung ke chat komunitas.",
|
||||
noGroupAdmin: "Ruang komunitas belum dibuat.",
|
||||
noGroupMember: "Kamu belum berada di ruang komunitas.",
|
||||
waitingApproval: "Menunggu persetujuan dari bot komunitas atau admin…",
|
||||
waitingApprovalManual: "Mode manual. Ketuk “Periksa undangan” setelah disetujui.",
|
||||
noInvite: "Belum ada undangan. Bot biasanya menyetujui dalam satu menit — ketuk “Periksa undangan” setelahnya.",
|
||||
pendingRequests: "{{count}} permintaan bergabung menunggu",
|
||||
pendingRequestsDesc: "Bot biasanya menyetujui secara otomatis. Jika macet, buka ruang di bawah dan ketuk Periksa permintaan.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Buat ruang komunitas",
|
||||
setupDeviceBody: "Ini memberi perangkat ini kunci untuk ruang komunitas. Admin atau bot menyetujuinya satu kali.",
|
||||
onboardingFounder1: "Buat ruang komunitas.",
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ const drift = {
|
|||
noGroupMember: "Non sei ancora nella chat privata di questa crew.",
|
||||
create: "Crea la chat privata",
|
||||
requestJoin: "Chiedi di unirti",
|
||||
requestAgain: "Request again",
|
||||
checkInvite: "Controlla l'invito",
|
||||
requested: "Richiesta inviata — un admin della crew la approverà.",
|
||||
noInvite: "Nessun invito in attesa. Chiedi a un admin di approvare la tua richiesta.",
|
||||
|
|
@ -400,6 +401,13 @@ const drift = {
|
|||
notLoggedIn: "Accedi per entrare nella chat della community.",
|
||||
noGroupAdmin: "La stanza della community non è ancora stata creata.",
|
||||
noGroupMember: "Non sei ancora nella stanza della community.",
|
||||
waitingApproval: "In attesa dell’approvazione del bot della community o di un admin…",
|
||||
waitingApprovalManual: "Modalità manuale. Tocca «Controlla l’invito» quando è approvato.",
|
||||
noInvite: "Nessun invito ancora. Il bot di solito approva entro un minuto — tocca «Controlla l’invito» dopo.",
|
||||
pendingRequests: "{{count}} richieste di adesione in attesa",
|
||||
pendingRequestsDesc: "Il bot di solito approva automaticamente. Se è bloccato, apri la stanza qui sotto e tocca Controlla le richieste.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Crea la stanza della community",
|
||||
setupDeviceBody: "Questo assegna a questo dispositivo una chiave per la stanza della community. Un admin o il bot la approva una volta.",
|
||||
onboardingFounder1: "Crea la stanza della community.",
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@ const drift: Dict = {
|
|||
noGroupMember: 'あなたはまだこのクルーのプライベートチャットに参加していません。',
|
||||
create: 'プライベートチャットを作成',
|
||||
requestJoin: '参加をリクエスト',
|
||||
requestAgain: "Request again",
|
||||
checkInvite: '招待を確認',
|
||||
requested: 'リクエストを送信しました — クルー管理者が承認します。',
|
||||
noInvite: 'まだ招待はありません。管理者にリクエストの承認を依頼してください。',
|
||||
|
|
@ -410,6 +411,13 @@ const drift: Dict = {
|
|||
notLoggedIn: "コミュニティチャットに参加するにはログインしてください。",
|
||||
noGroupAdmin: "コミュニティルームはまだ作成されていません。",
|
||||
noGroupMember: "あなたはまだコミュニティルームに参加していません。",
|
||||
waitingApproval: "コミュニティボットまたは管理者の承認を待っています…",
|
||||
waitingApprovalManual: "手動モードです。承認されたら「招待を確認」をタップしてください。",
|
||||
noInvite: "まだ招待がありません。ボットは通常1分以内に承認します。その後で「招待を確認」をタップしてください。",
|
||||
pendingRequests: "参加リクエスト {{count}} 件が待機中",
|
||||
pendingRequestsDesc: "ボットは通常自動で承認します。止まっている場合は、下のルームを開いて「リクエストを確認」をタップしてください。",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "コミュニティルームを作成",
|
||||
setupDeviceBody: "このデバイスにコミュニティルームの鍵を付与します。管理者またはボットが一度だけ承認します。",
|
||||
onboardingFounder1: "コミュニティルームを作成します。",
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@ const drift: Dict = {
|
|||
noGroupMember: '아직 이 크루의 비공개 채팅에 참여하지 않았습니다.',
|
||||
create: '비공개 채팅 만들기',
|
||||
requestJoin: '참여 요청',
|
||||
requestAgain: "Request again",
|
||||
checkInvite: '초대 확인',
|
||||
requested: '요청을 보냈습니다 — 크루 관리자가 승인합니다.',
|
||||
noInvite: '아직 대기 중인 초대가 없습니다. 관리자에게 요청 승인을 부탁하세요.',
|
||||
|
|
@ -410,6 +411,13 @@ const drift: Dict = {
|
|||
notLoggedIn: "커뮤니티 채팅에 참여하려면 로그인하세요.",
|
||||
noGroupAdmin: "커뮤니티 방이 아직 만들어지지 않았습니다.",
|
||||
noGroupMember: "아직 커뮤니티 방에 참여하지 않았습니다.",
|
||||
waitingApproval: "커뮤니티 봇 또는 관리자의 승인을 기다리는 중…",
|
||||
waitingApprovalManual: "수동 모드입니다. 승인되면 “초대 확인”을 탭하세요.",
|
||||
noInvite: "아직 초대가 없습니다. 봇은 보통 1분 이내에 승인합니다. 그 후 “초대 확인”을 탭하세요.",
|
||||
pendingRequests: "참여 요청 {{count}}개 대기 중",
|
||||
pendingRequestsDesc: "봇은 보통 자동으로 승인합니다. 멈춰 있으면 아래 방을 열고 요청 확인을 탭하세요.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "커뮤니티 방 만들기",
|
||||
setupDeviceBody: "이 기기에 커뮤니티 방 열쇠를 부여합니다. 관리자나 봇이 한 번 승인합니다.",
|
||||
onboardingFounder1: "커뮤니티 방을 만드세요.",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ const communityUi = {
|
|||
notLoggedIn: 'Log in to join the community chat.',
|
||||
noGroupAdmin: 'The community room hasn’t been created yet.',
|
||||
noGroupMember: 'You’re not in the community room yet.',
|
||||
waitingApproval: 'Waiting for the community bot or an admin to approve…',
|
||||
waitingApprovalManual: 'Manual mode. Tap “Check for invite” once it’s approved.',
|
||||
noInvite: 'No invite yet. The bot usually approves within a minute — tap “Check for invite” after that.',
|
||||
pendingRequests: '{{count}} join request(s) waiting',
|
||||
pendingRequestsDesc: 'The bot usually approves automatically. If it’s stuck, open the room below and tap Check requests.',
|
||||
botMissing: 'The community bot isn’t in this room right now, so an admin will review join requests.',
|
||||
secondDeviceHint: 'Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.',
|
||||
create: 'Create the community room',
|
||||
setupDeviceBody:
|
||||
'This gives this device a key for the community room. An admin or the bot approves it once.',
|
||||
|
|
@ -48,6 +55,13 @@ const communityUi = {
|
|||
notLoggedIn: 'Inicia sesión para entrar al chat de la comunidad.',
|
||||
noGroupAdmin: 'Aún no se ha creado la sala de la comunidad.',
|
||||
noGroupMember: 'Aún no estás en la sala de la comunidad.',
|
||||
waitingApproval: 'Esperando a que el bot de la comunidad o un administrador apruebe…',
|
||||
waitingApprovalManual: 'Modo manual. Pulsa «Buscar invitación» cuando se apruebe.',
|
||||
noInvite: 'Aún no hay invitación. El bot suele aprobar en un minuto; pulsa «Buscar invitación» después.',
|
||||
pendingRequests: '{{count}} solicitud(es) de entrada esperando',
|
||||
pendingRequestsDesc: 'El bot suele aprobar automáticamente. Si se ha quedado atascado, abre la sala y pulsa Buscar solicitudes.',
|
||||
botMissing: 'El bot de la comunidad no está en esta sala ahora mismo; un administrador revisará las solicitudes.',
|
||||
secondDeviceHint: '¿Ya eres miembro en otro dispositivo? Usa «Vincular dispositivo»; el bot no puede añadir una segunda clave a la misma cuenta.',
|
||||
create: 'Crear la sala de la comunidad',
|
||||
setupDeviceBody:
|
||||
'Esto da a este dispositivo una clave para la sala de la comunidad. Un administrador o el bot la aprueba una vez.',
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const cordnUi = {
|
|||
noGroupMember: 'You’re not in this crew’s private chat yet.',
|
||||
create: 'Create the private chat',
|
||||
requestJoin: 'Ask to join',
|
||||
requestAgain: 'Request again',
|
||||
checkInvite: 'Check for invite',
|
||||
requested: 'Request sent — a crew admin will approve it.',
|
||||
noInvite: 'No invite waiting yet. Ask an admin to approve your request.',
|
||||
|
|
@ -118,6 +119,7 @@ const cordnUi = {
|
|||
noGroupMember: 'Aún no estás en el chat privado de esta peña.',
|
||||
create: 'Crear el chat privado',
|
||||
requestJoin: 'Pedir entrar',
|
||||
requestAgain: 'Volver a pedir',
|
||||
checkInvite: 'Buscar invitación',
|
||||
requested: 'Solicitud enviada: un administrador la aprobará.',
|
||||
noInvite: 'Aún no hay invitación. Pide a un administrador que apruebe tu solicitud.',
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ const drift = {
|
|||
"noGroupMember": "Je zit nog niet in de privéchat van deze crew.",
|
||||
"create": "De privéchat aanmaken",
|
||||
"requestJoin": "Vragen om deel te nemen",
|
||||
"requestAgain": "Request again",
|
||||
"checkInvite": "Op uitnodiging controleren",
|
||||
"requested": "Verzoek verzonden — een crew-admin keurt het goed.",
|
||||
"noInvite": "Nog geen uitnodiging. Vraag een admin je verzoek goed te keuren.",
|
||||
|
|
@ -398,6 +399,13 @@ const drift = {
|
|||
"notLoggedIn": "Log in om mee te doen aan de communitychat.",
|
||||
"noGroupAdmin": "De communityruimte is nog niet aangemaakt.",
|
||||
"noGroupMember": "Je bent nog niet in de communityruimte.",
|
||||
"waitingApproval": "Wachten op goedkeuring door de communitybot of een beheerder…",
|
||||
"waitingApprovalManual": "Handmatige modus. Tik op ‘Op uitnodiging controleren’ zodra het is goedgekeurd.",
|
||||
"noInvite": "Nog geen uitnodiging. De bot keurt meestal binnen een minuut goed — tik daarna op ‘Op uitnodiging controleren’.",
|
||||
"pendingRequests": "{{count}} openstaande verzoeken om deel te nemen",
|
||||
"pendingRequestsDesc": "De bot keurt meestal automatisch goed. Als het vastloopt, open dan hieronder de ruimte en tik op Verzoeken controleren.",
|
||||
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
"create": "Communityruimte aanmaken",
|
||||
"setupDeviceBody": "Dit geeft dit apparaat een sleutel voor de communityruimte. Een beheerder of de bot keurt die één keer goed.",
|
||||
"onboardingFounder1": "Maak de communityruimte aan.",
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ const drift = {
|
|||
"noGroupMember": "Nie należysz jeszcze do prywatnego czatu tego crew.",
|
||||
"create": "Utwórz prywatny czat",
|
||||
"requestJoin": "Poproś o dołączenie",
|
||||
"requestAgain": "Request again",
|
||||
"checkInvite": "Sprawdź zaproszenie",
|
||||
"requested": "Prośba wysłana — administrator crew ją zatwierdzi.",
|
||||
"noInvite": "Brak oczekującego zaproszenia. Poproś administratora o zatwierdzenie prośby.",
|
||||
|
|
@ -398,6 +399,13 @@ const drift = {
|
|||
"notLoggedIn": "Zaloguj się, aby dołączyć do czatu społeczności.",
|
||||
"noGroupAdmin": "Pokój społeczności nie został jeszcze utworzony.",
|
||||
"noGroupMember": "Nie jesteś jeszcze w pokoju społeczności.",
|
||||
"waitingApproval": "Czekamy na zatwierdzenie przez bota społeczności lub administratora…",
|
||||
"waitingApprovalManual": "Tryb ręczny. Gdy zostanie zatwierdzone, dotknij „Sprawdź zaproszenie”.",
|
||||
"noInvite": "Brak zaproszenia. Bot zwykle zatwierdza w ciągu minuty — potem dotknij „Sprawdź zaproszenie”.",
|
||||
"pendingRequests": "{{count}} oczekujących próśb o dołączenie",
|
||||
"pendingRequestsDesc": "Bot zwykle zatwierdza automatycznie. Jeśli utknął, otwórz pokój poniżej i dotknij Sprawdź prośby.",
|
||||
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
"create": "Utwórz pokój społeczności",
|
||||
"setupDeviceBody": "To daje temu urządzeniu klucz do pokoju społeczności. Administrator lub bot zatwierdza go jednorazowo.",
|
||||
"onboardingFounder1": "Utwórz pokój społeczności.",
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ const drift = {
|
|||
noGroupMember: "Você ainda não está no chat privado desta crew.",
|
||||
create: "Criar o chat privado",
|
||||
requestJoin: "Pedir para entrar",
|
||||
requestAgain: "Request again",
|
||||
checkInvite: "Verificar convite",
|
||||
requested: "Solicitação enviada — um admin da crew vai aprovar.",
|
||||
noInvite: "Nenhum convite aguardando. Peça a um admin para aprovar sua solicitação.",
|
||||
|
|
@ -400,6 +401,13 @@ const drift = {
|
|||
notLoggedIn: "Entre para participar do chat da comunidade.",
|
||||
noGroupAdmin: "A sala da comunidade ainda não foi criada.",
|
||||
noGroupMember: "Você ainda não está na sala da comunidade.",
|
||||
waitingApproval: "Aguardando aprovação do bot da comunidade ou de um admin…",
|
||||
waitingApprovalManual: "Modo manual. Toque em “Verificar convite” quando for aprovado.",
|
||||
noInvite: "Nenhum convite ainda. O bot costuma aprovar em até um minuto — depois toque em “Verificar convite”.",
|
||||
pendingRequests: "{{count}} pedido(s) de entrada aguardando",
|
||||
pendingRequestsDesc: "O bot costuma aprovar automaticamente. Se travou, abra a sala abaixo e toque em Verificar pedidos.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Criar a sala da comunidade",
|
||||
setupDeviceBody: "Isso dá a este dispositivo uma chave para a sala da comunidade. Um admin ou o bot aprova uma vez.",
|
||||
onboardingFounder1: "Crie a sala da comunidade.",
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ const drift = {
|
|||
"noGroupMember": "Nu ești încă în chatul privat al acestui crew.",
|
||||
"create": "Creează chatul privat",
|
||||
"requestJoin": "Cere să te alături",
|
||||
"requestAgain": "Request again",
|
||||
"checkInvite": "Verifică invitația",
|
||||
"requested": "Cerere trimisă — un admin de crew o va aproba.",
|
||||
"noInvite": "Nicio invitație în așteptare încă. Cere unui admin să aprobe cererea ta.",
|
||||
|
|
@ -398,6 +399,13 @@ const drift = {
|
|||
"notLoggedIn": "Autentifică-te pentru a intra în chatul comunității.",
|
||||
"noGroupAdmin": "Camera comunității nu a fost încă creată.",
|
||||
"noGroupMember": "Nu ești încă în camera comunității.",
|
||||
"waitingApproval": "Se așteaptă aprobarea de la botul comunității sau de la un administrator…",
|
||||
"waitingApprovalManual": "Mod manual. Apasă „Verifică invitația” după ce este aprobat.",
|
||||
"noInvite": "Încă nicio invitație. Botul aprobă de obicei într-un minut — apoi apasă „Verifică invitația”.",
|
||||
"pendingRequests": "{{count}} cereri de alăturare în așteptare",
|
||||
"pendingRequestsDesc": "Botul aprobă de obicei automat. Dacă s-a blocat, deschide camera de mai jos și apasă Verifică cererile.",
|
||||
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
"create": "Creează camera comunității",
|
||||
"setupDeviceBody": "Aceasta oferă acestui dispozitiv o cheie pentru camera comunității. Un administrator sau botul o aprobă o dată.",
|
||||
"onboardingFounder1": "Creează camera comunității.",
|
||||
|
|
|
|||
|
|
@ -311,6 +311,7 @@ const drift = {
|
|||
noGroupMember: "Вы ещё не в приватном чате этого Crew.",
|
||||
create: "Создать приватный чат",
|
||||
requestJoin: "Попросить присоединиться",
|
||||
requestAgain: "Request again",
|
||||
checkInvite: "Проверить приглашение",
|
||||
requested: "Запрос отправлен — администратор Crew одобрит его.",
|
||||
noInvite: "Приглашение пока не ждёт. Попросите администратора одобрить ваш запрос.",
|
||||
|
|
@ -402,6 +403,13 @@ const drift = {
|
|||
notLoggedIn: "Войди, чтобы присоединиться к чату сообщества.",
|
||||
noGroupAdmin: "Комната сообщества ещё не создана.",
|
||||
noGroupMember: "Ты ещё не в комнате сообщества.",
|
||||
waitingApproval: "Ждём одобрения от бота сообщества или администратора…",
|
||||
waitingApprovalManual: "Ручной режим. Когда одобрят, нажми «Проверить приглашение».",
|
||||
noInvite: "Приглашения пока нет. Бот обычно одобряет в течение минуты — после этого нажми «Проверить приглашение».",
|
||||
pendingRequests: "{{count}} запросов на вступление в ожидании",
|
||||
pendingRequestsDesc: "Бот обычно одобряет автоматически. Если застряло, открой комнату ниже и нажми «Проверить запросы».",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Создать комнату сообщества",
|
||||
setupDeviceBody: "Это даёт устройству ключ от комнаты сообщества. Администратор или бот подтверждает его один раз.",
|
||||
onboardingFounder1: "Создай комнату сообщества.",
|
||||
|
|
|
|||
|
|
@ -311,6 +311,7 @@ const drift = {
|
|||
noGroupMember: "Du är inte med i det här crewets privata chatt än.",
|
||||
create: "Skapa den privata chatten",
|
||||
requestJoin: "Be om att gå med",
|
||||
requestAgain: "Request again",
|
||||
checkInvite: "Kontrollera inbjudan",
|
||||
requested: "Begäran skickad — en crewadmin godkänner den.",
|
||||
noInvite: "Ingen inbjudan väntar än. Be en admin godkänna din begäran.",
|
||||
|
|
@ -402,6 +403,13 @@ const drift = {
|
|||
notLoggedIn: "Logga in för att gå med i communitychatten.",
|
||||
noGroupAdmin: "Communityrummet har inte skapats än.",
|
||||
noGroupMember: "Du är inte i communityrummet än.",
|
||||
waitingApproval: "Väntar på godkännande från communityboten eller en admin…",
|
||||
waitingApprovalManual: "Manuellt läge. Tryck på ”Kontrollera inbjudan” när det är godkänt.",
|
||||
noInvite: "Ingen inbjudan än. Boten godkänner oftast inom en minut — tryck på ”Kontrollera inbjudan” efter det.",
|
||||
pendingRequests: "{{count}} ansökningar om att gå med väntar",
|
||||
pendingRequestsDesc: "Boten godkänner oftast automatiskt. Om det har fastnat, öppna rummet nedan och tryck på Kontrollera begäranden.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Skapa communityrummet",
|
||||
setupDeviceBody: "Detta ger enheten en nyckel till communityrummet. En admin eller boten godkänner den en gång.",
|
||||
onboardingFounder1: "Skapa communityrummet.",
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ const driftStrings = {
|
|||
noGroupMember: 'คุณยังไม่ได้อยู่ในแชทส่วนตัวของ crew นี้',
|
||||
create: 'สร้างแชทส่วนตัว',
|
||||
requestJoin: 'ขอเข้าร่วม',
|
||||
requestAgain: "Request again",
|
||||
checkInvite: 'ตรวจสอบคำเชิญ',
|
||||
requested: 'ส่งคำขอแล้ว — แอดมิน crew จะอนุมัติ',
|
||||
noInvite: 'ยังไม่มีคำเชิญที่รออยู่ ขอให้แอดมินอนุมัติคำขอของคุณ',
|
||||
|
|
@ -394,6 +395,13 @@ const driftStrings = {
|
|||
notLoggedIn: "เข้าสู่ระบบเพื่อเข้าร่วมแชทชุมชน",
|
||||
noGroupAdmin: "ยังไม่ได้สร้างห้องชุมชน",
|
||||
noGroupMember: "คุณยังไม่ได้อยู่ในห้องชุมชน",
|
||||
waitingApproval: "กำลังรอการอนุมัติจากบอทชุมชนหรือผู้ดูแล…",
|
||||
waitingApprovalManual: "โหมดกำหนดเอง แตะ “ตรวจสอบคำเชิญ” เมื่อได้รับการอนุมัติแล้ว",
|
||||
noInvite: "ยังไม่มีคำเชิญ บอทมักอนุมัติภายในหนึ่งนาที — หลังจากนั้นแตะ “ตรวจสอบคำเชิญ”",
|
||||
pendingRequests: "มีคำขอเข้าร่วม {{count}} รายการรออยู่",
|
||||
pendingRequestsDesc: "บอทมักอนุมัติโดยอัตโนมัติ หากค้างอยู่ ให้เปิดห้องด้านล่างแล้วแตะ ตรวจสอบคำขอ",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "สร้างห้องชุมชน",
|
||||
setupDeviceBody: "สิ่งนี้มอบกุญแจสำหรับห้องชุมชนให้อุปกรณ์นี้ ผู้ดูแลหรือบอทจะอนุมัติหนึ่งครั้ง",
|
||||
onboardingFounder1: "สร้างห้องชุมชน",
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ const driftStrings = {
|
|||
noGroupMember: "Henüz bu crew'in özel sohbetinde değilsin.",
|
||||
create: 'Özel sohbeti oluştur',
|
||||
requestJoin: 'Katılmayı iste',
|
||||
requestAgain: "Request again",
|
||||
checkInvite: 'Davetiye kontrol et',
|
||||
requested: 'İstek gönderildi — bir crew yöneticisi onaylayacak.',
|
||||
noInvite: 'Henüz bekleyen davetiye yok. Bir yöneticiden isteğini onaylamasını iste.',
|
||||
|
|
@ -394,6 +395,13 @@ const driftStrings = {
|
|||
notLoggedIn: "Topluluk sohbetine katılmak için giriş yap.",
|
||||
noGroupAdmin: "Topluluk odası henüz oluşturulmadı.",
|
||||
noGroupMember: "Henüz topluluk odasında değilsin.",
|
||||
waitingApproval: "Topluluk botunun veya bir yöneticinin onaylaması bekleniyor…",
|
||||
waitingApprovalManual: "Manuel mod. Onaylandığında “Davetiye kontrol et”e dokun.",
|
||||
noInvite: "Henüz davetiye yok. Bot genellikle bir dakika içinde onaylar — ardından “Davetiye kontrol et”e dokun.",
|
||||
pendingRequests: "{{count}} katılma isteği bekliyor",
|
||||
pendingRequestsDesc: "Bot genellikle otomatik onaylar. Takıldıysa aşağıdaki odayı aç ve İstekleri kontrol et’e dokun.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Topluluk odasını oluştur",
|
||||
setupDeviceBody: "Bu, bu cihaza topluluk odası için bir anahtar verir. Bir yönetici veya bot bunu bir kez onaylar.",
|
||||
onboardingFounder1: "Topluluk odasını oluştur.",
|
||||
|
|
|
|||
|
|
@ -311,6 +311,7 @@ const drift = {
|
|||
noGroupMember: "Ви ще не в приватному чаті цієї команди.",
|
||||
create: "Створити приватний чат",
|
||||
requestJoin: "Попросити приєднатися",
|
||||
requestAgain: "Request again",
|
||||
checkInvite: "Перевірити запрошення",
|
||||
requested: "Запит надіслано — адміністратор команди його схвалить.",
|
||||
noInvite: "Запрошення ще не чекає. Попросіть адміністратора схвалити ваш запит.",
|
||||
|
|
@ -402,6 +403,13 @@ const drift = {
|
|||
notLoggedIn: "Увійди, щоб приєднатися до чату спільноти.",
|
||||
noGroupAdmin: "Кімнату спільноти ще не створено.",
|
||||
noGroupMember: "Ти ще не в кімнаті спільноти.",
|
||||
waitingApproval: "Чекаємо на схвалення від бота спільноти або адміністратора…",
|
||||
waitingApprovalManual: "Ручний режим. Коли схвалять, натисни «Перевірити запрошення».",
|
||||
noInvite: "Запрошення ще немає. Бот зазвичай схвалює протягом хвилини — після цього натисни «Перевірити запрошення».",
|
||||
pendingRequests: "{{count}} запитів на приєднання в очікуванні",
|
||||
pendingRequestsDesc: "Бот зазвичай схвалює автоматично. Якщо застрягло, відкрий кімнату нижче й натисни «Перевірити запити».",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Створити кімнату спільноти",
|
||||
setupDeviceBody: "Це дає цьому пристрою ключ до кімнати спільноти. Адміністратор або бот підтверджує його один раз.",
|
||||
onboardingFounder1: "Створи кімнату спільноти.",
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ const driftStrings = {
|
|||
noGroupMember: 'Bạn chưa ở trong chat riêng tư của crew này.',
|
||||
create: 'Tạo chat riêng tư',
|
||||
requestJoin: 'Xin tham gia',
|
||||
requestAgain: "Request again",
|
||||
checkInvite: 'Kiểm tra lời mời',
|
||||
requested: 'Đã gửi yêu cầu — quản trị viên crew sẽ duyệt.',
|
||||
noInvite: 'Chưa có lời mời nào. Hãy nhờ quản trị viên duyệt yêu cầu của bạn.',
|
||||
|
|
@ -394,6 +395,13 @@ const driftStrings = {
|
|||
notLoggedIn: "Đăng nhập để tham gia trò chuyện cộng đồng.",
|
||||
noGroupAdmin: "Phòng cộng đồng chưa được tạo.",
|
||||
noGroupMember: "Bạn chưa ở trong phòng cộng đồng.",
|
||||
waitingApproval: "Đang chờ bot cộng đồng hoặc quản trị viên phê duyệt…",
|
||||
waitingApprovalManual: "Chế độ thủ công. Nhấn “Kiểm tra lời mời” sau khi được phê duyệt.",
|
||||
noInvite: "Chưa có lời mời. Bot thường phê duyệt trong vòng một phút — sau đó nhấn “Kiểm tra lời mời”.",
|
||||
pendingRequests: "{{count}} yêu cầu tham gia đang chờ",
|
||||
pendingRequestsDesc: "Bot thường phê duyệt tự động. Nếu bị kẹt, mở phòng bên dưới và nhấn Kiểm tra yêu cầu.",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "Tạo phòng cộng đồng",
|
||||
setupDeviceBody: "Việc này cấp cho thiết bị này một khóa cho phòng cộng đồng. Quản trị viên hoặc bot phê duyệt một lần.",
|
||||
onboardingFounder1: "Tạo phòng cộng đồng.",
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@ const drift: Dict = {
|
|||
noGroupMember: '你还没有加入此 Crew 的私密聊天。',
|
||||
create: '创建私密聊天',
|
||||
requestJoin: '请求加入',
|
||||
requestAgain: "Request again",
|
||||
checkInvite: '检查邀请',
|
||||
requested: '请求已发送——Crew 管理员会批准。',
|
||||
noInvite: '还没有等待中的邀请。请管理员批准你的请求。',
|
||||
|
|
@ -410,6 +411,13 @@ const drift: Dict = {
|
|||
notLoggedIn: "登录后即可加入社区聊天。",
|
||||
noGroupAdmin: "社区房间尚未创建。",
|
||||
noGroupMember: "你还没有加入社区房间。",
|
||||
waitingApproval: "正在等待社区机器人或管理员批准…",
|
||||
waitingApprovalManual: "手动模式。批准后请点按“检查邀请”。",
|
||||
noInvite: "还没有邀请。机器人通常在一分钟内批准——之后请点按“检查邀请”。",
|
||||
pendingRequests: "{{count}} 个加入请求正在等待",
|
||||
pendingRequestsDesc: "机器人通常会自动批准。如果卡住了,请打开下面的房间并点按“检查请求”。",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "创建社区房间",
|
||||
setupDeviceBody: "这会为此设备提供社区房间的密钥,管理员或机器人会批准一次。",
|
||||
onboardingFounder1: "创建社区房间。",
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@ const drift: Dict = {
|
|||
noGroupMember: '你還沒有加入此 Crew 的私密聊天。',
|
||||
create: '建立私密聊天',
|
||||
requestJoin: '要求加入',
|
||||
requestAgain: "Request again",
|
||||
checkInvite: '檢查邀請',
|
||||
requested: '要求已送出——Crew 管理員會批准。',
|
||||
noInvite: '還沒有等待中的邀請。請管理員批准你的要求。',
|
||||
|
|
@ -410,6 +411,13 @@ const drift: Dict = {
|
|||
notLoggedIn: "登入後即可加入社群聊天。",
|
||||
noGroupAdmin: "社群房間尚未建立。",
|
||||
noGroupMember: "你還沒有加入社群房間。",
|
||||
waitingApproval: "正在等待社群機器人或管理員核准…",
|
||||
waitingApprovalManual: "手動模式。核准後請點按「檢查邀請」。",
|
||||
noInvite: "還沒有邀請。機器人通常在一分鐘內核准——之後請點按「檢查邀請」。",
|
||||
pendingRequests: "{{count}} 個加入要求正在等待",
|
||||
pendingRequestsDesc: "機器人通常會自動核准。如果卡住了,請開啟下面的房間並點按「檢查要求」。",
|
||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
||||
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
|
||||
create: "建立社群房間",
|
||||
setupDeviceBody: "這會為此裝置提供社群房間的金鑰,管理員或機器人會核准一次。",
|
||||
onboardingFounder1: "建立社群房間。",
|
||||
|
|
|
|||
|
|
@ -457,9 +457,23 @@ export class CordnSession {
|
|||
return keyPackage;
|
||||
}
|
||||
|
||||
/** Pending join requests for a group (for an admin to approve). */
|
||||
async fetchJoinRequests(gid: string): Promise<JoinRequest[]> {
|
||||
const result = await this.getClient().fetchManyPendingJoinRequests({ groups: [{ gid }] });
|
||||
/**
|
||||
* Pending join requests for a group.
|
||||
*
|
||||
* Pass `consumed` after handling a request so the coordinator can retire it;
|
||||
* otherwise an already-approved request can sit in the queue forever and be
|
||||
* retried on every poll (MLS then rejects it as "already in the group").
|
||||
*/
|
||||
async fetchJoinRequests(
|
||||
gid: string,
|
||||
consumed: Array<{ pk: string; at: number }> = [],
|
||||
): Promise<JoinRequest[]> {
|
||||
const result = await this.getClient().fetchManyPendingJoinRequests({
|
||||
groups: [{ gid }],
|
||||
...(consumed.length > 0
|
||||
? { consumed: consumed.map((request) => ({ ...request, gid })) }
|
||||
: {}),
|
||||
});
|
||||
return result.requests;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowLeft, HeartHandshake, Lock, MessagesSquare } from 'lucide-react';
|
||||
import { ArrowLeft, HeartHandshake, Lock, MessagesSquare, UserPlus } from 'lucide-react';
|
||||
import { CordnNativePanel } from '@/components/crew/CordnNativePanel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
|
|
@ -20,6 +20,7 @@ export default function CommunityPage() {
|
|||
const { t } = useTranslation();
|
||||
const { user } = useCurrentUser();
|
||||
const canManage = isAdminPubkey(user?.pubkey);
|
||||
const [pendingRequests, setPendingRequests] = useState(0);
|
||||
|
||||
const inviteUrl = useMemo(() => {
|
||||
const origin = isNativePlatform() ? 'https://skate-spots.org' : window.location.origin;
|
||||
|
|
@ -59,6 +60,29 @@ export default function CommunityPage() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{canManage && pendingRequests > 0 && (
|
||||
<Card className="mb-4 border-amber-500/30 bg-amber-500/[0.06]" aria-live="polite">
|
||||
<CardContent className="flex items-start gap-3 py-3">
|
||||
<UserPlus className="mt-0.5 h-4 w-4 shrink-0 text-amber-300" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-bold text-amber-200">
|
||||
{t('communityUi.pendingRequests', { count: pendingRequests })}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-amber-200/70">
|
||||
{t('communityUi.pendingRequestsDesc')}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => document.getElementById('community-requests')?.scrollIntoView({ behavior: 'smooth', block: 'center' })}
|
||||
className="mt-1.5 text-xs font-bold text-amber-200 underline underline-offset-2 hover:text-amber-100"
|
||||
>
|
||||
{t('cordnUi.checkRequests')}
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<CordnNativePanel
|
||||
crew={COMMUNITY_ROOM}
|
||||
canManage={canManage}
|
||||
|
|
@ -67,6 +91,7 @@ export default function CommunityPage() {
|
|||
scope="community"
|
||||
inviteUrl={inviteUrl}
|
||||
botPubkey={COMMUNITY_BOT_PUBKEY || undefined}
|
||||
onRequestsChange={setPendingRequests}
|
||||
/>
|
||||
|
||||
<div className="mt-5 flex items-center justify-center gap-2 text-xs text-gray-500">
|
||||
|
|
|
|||
Loading…
Reference in a new issue