feat: Cordn inbox, outbox, room controls and animated splash
This commit is contained in:
parent
d07f8a696d
commit
a2ab5750ff
43 changed files with 1090 additions and 228 deletions
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -151,6 +151,7 @@
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"rollup-plugin-visualizer": "^7.1.1",
|
"rollup-plugin-visualizer": "^7.1.1",
|
||||||
|
"sharp": "0.35.5",
|
||||||
"tailwindcss": "^3.4.11",
|
"tailwindcss": "^3.4.11",
|
||||||
"tsx": "^4.22.4",
|
"tsx": "^4.22.4",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@
|
||||||
"check:i18n": "node scripts/check-i18n.mjs",
|
"check:i18n": "node scripts/check-i18n.mjs",
|
||||||
"test": "npm i --silent && tsc --noEmit && tsc -p cvm-server/tsconfig.json && tsc -p ai-proxy/tsconfig.json && tsc -p push-service/tsconfig.json && eslint && node scripts/check-i18n.mjs && vitest run --reporter=dot --silent && vite build -l error && cp dist/index.html dist/404.html && echo 'All tests passed!'",
|
"test": "npm i --silent && tsc --noEmit && tsc -p cvm-server/tsconfig.json && tsc -p ai-proxy/tsconfig.json && tsc -p push-service/tsconfig.json && eslint && node scripts/check-i18n.mjs && vitest run --reporter=dot --silent && vite build -l error && cp dist/index.html dist/404.html && echo 'All tests passed!'",
|
||||||
"deploy": "npm i --silent && npm run build && npx -y nostr-deploy-cli deploy --skip-setup",
|
"deploy": "npm i --silent && npm run build && npx -y nostr-deploy-cli deploy --skip-setup",
|
||||||
"android:init": "npm run android:build:apk && npx cap add android && npx cap sync android && node scripts/inject-amber-queries.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-signing-config.mjs",
|
"android:init": "npm run android:build:apk && npx cap add android && npx cap sync android && node scripts/inject-amber-queries.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-signing-config.mjs && node scripts/make-android-splash.mjs",
|
||||||
"android:sync": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-deep-links.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-signing-config.mjs",
|
"android:sync": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-deep-links.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-signing-config.mjs && node scripts/make-android-splash.mjs",
|
||||||
"android:open": "npx cap open android",
|
"android:open": "npx cap open android",
|
||||||
"android:version": "node scripts/sync-android-version.mjs",
|
"android:version": "node scripts/sync-android-version.mjs",
|
||||||
"android:build": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-deep-links.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-signing-config.mjs && cd android && ./gradlew assembleRelease",
|
"android:build": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-deep-links.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-signing-config.mjs && node scripts/make-android-splash.mjs && cd android && ./gradlew assembleRelease",
|
||||||
"android:build:apk": "CAPACITOR_BUILD=1 vite build && sed -i 's|<script type=\"module\" src=\"/shakespeare_tailwind[^\"]*\\.js\"></script>||g' dist/index.html && sed -i 's|<style type=\"text/tailwindcss\">|<style>|g' dist/index.html && cp dist/index.html dist/404.html",
|
"android:build:apk": "CAPACITOR_BUILD=1 vite build && sed -i 's|<script type=\"module\" src=\"/shakespeare_tailwind[^\"]*\\.js\"></script>||g' dist/index.html && sed -i 's|<style type=\"text/tailwindcss\">|<style>|g' dist/index.html && cp dist/index.html dist/404.html",
|
||||||
"newsletter:weekly": "tsx scripts/newsletter-weekly.ts",
|
"newsletter:weekly": "tsx scripts/newsletter-weekly.ts",
|
||||||
"translation:matrix": "node scripts/translation-matrix.mjs"
|
"translation:matrix": "node scripts/translation-matrix.mjs"
|
||||||
|
|
@ -173,6 +173,7 @@
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"rollup-plugin-visualizer": "^7.1.1",
|
"rollup-plugin-visualizer": "^7.1.1",
|
||||||
|
"sharp": "0.35.5",
|
||||||
"tailwindcss": "^3.4.11",
|
"tailwindcss": "^3.4.11",
|
||||||
"tsx": "^4.22.4",
|
"tsx": "^4.22.4",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
|
|
|
||||||
62
scripts/make-android-splash.mjs
Normal file
62
scripts/make-android-splash.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* Generate Android launch-screen bitmaps from the tracked brand source.
|
||||||
|
*
|
||||||
|
* `resources/splash.png` is the orange SkateSpots wheel. Capacitor's generated
|
||||||
|
* `android/` tree ships a stock blue placeholder, so every release must rebuild
|
||||||
|
* these drawables. Keeps the native first frame on-brand before the animated
|
||||||
|
* web splash takes over.
|
||||||
|
*/
|
||||||
|
import { mkdir } from 'node:fs/promises';
|
||||||
|
import { dirname, resolve } from 'node:path';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
const ROOT = resolve(import.meta.dirname, '..');
|
||||||
|
const SOURCE = resolve(ROOT, 'resources/splash.png');
|
||||||
|
const RES = resolve(ROOT, 'android/app/src/main/res');
|
||||||
|
|
||||||
|
const TARGETS = [
|
||||||
|
['drawable/splash.png', 480, 320],
|
||||||
|
['drawable-land-mdpi/splash.png', 480, 320],
|
||||||
|
['drawable-land-hdpi/splash.png', 800, 480],
|
||||||
|
['drawable-land-xhdpi/splash.png', 1280, 720],
|
||||||
|
['drawable-land-xxhdpi/splash.png', 1600, 960],
|
||||||
|
['drawable-land-xxxhdpi/splash.png', 1920, 1280],
|
||||||
|
['drawable-port-mdpi/splash.png', 320, 480],
|
||||||
|
['drawable-port-hdpi/splash.png', 480, 800],
|
||||||
|
['drawable-port-xhdpi/splash.png', 720, 1280],
|
||||||
|
['drawable-port-xxhdpi/splash.png', 960, 1600],
|
||||||
|
['drawable-port-xxxhdpi/splash.png', 1280, 1920],
|
||||||
|
];
|
||||||
|
|
||||||
|
const glow = (width, height) => Buffer.from(`
|
||||||
|
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="g" cx="50%" cy="50%" r="38%">
|
||||||
|
<stop offset="0" stop-color="#ff6a00" stop-opacity="0.30"/>
|
||||||
|
<stop offset="0.55" stop-color="#ff0040" stop-opacity="0.08"/>
|
||||||
|
<stop offset="1" stop-color="#050505" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" fill="url(#g)"/>
|
||||||
|
</svg>`);
|
||||||
|
|
||||||
|
for (const [relative, width, height] of TARGETS) {
|
||||||
|
const logoSize = Math.max(64, Math.round(Math.min(width, height) * 0.3));
|
||||||
|
const logo = await sharp(SOURCE)
|
||||||
|
.resize(logoSize, logoSize, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const out = resolve(RES, relative);
|
||||||
|
await mkdir(dirname(out), { recursive: true });
|
||||||
|
await sharp({
|
||||||
|
create: { width, height, channels: 4, background: '#050505' },
|
||||||
|
})
|
||||||
|
.composite([
|
||||||
|
{ input: glow(width, height), blend: 'over' },
|
||||||
|
{ input: logo, gravity: 'center' },
|
||||||
|
])
|
||||||
|
.png()
|
||||||
|
.toFile(out);
|
||||||
|
console.log(`splash ${relative} ${width}x${height}`);
|
||||||
|
}
|
||||||
|
|
@ -843,7 +843,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
||||||
alt="SkateSpots"
|
alt="SkateSpots"
|
||||||
className="w-9 h-9 rounded-lg flex-shrink-0 shadow-lg shadow-red-500/30"
|
className="w-9 h-9 rounded-lg flex-shrink-0 shadow-lg shadow-red-500/30"
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0 truncate font-display text-xl neon-red tracking-wide" style={{
|
<span className="min-w-0 truncate font-display text-lg neon-red tracking-wide" style={{
|
||||||
textShadow: '0 0 8px #ff0040'
|
textShadow: '0 0 8px #ff0040'
|
||||||
}}>SKATESPOTS</span>
|
}}>SKATESPOTS</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,19 @@ const MAX_MS = 2600; // hard cap — never block on a slow/offline network
|
||||||
const FADE_MS = 480;
|
const FADE_MS = 480;
|
||||||
const READY_HOLD_MS = 340; // savour the 100% "ready" beat before revealing
|
const READY_HOLD_MS = 340; // savour the 100% "ready" beat before revealing
|
||||||
|
|
||||||
|
const SPARKS = [
|
||||||
|
{ sx: '-8px', sy: '18px', ex: '-54px', ey: '44px', delay: '0ms' },
|
||||||
|
{ sx: '4px', sy: '22px', ex: '38px', ey: '58px', delay: '90ms' },
|
||||||
|
{ sx: '-16px', sy: '10px', ex: '-72px', ey: '18px', delay: '180ms' },
|
||||||
|
{ sx: '12px', sy: '14px', ex: '66px', ey: '30px', delay: '260ms' },
|
||||||
|
{ sx: '-4px', sy: '24px', ex: '-28px', ey: '76px', delay: '340ms' },
|
||||||
|
{ sx: '8px', sy: '24px', ex: '22px', ey: '78px', delay: '430ms' },
|
||||||
|
{ sx: '-12px', sy: '2px', ex: '-48px', ey: '-16px', delay: '520ms' },
|
||||||
|
{ sx: '14px', sy: '4px', ex: '52px', ey: '-12px', delay: '610ms' },
|
||||||
|
{ sx: '-2px', sy: '28px', ex: '-10px', ey: '88px', delay: '700ms' },
|
||||||
|
{ sx: '2px', sy: '28px', ex: '12px', ey: '90px', delay: '790ms' },
|
||||||
|
];
|
||||||
|
|
||||||
const STATUS_KEYS = [
|
const STATUS_KEYS = [
|
||||||
'bootUi.statusWaking',
|
'bootUi.statusWaking',
|
||||||
'bootUi.statusTuning',
|
'bootUi.statusTuning',
|
||||||
|
|
@ -171,34 +184,73 @@ export function SplashScreen() {
|
||||||
className="pointer-events-none absolute left-1/2 top-1/2 h-[520px] w-[520px] -translate-x-1/2 -translate-y-1/2 rounded-full blur-[110px]"
|
className="pointer-events-none absolute left-1/2 top-1/2 h-[520px] w-[520px] -translate-x-1/2 -translate-y-1/2 rounded-full blur-[110px]"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
'radial-gradient(circle, rgba(255,0,64,0.30) 0%, rgba(204,255,0,0.07) 46%, transparent 70%)',
|
'radial-gradient(circle, rgba(255,106,0,0.32) 0%, rgba(255,0,64,0.08) 46%, transparent 70%)',
|
||||||
...anim('splashGlow 3.4s ease-in-out infinite'),
|
...anim('splashGlow 3.4s ease-in-out infinite'),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-col items-center px-8">
|
<div className="relative z-10 flex flex-col items-center px-8">
|
||||||
{/* Logo in a glowing conic halo ring */}
|
{/* The orange wheel mark rolls in, keeps a slow roll, and throws sparks */}
|
||||||
<div className="relative" style={anim('splashRise 620ms cubic-bezier(0.22,1,0.36,1) both')}>
|
<div className="relative" style={anim('splashRise 620ms cubic-bezier(0.22,1,0.36,1) both')}>
|
||||||
{/* Rotating conic ring */}
|
{/* Rotating conic ring — orange to match the wheel, with a lime kick */}
|
||||||
<div
|
<div
|
||||||
className="absolute -inset-3 rounded-[26px]"
|
className="absolute -inset-3 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
'conic-gradient(from 0deg, transparent 0deg, #ff0040 90deg, #ccff00 180deg, transparent 300deg)',
|
'conic-gradient(from 0deg, transparent 0deg, #ff6a00 90deg, #ccff00 190deg, transparent 310deg)',
|
||||||
opacity: ready ? 0.9 : 0.55,
|
opacity: ready ? 0.9 : 0.58,
|
||||||
filter: 'blur(6px)',
|
filter: 'blur(7px)',
|
||||||
transition: 'opacity 400ms ease',
|
transition: 'opacity 400ms ease',
|
||||||
...anim('splashSpin 3.2s linear infinite'),
|
...anim('splashSpin 3.2s linear infinite'),
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
{/* Mask ring interior so only a thin halo shows */}
|
{/* Mask ring interior so only a thin halo shows */}
|
||||||
<div className="absolute -inset-1 rounded-[22px] bg-[#050505]" aria-hidden="true" />
|
<div className="absolute -inset-1 rounded-full bg-[#050505]" aria-hidden="true" />
|
||||||
<div style={anim('splashPulse 2.2s ease-in-out infinite')} className="relative">
|
|
||||||
|
{/* Sparks are decorative and disabled for reduced-motion users. */}
|
||||||
|
{!prefersReducedMotion && (
|
||||||
|
<div className="pointer-events-none absolute inset-0" aria-hidden="true">
|
||||||
|
{SPARKS.map((spark, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="splash-spark"
|
||||||
|
style={{
|
||||||
|
'--sx': spark.sx,
|
||||||
|
'--sy': spark.sy,
|
||||||
|
'--ex': spark.ex,
|
||||||
|
'--ey': spark.ey,
|
||||||
|
animationDelay: spark.delay,
|
||||||
|
} as React.CSSProperties}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bounce wrapper + rolling wheel. Nested transforms keep the spin
|
||||||
|
from fighting the entry animation. */}
|
||||||
|
<div
|
||||||
|
style={anim('splashLogoRollIn 720ms cubic-bezier(0.22,1,0.36,1) both')}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={anim('splashHover 1.9s ease-in-out infinite')}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<div style={anim('splashWheelRoll 5.4s linear infinite')} className="relative">
|
||||||
<img
|
<img
|
||||||
src="/images/icon-1024.png"
|
src="/images/icon-1024.png"
|
||||||
alt="SkateSpots"
|
alt="SkateSpots"
|
||||||
className="relative h-24 w-24 rounded-[20px] shadow-[0_0_50px_rgba(255,0,64,0.55)]"
|
draggable={false}
|
||||||
|
className="relative h-28 w-28 select-none drop-shadow-[0_0_34px_rgba(255,106,0,0.65)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Contact shadow / dust line under the wheel */}
|
||||||
|
<div
|
||||||
|
className="absolute -bottom-2 left-1/2 h-2 w-20 -translate-x-1/2 rounded-full bg-[#ff6a00]/30 blur-md"
|
||||||
|
style={anim('splashDust 1.9s ease-in-out infinite')}
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Reply,
|
Reply,
|
||||||
ScanLine,
|
ScanLine,
|
||||||
|
Search,
|
||||||
Send,
|
Send,
|
||||||
Share2,
|
Share2,
|
||||||
ShieldMinus,
|
ShieldMinus,
|
||||||
|
|
@ -24,6 +25,8 @@ import {
|
||||||
Trash2,
|
Trash2,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
Users,
|
Users,
|
||||||
|
Volume2,
|
||||||
|
VolumeX,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
@ -59,13 +62,15 @@ import { useNostrPublish } from '@/hooks/useNostrPublish';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useUploadFile } from '@/hooks/useUploadFile';
|
import { useUploadFile } from '@/hooks/useUploadFile';
|
||||||
import { useCordnNative } from '@/hooks/useCordnNative';
|
import { useCordnNative } from '@/hooks/useCordnNative';
|
||||||
|
import { useDmMute } from '@/hooks/useDmMute';
|
||||||
|
import { useCordnRoomRead } from '@/hooks/useCordnRoomRead';
|
||||||
|
import { enqueueCordnOutbox, markCordnOutboxAttempt, readCordnOutbox, removeCordnOutbox } from '@/lib/cordnOutbox';
|
||||||
import { shortNpub } from '@/lib/coordinator';
|
import { shortNpub } from '@/lib/coordinator';
|
||||||
import { isNativePlatform } from '@/lib/platform';
|
import { isNativePlatform } from '@/lib/platform';
|
||||||
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
|
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { CordnRoom } from '@/lib/cordn/room';
|
import type { CordnRoom } from '@/lib/cordn/room';
|
||||||
import { SKATE_CREW_KIND, type SkateCrew } from '@/lib/types';
|
import { SKATE_CREW_KIND, type SkateCrew } from '@/lib/types';
|
||||||
import { readCordnSeen, writeCordnSeen } from '@/lib/cordn/unread';
|
|
||||||
import { clearPendingJoin, readPendingJoin, writePendingJoin } from '@/lib/cordn/pendingJoin';
|
import { clearPendingJoin, readPendingJoin, writePendingJoin } from '@/lib/cordn/pendingJoin';
|
||||||
|
|
||||||
type RoomScope = 'crew' | 'community';
|
type RoomScope = 'crew' | 'community';
|
||||||
|
|
@ -98,6 +103,8 @@ interface PendingMessage {
|
||||||
text: string;
|
text: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
state: 'sending' | 'failed';
|
state: 'sending' | 'failed';
|
||||||
|
outboxId?: string;
|
||||||
|
attempts?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -176,6 +183,9 @@ export function CordnNativePanel({
|
||||||
const sendNative = native.send;
|
const sendNative = native.send;
|
||||||
const checkInviteNative = native.checkInvite;
|
const checkInviteNative = native.checkInvite;
|
||||||
const [draft, setDraft] = useState('');
|
const [draft, setDraft] = useState('');
|
||||||
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [online, setOnline] = useState(() => typeof navigator === 'undefined' || navigator.onLine);
|
||||||
const [replyTo, setReplyTo] = useState<{ id: string; content: string } | null>(null);
|
const [replyTo, setReplyTo] = useState<{ id: string; content: string } | null>(null);
|
||||||
const [reactingTo, setReactingTo] = useState<string | null>(null);
|
const [reactingTo, setReactingTo] = useState<string | null>(null);
|
||||||
const readSentRef = useRef<Set<string>>(new Set());
|
const readSentRef = useRef<Set<string>>(new Set());
|
||||||
|
|
@ -194,6 +204,10 @@ export function CordnNativePanel({
|
||||||
const [deviceLinkPending, setDeviceLinkPending] = useState(false);
|
const [deviceLinkPending, setDeviceLinkPending] = useState(false);
|
||||||
const [scannerOpen, setScannerOpen] = useState(false);
|
const [scannerOpen, setScannerOpen] = useState(false);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { isMuted, toggle: toggleMute } = useDmMute();
|
||||||
|
const { reads, markActive, markSeen } = useCordnRoomRead(user?.pubkey);
|
||||||
|
const muteKey = native.groupId ? `cordn:${native.groupId}` : undefined;
|
||||||
|
const muted = isMuted(muteKey);
|
||||||
const fileRef = useRef<HTMLInputElement>(null);
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
const checkingRef = useRef(false);
|
const checkingRef = useRef(false);
|
||||||
const loadedRequestsRef = useRef(false);
|
const loadedRequestsRef = useRef(false);
|
||||||
|
|
@ -229,6 +243,74 @@ export function CordnNativePanel({
|
||||||
setAwaiting(readPendingJoin(user.pubkey, native.groupId));
|
setAwaiting(readPendingJoin(user.pubkey, native.groupId));
|
||||||
}, [user?.pubkey, native.groupId]);
|
}, [user?.pubkey, native.groupId]);
|
||||||
|
|
||||||
|
const latestInboundAt = useMemo(
|
||||||
|
() =>
|
||||||
|
native.messages.reduce(
|
||||||
|
(max, message) =>
|
||||||
|
message.direction === 'inbound' && message.sender !== user?.pubkey
|
||||||
|
? Math.max(max, message.createdAt)
|
||||||
|
: max,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
[native.messages, user?.pubkey],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (native.groupId && latestInboundAt > 0) markActive(native.groupId, latestInboundAt);
|
||||||
|
}, [native.groupId, latestInboundAt, markActive]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (active && native.groupId && latestInboundAt > 0) markSeen(native.groupId, latestInboundAt);
|
||||||
|
}, [active, native.groupId, latestInboundAt, markSeen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onOnline = () => setOnline(true);
|
||||||
|
const onOffline = () => setOnline(false);
|
||||||
|
window.addEventListener('online', onOnline);
|
||||||
|
window.addEventListener('offline', onOffline);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('online', onOnline);
|
||||||
|
window.removeEventListener('offline', onOffline);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// A different room must not inherit the previous room's draft/pending bubble.
|
||||||
|
useEffect(() => {
|
||||||
|
setPending(null);
|
||||||
|
setReplyTo(null);
|
||||||
|
setReactingTo(null);
|
||||||
|
setSearch('');
|
||||||
|
setSearchOpen(false);
|
||||||
|
}, [native.groupId]);
|
||||||
|
|
||||||
|
// Restore the oldest failed send after a reload/room switch. It is retried
|
||||||
|
// automatically a few times when the app is online, then stays queued for a
|
||||||
|
// deliberate tap.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user?.pubkey || !native.groupId) return;
|
||||||
|
const [next] = readCordnOutbox(user.pubkey, native.groupId);
|
||||||
|
if (!next) return;
|
||||||
|
setPending((current) =>
|
||||||
|
current ?? {
|
||||||
|
text: next.text,
|
||||||
|
imageUrl: next.imageUrl,
|
||||||
|
state: 'failed',
|
||||||
|
outboxId: next.id,
|
||||||
|
attempts: next.attempts,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [user?.pubkey, native.groupId]);
|
||||||
|
|
||||||
|
const displayMessages = useMemo(() => {
|
||||||
|
const query = search.trim().toLowerCase();
|
||||||
|
if (!query) return native.messages;
|
||||||
|
return native.messages.filter(
|
||||||
|
(message) =>
|
||||||
|
message.content.toLowerCase().includes(query) ||
|
||||||
|
(message.imageUrl ?? '').toLowerCase().includes(query),
|
||||||
|
);
|
||||||
|
}, [native.messages, search]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (native.group && awaiting) {
|
if (native.group && awaiting) {
|
||||||
if (user?.pubkey) clearPendingJoin(user.pubkey, native.groupId);
|
if (user?.pubkey) clearPendingJoin(user.pubkey, native.groupId);
|
||||||
|
|
@ -264,36 +346,37 @@ export function CordnNativePanel({
|
||||||
onRequestsChange?.(canManage ? native.requests.length : 0);
|
onRequestsChange?.(canManage ? native.requests.length : 0);
|
||||||
}, [canManage, native.requests.length, onRequestsChange]);
|
}, [canManage, native.requests.length, onRequestsChange]);
|
||||||
|
|
||||||
// "New since last opened" marker. While the tab is visible we advance the
|
// "New since last opened" marker. The panel records the latest inbound
|
||||||
// cursor; while it is hidden we only compare inbound timestamps. The first
|
// timestamp; a hidden tab compares it with the local seen cursor. The first
|
||||||
// observation seeds the cursor so a room's old history is not "unread".
|
// observation seeds the cursor so old history never reads as unread.
|
||||||
const messageCursor = useMemo(() => {
|
|
||||||
const latest = native.messages
|
|
||||||
.filter((m) => m.direction === 'inbound' && m.sender !== user?.pubkey)
|
|
||||||
.reduce((max, m) => Math.max(max, m.createdAt), 0);
|
|
||||||
return { latest };
|
|
||||||
}, [native.messages, user?.pubkey]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user?.pubkey || !groupGid || !native.ready) {
|
if (!user?.pubkey || !groupGid || !native.ready) {
|
||||||
onUnreadChange?.(false);
|
onUnreadChange?.(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const gid = groupGid;
|
|
||||||
const latest = messageCursor.latest;
|
|
||||||
if (active) {
|
if (active) {
|
||||||
writeCordnSeen(user.pubkey, gid, latest);
|
|
||||||
onUnreadChange?.(false);
|
onUnreadChange?.(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const seen = readCordnSeen(user.pubkey, gid);
|
const state = reads[groupGid];
|
||||||
if (seen === null) {
|
if (!state) {
|
||||||
writeCordnSeen(user.pubkey, gid, latest);
|
markSeen(groupGid, latestInboundAt);
|
||||||
onUnreadChange?.(false);
|
onUnreadChange?.(false);
|
||||||
} else {
|
return;
|
||||||
onUnreadChange?.(latest > seen);
|
|
||||||
}
|
}
|
||||||
}, [active, groupGid, native.ready, user?.pubkey, messageCursor.latest, onUnreadChange]);
|
markActive(groupGid, latestInboundAt);
|
||||||
|
onUnreadChange?.(latestInboundAt > state.seenAt);
|
||||||
|
}, [
|
||||||
|
active,
|
||||||
|
groupGid,
|
||||||
|
native.ready,
|
||||||
|
user?.pubkey,
|
||||||
|
reads,
|
||||||
|
latestInboundAt,
|
||||||
|
markActive,
|
||||||
|
markSeen,
|
||||||
|
onUnreadChange,
|
||||||
|
]);
|
||||||
|
|
||||||
// Opening the private tab pulls anything missed while it was hidden. Silent
|
// Opening the private tab pulls anything missed while it was hidden. Silent
|
||||||
// signers also poll in the hook; this makes tab activation immediate.
|
// signers also poll in the hook; this makes tab activation immediate.
|
||||||
|
|
@ -335,18 +418,53 @@ export function CordnNativePanel({
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const deliver = useCallback(
|
const deliver = useCallback(
|
||||||
async (text: string, imageUrl?: string) => {
|
async (text: string, imageUrl?: string, outboxId?: string): Promise<boolean> => {
|
||||||
setNotice(null);
|
setNotice(null);
|
||||||
setPending({ text, imageUrl, state: 'sending' });
|
const currentAttempt = outboxId
|
||||||
|
? readCordnOutbox(user?.pubkey, native.groupId).find((item) => item.id === outboxId)?.attempts
|
||||||
|
: undefined;
|
||||||
|
setPending({
|
||||||
|
text,
|
||||||
|
imageUrl,
|
||||||
|
state: 'sending',
|
||||||
|
outboxId,
|
||||||
|
attempts: currentAttempt,
|
||||||
|
});
|
||||||
const sent = await sendNative(text, imageUrl);
|
const sent = await sendNative(text, imageUrl);
|
||||||
if (sent) {
|
if (sent) {
|
||||||
markSent(sent.id);
|
markSent(sent.id);
|
||||||
setPending(null);
|
if (outboxId) removeCordnOutbox(user?.pubkey, native.groupId, outboxId);
|
||||||
} else {
|
const [next] = readCordnOutbox(user?.pubkey, native.groupId);
|
||||||
setPending((current) => (current ? { ...current, state: 'failed' } : current));
|
setPending(next ? {
|
||||||
|
text: next.text,
|
||||||
|
imageUrl: next.imageUrl,
|
||||||
|
state: 'failed',
|
||||||
|
outboxId: next.id,
|
||||||
|
attempts: next.attempts,
|
||||||
|
} : null);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let nextOutboxId = outboxId;
|
||||||
|
let attempts = currentAttempt ?? 0;
|
||||||
|
if (nextOutboxId) {
|
||||||
|
markCordnOutboxAttempt(user?.pubkey, native.groupId, nextOutboxId);
|
||||||
|
attempts += 1;
|
||||||
|
} else {
|
||||||
|
const item = enqueueCordnOutbox(user?.pubkey, native.groupId, text, imageUrl);
|
||||||
|
nextOutboxId = item?.id;
|
||||||
|
attempts = item?.attempts ?? 1;
|
||||||
|
}
|
||||||
|
setPending({
|
||||||
|
text,
|
||||||
|
imageUrl,
|
||||||
|
state: 'failed',
|
||||||
|
outboxId: nextOutboxId,
|
||||||
|
attempts,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
[sendNative, markSent],
|
[sendNative, markSent, user?.pubkey, native.groupId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSend = useCallback(async () => {
|
const handleSend = useCallback(async () => {
|
||||||
|
|
@ -354,21 +472,41 @@ export function CordnNativePanel({
|
||||||
if (!body || native.busy) return;
|
if (!body || native.busy) return;
|
||||||
setDraft('');
|
setDraft('');
|
||||||
const content = withReply(body);
|
const content = withReply(body);
|
||||||
const sent = await native.send(content);
|
const ok = await deliver(content);
|
||||||
if (sent) {
|
if (ok) setReplyTo(null);
|
||||||
markSent(sent.id);
|
}, [draft, native.busy, withReply, deliver]);
|
||||||
setReplyTo(null);
|
|
||||||
setPending(null);
|
|
||||||
} else {
|
|
||||||
setPending({ text: content, state: 'failed' });
|
|
||||||
}
|
|
||||||
}, [draft, native, withReply, markSent]);
|
|
||||||
|
|
||||||
const retrySend = useCallback(async () => {
|
const retrySend = useCallback(async () => {
|
||||||
if (!pending || pending.state !== 'failed') return;
|
if (!pending || pending.state !== 'failed') return;
|
||||||
await deliver(pending.text, pending.imageUrl);
|
await deliver(pending.text, pending.imageUrl, pending.outboxId);
|
||||||
}, [pending, deliver]);
|
}, [pending, deliver]);
|
||||||
|
|
||||||
|
const discardPending = useCallback(() => {
|
||||||
|
if (pending?.outboxId) removeCordnOutbox(user?.pubkey, native.groupId, pending.outboxId);
|
||||||
|
const [next] = readCordnOutbox(user?.pubkey, native.groupId);
|
||||||
|
setPending(next ? {
|
||||||
|
text: next.text,
|
||||||
|
imageUrl: next.imageUrl,
|
||||||
|
state: 'failed',
|
||||||
|
outboxId: next.id,
|
||||||
|
attempts: next.attempts,
|
||||||
|
} : null);
|
||||||
|
}, [pending, user?.pubkey, native.groupId]);
|
||||||
|
|
||||||
|
// A failed send is retried a few times when the app is online, then left for
|
||||||
|
// the explicit Retry button. This avoids both data loss and infinite retry.
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!pending ||
|
||||||
|
pending.state !== 'failed' ||
|
||||||
|
!online ||
|
||||||
|
!native.ready ||
|
||||||
|
(pending.attempts ?? 0) >= 3
|
||||||
|
) return;
|
||||||
|
const timer = window.setTimeout(() => void retrySend(), 12_000);
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, [pending, online, native.ready, retrySend]);
|
||||||
|
|
||||||
const onPickImage = useCallback(
|
const onPickImage = useCallback(
|
||||||
async (file: File | undefined) => {
|
async (file: File | undefined) => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
@ -586,6 +724,36 @@ export function CordnNativePanel({
|
||||||
<Lock className="h-4 w-4 text-teal-400" /> {copy.title}
|
<Lock className="h-4 w-4 text-teal-400" /> {copy.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
{native.group && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 text-gray-400 hover:text-teal-300"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchOpen((open) => !open);
|
||||||
|
if (searchOpen) setSearch('');
|
||||||
|
}}
|
||||||
|
aria-label={t('common.search')}
|
||||||
|
title={t('common.search')}
|
||||||
|
>
|
||||||
|
<Search className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
{muteKey && (
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className={cn('h-7 w-7 hover:text-amber-300', muted ? 'text-amber-400' : 'text-gray-400')}
|
||||||
|
onClick={() => toggleMute(muteKey)}
|
||||||
|
aria-pressed={muted}
|
||||||
|
aria-label={muted ? t('walletUi.trust.unmute') : t('walletUi.trust.mute')}
|
||||||
|
title={muted ? t('walletUi.trust.unmute') : t('walletUi.trust.mute')}
|
||||||
|
>
|
||||||
|
{muted ? <Volume2 className="h-3.5 w-3.5" /> : <VolumeX className="h-3.5 w-3.5" />}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{native.group && (
|
{native.group && (
|
||||||
<Badge variant="secondary" className="text-[10px]">
|
<Badge variant="secondary" className="text-[10px]">
|
||||||
{t('cordnUi.memberCount', { count: native.group.members.length })}
|
{t('cordnUi.memberCount', { count: native.group.members.length })}
|
||||||
|
|
@ -646,6 +814,18 @@ export function CordnNativePanel({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="pt-1 text-xs text-gray-500">{copy.subtitle}</p>
|
<p className="pt-1 text-xs text-gray-500">{copy.subtitle}</p>
|
||||||
|
{!isCommunity && (
|
||||||
|
<details className="group mt-1 rounded-md border border-white/10 bg-black/20 px-2.5 py-1.5">
|
||||||
|
<summary className="flex cursor-pointer list-none items-center gap-1 text-[11px] text-gray-400">
|
||||||
|
<ChevronDown className="h-3 w-3 transition-transform group-open:rotate-180" />
|
||||||
|
{t('cordnUi.compareTitle')}
|
||||||
|
</summary>
|
||||||
|
<div className="mt-1.5 space-y-1 text-[11px] leading-relaxed text-gray-500">
|
||||||
|
<p>{t('cordnUi.compareChat')}</p>
|
||||||
|
<p>{t('cordnUi.comparePrivate')}</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
|
|
@ -793,6 +973,7 @@ export function CordnNativePanel({
|
||||||
groupRef={native.groupRef}
|
groupRef={native.groupRef}
|
||||||
coordinatorPubkey={native.coordinator.serverPubkey}
|
coordinatorPubkey={native.coordinator.serverPubkey}
|
||||||
keyPackageReady={native.keyPackageReady}
|
keyPackageReady={native.keyPackageReady}
|
||||||
|
lastCheckedAt={native.lastCheckedAt}
|
||||||
copied={copied}
|
copied={copied}
|
||||||
onCopyRef={() => void copyRef()}
|
onCopyRef={() => void copyRef()}
|
||||||
/>
|
/>
|
||||||
|
|
@ -821,7 +1002,6 @@ export function CordnNativePanel({
|
||||||
onToggleAdmin={() => void updateAdminRole(member.pubkey, roleOf(member.pubkey) !== 'admin')}
|
onToggleAdmin={() => void updateAdminRole(member.pubkey, roleOf(member.pubkey) !== 'admin')}
|
||||||
canRemove={canManage && member.pubkey !== user.pubkey && member.pubkey !== crew.pubkey}
|
canRemove={canManage && member.pubkey !== user.pubkey && member.pubkey !== crew.pubkey}
|
||||||
onRemove={() => {
|
onRemove={() => {
|
||||||
if (!window.confirm(t('cordnUi.removeMemberConfirm', { name: shortNpub(member.pubkey) }))) return;
|
|
||||||
void native.removeMember(member.leafIndex).then((ok) => {
|
void native.removeMember(member.leafIndex).then((ok) => {
|
||||||
if (!ok) setNotice(t('cordnUi.removeMemberFailed'));
|
if (!ok) setNotice(t('cordnUi.removeMemberFailed'));
|
||||||
});
|
});
|
||||||
|
|
@ -848,16 +1028,41 @@ export function CordnNativePanel({
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
{searchOpen && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
|
placeholder={t('common.search')}
|
||||||
|
aria-label={t('common.search')}
|
||||||
|
className="h-8 border-white/10 bg-black/40 text-xs"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{search && (
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 text-gray-400"
|
||||||
|
onClick={() => setSearch('')}
|
||||||
|
aria-label={t('common.clear')}
|
||||||
|
title={t('common.clear')}
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="max-h-80 space-y-2 overflow-y-auto rounded-md border border-white/10 bg-black/30 p-3"
|
className="max-h-80 space-y-2 overflow-y-auto rounded-md border border-white/10 bg-black/30 p-3"
|
||||||
>
|
>
|
||||||
{native.messages.length === 0 && !pending ? (
|
{displayMessages.length === 0 && !pending ? (
|
||||||
<p className="py-6 text-center text-xs text-gray-500">
|
<p className="py-6 text-center text-xs text-gray-500">
|
||||||
{t('cordnUi.placeholder')}
|
{search.trim() ? t('common.noResults') : t('cordnUi.placeholder')}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
native.messages.map((m) => (
|
displayMessages.map((m) => (
|
||||||
<div
|
<div
|
||||||
key={m.id}
|
key={m.id}
|
||||||
className={cn('flex flex-col gap-0.5', m.direction === 'outbound' && 'items-end')}
|
className={cn('flex flex-col gap-0.5', m.direction === 'outbound' && 'items-end')}
|
||||||
|
|
@ -993,6 +1198,9 @@ export function CordnNativePanel({
|
||||||
{pending.state === 'sending'
|
{pending.state === 'sending'
|
||||||
? t('cordnUi.sending')
|
? t('cordnUi.sending')
|
||||||
: t('cordnUi.sendFailed')}
|
: t('cordnUi.sendFailed')}
|
||||||
|
{pending.state === 'failed' && pending.outboxId && (
|
||||||
|
<span className="ml-1 text-gray-500">· {t('cordnUi.queued')}</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-[85%] space-y-1 overflow-hidden rounded-2xl bg-teal-500/20 px-3 py-1.5 text-sm text-teal-100 opacity-80">
|
<div className="max-w-[85%] space-y-1 overflow-hidden rounded-2xl bg-teal-500/20 px-3 py-1.5 text-sm text-teal-100 opacity-80">
|
||||||
|
|
@ -1021,7 +1229,7 @@ export function CordnNativePanel({
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-7 gap-1 px-2 text-xs text-gray-400"
|
className="h-7 gap-1 px-2 text-xs text-gray-400"
|
||||||
onClick={() => setPending(null)}
|
onClick={discardPending}
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" /> {t('cordnUi.dismiss')}
|
<X className="h-3 w-3" /> {t('cordnUi.dismiss')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -1116,6 +1324,7 @@ export function CordnNativePanel({
|
||||||
groupRef={native.groupRef}
|
groupRef={native.groupRef}
|
||||||
coordinatorPubkey={native.coordinator.serverPubkey}
|
coordinatorPubkey={native.coordinator.serverPubkey}
|
||||||
keyPackageReady={native.keyPackageReady || !!native.group}
|
keyPackageReady={native.keyPackageReady || !!native.group}
|
||||||
|
lastCheckedAt={native.lastCheckedAt}
|
||||||
copied={copied}
|
copied={copied}
|
||||||
onCopyRef={() => void copyRef()}
|
onCopyRef={() => void copyRef()}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1263,12 +1472,14 @@ function AdvancedPanel({
|
||||||
groupRef,
|
groupRef,
|
||||||
coordinatorPubkey,
|
coordinatorPubkey,
|
||||||
keyPackageReady,
|
keyPackageReady,
|
||||||
|
lastCheckedAt,
|
||||||
copied,
|
copied,
|
||||||
onCopyRef,
|
onCopyRef,
|
||||||
}: {
|
}: {
|
||||||
groupRef: string | null;
|
groupRef: string | null;
|
||||||
coordinatorPubkey: string;
|
coordinatorPubkey: string;
|
||||||
keyPackageReady: boolean;
|
keyPackageReady: boolean;
|
||||||
|
lastCheckedAt: number | null;
|
||||||
copied: boolean;
|
copied: boolean;
|
||||||
onCopyRef: () => void;
|
onCopyRef: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -1305,6 +1516,14 @@ function AdvancedPanel({
|
||||||
{keyPackageReady ? t('cordnUi.keyPackageReady') : t('cordnUi.keyPackageMissing')}
|
{keyPackageReady ? t('cordnUi.keyPackageReady') : t('cordnUi.keyPackageMissing')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-2 text-[11px] text-gray-500">
|
||||||
|
<span>{t('cordnUi.lastChecked')}</span>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
{lastCheckedAt
|
||||||
|
? formatDistanceToNow(new Date(lastCheckedAt), { addSuffix: true })
|
||||||
|
: '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{groupRef && (
|
{groupRef && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 text-[11px] text-gray-500">
|
<div className="flex items-center gap-2 text-[11px] text-gray-500">
|
||||||
|
|
@ -1454,16 +1673,31 @@ function MemberRow({
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canRemove && onRemove && (
|
{canRemove && onRemove && (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 w-6 text-gray-500 hover:bg-red-500/10 hover:text-red-300"
|
className="h-6 w-6 text-gray-500 hover:bg-red-500/10 hover:text-red-300"
|
||||||
onClick={onRemove}
|
|
||||||
aria-label={t('common.remove')}
|
aria-label={t('common.remove')}
|
||||||
title={t('common.remove')}
|
title={t('common.remove')}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{t('common.remove')}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t('cordnUi.removeMemberConfirm', { name: shortNpub(pubkey) })}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={onRemove}>{t('common.remove')}</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,8 @@ export interface UseCordnNativeResult {
|
||||||
groupRef: string | null;
|
groupRef: string | null;
|
||||||
/** True when this device is set up: it holds the group or a published key package. */
|
/** True when this device is set up: it holds the group or a published key package. */
|
||||||
keyPackageReady: boolean;
|
keyPackageReady: boolean;
|
||||||
|
/** Last completed coordinator/session check, or null before the first one. */
|
||||||
|
lastCheckedAt: number | null;
|
||||||
/** Re-open the session after a connection error (the Retry button). */
|
/** Re-open the session after a connection error (the Retry button). */
|
||||||
retry: () => void;
|
retry: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +153,7 @@ export function useCordnNative({
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [groupRef, setGroupRef] = useState<string | null>(null);
|
const [groupRef, setGroupRef] = useState<string | null>(null);
|
||||||
const [keyPackageReady, setKeyPackageReady] = useState(false);
|
const [keyPackageReady, setKeyPackageReady] = useState(false);
|
||||||
|
const [lastCheckedAt, setLastCheckedAt] = useState<number | null>(null);
|
||||||
const [reloadKey, setReloadKey] = useState(0);
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
|
|
||||||
const interactive = isInteractiveSigner(user, logins);
|
const interactive = isInteractiveSigner(user, logins);
|
||||||
|
|
@ -209,9 +212,13 @@ export function useCordnNative({
|
||||||
opened.listKeyPackages().some((kp) => kp.publishedAt !== undefined && !kp.consumed),
|
opened.listKeyPackages().some((kp) => kp.publishedAt !== undefined && !kp.consumed),
|
||||||
);
|
);
|
||||||
setReady(true);
|
setReady(true);
|
||||||
|
setLastCheckedAt(Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (!cancelled) setError(err instanceof Error ? err.message : String(err));
|
if (!cancelled) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
setLastCheckedAt(Date.now());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|
@ -233,8 +240,10 @@ export function useCordnNative({
|
||||||
setMessages(next);
|
setMessages(next);
|
||||||
setGroup(session.listGroups().find((g) => g.gid === group.gid) ?? group);
|
setGroup(session.listGroups().find((g) => g.gid === group.gid) ?? group);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setLastCheckedAt(Date.now());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
setLastCheckedAt(Date.now());
|
||||||
}
|
}
|
||||||
}, [group]);
|
}, [group]);
|
||||||
|
|
||||||
|
|
@ -257,6 +266,7 @@ export function useCordnNative({
|
||||||
return undefined;
|
return undefined;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
|
setLastCheckedAt(Date.now());
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -549,6 +559,7 @@ export function useCordnNative({
|
||||||
passiveSigner,
|
passiveSigner,
|
||||||
groupRef,
|
groupRef,
|
||||||
keyPackageReady,
|
keyPackageReady,
|
||||||
|
lastCheckedAt,
|
||||||
retry,
|
retry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
src/hooks/useCordnRoomRead.ts
Normal file
47
src/hooks/useCordnRoomRead.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
cordnRoomReadEventName,
|
||||||
|
markCordnRoomActive,
|
||||||
|
markCordnRoomSeen,
|
||||||
|
readCordnRoomReads,
|
||||||
|
type CordnRoomReadMap,
|
||||||
|
} from '@/lib/cordnRoomRead';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reactive view of {@link readCordnRoomReads}. Safe to mount on list pages:
|
||||||
|
* it only reads localStorage and listens for same-tab/cross-tab updates.
|
||||||
|
*/
|
||||||
|
export function useCordnRoomRead(pubkey: string | undefined) {
|
||||||
|
const [reads, setReads] = useState<CordnRoomReadMap>(() => readCordnRoomReads(pubkey));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setReads(readCordnRoomReads(pubkey));
|
||||||
|
const onChange = (event: Event) => {
|
||||||
|
const detail = (event as CustomEvent<{ pubkey?: string; reads?: CordnRoomReadMap }>).detail;
|
||||||
|
if (!detail || detail.pubkey !== pubkey || !detail.reads) return;
|
||||||
|
setReads(detail.reads);
|
||||||
|
};
|
||||||
|
window.addEventListener(cordnRoomReadEventName(), onChange);
|
||||||
|
return () => window.removeEventListener(cordnRoomReadEventName(), onChange);
|
||||||
|
}, [pubkey]);
|
||||||
|
|
||||||
|
const isUnread = useCallback(
|
||||||
|
(gid: string | undefined) => {
|
||||||
|
if (!gid) return false;
|
||||||
|
const state = reads[gid];
|
||||||
|
return !!state && state.latestAt > state.seenAt;
|
||||||
|
},
|
||||||
|
[reads],
|
||||||
|
);
|
||||||
|
|
||||||
|
const markActive = useCallback(
|
||||||
|
(gid: string, latestAt: number) => markCordnRoomActive(pubkey, gid, latestAt),
|
||||||
|
[pubkey],
|
||||||
|
);
|
||||||
|
const markSeen = useCallback(
|
||||||
|
(gid: string, latestAt: number) => markCordnRoomSeen(pubkey, gid, latestAt),
|
||||||
|
[pubkey],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { reads, isUnread, markActive, markSeen };
|
||||||
|
}
|
||||||
|
|
@ -918,10 +918,10 @@ const batch1 = {
|
||||||
"noGroupMember": "Encara no ets al xat privat d'aquesta colla.",
|
"noGroupMember": "Encara no ets al xat privat d'aquesta colla.",
|
||||||
"create": "Crea el xat privat",
|
"create": "Crea el xat privat",
|
||||||
"requestJoin": "Demana d'unir-t'hi",
|
"requestJoin": "Demana d'unir-t'hi",
|
||||||
"requestAgain": "Request again",
|
"requestAgain": "Torna a sol·licitar",
|
||||||
"requestHandled": "Request handled",
|
"requestHandled": "Sol·licitud gestionada",
|
||||||
"copyCordnLink": "Copy Cordn link",
|
"copyCordnLink": "Copia l’enllaç de Cordn",
|
||||||
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
"cordnWebCaveat": "Cordn web es pot encallar després d’iniciar sessió; unir-te des de l’app és més fiable.",
|
||||||
"checkInvite": "Comprova si hi ha invitació",
|
"checkInvite": "Comprova si hi ha invitació",
|
||||||
"requested": "Sol·licitud enviada: un administrador de la colla l'aprovarà.",
|
"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.",
|
"noInvite": "Encara no hi ha cap invitació. Demana a un administrador que aprovi la teva sol·licitud.",
|
||||||
|
|
@ -964,7 +964,11 @@ const batch1 = {
|
||||||
onboardingMember3: "Ja hi ets: els missatges van xifrats.",
|
onboardingMember3: "Ja hi ets: els missatges van xifrats.",
|
||||||
encryptedNote: "Només ho poden llegir els membres de la colla; el coordinador només retransmet missatges xifrats.",
|
encryptedNote: "Només ho poden llegir els membres de la colla; el coordinador només retransmet missatges xifrats.",
|
||||||
advanced: "Avançat",
|
advanced: "Avançat",
|
||||||
|
compareTitle: "Xat vs xat privat",
|
||||||
|
compareChat: "El xat és un DM de grup NIP-17 estàndard. Funciona amb altres clients Nostr i no té coordinador.",
|
||||||
|
comparePrivate: "El xat privat utilitza MLS: control de membres més fort i secret de reenviament. El coordinador veu les metadades del grup i el text xifrat, mai el text dels missatges.",
|
||||||
coordinator: "Coordinador",
|
coordinator: "Coordinador",
|
||||||
|
lastChecked: "Darrera comprovació",
|
||||||
keyPackage: "Paquet de claus",
|
keyPackage: "Paquet de claus",
|
||||||
keyPackageReady: "A punt en aquest dispositiu",
|
keyPackageReady: "A punt en aquest dispositiu",
|
||||||
keyPackageMissing: "No configurat en aquest dispositiu",
|
keyPackageMissing: "No configurat en aquest dispositiu",
|
||||||
|
|
@ -988,6 +992,7 @@ const batch1 = {
|
||||||
qrTitle: "Escaneja per unir-te al xat privat",
|
qrTitle: "Escaneja per unir-te al xat privat",
|
||||||
qrHint: "Els membres de la colla ho escanegen amb la càmera del mòbil. L’enllaç obre la pestanya Xat privat.",
|
qrHint: "Els membres de la colla ho escanegen amb la càmera del mòbil. L’enllaç obre la pestanya Xat privat.",
|
||||||
sendFailed: "No s’ha pogut enviar: toca Reintenta.",
|
sendFailed: "No s’ha pogut enviar: toca Reintenta.",
|
||||||
|
queued: "En cua en aquest dispositiu",
|
||||||
sending: "S’està enviant…",
|
sending: "S’està enviant…",
|
||||||
sent: "Enviat",
|
sent: "Enviat",
|
||||||
retry: "Reintenta",
|
retry: "Reintenta",
|
||||||
|
|
@ -1023,7 +1028,7 @@ const batch1 = {
|
||||||
"noInvite": "Encara no hi ha invitació. El bot normalment aprova en un minut; després toca «Comprova si hi ha invitació».",
|
"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",
|
"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.",
|
"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.",
|
"botMissing": "El bot de la comunitat no és en aquesta sala ara mateix, així que un admin revisarà les sol·licituds d’unió.",
|
||||||
"secondDeviceHint": "Ja ets membre en un altre dispositiu? Fes servir «Enllaça aquest dispositiu» per a un traspàs directe, o torna a demanar d’entrar amb aquest dispositiu.",
|
"secondDeviceHint": "Ja ets membre en un altre dispositiu? Fes servir «Enllaça aquest dispositiu» per a un traspàs directe, o torna a demanar d’entrar amb aquest dispositiu.",
|
||||||
"create": "Crea la sala de la comunitat",
|
"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.",
|
"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.",
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,7 @@ const batch2 = {
|
||||||
newMessage: "Missatge nou",
|
newMessage: "Missatge nou",
|
||||||
groups: "Grups",
|
groups: "Grups",
|
||||||
refresh: "Actualitza",
|
refresh: "Actualitza",
|
||||||
|
privateRooms: "Sales privades",
|
||||||
crews: "Crews",
|
crews: "Crews",
|
||||||
crewFounder: "Fundador",
|
crewFounder: "Fundador",
|
||||||
crewMember: "Membre",
|
crewMember: "Membre",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
"dms": {
|
"dms": {
|
||||||
"refresh": "Obnovit",
|
"refresh": "Obnovit",
|
||||||
|
"privateRooms": "Soukromé místnosti",
|
||||||
"crews": "Crew",
|
"crews": "Crew",
|
||||||
"crewFounder": "Zakladatel",
|
"crewFounder": "Zakladatel",
|
||||||
"crewMember": "Člen",
|
"crewMember": "Člen",
|
||||||
|
|
@ -307,10 +308,10 @@ const drift = {
|
||||||
"noGroupMember": "V soukromém chatu této crew zatím nejsi.",
|
"noGroupMember": "V soukromém chatu této crew zatím nejsi.",
|
||||||
"create": "Vytvořit soukromý chat",
|
"create": "Vytvořit soukromý chat",
|
||||||
"requestJoin": "Požádat o připojení",
|
"requestJoin": "Požádat o připojení",
|
||||||
"requestAgain": "Request again",
|
"requestAgain": "Požádat znovu",
|
||||||
"requestHandled": "Request handled",
|
"requestHandled": "Žádost vyřízena",
|
||||||
"copyCordnLink": "Copy Cordn link",
|
"copyCordnLink": "Kopírovat odkaz Cordn",
|
||||||
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
"cordnWebCaveat": "Cordn web může po přihlášení zamrznout; připojení v aplikaci je spolehlivější.",
|
||||||
"checkInvite": "Zkontrolovat pozvánku",
|
"checkInvite": "Zkontrolovat pozvánku",
|
||||||
"requested": "Žádost odeslána — administrátor crew ji schválí.",
|
"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.",
|
"noInvite": "Zatím nečeká žádná pozvánka. Požádej administrátora o schválení žádosti.",
|
||||||
|
|
@ -350,7 +351,11 @@ const drift = {
|
||||||
onboardingMember3: "Jsi uvnitř — zprávy jsou šifrované.",
|
onboardingMember3: "Jsi uvnitř — zprávy jsou šifrované.",
|
||||||
encryptedNote: "Číst to mohou jen členové crew; koordinátor pouze přenáší šifrované zprávy.",
|
encryptedNote: "Číst to mohou jen členové crew; koordinátor pouze přenáší šifrované zprávy.",
|
||||||
advanced: "Pokročilé",
|
advanced: "Pokročilé",
|
||||||
|
compareTitle: "Chat vs. soukromý chat",
|
||||||
|
compareChat: "Chat je standardní skupinová DM podle NIP-17. Funguje s jinými klienty Nostr a nemá žádného koordinátora.",
|
||||||
|
comparePrivate: "Soukromý chat používá MLS: silnější kontrolu členství a dopředné utajení. Koordinátor vidí metadata skupiny a šifrovaný text, nikdy obsah zpráv.",
|
||||||
coordinator: "Koordinátor",
|
coordinator: "Koordinátor",
|
||||||
|
lastChecked: "Naposledy zkontrolováno",
|
||||||
keyPackage: "Balíček klíčů",
|
keyPackage: "Balíček klíčů",
|
||||||
keyPackageReady: "Připraveno na tomto zařízení",
|
keyPackageReady: "Připraveno na tomto zařízení",
|
||||||
keyPackageMissing: "Není nastaveno na tomto zařízení",
|
keyPackageMissing: "Není nastaveno na tomto zařízení",
|
||||||
|
|
@ -374,6 +379,7 @@ const drift = {
|
||||||
qrTitle: "Naskenuj a připoj se k soukromému chatu",
|
qrTitle: "Naskenuj a připoj se k soukromému chatu",
|
||||||
qrHint: "Členové crew to naskenují kamerou telefonu. Odkaz otevře kartu Soukromý chat.",
|
qrHint: "Členové crew to naskenují kamerou telefonu. Odkaz otevře kartu Soukromý chat.",
|
||||||
sendFailed: "Nepodařilo se odeslat — klepni na Zkusit znovu.",
|
sendFailed: "Nepodařilo se odeslat — klepni na Zkusit znovu.",
|
||||||
|
queued: "Ve frontě na tomto zařízení",
|
||||||
sending: "Odesílání…",
|
sending: "Odesílání…",
|
||||||
sent: "Odesláno",
|
sent: "Odesláno",
|
||||||
retry: "Zkusit znovu",
|
retry: "Zkusit znovu",
|
||||||
|
|
@ -409,7 +415,7 @@ const drift = {
|
||||||
"noInvite": "Zatím žádná pozvánka. Bot obvykle schválí do minuty — potom 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í",
|
"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.",
|
"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.",
|
"botMissing": "Komunitní bot teď v této místnosti není, takže žádosti o připojení zkontroluje administrátor.",
|
||||||
"secondDeviceHint": "Už jsi členem na jiném zařízení? Použij „Propojit toto zařízení“ pro přímé předání, nebo požádej o připojení znovu s tímto zařízením.",
|
"secondDeviceHint": "Už jsi členem na jiném zařízení? Použij „Propojit toto zařízení“ pro přímé předání, nebo požádej o připojení znovu s tímto zařízením.",
|
||||||
"create": "Vytvořit komunitní místnost",
|
"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.",
|
"setupDeviceBody": "Toto zařízení tím získá klíč do komunitní místnosti. Schválí ho jednou administrátor nebo bot.",
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: "Aktualisieren",
|
refresh: "Aktualisieren",
|
||||||
|
privateRooms: "Private Räume",
|
||||||
crews: "Crews",
|
crews: "Crews",
|
||||||
crewFounder: "Gründer",
|
crewFounder: "Gründer",
|
||||||
crewMember: "Mitglied",
|
crewMember: "Mitglied",
|
||||||
|
|
@ -309,10 +310,10 @@ const drift = {
|
||||||
noGroupMember: "Du bist noch nicht im privaten Chat dieser Crew.",
|
noGroupMember: "Du bist noch nicht im privaten Chat dieser Crew.",
|
||||||
create: "Privaten Chat erstellen",
|
create: "Privaten Chat erstellen",
|
||||||
requestJoin: "Um Beitritt bitten",
|
requestJoin: "Um Beitritt bitten",
|
||||||
requestAgain: "Request again",
|
requestAgain: "Erneut anfragen",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Anfrage bearbeitet",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Cordn-Link kopieren",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn Web kann nach der Anmeldung hängen bleiben; der Beitritt in der App ist zuverlässiger.",
|
||||||
checkInvite: "Nach Einladung suchen",
|
checkInvite: "Nach Einladung suchen",
|
||||||
requested: "Anfrage gesendet – ein Crew-Admin wird sie genehmigen.",
|
requested: "Anfrage gesendet – ein Crew-Admin wird sie genehmigen.",
|
||||||
noInvite: "Noch keine Einladung. Bitte einen Admin, deine Anfrage zu genehmigen.",
|
noInvite: "Noch keine Einladung. Bitte einen Admin, deine Anfrage zu genehmigen.",
|
||||||
|
|
@ -352,7 +353,11 @@ const drift = {
|
||||||
onboardingMember3: "Du bist dabei – Nachrichten sind verschlüsselt.",
|
onboardingMember3: "Du bist dabei – Nachrichten sind verschlüsselt.",
|
||||||
encryptedNote: "Nur Crew-Mitglieder können das lesen; der Koordinator leitet nur verschlüsselte Nachrichten weiter.",
|
encryptedNote: "Nur Crew-Mitglieder können das lesen; der Koordinator leitet nur verschlüsselte Nachrichten weiter.",
|
||||||
advanced: "Erweitert",
|
advanced: "Erweitert",
|
||||||
|
compareTitle: "Chat vs. privater Chat",
|
||||||
|
compareChat: "Der Chat ist eine standardmäßige NIP-17-Gruppen-DM. Er funktioniert mit anderen Nostr-Clients und hat keinen Koordinator.",
|
||||||
|
comparePrivate: "Der private Chat nutzt MLS: stärkere Mitgliederverwaltung und Forward Secrecy. Der Koordinator sieht Gruppenmetadaten und Chiffretext, niemals den Nachrichtentext.",
|
||||||
coordinator: "Koordinator",
|
coordinator: "Koordinator",
|
||||||
|
lastChecked: "Zuletzt geprüft",
|
||||||
keyPackage: "Schlüsselpaket",
|
keyPackage: "Schlüsselpaket",
|
||||||
keyPackageReady: "Auf diesem Gerät bereit",
|
keyPackageReady: "Auf diesem Gerät bereit",
|
||||||
keyPackageMissing: "Auf diesem Gerät nicht eingerichtet",
|
keyPackageMissing: "Auf diesem Gerät nicht eingerichtet",
|
||||||
|
|
@ -376,6 +381,7 @@ const drift = {
|
||||||
qrTitle: "Scannen und dem privaten Chat beitreten",
|
qrTitle: "Scannen und dem privaten Chat beitreten",
|
||||||
qrHint: "Crew-Mitglieder scannen das mit der Handykamera. Der Link öffnet den Tab „Privater Chat“.",
|
qrHint: "Crew-Mitglieder scannen das mit der Handykamera. Der Link öffnet den Tab „Privater Chat“.",
|
||||||
sendFailed: "Senden fehlgeschlagen – tippe auf Wiederholen.",
|
sendFailed: "Senden fehlgeschlagen – tippe auf Wiederholen.",
|
||||||
|
queued: "Auf diesem Gerät in der Warteschlange",
|
||||||
sending: "Senden…",
|
sending: "Senden…",
|
||||||
sent: "Gesendet",
|
sent: "Gesendet",
|
||||||
retry: "Wiederholen",
|
retry: "Wiederholen",
|
||||||
|
|
@ -411,7 +417,7 @@ const drift = {
|
||||||
noInvite: "Noch keine Einladung. Der Bot genehmigt normalerweise innerhalb einer Minute – tippe danach auf „Nach Einladung suchen“.",
|
noInvite: "Noch keine Einladung. Der Bot genehmigt normalerweise innerhalb einer Minute – tippe danach auf „Nach Einladung suchen“.",
|
||||||
pendingRequests: "{{count}} offene Beitrittsanfragen",
|
pendingRequests: "{{count}} offene Beitrittsanfragen",
|
||||||
pendingRequestsDesc: "Der Bot genehmigt normalerweise automatisch. Wenn es hängt, öffne unten den Raum und tippe auf Anfragen prüfen.",
|
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.",
|
botMissing: "Der Community-Bot ist gerade nicht in diesem Raum, daher prüft ein Admin die Beitrittsanfragen.",
|
||||||
secondDeviceHint: "Schon Mitglied auf einem anderen Gerät? Nutze „Dieses Gerät verknüpfen“ für eine direkte Übergabe oder bitte mit diesem Gerät erneut um Beitritt.",
|
secondDeviceHint: "Schon Mitglied auf einem anderen Gerät? Nutze „Dieses Gerät verknüpfen“ für eine direkte Übergabe oder bitte mit diesem Gerät erneut um Beitritt.",
|
||||||
create: "Community-Raum erstellen",
|
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.",
|
setupDeviceBody: "Damit erhält dieses Gerät einen Schlüssel für den Community-Raum. Ein Admin oder der Bot bestätigt ihn einmal.",
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: "Ανανέωση",
|
refresh: "Ανανέωση",
|
||||||
|
privateRooms: "Ιδιωτικά δωμάτια",
|
||||||
crews: "Crews",
|
crews: "Crews",
|
||||||
crewFounder: "Ιδρυτής",
|
crewFounder: "Ιδρυτής",
|
||||||
crewMember: "Μέλος",
|
crewMember: "Μέλος",
|
||||||
|
|
@ -311,10 +312,10 @@ const drift = {
|
||||||
noGroupMember: "Δεν είσαι ακόμη στην ιδιωτική συνομιλία αυτού του crew.",
|
noGroupMember: "Δεν είσαι ακόμη στην ιδιωτική συνομιλία αυτού του crew.",
|
||||||
create: "Δημιουργία ιδιωτικής συνομιλίας",
|
create: "Δημιουργία ιδιωτικής συνομιλίας",
|
||||||
requestJoin: "Ζήτα συμμετοχή",
|
requestJoin: "Ζήτα συμμετοχή",
|
||||||
requestAgain: "Request again",
|
requestAgain: "Νέο αίτημα",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Το αίτημα διεκπεραιώθηκε",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Αντιγραφή συνδέσμου Cordn",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Το Cordn web μπορεί να κολλήσει μετά τη σύνδεση· η συμμετοχή από την εφαρμογή είναι πιο αξιόπιστη.",
|
||||||
checkInvite: "Έλεγχος πρόσκλησης",
|
checkInvite: "Έλεγχος πρόσκλησης",
|
||||||
requested: "Το αίτημα στάλθηκε — ένας διαχειριστής του crew θα το εγκρίνει.",
|
requested: "Το αίτημα στάλθηκε — ένας διαχειριστής του crew θα το εγκρίνει.",
|
||||||
noInvite: "Δεν περιμένει ακόμη πρόσκληση. Ζήτα από έναν διαχειριστή να εγκρίνει το αίτημά σου.",
|
noInvite: "Δεν περιμένει ακόμη πρόσκληση. Ζήτα από έναν διαχειριστή να εγκρίνει το αίτημά σου.",
|
||||||
|
|
@ -354,7 +355,11 @@ const drift = {
|
||||||
onboardingMember3: "Είστε μέσα — τα μηνύματα είναι κρυπτογραφημένα.",
|
onboardingMember3: "Είστε μέσα — τα μηνύματα είναι κρυπτογραφημένα.",
|
||||||
encryptedNote: "Μόνο τα μέλη του crew μπορούν να το διαβάσουν· ο συντονιστής απλώς προωθεί κρυπτογραφημένα μηνύματα.",
|
encryptedNote: "Μόνο τα μέλη του crew μπορούν να το διαβάσουν· ο συντονιστής απλώς προωθεί κρυπτογραφημένα μηνύματα.",
|
||||||
advanced: "Για προχωρημένους",
|
advanced: "Για προχωρημένους",
|
||||||
|
compareTitle: "Συνομιλία vs. ιδιωτική συνομιλία",
|
||||||
|
compareChat: "Η συνομιλία είναι ένα τυπικό group DM του NIP-17. Λειτουργεί με άλλους πελάτες Nostr και δεν έχει συντονιστή.",
|
||||||
|
comparePrivate: "Η ιδιωτική συνομιλία χρησιμοποιεί MLS: ισχυρότερο έλεγχο μελών και εμπιστευτικότητα προς τα εμπρός. Ο συντονιστής βλέπει μεταδεδομένα ομάδας και κρυπτοκείμενο, ποτέ το κείμενο των μηνυμάτων.",
|
||||||
coordinator: "Συντονιστής",
|
coordinator: "Συντονιστής",
|
||||||
|
lastChecked: "Τελευταίος έλεγχος",
|
||||||
keyPackage: "Πακέτο κλειδιών",
|
keyPackage: "Πακέτο κλειδιών",
|
||||||
keyPackageReady: "Έτοιμο σε αυτή τη συσκευή",
|
keyPackageReady: "Έτοιμο σε αυτή τη συσκευή",
|
||||||
keyPackageMissing: "Δεν έχει ρυθμιστεί σε αυτή τη συσκευή",
|
keyPackageMissing: "Δεν έχει ρυθμιστεί σε αυτή τη συσκευή",
|
||||||
|
|
@ -378,6 +383,7 @@ const drift = {
|
||||||
qrTitle: "Σαρώστε για να μπείτε στην ιδιωτική συνομιλία",
|
qrTitle: "Σαρώστε για να μπείτε στην ιδιωτική συνομιλία",
|
||||||
qrHint: "Τα μέλη του crew το σαρώνουν με την κάμερα του κινητού. Ο σύνδεσμος ανοίγει την καρτέλα Ιδιωτική συνομιλία.",
|
qrHint: "Τα μέλη του crew το σαρώνουν με την κάμερα του κινητού. Ο σύνδεσμος ανοίγει την καρτέλα Ιδιωτική συνομιλία.",
|
||||||
sendFailed: "Δεν στάλθηκε — πατήστε Επανάληψη.",
|
sendFailed: "Δεν στάλθηκε — πατήστε Επανάληψη.",
|
||||||
|
queued: "Σε ουρά σε αυτή τη συσκευή",
|
||||||
sending: "Αποστολή…",
|
sending: "Αποστολή…",
|
||||||
sent: "Στάλθηκε",
|
sent: "Στάλθηκε",
|
||||||
retry: "Επανάληψη",
|
retry: "Επανάληψη",
|
||||||
|
|
@ -413,7 +419,7 @@ const drift = {
|
||||||
noInvite: "Δεν υπάρχει ακόμη πρόσκληση. Το bot εγκρίνει συνήθως μέσα σε ένα λεπτό — μετά πάτησε «Έλεγχος πρόσκλησης».",
|
noInvite: "Δεν υπάρχει ακόμη πρόσκληση. Το bot εγκρίνει συνήθως μέσα σε ένα λεπτό — μετά πάτησε «Έλεγχος πρόσκλησης».",
|
||||||
pendingRequests: "{{count}} αιτήματα συμμετοχής σε αναμονή",
|
pendingRequests: "{{count}} αιτήματα συμμετοχής σε αναμονή",
|
||||||
pendingRequestsDesc: "Το bot εγκρίνει συνήθως αυτόματα. Αν έχει κολλήσει, άνοιξε το δωμάτιο παρακάτω και πάτησε Έλεγχος αιτημάτων.",
|
pendingRequestsDesc: "Το bot εγκρίνει συνήθως αυτόματα. Αν έχει κολλήσει, άνοιξε το δωμάτιο παρακάτω και πάτησε Έλεγχος αιτημάτων.",
|
||||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
botMissing: "Το bot της κοινότητας δεν είναι αυτή τη στιγμή σε αυτό το δωμάτιο, οπότε ένας διαχειριστής θα εξετάσει τα αιτήματα συμμετοχής.",
|
||||||
secondDeviceHint: "Είσαι ήδη μέλος σε άλλη συσκευή; Χρησιμοποίησε «Σύνδεση αυτής της συσκευής» για άμεση μεταβίβαση ή ζήτησε ξανά να συμμετάσχεις με αυτή τη συσκευή.",
|
secondDeviceHint: "Είσαι ήδη μέλος σε άλλη συσκευή; Χρησιμοποίησε «Σύνδεση αυτής της συσκευής» για άμεση μεταβίβαση ή ζήτησε ξανά να συμμετάσχεις με αυτή τη συσκευή.",
|
||||||
create: "Δημιουργία δωματίου κοινότητας",
|
create: "Δημιουργία δωματίου κοινότητας",
|
||||||
setupDeviceBody: "Αυτό δίνει σε αυτή τη συσκευή ένα κλειδί για το δωμάτιο της κοινότητας. Ένας διαχειριστής ή το bot το εγκρίνει μία φορά.",
|
setupDeviceBody: "Αυτό δίνει σε αυτή τη συσκευή ένα κλειδί για το δωμάτιο της κοινότητας. Ένας διαχειριστής ή το bot το εγκρίνει μία φορά.",
|
||||||
|
|
|
||||||
|
|
@ -4090,6 +4090,7 @@ socialUi: {
|
||||||
newMessage: 'New Message',
|
newMessage: 'New Message',
|
||||||
groups: 'Groups',
|
groups: 'Groups',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
|
privateRooms: 'Private rooms',
|
||||||
crews: 'Crews',
|
crews: 'Crews',
|
||||||
crewFounder: 'Founder',
|
crewFounder: 'Founder',
|
||||||
crewMember: 'Member',
|
crewMember: 'Member',
|
||||||
|
|
|
||||||
|
|
@ -4090,6 +4090,7 @@ socialUi: {
|
||||||
newMessage: 'Nuevo mensaje',
|
newMessage: 'Nuevo mensaje',
|
||||||
groups: 'Grupos',
|
groups: 'Grupos',
|
||||||
refresh: 'Actualizar',
|
refresh: 'Actualizar',
|
||||||
|
privateRooms: 'Salas privadas',
|
||||||
crews: 'Peñas',
|
crews: 'Peñas',
|
||||||
crewFounder: 'Fundador',
|
crewFounder: 'Fundador',
|
||||||
crewMember: 'Miembro',
|
crewMember: 'Miembro',
|
||||||
|
|
|
||||||
|
|
@ -918,10 +918,10 @@ const batch1 = {
|
||||||
noGroupMember: "Oraindik ez zaude talde honen txat pribatuan.",
|
noGroupMember: "Oraindik ez zaude talde honen txat pribatuan.",
|
||||||
create: "Sortu txat pribatua",
|
create: "Sortu txat pribatua",
|
||||||
requestJoin: "Eskatu sartzea",
|
requestJoin: "Eskatu sartzea",
|
||||||
requestAgain: "Request again",
|
requestAgain: "Eskatu berriro",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Eskaera kudeatuta",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Kopiatu Cordn esteka",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web blokeatu egin daiteke saioa hasi ondoren; app barruan bat egitea fidagarriagoa da.",
|
||||||
checkInvite: "Egiaztatu gonbidapena",
|
checkInvite: "Egiaztatu gonbidapena",
|
||||||
requested: "Eskaera bidali da — taldeko administratzaile batek onartuko du.",
|
requested: "Eskaera bidali da — taldeko administratzaile batek onartuko du.",
|
||||||
noInvite: "Oraindik ez dago gonbidapenik zain. Eskatu administratzaile bati zure eskaera onartzeko.",
|
noInvite: "Oraindik ez dago gonbidapenik zain. Eskatu administratzaile bati zure eskaera onartzeko.",
|
||||||
|
|
@ -964,7 +964,11 @@ const batch1 = {
|
||||||
onboardingMember3: "Barruan zaude — mezuak zifratuta daude.",
|
onboardingMember3: "Barruan zaude — mezuak zifratuta daude.",
|
||||||
encryptedNote: "Taldeko kideek soilik irakur dezakete hau; koordinatzaileak mezu zifratuak soilik birbidaltzen ditu.",
|
encryptedNote: "Taldeko kideek soilik irakur dezakete hau; koordinatzaileak mezu zifratuak soilik birbidaltzen ditu.",
|
||||||
advanced: "Aurreratua",
|
advanced: "Aurreratua",
|
||||||
|
compareTitle: "Txata vs txat pribatua",
|
||||||
|
compareChat: "Txata NIP-17 talde-DM estandar bat da. Beste Nostr bezeroekin funtzionatzen du eta ez du koordinatzailerik.",
|
||||||
|
comparePrivate: "Txat pribatuak MLS erabiltzen du: kideen kontrola sendoagoa eta aurreranzko sekretua. Koordinatzaileak taldearen metadatuak eta testu zifratua ikusten ditu, inoiz ez mezuen testua.",
|
||||||
coordinator: "Koordinatzailea",
|
coordinator: "Koordinatzailea",
|
||||||
|
lastChecked: "Azken egiaztapena",
|
||||||
keyPackage: "Gako-paketea",
|
keyPackage: "Gako-paketea",
|
||||||
keyPackageReady: "Prest gailu honetan",
|
keyPackageReady: "Prest gailu honetan",
|
||||||
keyPackageMissing: "Ez dago konfiguratuta gailu honetan",
|
keyPackageMissing: "Ez dago konfiguratuta gailu honetan",
|
||||||
|
|
@ -988,6 +992,7 @@ const batch1 = {
|
||||||
qrTitle: "Eskaneatu txat pribatura sartzeko",
|
qrTitle: "Eskaneatu txat pribatura sartzeko",
|
||||||
qrHint: "Taldeko kideek telefonoaren kamerarekin eskaneatzen dute. Estekak Txat pribatua fitxa irekitzen du.",
|
qrHint: "Taldeko kideek telefonoaren kamerarekin eskaneatzen dute. Estekak Txat pribatua fitxa irekitzen du.",
|
||||||
sendFailed: "Ezin izan da bidali — sakatu Berriro saiatu.",
|
sendFailed: "Ezin izan da bidali — sakatu Berriro saiatu.",
|
||||||
|
queued: "Gailu honetan ilaran",
|
||||||
sending: "Bidaltzen…",
|
sending: "Bidaltzen…",
|
||||||
sent: "Bidalita",
|
sent: "Bidalita",
|
||||||
retry: "Berriro saiatu",
|
retry: "Berriro saiatu",
|
||||||
|
|
@ -1023,7 +1028,7 @@ const batch1 = {
|
||||||
noInvite: "Oraindik ez dago gonbidapenik. Botak normalean minutu batean onartzen du — gero sakatu «Egiaztatu gonbidapena».",
|
noInvite: "Oraindik ez dago gonbidapenik. Botak normalean minutu batean onartzen du — gero sakatu «Egiaztatu gonbidapena».",
|
||||||
pendingRequests: "{{count}} sartzeko eskaera zain",
|
pendingRequests: "{{count}} sartzeko eskaera zain",
|
||||||
pendingRequestsDesc: "Botak normalean automatikoki onartzen du. Blokeatuta badago, ireki beheko gela eta sakatu Egiaztatu eskaerak.",
|
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.",
|
botMissing: "Komunitatearen bot-a ez dago gela honetan oraintxe, beraz administratzaile batek berrikusiko ditu bat egiteko eskaerak.",
|
||||||
secondDeviceHint: "Beste gailu batean kide zara jada? Erabili «Estekatu gailu hau» zuzeneko transferentzia egiteko, edo eskatu berriro gailu honekin bat egiteko.",
|
secondDeviceHint: "Beste gailu batean kide zara jada? Erabili «Estekatu gailu hau» zuzeneko transferentzia egiteko, edo eskatu berriro gailu honekin bat egiteko.",
|
||||||
create: "Sortu komunitate gela",
|
create: "Sortu komunitate gela",
|
||||||
setupDeviceBody: "Honek gailu honi komunitate gelarako gako bat ematen dio. Administratzaile batek edo botak behin onartzen du.",
|
setupDeviceBody: "Honek gailu honi komunitate gelarako gako bat ematen dio. Administratzaile batek edo botak behin onartzen du.",
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,7 @@ const batch2 = {
|
||||||
newMessage: "Mezu berria",
|
newMessage: "Mezu berria",
|
||||||
groups: "Taldeak",
|
groups: "Taldeak",
|
||||||
refresh: "Freskatu",
|
refresh: "Freskatu",
|
||||||
|
privateRooms: "Gela pribatuak",
|
||||||
crews: "Koadrilak",
|
crews: "Koadrilak",
|
||||||
crewFounder: "Sortzailea",
|
crewFounder: "Sortzailea",
|
||||||
crewMember: "Kidea",
|
crewMember: "Kidea",
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: "Actualiser",
|
refresh: "Actualiser",
|
||||||
|
privateRooms: "Salons privés",
|
||||||
crews: "Crews",
|
crews: "Crews",
|
||||||
crewFounder: "Fondateur",
|
crewFounder: "Fondateur",
|
||||||
crewMember: "Membre",
|
crewMember: "Membre",
|
||||||
|
|
@ -309,10 +310,10 @@ const drift = {
|
||||||
noGroupMember: "Vous n'êtes pas encore dans le chat privé de cette Crew.",
|
noGroupMember: "Vous n'êtes pas encore dans le chat privé de cette Crew.",
|
||||||
create: "Créer le chat privé",
|
create: "Créer le chat privé",
|
||||||
requestJoin: "Demander à rejoindre",
|
requestJoin: "Demander à rejoindre",
|
||||||
requestAgain: "Request again",
|
requestAgain: "Redemander",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Demande traitée",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Copier le lien Cordn",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web peut se bloquer après la connexion ; rejoindre depuis l’app est plus fiable.",
|
||||||
checkInvite: "Vérifier l'invitation",
|
checkInvite: "Vérifier l'invitation",
|
||||||
requested: "Demande envoyée — un admin de la Crew l'approuvera.",
|
requested: "Demande envoyée — un admin de la Crew l'approuvera.",
|
||||||
noInvite: "Aucune invitation en attente. Demandez à un admin d'approuver votre demande.",
|
noInvite: "Aucune invitation en attente. Demandez à un admin d'approuver votre demande.",
|
||||||
|
|
@ -352,7 +353,11 @@ const drift = {
|
||||||
onboardingMember3: "Vous y êtes — les messages sont chiffrés.",
|
onboardingMember3: "Vous y êtes — les messages sont chiffrés.",
|
||||||
encryptedNote: "Seuls les membres de la Crew peuvent le lire ; le coordinateur ne relaie que des messages chiffrés.",
|
encryptedNote: "Seuls les membres de la Crew peuvent le lire ; le coordinateur ne relaie que des messages chiffrés.",
|
||||||
advanced: "Avancé",
|
advanced: "Avancé",
|
||||||
|
compareTitle: "Chat vs chat privé",
|
||||||
|
compareChat: "Le chat est un DM de groupe NIP-17 standard. Il fonctionne avec d'autres clients Nostr et n'a pas de coordinateur.",
|
||||||
|
comparePrivate: "Le chat privé utilise MLS : un contrôle des membres plus fort et une confidentialité persistante. Le coordinateur voit les métadonnées du groupe et le texte chiffré, jamais le contenu des messages.",
|
||||||
coordinator: "Coordinateur",
|
coordinator: "Coordinateur",
|
||||||
|
lastChecked: "Dernière vérification",
|
||||||
keyPackage: "Paquet de clés",
|
keyPackage: "Paquet de clés",
|
||||||
keyPackageReady: "Prêt sur cet appareil",
|
keyPackageReady: "Prêt sur cet appareil",
|
||||||
keyPackageMissing: "Non configuré sur cet appareil",
|
keyPackageMissing: "Non configuré sur cet appareil",
|
||||||
|
|
@ -376,6 +381,7 @@ const drift = {
|
||||||
qrTitle: "Scannez pour rejoindre le chat privé",
|
qrTitle: "Scannez pour rejoindre le chat privé",
|
||||||
qrHint: "Les membres de la Crew le scannent avec l’appareil photo de leur téléphone. Le lien ouvre l’onglet Chat privé.",
|
qrHint: "Les membres de la Crew le scannent avec l’appareil photo de leur téléphone. Le lien ouvre l’onglet Chat privé.",
|
||||||
sendFailed: "Échec de l’envoi — touchez Réessayer.",
|
sendFailed: "Échec de l’envoi — touchez Réessayer.",
|
||||||
|
queued: "En file sur cet appareil",
|
||||||
sending: "Envoi…",
|
sending: "Envoi…",
|
||||||
sent: "Envoyé",
|
sent: "Envoyé",
|
||||||
retry: "Réessayer",
|
retry: "Réessayer",
|
||||||
|
|
@ -411,7 +417,7 @@ const drift = {
|
||||||
noInvite: "Pas encore d’invitation. Le bot approuve généralement en moins d’une minute — appuie ensuite sur « Vérifier l’invitation ».",
|
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",
|
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.",
|
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.",
|
botMissing: "Le bot de la communauté n’est pas dans ce salon pour le moment ; un admin examinera les demandes d’adhésion.",
|
||||||
secondDeviceHint: "Déjà membre sur un autre appareil ? Utilise « Lier cet appareil » pour un transfert direct, ou redemande à rejoindre avec cet appareil.",
|
secondDeviceHint: "Déjà membre sur un autre appareil ? Utilise « Lier cet appareil » pour un transfert direct, ou redemande à rejoindre avec cet appareil.",
|
||||||
create: "Créer le salon de la communauté",
|
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.",
|
setupDeviceBody: "Cela donne à cet appareil une clé pour le salon de la communauté. Un admin ou le bot l’approuve une fois.",
|
||||||
|
|
|
||||||
|
|
@ -918,10 +918,10 @@ const batch1 = {
|
||||||
noGroupMember: "Aínda non estás no chat privado desta crew.",
|
noGroupMember: "Aínda non estás no chat privado desta crew.",
|
||||||
create: "Crear o chat privado",
|
create: "Crear o chat privado",
|
||||||
requestJoin: "Pedir acceso",
|
requestJoin: "Pedir acceso",
|
||||||
requestAgain: "Request again",
|
requestAgain: "Solicitar de novo",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Solicitude xestionada",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Copiar a ligazón de Cordn",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web pode bloquearse despois de iniciar sesión; unirse desde a app é máis fiable.",
|
||||||
checkInvite: "Comprobar se hai invitación",
|
checkInvite: "Comprobar se hai invitación",
|
||||||
requested: "Solicitude enviada: un administrador da crew aprobaraa.",
|
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.",
|
noInvite: "Aínda non hai ningunha invitación. Pídele a un administrador que aprobe a túa solicitude.",
|
||||||
|
|
@ -964,7 +964,11 @@ const batch1 = {
|
||||||
onboardingMember3: "Xa estás dentro: as mensaxes van cifradas.",
|
onboardingMember3: "Xa estás dentro: as mensaxes van cifradas.",
|
||||||
encryptedNote: "Só os membros da crew poden ler isto; o coordinador só retransmite mensaxes cifradas.",
|
encryptedNote: "Só os membros da crew poden ler isto; o coordinador só retransmite mensaxes cifradas.",
|
||||||
advanced: "Avanzado",
|
advanced: "Avanzado",
|
||||||
|
compareTitle: "Chat vs chat privado",
|
||||||
|
compareChat: "O chat é un DM de grupo NIP-17 estándar. Funciona con outros clientes Nostr e non ten coordinador.",
|
||||||
|
comparePrivate: "O chat privado usa MLS: control de membros máis forte e segredo cara adiante. O coordinador ve os metadatos do grupo e o texto cifrado, nunca o texto das mensaxes.",
|
||||||
coordinator: "Coordinador",
|
coordinator: "Coordinador",
|
||||||
|
lastChecked: "Última comprobación",
|
||||||
keyPackage: "Paquete de chaves",
|
keyPackage: "Paquete de chaves",
|
||||||
keyPackageReady: "Listo neste dispositivo",
|
keyPackageReady: "Listo neste dispositivo",
|
||||||
keyPackageMissing: "Sen configurar neste dispositivo",
|
keyPackageMissing: "Sen configurar neste dispositivo",
|
||||||
|
|
@ -988,6 +992,7 @@ const batch1 = {
|
||||||
qrTitle: "Escanea para entrar no chat privado",
|
qrTitle: "Escanea para entrar no chat privado",
|
||||||
qrHint: "Os membros da crew escanéano coa cámara do móbil. A ligazón abre a pestana Chat privado.",
|
qrHint: "Os membros da crew escanéano coa cámara do móbil. A ligazón abre a pestana Chat privado.",
|
||||||
sendFailed: "Non se puido enviar: toca Reintentar.",
|
sendFailed: "Non se puido enviar: toca Reintentar.",
|
||||||
|
queued: "En cola neste dispositivo",
|
||||||
sending: "Enviando…",
|
sending: "Enviando…",
|
||||||
sent: "Enviado",
|
sent: "Enviado",
|
||||||
retry: "Reintentar",
|
retry: "Reintentar",
|
||||||
|
|
@ -1023,7 +1028,7 @@ const batch1 = {
|
||||||
noInvite: "Aínda non hai invitación. O bot adoita aprobar nun minuto; despois toca «Comprobar se hai invitación».",
|
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",
|
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.",
|
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.",
|
botMissing: "O bot da comunidade non está nesta sala agora mesmo, así que un administrador revisará as solicitudes de unión.",
|
||||||
secondDeviceHint: "Xa es membro noutro dispositivo? Usa «Vincular este dispositivo» para un traspaso directo, ou pide entrar de novo con este dispositivo.",
|
secondDeviceHint: "Xa es membro noutro dispositivo? Usa «Vincular este dispositivo» para un traspaso directo, ou pide entrar de novo con este dispositivo.",
|
||||||
create: "Crear a sala da comunidade",
|
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.",
|
setupDeviceBody: "Isto dálle a este dispositivo unha chave para a sala da comunidade. Un administrador ou o bot apruxaa unha vez.",
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,7 @@ const batch2 = {
|
||||||
newMessage: "Nova mensaxe",
|
newMessage: "Nova mensaxe",
|
||||||
groups: "Grupos",
|
groups: "Grupos",
|
||||||
refresh: "Actualizar",
|
refresh: "Actualizar",
|
||||||
|
privateRooms: "Salas privadas",
|
||||||
crews: "Crews",
|
crews: "Crews",
|
||||||
crewFounder: "Fundador",
|
crewFounder: "Fundador",
|
||||||
crewMember: "Membro",
|
crewMember: "Membro",
|
||||||
|
|
|
||||||
|
|
@ -918,10 +918,10 @@ const batch1 = {
|
||||||
"noGroupMember": "आप अभी इस क्रू की निजी चैट में नहीं हैं।",
|
"noGroupMember": "आप अभी इस क्रू की निजी चैट में नहीं हैं।",
|
||||||
"create": "निजी चैट बनाएँ",
|
"create": "निजी चैट बनाएँ",
|
||||||
"requestJoin": "शामिल होने का अनुरोध करें",
|
"requestJoin": "शामिल होने का अनुरोध करें",
|
||||||
"requestAgain": "Request again",
|
"requestAgain": "फिर से अनुरोध करें",
|
||||||
"requestHandled": "Request handled",
|
"requestHandled": "अनुरोध संभाला गया",
|
||||||
"copyCordnLink": "Copy Cordn link",
|
"copyCordnLink": "Cordn लिंक कॉपी करें",
|
||||||
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
"cordnWebCaveat": "Cordn वेब साइन-इन के बाद अटक सकता है; ऐप में शामिल होना ज़्यादा भरोसेमंद है।",
|
||||||
"checkInvite": "निमंत्रण जाँचें",
|
"checkInvite": "निमंत्रण जाँचें",
|
||||||
"requested": "अनुरोध भेजा गया — क्रू एडमिन इसे स्वीकृत करेगा।",
|
"requested": "अनुरोध भेजा गया — क्रू एडमिन इसे स्वीकृत करेगा।",
|
||||||
"noInvite": "अभी कोई निमंत्रण प्रतीक्षा में नहीं। एडमिन से अपना अनुरोध स्वीकृत करने को कहें।",
|
"noInvite": "अभी कोई निमंत्रण प्रतीक्षा में नहीं। एडमिन से अपना अनुरोध स्वीकृत करने को कहें।",
|
||||||
|
|
@ -964,7 +964,11 @@ const batch1 = {
|
||||||
onboardingMember3: "आप अंदर हैं — संदेश एन्क्रिप्टेड हैं।",
|
onboardingMember3: "आप अंदर हैं — संदेश एन्क्रिप्टेड हैं।",
|
||||||
encryptedNote: "इसे केवल क्रू सदस्य ही पढ़ सकते हैं; कोऑर्डिनेटर केवल एन्क्रिप्टेड संदेश आगे भेजता है।",
|
encryptedNote: "इसे केवल क्रू सदस्य ही पढ़ सकते हैं; कोऑर्डिनेटर केवल एन्क्रिप्टेड संदेश आगे भेजता है।",
|
||||||
advanced: "उन्नत",
|
advanced: "उन्नत",
|
||||||
|
compareTitle: "चैट बनाम निजी चैट",
|
||||||
|
compareChat: "चैट एक मानक NIP-17 समूह DM है। यह अन्य Nostr क्लाइंट के साथ काम करता है और इसमें कोई समन्वयक नहीं होता।",
|
||||||
|
comparePrivate: "निजी चैट MLS का उपयोग करता है: सदस्यों पर मज़बूत नियंत्रण और फ़ॉरवर्ड गोपनीयता। समन्वयक समूह मेटाडेटा और सिफ़रटेक्स्ट देखता है, संदेश का पाठ कभी नहीं।",
|
||||||
coordinator: "कोऑर्डिनेटर",
|
coordinator: "कोऑर्डिनेटर",
|
||||||
|
lastChecked: "अंतिम जाँच",
|
||||||
keyPackage: "कुंजी पैकेज",
|
keyPackage: "कुंजी पैकेज",
|
||||||
keyPackageReady: "इस डिवाइस पर तैयार",
|
keyPackageReady: "इस डिवाइस पर तैयार",
|
||||||
keyPackageMissing: "इस डिवाइस पर सेट नहीं है",
|
keyPackageMissing: "इस डिवाइस पर सेट नहीं है",
|
||||||
|
|
@ -988,6 +992,7 @@ const batch1 = {
|
||||||
qrTitle: "निजी चैट में शामिल होने के लिए स्कैन करें",
|
qrTitle: "निजी चैट में शामिल होने के लिए स्कैन करें",
|
||||||
qrHint: "क्रू सदस्य इसे फ़ोन कैमरे से स्कैन करते हैं। लिंक निजी चैट टैब खोलता है।",
|
qrHint: "क्रू सदस्य इसे फ़ोन कैमरे से स्कैन करते हैं। लिंक निजी चैट टैब खोलता है।",
|
||||||
sendFailed: "भेजा नहीं जा सका — रीट्राई टैप करें।",
|
sendFailed: "भेजा नहीं जा सका — रीट्राई टैप करें।",
|
||||||
|
queued: "इस डिवाइस पर कतार में",
|
||||||
sending: "भेजा जा रहा है…",
|
sending: "भेजा जा रहा है…",
|
||||||
sent: "भेज दिया",
|
sent: "भेज दिया",
|
||||||
retry: "रीट्राई",
|
retry: "रीट्राई",
|
||||||
|
|
@ -1023,7 +1028,7 @@ const batch1 = {
|
||||||
"noInvite": "अभी कोई निमंत्रण नहीं। बॉट आमतौर पर एक मिनट में मंज़ूरी दे देता है — उसके बाद “निमंत्रण जाँचें” टैप करें।",
|
"noInvite": "अभी कोई निमंत्रण नहीं। बॉट आमतौर पर एक मिनट में मंज़ूरी दे देता है — उसके बाद “निमंत्रण जाँचें” टैप करें।",
|
||||||
"pendingRequests": "{{count}} शामिल होने के अनुरोध इंतज़ार में",
|
"pendingRequests": "{{count}} शामिल होने के अनुरोध इंतज़ार में",
|
||||||
"pendingRequestsDesc": "बॉट आमतौर पर अपने आप मंज़ूरी देता है। अगर अटका हुआ है, तो नीचे रूम खोलें और “अनुरोध जाँचें” टैप करें।",
|
"pendingRequestsDesc": "बॉट आमतौर पर अपने आप मंज़ूरी देता है। अगर अटका हुआ है, तो नीचे रूम खोलें और “अनुरोध जाँचें” टैप करें।",
|
||||||
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
|
"botMissing": "कम्युनिटी बॉट अभी इस रूम में नहीं है, इसलिए एक एडमिन शामिल होने के अनुरोधों की समीक्षा करेगा।",
|
||||||
"secondDeviceHint": "क्या आप पहले से किसी दूसरे डिवाइस पर सदस्य हैं? सीधे हैंडऑफ़ के लिए “इस डिवाइस को लिंक करें” का उपयोग करें, या इस डिवाइस से दोबारा जुड़ने का अनुरोध करें।",
|
"secondDeviceHint": "क्या आप पहले से किसी दूसरे डिवाइस पर सदस्य हैं? सीधे हैंडऑफ़ के लिए “इस डिवाइस को लिंक करें” का उपयोग करें, या इस डिवाइस से दोबारा जुड़ने का अनुरोध करें।",
|
||||||
"create": "कम्युनिटी रूम बनाएँ",
|
"create": "कम्युनिटी रूम बनाएँ",
|
||||||
"setupDeviceBody": "इससे इस डिवाइस को कम्युनिटी रूम की एक कुंजी मिलती है। कोई एडमिन या बॉट इसे एक बार मंज़ूर करता है।",
|
"setupDeviceBody": "इससे इस डिवाइस को कम्युनिटी रूम की एक कुंजी मिलती है। कोई एडमिन या बॉट इसे एक बार मंज़ूर करता है।",
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,7 @@ const batch2 = {
|
||||||
"newMessage": "नया संदेश",
|
"newMessage": "नया संदेश",
|
||||||
"groups": "ग्रुप",
|
"groups": "ग्रुप",
|
||||||
"refresh": "रीफ़्रेश",
|
"refresh": "रीफ़्रेश",
|
||||||
|
"privateRooms": "निजी कमरे",
|
||||||
"crews": "क्रू",
|
"crews": "क्रू",
|
||||||
"crewFounder": "संस्थापक",
|
"crewFounder": "संस्थापक",
|
||||||
"crewMember": "सदस्य",
|
"crewMember": "सदस्य",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ const driftStrings = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: 'Muat ulang',
|
refresh: 'Muat ulang',
|
||||||
|
privateRooms: 'Ruang privat',
|
||||||
crews: 'Crew',
|
crews: 'Crew',
|
||||||
crewFounder: 'Pendiri',
|
crewFounder: 'Pendiri',
|
||||||
crewMember: 'Anggota',
|
crewMember: 'Anggota',
|
||||||
|
|
@ -303,10 +304,10 @@ const driftStrings = {
|
||||||
noGroupMember: 'Kamu belum ada di chat privat crew ini.',
|
noGroupMember: 'Kamu belum ada di chat privat crew ini.',
|
||||||
create: 'Buat chat privat',
|
create: 'Buat chat privat',
|
||||||
requestJoin: 'Minta gabung',
|
requestJoin: 'Minta gabung',
|
||||||
requestAgain: "Request again",
|
requestAgain: "Minta lagi",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Permintaan ditangani",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Salin tautan Cordn",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web bisa macet setelah masuk; bergabung lewat aplikasi lebih andal.",
|
||||||
checkInvite: 'Periksa undangan',
|
checkInvite: 'Periksa undangan',
|
||||||
requested: 'Permintaan terkirim — admin crew akan menyetujuinya.',
|
requested: 'Permintaan terkirim — admin crew akan menyetujuinya.',
|
||||||
noInvite: 'Belum ada undangan menunggu. Minta admin menyetujui permintaanmu.',
|
noInvite: 'Belum ada undangan menunggu. Minta admin menyetujui permintaanmu.',
|
||||||
|
|
@ -346,7 +347,11 @@ const driftStrings = {
|
||||||
onboardingMember3: "Kamu sudah masuk — pesan terenkripsi.",
|
onboardingMember3: "Kamu sudah masuk — pesan terenkripsi.",
|
||||||
encryptedNote: "Hanya anggota crew yang bisa membacanya; koordinator hanya meneruskan pesan terenkripsi.",
|
encryptedNote: "Hanya anggota crew yang bisa membacanya; koordinator hanya meneruskan pesan terenkripsi.",
|
||||||
advanced: "Lanjutan",
|
advanced: "Lanjutan",
|
||||||
|
compareTitle: "Chat vs chat privat",
|
||||||
|
compareChat: "Chat adalah DM grup NIP-17 standar. Bekerja dengan klien Nostr lain dan tidak memiliki koordinator.",
|
||||||
|
comparePrivate: "Chat privat menggunakan MLS: kontrol keanggotaan lebih kuat dan kerahasiaan ke depan. Koordinator melihat metadata grup dan ciphertext, bukan teks pesan.",
|
||||||
coordinator: "Koordinator",
|
coordinator: "Koordinator",
|
||||||
|
lastChecked: "Terakhir diperiksa",
|
||||||
keyPackage: "Paket kunci",
|
keyPackage: "Paket kunci",
|
||||||
keyPackageReady: "Siap di perangkat ini",
|
keyPackageReady: "Siap di perangkat ini",
|
||||||
keyPackageMissing: "Belum disiapkan di perangkat ini",
|
keyPackageMissing: "Belum disiapkan di perangkat ini",
|
||||||
|
|
@ -370,6 +375,7 @@ const driftStrings = {
|
||||||
qrTitle: "Pindai untuk bergabung ke chat privat",
|
qrTitle: "Pindai untuk bergabung ke chat privat",
|
||||||
qrHint: "Anggota crew memindai ini dengan kamera ponsel. Tautan membuka tab Chat privat.",
|
qrHint: "Anggota crew memindai ini dengan kamera ponsel. Tautan membuka tab Chat privat.",
|
||||||
sendFailed: "Gagal mengirim — ketuk Coba lagi.",
|
sendFailed: "Gagal mengirim — ketuk Coba lagi.",
|
||||||
|
queued: "Dalam antrean di perangkat ini",
|
||||||
sending: "Mengirim…",
|
sending: "Mengirim…",
|
||||||
sent: "Terkirim",
|
sent: "Terkirim",
|
||||||
retry: "Coba lagi",
|
retry: "Coba lagi",
|
||||||
|
|
@ -405,7 +411,7 @@ const driftStrings = {
|
||||||
noInvite: "Belum ada undangan. Bot biasanya menyetujui dalam satu menit — ketuk “Periksa undangan” setelahnya.",
|
noInvite: "Belum ada undangan. Bot biasanya menyetujui dalam satu menit — ketuk “Periksa undangan” setelahnya.",
|
||||||
pendingRequests: "{{count}} permintaan bergabung menunggu",
|
pendingRequests: "{{count}} permintaan bergabung menunggu",
|
||||||
pendingRequestsDesc: "Bot biasanya menyetujui secara otomatis. Jika macet, buka ruang di bawah dan ketuk Periksa permintaan.",
|
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.",
|
botMissing: "Bot komunitas sedang tidak ada di ruangan ini, jadi admin akan meninjau permintaan bergabung.",
|
||||||
secondDeviceHint: "Sudah menjadi anggota di perangkat lain? Gunakan “Tautkan perangkat ini” untuk serah terima langsung, atau minta bergabung lagi dengan perangkat ini.",
|
secondDeviceHint: "Sudah menjadi anggota di perangkat lain? Gunakan “Tautkan perangkat ini” untuk serah terima langsung, atau minta bergabung lagi dengan perangkat ini.",
|
||||||
create: "Buat ruang komunitas",
|
create: "Buat ruang komunitas",
|
||||||
setupDeviceBody: "Ini memberi perangkat ini kunci untuk ruang komunitas. Admin atau bot menyetujuinya satu kali.",
|
setupDeviceBody: "Ini memberi perangkat ini kunci untuk ruang komunitas. Admin atau bot menyetujuinya satu kali.",
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: "Aggiorna",
|
refresh: "Aggiorna",
|
||||||
|
privateRooms: "Stanze private",
|
||||||
crews: "Crew",
|
crews: "Crew",
|
||||||
crewFounder: "Fondatore",
|
crewFounder: "Fondatore",
|
||||||
crewMember: "Membro",
|
crewMember: "Membro",
|
||||||
|
|
@ -309,10 +310,10 @@ const drift = {
|
||||||
noGroupMember: "Non sei ancora nella chat privata di questa crew.",
|
noGroupMember: "Non sei ancora nella chat privata di questa crew.",
|
||||||
create: "Crea la chat privata",
|
create: "Crea la chat privata",
|
||||||
requestJoin: "Chiedi di unirti",
|
requestJoin: "Chiedi di unirti",
|
||||||
requestAgain: "Request again",
|
requestAgain: "Richiedi di nuovo",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Richiesta gestita",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Copia link Cordn",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web può bloccarsi dopo l’accesso; l’adesione dall’app è più affidabile.",
|
||||||
checkInvite: "Controlla l'invito",
|
checkInvite: "Controlla l'invito",
|
||||||
requested: "Richiesta inviata — un admin della crew la approverà.",
|
requested: "Richiesta inviata — un admin della crew la approverà.",
|
||||||
noInvite: "Nessun invito in attesa. Chiedi a un admin di approvare la tua richiesta.",
|
noInvite: "Nessun invito in attesa. Chiedi a un admin di approvare la tua richiesta.",
|
||||||
|
|
@ -352,7 +353,11 @@ const drift = {
|
||||||
onboardingMember3: "Sei dentro — i messaggi sono cifrati.",
|
onboardingMember3: "Sei dentro — i messaggi sono cifrati.",
|
||||||
encryptedNote: "Solo i membri della crew possono leggerlo; il coordinatore inoltra solo messaggi cifrati.",
|
encryptedNote: "Solo i membri della crew possono leggerlo; il coordinatore inoltra solo messaggi cifrati.",
|
||||||
advanced: "Avanzate",
|
advanced: "Avanzate",
|
||||||
|
compareTitle: "Chat vs chat privata",
|
||||||
|
compareChat: "La chat è un DM di gruppo NIP-17 standard. Funziona con altri client Nostr e non ha un coordinatore.",
|
||||||
|
comparePrivate: "La chat privata usa MLS: controllo dei membri più forte e segretezza in avanti. Il coordinatore vede i metadati del gruppo e il testo cifrato, mai il testo dei messaggi.",
|
||||||
coordinator: "Coordinatore",
|
coordinator: "Coordinatore",
|
||||||
|
lastChecked: "Ultimo controllo",
|
||||||
keyPackage: "Pacchetto di chiavi",
|
keyPackage: "Pacchetto di chiavi",
|
||||||
keyPackageReady: "Pronto su questo dispositivo",
|
keyPackageReady: "Pronto su questo dispositivo",
|
||||||
keyPackageMissing: "Non configurato su questo dispositivo",
|
keyPackageMissing: "Non configurato su questo dispositivo",
|
||||||
|
|
@ -376,6 +381,7 @@ const drift = {
|
||||||
qrTitle: "Scansiona per unirti alla chat privata",
|
qrTitle: "Scansiona per unirti alla chat privata",
|
||||||
qrHint: "I membri della crew lo scansionano con la fotocamera del telefono. Il link apre la scheda Chat privata.",
|
qrHint: "I membri della crew lo scansionano con la fotocamera del telefono. Il link apre la scheda Chat privata.",
|
||||||
sendFailed: "Invio non riuscito — tocca Riprova.",
|
sendFailed: "Invio non riuscito — tocca Riprova.",
|
||||||
|
queued: "In coda su questo dispositivo",
|
||||||
sending: "Invio…",
|
sending: "Invio…",
|
||||||
sent: "Inviato",
|
sent: "Inviato",
|
||||||
retry: "Riprova",
|
retry: "Riprova",
|
||||||
|
|
@ -411,7 +417,7 @@ const drift = {
|
||||||
noInvite: "Nessun invito ancora. Il bot di solito approva entro un minuto — tocca «Controlla l’invito» dopo.",
|
noInvite: "Nessun invito ancora. Il bot di solito approva entro un minuto — tocca «Controlla l’invito» dopo.",
|
||||||
pendingRequests: "{{count}} richieste di adesione in attesa",
|
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.",
|
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.",
|
botMissing: "Il bot della community non è in questa stanza al momento, quindi un admin esaminerà le richieste di accesso.",
|
||||||
secondDeviceHint: "Sei già membro su un altro dispositivo? Usa «Collega questo dispositivo» per un passaggio diretto, oppure chiedi di nuovo di entrare con questo dispositivo.",
|
secondDeviceHint: "Sei già membro su un altro dispositivo? Usa «Collega questo dispositivo» per un passaggio diretto, oppure chiedi di nuovo di entrare con questo dispositivo.",
|
||||||
create: "Crea la stanza della community",
|
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.",
|
setupDeviceBody: "Questo assegna a questo dispositivo una chiave per la stanza della community. Un admin o il bot la approva una volta.",
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ const drift: Dict = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: '更新',
|
refresh: '更新',
|
||||||
|
privateRooms: 'プライベートルーム',
|
||||||
crews: 'クルー',
|
crews: 'クルー',
|
||||||
crewFounder: '創設者',
|
crewFounder: '創設者',
|
||||||
crewMember: 'メンバー',
|
crewMember: 'メンバー',
|
||||||
|
|
@ -319,10 +320,10 @@ const drift: Dict = {
|
||||||
noGroupMember: 'あなたはまだこのクルーのプライベートチャットに参加していません。',
|
noGroupMember: 'あなたはまだこのクルーのプライベートチャットに参加していません。',
|
||||||
create: 'プライベートチャットを作成',
|
create: 'プライベートチャットを作成',
|
||||||
requestJoin: '参加をリクエスト',
|
requestJoin: '参加をリクエスト',
|
||||||
requestAgain: "Request again",
|
requestAgain: "もう一度リクエスト",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "リクエストを処理しました",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Cordnリンクをコピー",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn webはサインイン後に止まることがあります。アプリ内からの参加がより確実です。",
|
||||||
checkInvite: '招待を確認',
|
checkInvite: '招待を確認',
|
||||||
requested: 'リクエストを送信しました — クルー管理者が承認します。',
|
requested: 'リクエストを送信しました — クルー管理者が承認します。',
|
||||||
noInvite: 'まだ招待はありません。管理者にリクエストの承認を依頼してください。',
|
noInvite: 'まだ招待はありません。管理者にリクエストの承認を依頼してください。',
|
||||||
|
|
@ -362,7 +363,11 @@ const drift: Dict = {
|
||||||
onboardingMember3: "参加しました — メッセージは暗号化されています。",
|
onboardingMember3: "参加しました — メッセージは暗号化されています。",
|
||||||
encryptedNote: "読めるのはクルーメンバーだけです。コーディネーターは暗号化されたメッセージを中継するだけです。",
|
encryptedNote: "読めるのはクルーメンバーだけです。コーディネーターは暗号化されたメッセージを中継するだけです。",
|
||||||
advanced: "詳細設定",
|
advanced: "詳細設定",
|
||||||
|
compareTitle: "チャットとプライベートチャット",
|
||||||
|
compareChat: "チャットは標準的な NIP-17 のグループDMです。他の Nostr クライアントとも動作し、コーディネーターは存在しません。",
|
||||||
|
comparePrivate: "プライベートチャットは MLS を使用し、メンバー管理がより強固で、前方秘匿性を備えています。コーディネーターはグループのメタデータと暗号文を目にしますが、メッセージ本文を見ることはありません。",
|
||||||
coordinator: "コーディネーター",
|
coordinator: "コーディネーター",
|
||||||
|
lastChecked: "最終確認",
|
||||||
keyPackage: "キーパッケージ",
|
keyPackage: "キーパッケージ",
|
||||||
keyPackageReady: "このデバイスで準備完了",
|
keyPackageReady: "このデバイスで準備完了",
|
||||||
keyPackageMissing: "このデバイスでは未設定",
|
keyPackageMissing: "このデバイスでは未設定",
|
||||||
|
|
@ -386,6 +391,7 @@ const drift: Dict = {
|
||||||
qrTitle: "スキャンしてプライベートチャットに参加",
|
qrTitle: "スキャンしてプライベートチャットに参加",
|
||||||
qrHint: "クルーメンバーがスマートフォンのカメラでスキャンします。リンクからプライベートチャットタブが開きます。",
|
qrHint: "クルーメンバーがスマートフォンのカメラでスキャンします。リンクからプライベートチャットタブが開きます。",
|
||||||
sendFailed: "送信できませんでした — 「再試行」をタップしてください。",
|
sendFailed: "送信できませんでした — 「再試行」をタップしてください。",
|
||||||
|
queued: "このデバイスで待機中",
|
||||||
sending: "送信中…",
|
sending: "送信中…",
|
||||||
sent: "送信済み",
|
sent: "送信済み",
|
||||||
retry: "再試行",
|
retry: "再試行",
|
||||||
|
|
@ -421,7 +427,7 @@ const drift: Dict = {
|
||||||
noInvite: "まだ招待がありません。ボットは通常1分以内に承認します。その後で「招待を確認」をタップしてください。",
|
noInvite: "まだ招待がありません。ボットは通常1分以内に承認します。その後で「招待を確認」をタップしてください。",
|
||||||
pendingRequests: "参加リクエスト {{count}} 件が待機中",
|
pendingRequests: "参加リクエスト {{count}} 件が待機中",
|
||||||
pendingRequestsDesc: "ボットは通常自動で承認します。止まっている場合は、下のルームを開いて「リクエストを確認」をタップしてください。",
|
pendingRequestsDesc: "ボットは通常自動で承認します。止まっている場合は、下のルームを開いて「リクエストを確認」をタップしてください。",
|
||||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
botMissing: "コミュニティボットが現在このルームにいないため、管理者が参加リクエストを確認します。",
|
||||||
secondDeviceHint: "別のデバイスで既にメンバーですか?直接引き継ぐには「このデバイスをリンク」を使うか、このデバイスで改めて参加をリクエストしてください。",
|
secondDeviceHint: "別のデバイスで既にメンバーですか?直接引き継ぐには「このデバイスをリンク」を使うか、このデバイスで改めて参加をリクエストしてください。",
|
||||||
create: "コミュニティルームを作成",
|
create: "コミュニティルームを作成",
|
||||||
setupDeviceBody: "このデバイスにコミュニティルームの鍵を付与します。管理者またはボットが一度だけ承認します。",
|
setupDeviceBody: "このデバイスにコミュニティルームの鍵を付与します。管理者またはボットが一度だけ承認します。",
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ const drift: Dict = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: '새로고침',
|
refresh: '새로고침',
|
||||||
|
privateRooms: '비공개 방',
|
||||||
crews: '크루',
|
crews: '크루',
|
||||||
crewFounder: '창립자',
|
crewFounder: '창립자',
|
||||||
crewMember: '멤버',
|
crewMember: '멤버',
|
||||||
|
|
@ -319,10 +320,10 @@ const drift: Dict = {
|
||||||
noGroupMember: '아직 이 크루의 비공개 채팅에 참여하지 않았습니다.',
|
noGroupMember: '아직 이 크루의 비공개 채팅에 참여하지 않았습니다.',
|
||||||
create: '비공개 채팅 만들기',
|
create: '비공개 채팅 만들기',
|
||||||
requestJoin: '참여 요청',
|
requestJoin: '참여 요청',
|
||||||
requestAgain: "Request again",
|
requestAgain: "다시 요청",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "요청 처리됨",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Cordn 링크 복사",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn 웹은 로그인 후 멈출 수 있습니다. 앱에서 참여하는 편이 더 안정적입니다.",
|
||||||
checkInvite: '초대 확인',
|
checkInvite: '초대 확인',
|
||||||
requested: '요청을 보냈습니다 — 크루 관리자가 승인합니다.',
|
requested: '요청을 보냈습니다 — 크루 관리자가 승인합니다.',
|
||||||
noInvite: '아직 대기 중인 초대가 없습니다. 관리자에게 요청 승인을 부탁하세요.',
|
noInvite: '아직 대기 중인 초대가 없습니다. 관리자에게 요청 승인을 부탁하세요.',
|
||||||
|
|
@ -362,7 +363,11 @@ const drift: Dict = {
|
||||||
onboardingMember3: "참여했습니다 — 메시지는 암호화됩니다.",
|
onboardingMember3: "참여했습니다 — 메시지는 암호화됩니다.",
|
||||||
encryptedNote: "크루 멤버만 읽을 수 있으며, 코디네이터는 암호화된 메시지만 중계합니다.",
|
encryptedNote: "크루 멤버만 읽을 수 있으며, 코디네이터는 암호화된 메시지만 중계합니다.",
|
||||||
advanced: "고급",
|
advanced: "고급",
|
||||||
|
compareTitle: "채팅 vs 비공개 채팅",
|
||||||
|
compareChat: "채팅은 표준 NIP-17 그룹 DM입니다. 다른 Nostr 클라이언트와 호환되며 코디네이터가 없습니다.",
|
||||||
|
comparePrivate: "비공개 채팅은 MLS를 사용하여 멤버 관리가 더 강력하고 전방 비밀성이 보장됩니다. 코디네이터는 그룹 메타데이터와 암호문만 볼 수 있으며 메시지 본문은 절대 보지 못합니다.",
|
||||||
coordinator: "코디네이터",
|
coordinator: "코디네이터",
|
||||||
|
lastChecked: "마지막 확인",
|
||||||
keyPackage: "키 패키지",
|
keyPackage: "키 패키지",
|
||||||
keyPackageReady: "이 기기에서 준비됨",
|
keyPackageReady: "이 기기에서 준비됨",
|
||||||
keyPackageMissing: "이 기기에서 설정되지 않음",
|
keyPackageMissing: "이 기기에서 설정되지 않음",
|
||||||
|
|
@ -386,6 +391,7 @@ const drift: Dict = {
|
||||||
qrTitle: "스캔하여 비공개 채팅 참여",
|
qrTitle: "스캔하여 비공개 채팅 참여",
|
||||||
qrHint: "크루 멤버가 휴대폰 카메라로 스캔합니다. 링크를 열면 비공개 채팅 탭으로 이동합니다.",
|
qrHint: "크루 멤버가 휴대폰 카메라로 스캔합니다. 링크를 열면 비공개 채팅 탭으로 이동합니다.",
|
||||||
sendFailed: "보내지 못했습니다 — 다시 시도를 누르세요.",
|
sendFailed: "보내지 못했습니다 — 다시 시도를 누르세요.",
|
||||||
|
queued: "이 기기에서 대기 중",
|
||||||
sending: "보내는 중…",
|
sending: "보내는 중…",
|
||||||
sent: "보냄",
|
sent: "보냄",
|
||||||
retry: "다시 시도",
|
retry: "다시 시도",
|
||||||
|
|
@ -421,7 +427,7 @@ const drift: Dict = {
|
||||||
noInvite: "아직 초대가 없습니다. 봇은 보통 1분 이내에 승인합니다. 그 후 “초대 확인”을 탭하세요.",
|
noInvite: "아직 초대가 없습니다. 봇은 보통 1분 이내에 승인합니다. 그 후 “초대 확인”을 탭하세요.",
|
||||||
pendingRequests: "참여 요청 {{count}}개 대기 중",
|
pendingRequests: "참여 요청 {{count}}개 대기 중",
|
||||||
pendingRequestsDesc: "봇은 보통 자동으로 승인합니다. 멈춰 있으면 아래 방을 열고 요청 확인을 탭하세요.",
|
pendingRequestsDesc: "봇은 보통 자동으로 승인합니다. 멈춰 있으면 아래 방을 열고 요청 확인을 탭하세요.",
|
||||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
botMissing: "커뮤니티 봇이 현재 이 방에 없어서 관리자가 참여 요청을 검토합니다.",
|
||||||
secondDeviceHint: "다른 기기에서 이미 멤버인가요? 직접 인계하려면 “이 기기 연결”을 사용하거나, 이 기기로 다시 참여를 요청하세요.",
|
secondDeviceHint: "다른 기기에서 이미 멤버인가요? 직접 인계하려면 “이 기기 연결”을 사용하거나, 이 기기로 다시 참여를 요청하세요.",
|
||||||
create: "커뮤니티 방 만들기",
|
create: "커뮤니티 방 만들기",
|
||||||
setupDeviceBody: "이 기기에 커뮤니티 방 열쇠를 부여합니다. 관리자나 봇이 한 번 승인합니다.",
|
setupDeviceBody: "이 기기에 커뮤니티 방 열쇠를 부여합니다. 관리자나 봇이 한 번 승인합니다.",
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,11 @@ const cordnUi = {
|
||||||
encryptedNote:
|
encryptedNote:
|
||||||
'Only crew members can read this; the coordinator only relays encrypted messages.',
|
'Only crew members can read this; the coordinator only relays encrypted messages.',
|
||||||
advanced: 'Advanced',
|
advanced: 'Advanced',
|
||||||
|
compareTitle: 'Chat vs Private chat',
|
||||||
|
compareChat: 'Chat is a standard NIP-17 group DM. It works with other Nostr clients and has no coordinator.',
|
||||||
|
comparePrivate: 'Private chat uses MLS: stronger membership control and forward secrecy. The coordinator sees group metadata and ciphertext, never message text.',
|
||||||
coordinator: 'Coordinator',
|
coordinator: 'Coordinator',
|
||||||
|
lastChecked: 'Last checked',
|
||||||
keyPackage: 'Key package',
|
keyPackage: 'Key package',
|
||||||
keyPackageReady: 'Ready on this device',
|
keyPackageReady: 'Ready on this device',
|
||||||
keyPackageMissing: 'Not set up on this device',
|
keyPackageMissing: 'Not set up on this device',
|
||||||
|
|
@ -91,6 +95,7 @@ const cordnUi = {
|
||||||
qrHint:
|
qrHint:
|
||||||
'Crew members scan this with their phone camera. The link opens the Private chat tab.',
|
'Crew members scan this with their phone camera. The link opens the Private chat tab.',
|
||||||
sendFailed: 'Couldn’t send — tap Retry.',
|
sendFailed: 'Couldn’t send — tap Retry.',
|
||||||
|
queued: 'Queued on this device',
|
||||||
sending: 'Sending…',
|
sending: 'Sending…',
|
||||||
sent: 'Sent',
|
sent: 'Sent',
|
||||||
retry: 'Retry',
|
retry: 'Retry',
|
||||||
|
|
@ -174,7 +179,11 @@ const cordnUi = {
|
||||||
encryptedNote:
|
encryptedNote:
|
||||||
'Solo los miembros de la peña pueden leerlo; el coordinador solo retransmite mensajes cifrados.',
|
'Solo los miembros de la peña pueden leerlo; el coordinador solo retransmite mensajes cifrados.',
|
||||||
advanced: 'Avanzado',
|
advanced: 'Avanzado',
|
||||||
|
compareTitle: 'Chat vs chat privado',
|
||||||
|
compareChat: 'El chat es un DM grupal NIP-17 estándar. Funciona con otros clientes Nostr y no usa coordinador.',
|
||||||
|
comparePrivate: 'El chat privado usa MLS: más control de miembros y secreto futuro. El coordinador ve metadatos del grupo y texto cifrado, nunca el contenido.',
|
||||||
coordinator: 'Coordinador',
|
coordinator: 'Coordinador',
|
||||||
|
lastChecked: 'Última comprobación',
|
||||||
keyPackage: 'Paquete de claves',
|
keyPackage: 'Paquete de claves',
|
||||||
keyPackageReady: 'Listo en este dispositivo',
|
keyPackageReady: 'Listo en este dispositivo',
|
||||||
keyPackageMissing: 'Sin configurar en este dispositivo',
|
keyPackageMissing: 'Sin configurar en este dispositivo',
|
||||||
|
|
@ -199,6 +208,7 @@ const cordnUi = {
|
||||||
qrHint:
|
qrHint:
|
||||||
'Los miembros de la peña pueden escanearlo con la cámara del móvil. El enlace abre la pestaña del chat privado.',
|
'Los miembros de la peña pueden escanearlo con la cámara del móvil. El enlace abre la pestaña del chat privado.',
|
||||||
sendFailed: 'No se pudo enviar: pulsa Reintentar.',
|
sendFailed: 'No se pudo enviar: pulsa Reintentar.',
|
||||||
|
queued: 'En cola en este dispositivo',
|
||||||
sending: 'Enviando…',
|
sending: 'Enviando…',
|
||||||
sent: 'Enviado',
|
sent: 'Enviado',
|
||||||
retry: 'Reintentar',
|
retry: 'Reintentar',
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
"dms": {
|
"dms": {
|
||||||
"refresh": "Vernieuwen",
|
"refresh": "Vernieuwen",
|
||||||
|
"privateRooms": "Privéruimtes",
|
||||||
"crews": "Crews",
|
"crews": "Crews",
|
||||||
"crewFounder": "Oprichter",
|
"crewFounder": "Oprichter",
|
||||||
"crewMember": "Lid",
|
"crewMember": "Lid",
|
||||||
|
|
@ -307,10 +308,10 @@ const drift = {
|
||||||
"noGroupMember": "Je zit nog niet in de privéchat van deze crew.",
|
"noGroupMember": "Je zit nog niet in de privéchat van deze crew.",
|
||||||
"create": "De privéchat aanmaken",
|
"create": "De privéchat aanmaken",
|
||||||
"requestJoin": "Vragen om deel te nemen",
|
"requestJoin": "Vragen om deel te nemen",
|
||||||
"requestAgain": "Request again",
|
"requestAgain": "Opnieuw aanvragen",
|
||||||
"requestHandled": "Request handled",
|
"requestHandled": "Aanvraag afgehandeld",
|
||||||
"copyCordnLink": "Copy Cordn link",
|
"copyCordnLink": "Cordn-link kopiëren",
|
||||||
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
"cordnWebCaveat": "Cordn web kan na inloggen vastlopen; deelnemen in de app is betrouwbaarder.",
|
||||||
"checkInvite": "Op uitnodiging controleren",
|
"checkInvite": "Op uitnodiging controleren",
|
||||||
"requested": "Verzoek verzonden — een crew-admin keurt het goed.",
|
"requested": "Verzoek verzonden — een crew-admin keurt het goed.",
|
||||||
"noInvite": "Nog geen uitnodiging. Vraag een admin je verzoek goed te keuren.",
|
"noInvite": "Nog geen uitnodiging. Vraag een admin je verzoek goed te keuren.",
|
||||||
|
|
@ -350,7 +351,11 @@ const drift = {
|
||||||
onboardingMember3: "Je doet mee — berichten zijn versleuteld.",
|
onboardingMember3: "Je doet mee — berichten zijn versleuteld.",
|
||||||
encryptedNote: "Alleen crewleden kunnen dit lezen; de coördinator geeft alleen versleutelde berichten door.",
|
encryptedNote: "Alleen crewleden kunnen dit lezen; de coördinator geeft alleen versleutelde berichten door.",
|
||||||
advanced: "Geavanceerd",
|
advanced: "Geavanceerd",
|
||||||
|
compareTitle: "Chat vs. privéchat",
|
||||||
|
compareChat: "Chat is een standaard NIP-17-groeps-DM. Het werkt met andere Nostr-clients en heeft geen coördinator.",
|
||||||
|
comparePrivate: "Privéchat gebruikt MLS: sterkere ledenbeheer en forward secrecy. De coördinator ziet groepsmetadata en versleutelde tekst, nooit de berichttekst.",
|
||||||
coordinator: "Coördinator",
|
coordinator: "Coördinator",
|
||||||
|
lastChecked: "Laatst gecontroleerd",
|
||||||
keyPackage: "Sleutelpakket",
|
keyPackage: "Sleutelpakket",
|
||||||
keyPackageReady: "Klaar op dit apparaat",
|
keyPackageReady: "Klaar op dit apparaat",
|
||||||
keyPackageMissing: "Niet ingesteld op dit apparaat",
|
keyPackageMissing: "Niet ingesteld op dit apparaat",
|
||||||
|
|
@ -374,6 +379,7 @@ const drift = {
|
||||||
qrTitle: "Scan om deel te nemen aan de privéchat",
|
qrTitle: "Scan om deel te nemen aan de privéchat",
|
||||||
qrHint: "Crewleden scannen dit met de camera van hun telefoon. De link opent het tabblad Privéchat.",
|
qrHint: "Crewleden scannen dit met de camera van hun telefoon. De link opent het tabblad Privéchat.",
|
||||||
sendFailed: "Verzenden mislukt — tik op Opnieuw.",
|
sendFailed: "Verzenden mislukt — tik op Opnieuw.",
|
||||||
|
queued: "In wachtrij op dit apparaat",
|
||||||
sending: "Verzenden…",
|
sending: "Verzenden…",
|
||||||
sent: "Verzonden",
|
sent: "Verzonden",
|
||||||
retry: "Opnieuw",
|
retry: "Opnieuw",
|
||||||
|
|
@ -409,7 +415,7 @@ const drift = {
|
||||||
"noInvite": "Nog geen uitnodiging. De bot keurt meestal binnen een minuut goed — tik daarna op ‘Op uitnodiging controleren’.",
|
"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",
|
"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.",
|
"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.",
|
"botMissing": "De communitybot is nu niet in deze kamer, dus een beheerder bekijkt de deelnameverzoeken.",
|
||||||
"secondDeviceHint": "Al lid op een ander apparaat? Gebruik ‘Dit apparaat koppelen’ voor een directe overdracht, of vraag opnieuw om lid te worden met dit apparaat.",
|
"secondDeviceHint": "Al lid op een ander apparaat? Gebruik ‘Dit apparaat koppelen’ voor een directe overdracht, of vraag opnieuw om lid te worden met dit apparaat.",
|
||||||
"create": "Communityruimte aanmaken",
|
"create": "Communityruimte aanmaken",
|
||||||
"setupDeviceBody": "Dit geeft dit apparaat een sleutel voor de communityruimte. Een beheerder of de bot keurt die één keer goed.",
|
"setupDeviceBody": "Dit geeft dit apparaat een sleutel voor de communityruimte. Een beheerder of de bot keurt die één keer goed.",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
"dms": {
|
"dms": {
|
||||||
"refresh": "Odśwież",
|
"refresh": "Odśwież",
|
||||||
|
"privateRooms": "Pokoje prywatne",
|
||||||
"crews": "Crewy",
|
"crews": "Crewy",
|
||||||
"crewFounder": "Założyciel",
|
"crewFounder": "Założyciel",
|
||||||
"crewMember": "Członek",
|
"crewMember": "Członek",
|
||||||
|
|
@ -307,10 +308,10 @@ const drift = {
|
||||||
"noGroupMember": "Nie należysz jeszcze do prywatnego czatu tego crew.",
|
"noGroupMember": "Nie należysz jeszcze do prywatnego czatu tego crew.",
|
||||||
"create": "Utwórz prywatny czat",
|
"create": "Utwórz prywatny czat",
|
||||||
"requestJoin": "Poproś o dołączenie",
|
"requestJoin": "Poproś o dołączenie",
|
||||||
"requestAgain": "Request again",
|
"requestAgain": "Poproś ponownie",
|
||||||
"requestHandled": "Request handled",
|
"requestHandled": "Prośba obsłużona",
|
||||||
"copyCordnLink": "Copy Cordn link",
|
"copyCordnLink": "Kopiuj link Cordn",
|
||||||
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
"cordnWebCaveat": "Cordn web może się zawiesić po zalogowaniu; dołączanie w aplikacji jest pewniejsze.",
|
||||||
"checkInvite": "Sprawdź zaproszenie",
|
"checkInvite": "Sprawdź zaproszenie",
|
||||||
"requested": "Prośba wysłana — administrator crew ją zatwierdzi.",
|
"requested": "Prośba wysłana — administrator crew ją zatwierdzi.",
|
||||||
"noInvite": "Brak oczekującego zaproszenia. Poproś administratora o zatwierdzenie prośby.",
|
"noInvite": "Brak oczekującego zaproszenia. Poproś administratora o zatwierdzenie prośby.",
|
||||||
|
|
@ -350,7 +351,11 @@ const drift = {
|
||||||
onboardingMember3: "Jesteś w środku — wiadomości są szyfrowane.",
|
onboardingMember3: "Jesteś w środku — wiadomości są szyfrowane.",
|
||||||
encryptedNote: "Mogą to czytać tylko członkowie crew; koordynator przekazuje wyłącznie zaszyfrowane wiadomości.",
|
encryptedNote: "Mogą to czytać tylko członkowie crew; koordynator przekazuje wyłącznie zaszyfrowane wiadomości.",
|
||||||
advanced: "Zaawansowane",
|
advanced: "Zaawansowane",
|
||||||
|
compareTitle: "Czat a czat prywatny",
|
||||||
|
compareChat: "Czat to standardowa grupowa wiadomość DM w NIP-17. Działa z innymi klientami Nostr i nie ma koordynatora.",
|
||||||
|
comparePrivate: "Czat prywatny korzysta z MLS: silniejsza kontrola członkostwa i tajność forward. Koordynator widzi metadane grupy i szyfrogram, nigdy treść wiadomości.",
|
||||||
coordinator: "Koordynator",
|
coordinator: "Koordynator",
|
||||||
|
lastChecked: "Ostatnio sprawdzono",
|
||||||
keyPackage: "Pakiet kluczy",
|
keyPackage: "Pakiet kluczy",
|
||||||
keyPackageReady: "Gotowe na tym urządzeniu",
|
keyPackageReady: "Gotowe na tym urządzeniu",
|
||||||
keyPackageMissing: "Nieskonfigurowane na tym urządzeniu",
|
keyPackageMissing: "Nieskonfigurowane na tym urządzeniu",
|
||||||
|
|
@ -374,6 +379,7 @@ const drift = {
|
||||||
qrTitle: "Zeskanuj, aby dołączyć do prywatnego czatu",
|
qrTitle: "Zeskanuj, aby dołączyć do prywatnego czatu",
|
||||||
qrHint: "Członkowie crew skanują to aparatem telefonu. Link otwiera kartę Prywatny czat.",
|
qrHint: "Członkowie crew skanują to aparatem telefonu. Link otwiera kartę Prywatny czat.",
|
||||||
sendFailed: "Nie udało się wysłać — dotknij Ponów.",
|
sendFailed: "Nie udało się wysłać — dotknij Ponów.",
|
||||||
|
queued: "W kolejce na tym urządzeniu",
|
||||||
sending: "Wysyłanie…",
|
sending: "Wysyłanie…",
|
||||||
sent: "Wysłano",
|
sent: "Wysłano",
|
||||||
retry: "Ponów",
|
retry: "Ponów",
|
||||||
|
|
@ -409,7 +415,7 @@ const drift = {
|
||||||
"noInvite": "Brak zaproszenia. Bot zwykle zatwierdza w ciągu minuty — potem 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",
|
"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.",
|
"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.",
|
"botMissing": "Bota społeczności nie ma teraz w tym pokoju, więc prośby o dołączenie sprawdzi administrator.",
|
||||||
"secondDeviceHint": "Masz już konto na innym urządzeniu? Użyj „Połącz to urządzenie”, aby przenieść je bezpośrednio, albo poproś ponownie o dołączenie z tego urządzenia.",
|
"secondDeviceHint": "Masz już konto na innym urządzeniu? Użyj „Połącz to urządzenie”, aby przenieść je bezpośrednio, albo poproś ponownie o dołączenie z tego urządzenia.",
|
||||||
"create": "Utwórz pokój społeczności",
|
"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.",
|
"setupDeviceBody": "To daje temu urządzeniu klucz do pokoju społeczności. Administrator lub bot zatwierdza go jednorazowo.",
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: "Atualizar",
|
refresh: "Atualizar",
|
||||||
|
privateRooms: "Salas privadas",
|
||||||
crews: "Crews",
|
crews: "Crews",
|
||||||
crewFounder: "Fundador",
|
crewFounder: "Fundador",
|
||||||
crewMember: "Membro",
|
crewMember: "Membro",
|
||||||
|
|
@ -309,10 +310,10 @@ const drift = {
|
||||||
noGroupMember: "Você ainda não está no chat privado desta crew.",
|
noGroupMember: "Você ainda não está no chat privado desta crew.",
|
||||||
create: "Criar o chat privado",
|
create: "Criar o chat privado",
|
||||||
requestJoin: "Pedir para entrar",
|
requestJoin: "Pedir para entrar",
|
||||||
requestAgain: "Request again",
|
requestAgain: "Pedir novamente",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Pedido tratado",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Copiar link do Cordn",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "O Cordn web pode travar após o login; entrar pelo app é mais confiável.",
|
||||||
checkInvite: "Verificar convite",
|
checkInvite: "Verificar convite",
|
||||||
requested: "Solicitação enviada — um admin da crew vai aprovar.",
|
requested: "Solicitação enviada — um admin da crew vai aprovar.",
|
||||||
noInvite: "Nenhum convite aguardando. Peça a um admin para aprovar sua solicitação.",
|
noInvite: "Nenhum convite aguardando. Peça a um admin para aprovar sua solicitação.",
|
||||||
|
|
@ -352,7 +353,11 @@ const drift = {
|
||||||
onboardingMember3: "Você entrou — as mensagens são criptografadas.",
|
onboardingMember3: "Você entrou — as mensagens são criptografadas.",
|
||||||
encryptedNote: "Apenas membros da crew podem ler isto; o coordenador só repassa mensagens criptografadas.",
|
encryptedNote: "Apenas membros da crew podem ler isto; o coordenador só repassa mensagens criptografadas.",
|
||||||
advanced: "Avançado",
|
advanced: "Avançado",
|
||||||
|
compareTitle: "Chat vs chat privado",
|
||||||
|
compareChat: "O chat é um DM de grupo NIP-17 padrão. Funciona com outros clientes Nostr e não tem coordenador.",
|
||||||
|
comparePrivate: "O chat privado usa MLS: controle de membros mais forte e sigilo encaminhado. O coordenador vê metadados do grupo e texto cifrado, nunca o texto das mensagens.",
|
||||||
coordinator: "Coordenador",
|
coordinator: "Coordenador",
|
||||||
|
lastChecked: "Última verificação",
|
||||||
keyPackage: "Pacote de chaves",
|
keyPackage: "Pacote de chaves",
|
||||||
keyPackageReady: "Pronto neste dispositivo",
|
keyPackageReady: "Pronto neste dispositivo",
|
||||||
keyPackageMissing: "Não configurado neste dispositivo",
|
keyPackageMissing: "Não configurado neste dispositivo",
|
||||||
|
|
@ -376,6 +381,7 @@ const drift = {
|
||||||
qrTitle: "Escaneie para entrar no chat privado",
|
qrTitle: "Escaneie para entrar no chat privado",
|
||||||
qrHint: "Os membros da crew escaneiam com a câmera do celular. O link abre a aba Chat privado.",
|
qrHint: "Os membros da crew escaneiam com a câmera do celular. O link abre a aba Chat privado.",
|
||||||
sendFailed: "Não foi possível enviar — toque em Tentar novamente.",
|
sendFailed: "Não foi possível enviar — toque em Tentar novamente.",
|
||||||
|
queued: "Na fila neste dispositivo",
|
||||||
sending: "Enviando…",
|
sending: "Enviando…",
|
||||||
sent: "Enviado",
|
sent: "Enviado",
|
||||||
retry: "Tentar novamente",
|
retry: "Tentar novamente",
|
||||||
|
|
@ -411,7 +417,7 @@ const drift = {
|
||||||
noInvite: "Nenhum convite ainda. O bot costuma aprovar em até um minuto — depois toque em “Verificar convite”.",
|
noInvite: "Nenhum convite ainda. O bot costuma aprovar em até um minuto — depois toque em “Verificar convite”.",
|
||||||
pendingRequests: "{{count}} pedido(s) de entrada aguardando",
|
pendingRequests: "{{count}} pedido(s) de entrada aguardando",
|
||||||
pendingRequestsDesc: "O bot costuma aprovar automaticamente. Se travou, abra a sala abaixo e toque em Verificar pedidos.",
|
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.",
|
botMissing: "O bot da comunidade não está nesta sala agora, então um admin analisará os pedidos de entrada.",
|
||||||
secondDeviceHint: "Já é membro em outro dispositivo? Use “Vincular este dispositivo” para uma transferência direta, ou peça para entrar de novo com este dispositivo.",
|
secondDeviceHint: "Já é membro em outro dispositivo? Use “Vincular este dispositivo” para uma transferência direta, ou peça para entrar de novo com este dispositivo.",
|
||||||
create: "Criar a sala da comunidade",
|
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.",
|
setupDeviceBody: "Isso dá a este dispositivo uma chave para a sala da comunidade. Um admin ou o bot aprova uma vez.",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
"dms": {
|
"dms": {
|
||||||
"refresh": "Reîmprospătează",
|
"refresh": "Reîmprospătează",
|
||||||
|
"privateRooms": "Camere private",
|
||||||
"crews": "Crews",
|
"crews": "Crews",
|
||||||
"crewFounder": "Fondator",
|
"crewFounder": "Fondator",
|
||||||
"crewMember": "Membru",
|
"crewMember": "Membru",
|
||||||
|
|
@ -307,10 +308,10 @@ const drift = {
|
||||||
"noGroupMember": "Nu ești încă în chatul privat al acestui crew.",
|
"noGroupMember": "Nu ești încă în chatul privat al acestui crew.",
|
||||||
"create": "Creează chatul privat",
|
"create": "Creează chatul privat",
|
||||||
"requestJoin": "Cere să te alături",
|
"requestJoin": "Cere să te alături",
|
||||||
"requestAgain": "Request again",
|
"requestAgain": "Solicită din nou",
|
||||||
"requestHandled": "Request handled",
|
"requestHandled": "Cerere soluționată",
|
||||||
"copyCordnLink": "Copy Cordn link",
|
"copyCordnLink": "Copiază linkul Cordn",
|
||||||
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
"cordnWebCaveat": "Cordn web se poate bloca după autentificare; alăturarea din aplicație este mai sigură.",
|
||||||
"checkInvite": "Verifică invitația",
|
"checkInvite": "Verifică invitația",
|
||||||
"requested": "Cerere trimisă — un admin de crew o va aproba.",
|
"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.",
|
"noInvite": "Nicio invitație în așteptare încă. Cere unui admin să aprobe cererea ta.",
|
||||||
|
|
@ -350,7 +351,11 @@ const drift = {
|
||||||
onboardingMember3: "Ai intrat — mesajele sunt criptate.",
|
onboardingMember3: "Ai intrat — mesajele sunt criptate.",
|
||||||
encryptedNote: "Doar membrii crew-ului pot citi asta; coordonatorul doar retransmite mesaje criptate.",
|
encryptedNote: "Doar membrii crew-ului pot citi asta; coordonatorul doar retransmite mesaje criptate.",
|
||||||
advanced: "Avansat",
|
advanced: "Avansat",
|
||||||
|
compareTitle: "Chat vs chat privat",
|
||||||
|
compareChat: "Chatul este un DM de grup NIP-17 standard. Funcționează cu alți clienți Nostr și nu are coordonator.",
|
||||||
|
comparePrivate: "Chatul privat folosește MLS: control mai strict al membrilor și secretizare forward. Coordonatorul vede metadatele grupului și textul cifrat, niciodată textul mesajelor.",
|
||||||
coordinator: "Coordonator",
|
coordinator: "Coordonator",
|
||||||
|
lastChecked: "Ultima verificare",
|
||||||
keyPackage: "Pachet de chei",
|
keyPackage: "Pachet de chei",
|
||||||
keyPackageReady: "Pregătit pe acest dispozitiv",
|
keyPackageReady: "Pregătit pe acest dispozitiv",
|
||||||
keyPackageMissing: "Neconfigurat pe acest dispozitiv",
|
keyPackageMissing: "Neconfigurat pe acest dispozitiv",
|
||||||
|
|
@ -374,6 +379,7 @@ const drift = {
|
||||||
qrTitle: "Scanează pentru a intra în chatul privat",
|
qrTitle: "Scanează pentru a intra în chatul privat",
|
||||||
qrHint: "Membrii crew-ului scanează cu camera telefonului. Linkul deschide fila Chat privat.",
|
qrHint: "Membrii crew-ului scanează cu camera telefonului. Linkul deschide fila Chat privat.",
|
||||||
sendFailed: "Trimiterea a eșuat — apasă Reîncearcă.",
|
sendFailed: "Trimiterea a eșuat — apasă Reîncearcă.",
|
||||||
|
queued: "În coadă pe acest dispozitiv",
|
||||||
sending: "Se trimite…",
|
sending: "Se trimite…",
|
||||||
sent: "Trimis",
|
sent: "Trimis",
|
||||||
retry: "Reîncearcă",
|
retry: "Reîncearcă",
|
||||||
|
|
@ -409,7 +415,7 @@ const drift = {
|
||||||
"noInvite": "Încă nicio invitație. Botul aprobă de obicei într-un minut — apoi apasă „Verifică invitația”.",
|
"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",
|
"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.",
|
"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.",
|
"botMissing": "Botul comunității nu este în această cameră acum, așa că un administrator va examina cererile de alăturare.",
|
||||||
"secondDeviceHint": "Ești deja membru pe alt dispozitiv? Folosește „Conectează acest dispozitiv” pentru un transfer direct sau cere din nou să te alături cu acest dispozitiv.",
|
"secondDeviceHint": "Ești deja membru pe alt dispozitiv? Folosește „Conectează acest dispozitiv” pentru un transfer direct sau cere din nou să te alături cu acest dispozitiv.",
|
||||||
"create": "Creează camera comunității",
|
"create": "Creează camera comunității",
|
||||||
"setupDeviceBody": "Aceasta oferă acestui dispozitiv o cheie pentru camera comunității. Un administrator sau botul o aprobă o dată.",
|
"setupDeviceBody": "Aceasta oferă acestui dispozitiv o cheie pentru camera comunității. Un administrator sau botul o aprobă o dată.",
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: "Обновить",
|
refresh: "Обновить",
|
||||||
|
privateRooms: "Приватные комнаты",
|
||||||
crews: "Crew",
|
crews: "Crew",
|
||||||
crewFounder: "Основатель",
|
crewFounder: "Основатель",
|
||||||
crewMember: "Участник",
|
crewMember: "Участник",
|
||||||
|
|
@ -311,10 +312,10 @@ const drift = {
|
||||||
noGroupMember: "Вы ещё не в приватном чате этого Crew.",
|
noGroupMember: "Вы ещё не в приватном чате этого Crew.",
|
||||||
create: "Создать приватный чат",
|
create: "Создать приватный чат",
|
||||||
requestJoin: "Попросить присоединиться",
|
requestJoin: "Попросить присоединиться",
|
||||||
requestAgain: "Request again",
|
requestAgain: "Запросить снова",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Заявка обработана",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Копировать ссылку Cordn",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web может зависнуть после входа; присоединение в приложении надёжнее.",
|
||||||
checkInvite: "Проверить приглашение",
|
checkInvite: "Проверить приглашение",
|
||||||
requested: "Запрос отправлен — администратор Crew одобрит его.",
|
requested: "Запрос отправлен — администратор Crew одобрит его.",
|
||||||
noInvite: "Приглашение пока не ждёт. Попросите администратора одобрить ваш запрос.",
|
noInvite: "Приглашение пока не ждёт. Попросите администратора одобрить ваш запрос.",
|
||||||
|
|
@ -354,7 +355,11 @@ const drift = {
|
||||||
onboardingMember3: "Вы внутри — сообщения зашифрованы.",
|
onboardingMember3: "Вы внутри — сообщения зашифрованы.",
|
||||||
encryptedNote: "Читать могут только участники Crew; координатор лишь передаёт зашифрованные сообщения.",
|
encryptedNote: "Читать могут только участники Crew; координатор лишь передаёт зашифрованные сообщения.",
|
||||||
advanced: "Дополнительно",
|
advanced: "Дополнительно",
|
||||||
|
compareTitle: "Чат и приватный чат",
|
||||||
|
compareChat: "Чат — это стандартный групповой DM по NIP-17. Он работает с другими клиентами Nostr и не требует координатора.",
|
||||||
|
comparePrivate: "Приватный чат использует MLS: более строгий контроль участников и прямая секретность. Координатор видит метаданные группы и шифротекст, но никогда — текст сообщений.",
|
||||||
coordinator: "Координатор",
|
coordinator: "Координатор",
|
||||||
|
lastChecked: "Последняя проверка",
|
||||||
keyPackage: "Пакет ключей",
|
keyPackage: "Пакет ключей",
|
||||||
keyPackageReady: "Готово на этом устройстве",
|
keyPackageReady: "Готово на этом устройстве",
|
||||||
keyPackageMissing: "Не настроено на этом устройстве",
|
keyPackageMissing: "Не настроено на этом устройстве",
|
||||||
|
|
@ -378,6 +383,7 @@ const drift = {
|
||||||
qrTitle: "Отсканируйте, чтобы войти в приватный чат",
|
qrTitle: "Отсканируйте, чтобы войти в приватный чат",
|
||||||
qrHint: "Участники Crew сканируют код камерой телефона. Ссылка открывает вкладку «Приватный чат».",
|
qrHint: "Участники Crew сканируют код камерой телефона. Ссылка открывает вкладку «Приватный чат».",
|
||||||
sendFailed: "Не удалось отправить — нажмите «Повторить».",
|
sendFailed: "Не удалось отправить — нажмите «Повторить».",
|
||||||
|
queued: "В очереди на этом устройстве",
|
||||||
sending: "Отправка…",
|
sending: "Отправка…",
|
||||||
sent: "Отправлено",
|
sent: "Отправлено",
|
||||||
retry: "Повторить",
|
retry: "Повторить",
|
||||||
|
|
@ -413,7 +419,7 @@ const drift = {
|
||||||
noInvite: "Приглашения пока нет. Бот обычно одобряет в течение минуты — после этого нажми «Проверить приглашение».",
|
noInvite: "Приглашения пока нет. Бот обычно одобряет в течение минуты — после этого нажми «Проверить приглашение».",
|
||||||
pendingRequests: "{{count}} запросов на вступление в ожидании",
|
pendingRequests: "{{count}} запросов на вступление в ожидании",
|
||||||
pendingRequestsDesc: "Бот обычно одобряет автоматически. Если застряло, открой комнату ниже и нажми «Проверить запросы».",
|
pendingRequestsDesc: "Бот обычно одобряет автоматически. Если застряло, открой комнату ниже и нажми «Проверить запросы».",
|
||||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
botMissing: "Бота сообщества сейчас нет в этой комнате, поэтому заявки на вступление рассмотрит администратор.",
|
||||||
secondDeviceHint: "Уже участник на другом устройстве? Используй «Привязать это устройство» для прямой передачи или попроси снова присоединиться с этого устройства.",
|
secondDeviceHint: "Уже участник на другом устройстве? Используй «Привязать это устройство» для прямой передачи или попроси снова присоединиться с этого устройства.",
|
||||||
create: "Создать комнату сообщества",
|
create: "Создать комнату сообщества",
|
||||||
setupDeviceBody: "Это даёт устройству ключ от комнаты сообщества. Администратор или бот подтверждает его один раз.",
|
setupDeviceBody: "Это даёт устройству ключ от комнаты сообщества. Администратор или бот подтверждает его один раз.",
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: "Uppdatera",
|
refresh: "Uppdatera",
|
||||||
|
privateRooms: "Privata rum",
|
||||||
crews: "Crews",
|
crews: "Crews",
|
||||||
crewFounder: "Grundare",
|
crewFounder: "Grundare",
|
||||||
crewMember: "Medlem",
|
crewMember: "Medlem",
|
||||||
|
|
@ -311,10 +312,10 @@ const drift = {
|
||||||
noGroupMember: "Du är inte med i det här crewets privata chatt än.",
|
noGroupMember: "Du är inte med i det här crewets privata chatt än.",
|
||||||
create: "Skapa den privata chatten",
|
create: "Skapa den privata chatten",
|
||||||
requestJoin: "Be om att gå med",
|
requestJoin: "Be om att gå med",
|
||||||
requestAgain: "Request again",
|
requestAgain: "Begär igen",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Förfrågan hanterad",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Kopiera Cordn-länk",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web kan hänga sig efter inloggning; att gå med i appen är mer tillförlitligt.",
|
||||||
checkInvite: "Kontrollera inbjudan",
|
checkInvite: "Kontrollera inbjudan",
|
||||||
requested: "Begäran skickad — en crewadmin godkänner den.",
|
requested: "Begäran skickad — en crewadmin godkänner den.",
|
||||||
noInvite: "Ingen inbjudan väntar än. Be en admin godkänna din begäran.",
|
noInvite: "Ingen inbjudan väntar än. Be en admin godkänna din begäran.",
|
||||||
|
|
@ -354,7 +355,11 @@ const drift = {
|
||||||
onboardingMember3: "Du är med — meddelandena är krypterade.",
|
onboardingMember3: "Du är med — meddelandena är krypterade.",
|
||||||
encryptedNote: "Bara crewmedlemmar kan läsa det här; koordinatorn vidarebefordrar bara krypterade meddelanden.",
|
encryptedNote: "Bara crewmedlemmar kan läsa det här; koordinatorn vidarebefordrar bara krypterade meddelanden.",
|
||||||
advanced: "Avancerat",
|
advanced: "Avancerat",
|
||||||
|
compareTitle: "Chatt vs privat chatt",
|
||||||
|
compareChat: "Chatt är en vanlig NIP-17-grupp-DM. Den fungerar med andra Nostr-klienter och har ingen koordinator.",
|
||||||
|
comparePrivate: "Privat chatt använder MLS: starkare medlemskontroll och forward secrecy. Koordinatorn ser gruppmetadata och chiffertext, aldrig meddelandetexten.",
|
||||||
coordinator: "Koordinator",
|
coordinator: "Koordinator",
|
||||||
|
lastChecked: "Senast kontrollerad",
|
||||||
keyPackage: "Nyckelpaket",
|
keyPackage: "Nyckelpaket",
|
||||||
keyPackageReady: "Redo på den här enheten",
|
keyPackageReady: "Redo på den här enheten",
|
||||||
keyPackageMissing: "Inte inställt på den här enheten",
|
keyPackageMissing: "Inte inställt på den här enheten",
|
||||||
|
|
@ -378,6 +383,7 @@ const drift = {
|
||||||
qrTitle: "Skanna för att gå med i den privata chatten",
|
qrTitle: "Skanna för att gå med i den privata chatten",
|
||||||
qrHint: "Crewmedlemmar skannar det här med telefonkameran. Länken öppnar fliken Privat chatt.",
|
qrHint: "Crewmedlemmar skannar det här med telefonkameran. Länken öppnar fliken Privat chatt.",
|
||||||
sendFailed: "Kunde inte skicka — tryck på Försök igen.",
|
sendFailed: "Kunde inte skicka — tryck på Försök igen.",
|
||||||
|
queued: "I kö på den här enheten",
|
||||||
sending: "Skickar…",
|
sending: "Skickar…",
|
||||||
sent: "Skickat",
|
sent: "Skickat",
|
||||||
retry: "Försök igen",
|
retry: "Försök igen",
|
||||||
|
|
@ -413,7 +419,7 @@ const drift = {
|
||||||
noInvite: "Ingen inbjudan än. Boten godkänner oftast inom en minut — tryck på ”Kontrollera inbjudan” efter det.",
|
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",
|
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.",
|
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.",
|
botMissing: "Communityboten är inte i det här rummet just nu, så en admin granskar ansökningarna om att gå med.",
|
||||||
secondDeviceHint: "Redan medlem på en annan enhet? Använd ”Länka den här enheten” för en direkt överlämning, eller be att få gå med igen med den här enheten.",
|
secondDeviceHint: "Redan medlem på en annan enhet? Använd ”Länka den här enheten” för en direkt överlämning, eller be att få gå med igen med den här enheten.",
|
||||||
create: "Skapa communityrummet",
|
create: "Skapa communityrummet",
|
||||||
setupDeviceBody: "Detta ger enheten en nyckel till communityrummet. En admin eller boten godkänner den en gång.",
|
setupDeviceBody: "Detta ger enheten en nyckel till communityrummet. En admin eller boten godkänner den en gång.",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ const driftStrings = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: 'รีเฟรช',
|
refresh: 'รีเฟรช',
|
||||||
|
privateRooms: 'ห้องส่วนตัว',
|
||||||
crews: 'Crews',
|
crews: 'Crews',
|
||||||
crewFounder: 'ผู้ก่อตั้ง',
|
crewFounder: 'ผู้ก่อตั้ง',
|
||||||
crewMember: 'สมาชิก',
|
crewMember: 'สมาชิก',
|
||||||
|
|
@ -303,10 +304,10 @@ const driftStrings = {
|
||||||
noGroupMember: 'คุณยังไม่ได้อยู่ในแชทส่วนตัวของ crew นี้',
|
noGroupMember: 'คุณยังไม่ได้อยู่ในแชทส่วนตัวของ crew นี้',
|
||||||
create: 'สร้างแชทส่วนตัว',
|
create: 'สร้างแชทส่วนตัว',
|
||||||
requestJoin: 'ขอเข้าร่วม',
|
requestJoin: 'ขอเข้าร่วม',
|
||||||
requestAgain: "Request again",
|
requestAgain: "ขออีกครั้ง",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "จัดการคำขอแล้ว",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "คัดลอกลิงก์ Cordn",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web อาจค้างหลังเข้าสู่ระบบ การเข้าร่วมในแอปเชื่อถือได้มากกว่า",
|
||||||
checkInvite: 'ตรวจสอบคำเชิญ',
|
checkInvite: 'ตรวจสอบคำเชิญ',
|
||||||
requested: 'ส่งคำขอแล้ว — แอดมิน crew จะอนุมัติ',
|
requested: 'ส่งคำขอแล้ว — แอดมิน crew จะอนุมัติ',
|
||||||
noInvite: 'ยังไม่มีคำเชิญที่รออยู่ ขอให้แอดมินอนุมัติคำขอของคุณ',
|
noInvite: 'ยังไม่มีคำเชิญที่รออยู่ ขอให้แอดมินอนุมัติคำขอของคุณ',
|
||||||
|
|
@ -346,7 +347,11 @@ const driftStrings = {
|
||||||
onboardingMember3: "คุณเข้าร่วมแล้ว — ข้อความถูกเข้ารหัส",
|
onboardingMember3: "คุณเข้าร่วมแล้ว — ข้อความถูกเข้ารหัส",
|
||||||
encryptedNote: "เฉพาะสมาชิก crew เท่านั้นที่อ่านได้ coordinator เพียงส่งต่อข้อความที่เข้ารหัส",
|
encryptedNote: "เฉพาะสมาชิก crew เท่านั้นที่อ่านได้ coordinator เพียงส่งต่อข้อความที่เข้ารหัส",
|
||||||
advanced: "ขั้นสูง",
|
advanced: "ขั้นสูง",
|
||||||
|
compareTitle: "แชทกับแชทส่วนตัว",
|
||||||
|
compareChat: "แชทคือ DM กลุ่มมาตรฐานแบบ NIP-17 ใช้ได้กับไคลเอนต์ Nostr อื่นและไม่มีผู้ประสานงาน",
|
||||||
|
comparePrivate: "แชทส่วนตัวใช้ MLS: ควบคุมสมาชิกได้เข้มงวดกว่าและมีความลับไปข้างหน้า ผู้ประสานงานเห็นข้อมูลเมตาของกลุ่มและข้อความที่เข้ารหัส แต่ไม่เห็นเนื้อหาข้อความ",
|
||||||
coordinator: "coordinator",
|
coordinator: "coordinator",
|
||||||
|
lastChecked: "ตรวจสอบล่าสุด",
|
||||||
keyPackage: "แพ็กเกจคีย์",
|
keyPackage: "แพ็กเกจคีย์",
|
||||||
keyPackageReady: "พร้อมบนอุปกรณ์นี้",
|
keyPackageReady: "พร้อมบนอุปกรณ์นี้",
|
||||||
keyPackageMissing: "ยังไม่ได้ตั้งค่าบนอุปกรณ์นี้",
|
keyPackageMissing: "ยังไม่ได้ตั้งค่าบนอุปกรณ์นี้",
|
||||||
|
|
@ -370,6 +375,7 @@ const driftStrings = {
|
||||||
qrTitle: "สแกนเพื่อเข้าร่วมแชทส่วนตัว",
|
qrTitle: "สแกนเพื่อเข้าร่วมแชทส่วนตัว",
|
||||||
qrHint: "สมาชิก crew สแกนด้วยกล้องโทรศัพท์ ลิงก์จะเปิดแท็บแชทส่วนตัว",
|
qrHint: "สมาชิก crew สแกนด้วยกล้องโทรศัพท์ ลิงก์จะเปิดแท็บแชทส่วนตัว",
|
||||||
sendFailed: "ส่งไม่สำเร็จ — แตะลองใหม่",
|
sendFailed: "ส่งไม่สำเร็จ — แตะลองใหม่",
|
||||||
|
queued: "อยู่ในคิวบนอุปกรณ์นี้",
|
||||||
sending: "กำลังส่ง…",
|
sending: "กำลังส่ง…",
|
||||||
sent: "ส่งแล้ว",
|
sent: "ส่งแล้ว",
|
||||||
retry: "ลองใหม่",
|
retry: "ลองใหม่",
|
||||||
|
|
@ -405,7 +411,7 @@ const driftStrings = {
|
||||||
noInvite: "ยังไม่มีคำเชิญ บอทมักอนุมัติภายในหนึ่งนาที — หลังจากนั้นแตะ “ตรวจสอบคำเชิญ”",
|
noInvite: "ยังไม่มีคำเชิญ บอทมักอนุมัติภายในหนึ่งนาที — หลังจากนั้นแตะ “ตรวจสอบคำเชิญ”",
|
||||||
pendingRequests: "มีคำขอเข้าร่วม {{count}} รายการรออยู่",
|
pendingRequests: "มีคำขอเข้าร่วม {{count}} รายการรออยู่",
|
||||||
pendingRequestsDesc: "บอทมักอนุมัติโดยอัตโนมัติ หากค้างอยู่ ให้เปิดห้องด้านล่างแล้วแตะ ตรวจสอบคำขอ",
|
pendingRequestsDesc: "บอทมักอนุมัติโดยอัตโนมัติ หากค้างอยู่ ให้เปิดห้องด้านล่างแล้วแตะ ตรวจสอบคำขอ",
|
||||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
botMissing: "ขณะนี้บอทของชุมชนไม่ได้อยู่ในห้องนี้ แอดมินจะตรวจสอบคำขอเข้าร่วม",
|
||||||
secondDeviceHint: "เป็นสมาชิกบนอุปกรณ์อื่นอยู่แล้วหรือ? ใช้ “เชื่อมโยงอุปกรณ์นี้” เพื่อโอนย้ายโดยตรง หรือขอเข้าร่วมอีกครั้งด้วยอุปกรณ์นี้",
|
secondDeviceHint: "เป็นสมาชิกบนอุปกรณ์อื่นอยู่แล้วหรือ? ใช้ “เชื่อมโยงอุปกรณ์นี้” เพื่อโอนย้ายโดยตรง หรือขอเข้าร่วมอีกครั้งด้วยอุปกรณ์นี้",
|
||||||
create: "สร้างห้องชุมชน",
|
create: "สร้างห้องชุมชน",
|
||||||
setupDeviceBody: "สิ่งนี้มอบกุญแจสำหรับห้องชุมชนให้อุปกรณ์นี้ ผู้ดูแลหรือบอทจะอนุมัติหนึ่งครั้ง",
|
setupDeviceBody: "สิ่งนี้มอบกุญแจสำหรับห้องชุมชนให้อุปกรณ์นี้ ผู้ดูแลหรือบอทจะอนุมัติหนึ่งครั้ง",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ const driftStrings = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: 'Yenile',
|
refresh: 'Yenile',
|
||||||
|
privateRooms: 'Özel odalar',
|
||||||
crews: "Crew'ler",
|
crews: "Crew'ler",
|
||||||
crewFounder: 'Kurucu',
|
crewFounder: 'Kurucu',
|
||||||
crewMember: 'Üye',
|
crewMember: 'Üye',
|
||||||
|
|
@ -303,10 +304,10 @@ const driftStrings = {
|
||||||
noGroupMember: "Henüz bu crew'in özel sohbetinde değilsin.",
|
noGroupMember: "Henüz bu crew'in özel sohbetinde değilsin.",
|
||||||
create: 'Özel sohbeti oluştur',
|
create: 'Özel sohbeti oluştur',
|
||||||
requestJoin: 'Katılmayı iste',
|
requestJoin: 'Katılmayı iste',
|
||||||
requestAgain: "Request again",
|
requestAgain: "Tekrar iste",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "İstek işlendi",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Cordn bağlantısını kopyala",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web oturum açtıktan sonra takılabilir; uygulama içinden katılmak daha güvenilir.",
|
||||||
checkInvite: 'Davetiye kontrol et',
|
checkInvite: 'Davetiye kontrol et',
|
||||||
requested: 'İstek gönderildi — bir crew yöneticisi onaylayacak.',
|
requested: 'İstek gönderildi — bir crew yöneticisi onaylayacak.',
|
||||||
noInvite: 'Henüz bekleyen davetiye yok. Bir yöneticiden isteğini onaylamasını iste.',
|
noInvite: 'Henüz bekleyen davetiye yok. Bir yöneticiden isteğini onaylamasını iste.',
|
||||||
|
|
@ -346,7 +347,11 @@ const driftStrings = {
|
||||||
onboardingMember3: "İçerdesin — mesajlar şifreli.",
|
onboardingMember3: "İçerdesin — mesajlar şifreli.",
|
||||||
encryptedNote: "Bunu yalnızca crew üyeleri okuyabilir; koordinatör yalnızca şifreli mesajları iletir.",
|
encryptedNote: "Bunu yalnızca crew üyeleri okuyabilir; koordinatör yalnızca şifreli mesajları iletir.",
|
||||||
advanced: "Gelişmiş",
|
advanced: "Gelişmiş",
|
||||||
|
compareTitle: "Sohbet ve özel sohbet",
|
||||||
|
compareChat: "Sohbet, standart bir NIP-17 grup DM'sidir. Diğer Nostr istemcileriyle çalışır ve koordinatörü yoktur.",
|
||||||
|
comparePrivate: "Özel sohbet MLS kullanır: daha güçlü üyelik kontrolü ve ileriye dönük gizlilik. Koordinatör grup meta verilerini ve şifreli metni görür, mesaj metnini asla görmez.",
|
||||||
coordinator: "Koordinatör",
|
coordinator: "Koordinatör",
|
||||||
|
lastChecked: "Son kontrol",
|
||||||
keyPackage: "Anahtar paketi",
|
keyPackage: "Anahtar paketi",
|
||||||
keyPackageReady: "Bu cihazda hazır",
|
keyPackageReady: "Bu cihazda hazır",
|
||||||
keyPackageMissing: "Bu cihazda ayarlanmadı",
|
keyPackageMissing: "Bu cihazda ayarlanmadı",
|
||||||
|
|
@ -370,6 +375,7 @@ const driftStrings = {
|
||||||
qrTitle: "Özel sohbete katılmak için tara",
|
qrTitle: "Özel sohbete katılmak için tara",
|
||||||
qrHint: "Crew üyeleri bunu telefon kamerasıyla tarar. Bağlantı Özel sohbet sekmesini açar.",
|
qrHint: "Crew üyeleri bunu telefon kamerasıyla tarar. Bağlantı Özel sohbet sekmesini açar.",
|
||||||
sendFailed: "Gönderilemedi — Yeniden dene’ye dokun.",
|
sendFailed: "Gönderilemedi — Yeniden dene’ye dokun.",
|
||||||
|
queued: "Bu cihazda sırada",
|
||||||
sending: "Gönderiliyor…",
|
sending: "Gönderiliyor…",
|
||||||
sent: "Gönderildi",
|
sent: "Gönderildi",
|
||||||
retry: "Yeniden dene",
|
retry: "Yeniden dene",
|
||||||
|
|
@ -405,7 +411,7 @@ const driftStrings = {
|
||||||
noInvite: "Henüz davetiye yok. Bot genellikle bir dakika içinde onaylar — ardından “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",
|
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.",
|
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.",
|
botMissing: "Topluluk botu şu anda bu odada değil, bu yüzden katılma isteklerini bir yönetici inceleyecek.",
|
||||||
secondDeviceHint: "Başka bir cihazda zaten üye misiniz? Doğrudan devir için “Bu cihazı bağla”yı kullanın veya bu cihazla yeniden katılma isteği gönderin.",
|
secondDeviceHint: "Başka bir cihazda zaten üye misiniz? Doğrudan devir için “Bu cihazı bağla”yı kullanın veya bu cihazla yeniden katılma isteği gönderin.",
|
||||||
create: "Topluluk odasını oluştur",
|
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.",
|
setupDeviceBody: "Bu, bu cihaza topluluk odası için bir anahtar verir. Bir yönetici veya bot bunu bir kez onaylar.",
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ const drift = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: "Оновити",
|
refresh: "Оновити",
|
||||||
|
privateRooms: "Приватні кімнати",
|
||||||
crews: "Команди",
|
crews: "Команди",
|
||||||
crewFounder: "Засновник",
|
crewFounder: "Засновник",
|
||||||
crewMember: "Учасник",
|
crewMember: "Учасник",
|
||||||
|
|
@ -311,10 +312,10 @@ const drift = {
|
||||||
noGroupMember: "Ви ще не в приватному чаті цієї команди.",
|
noGroupMember: "Ви ще не в приватному чаті цієї команди.",
|
||||||
create: "Створити приватний чат",
|
create: "Створити приватний чат",
|
||||||
requestJoin: "Попросити приєднатися",
|
requestJoin: "Попросити приєднатися",
|
||||||
requestAgain: "Request again",
|
requestAgain: "Запросити знову",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Заявку оброблено",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Копіювати посилання Cordn",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web може зависнути після входу; приєднання в застосунку надійніше.",
|
||||||
checkInvite: "Перевірити запрошення",
|
checkInvite: "Перевірити запрошення",
|
||||||
requested: "Запит надіслано — адміністратор команди його схвалить.",
|
requested: "Запит надіслано — адміністратор команди його схвалить.",
|
||||||
noInvite: "Запрошення ще не чекає. Попросіть адміністратора схвалити ваш запит.",
|
noInvite: "Запрошення ще не чекає. Попросіть адміністратора схвалити ваш запит.",
|
||||||
|
|
@ -354,7 +355,11 @@ const drift = {
|
||||||
onboardingMember3: "Ви всередині — повідомлення зашифровані.",
|
onboardingMember3: "Ви всередині — повідомлення зашифровані.",
|
||||||
encryptedNote: "Читати можуть лише учасники команди; координатор лише передає зашифровані повідомлення.",
|
encryptedNote: "Читати можуть лише учасники команди; координатор лише передає зашифровані повідомлення.",
|
||||||
advanced: "Додатково",
|
advanced: "Додатково",
|
||||||
|
compareTitle: "Чат і приватний чат",
|
||||||
|
compareChat: "Чат — це стандартний груповий DM за NIP-17. Він працює з іншими клієнтами Nostr і не потребує координатора.",
|
||||||
|
comparePrivate: "Приватний чат використовує MLS: суворіший контроль учасників і пряма секретність. Координатор бачить метадані групи та шифротекст, але ніколи — текст повідомлень.",
|
||||||
coordinator: "Координатор",
|
coordinator: "Координатор",
|
||||||
|
lastChecked: "Остання перевірка",
|
||||||
keyPackage: "Пакет ключів",
|
keyPackage: "Пакет ключів",
|
||||||
keyPackageReady: "Готово на цьому пристрої",
|
keyPackageReady: "Готово на цьому пристрої",
|
||||||
keyPackageMissing: "Не налаштовано на цьому пристрої",
|
keyPackageMissing: "Не налаштовано на цьому пристрої",
|
||||||
|
|
@ -378,6 +383,7 @@ const drift = {
|
||||||
qrTitle: "Відскануйте, щоб увійти до приватного чату",
|
qrTitle: "Відскануйте, щоб увійти до приватного чату",
|
||||||
qrHint: "Учасники команди сканують код камерою телефона. Посилання відкриває вкладку «Приватний чат».",
|
qrHint: "Учасники команди сканують код камерою телефона. Посилання відкриває вкладку «Приватний чат».",
|
||||||
sendFailed: "Не вдалося надіслати — натисніть «Повторити».",
|
sendFailed: "Не вдалося надіслати — натисніть «Повторити».",
|
||||||
|
queued: "У черзі на цьому пристрої",
|
||||||
sending: "Надсилання…",
|
sending: "Надсилання…",
|
||||||
sent: "Надіслано",
|
sent: "Надіслано",
|
||||||
retry: "Повторити",
|
retry: "Повторити",
|
||||||
|
|
@ -413,7 +419,7 @@ const drift = {
|
||||||
noInvite: "Запрошення ще немає. Бот зазвичай схвалює протягом хвилини — після цього натисни «Перевірити запрошення».",
|
noInvite: "Запрошення ще немає. Бот зазвичай схвалює протягом хвилини — після цього натисни «Перевірити запрошення».",
|
||||||
pendingRequests: "{{count}} запитів на приєднання в очікуванні",
|
pendingRequests: "{{count}} запитів на приєднання в очікуванні",
|
||||||
pendingRequestsDesc: "Бот зазвичай схвалює автоматично. Якщо застрягло, відкрий кімнату нижче й натисни «Перевірити запити».",
|
pendingRequestsDesc: "Бот зазвичай схвалює автоматично. Якщо застрягло, відкрий кімнату нижче й натисни «Перевірити запити».",
|
||||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
botMissing: "Бота спільноти зараз немає в цій кімнаті, тож заявки на вступ розгляне адміністратор.",
|
||||||
secondDeviceHint: "Уже є учасником на іншому пристрої? Використай «Прив’язати цей пристрій» для прямого перенесення або попроси знову долучитися з цього пристрою.",
|
secondDeviceHint: "Уже є учасником на іншому пристрої? Використай «Прив’язати цей пристрій» для прямого перенесення або попроси знову долучитися з цього пристрою.",
|
||||||
create: "Створити кімнату спільноти",
|
create: "Створити кімнату спільноти",
|
||||||
setupDeviceBody: "Це дає цьому пристрою ключ до кімнати спільноти. Адміністратор або бот підтверджує його один раз.",
|
setupDeviceBody: "Це дає цьому пристрою ключ до кімнати спільноти. Адміністратор або бот підтверджує його один раз.",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ const driftStrings = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: 'Làm mới',
|
refresh: 'Làm mới',
|
||||||
|
privateRooms: 'Phòng riêng',
|
||||||
crews: 'Crews',
|
crews: 'Crews',
|
||||||
crewFounder: 'Người sáng lập',
|
crewFounder: 'Người sáng lập',
|
||||||
crewMember: 'Thành viên',
|
crewMember: 'Thành viên',
|
||||||
|
|
@ -303,10 +304,10 @@ const driftStrings = {
|
||||||
noGroupMember: 'Bạn chưa ở trong chat riêng tư của crew này.',
|
noGroupMember: 'Bạn chưa ở trong chat riêng tư của crew này.',
|
||||||
create: 'Tạo chat riêng tư',
|
create: 'Tạo chat riêng tư',
|
||||||
requestJoin: 'Xin tham gia',
|
requestJoin: 'Xin tham gia',
|
||||||
requestAgain: "Request again",
|
requestAgain: "Yêu cầu lại",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "Đã xử lý yêu cầu",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "Sao chép liên kết Cordn",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn web có thể bị treo sau khi đăng nhập; tham gia trong ứng dụng đáng tin cậy hơn.",
|
||||||
checkInvite: 'Kiểm tra lời mời',
|
checkInvite: 'Kiểm tra lời mời',
|
||||||
requested: 'Đã gửi yêu cầu — quản trị viên crew sẽ duyệt.',
|
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.',
|
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.',
|
||||||
|
|
@ -346,7 +347,11 @@ const driftStrings = {
|
||||||
onboardingMember3: "Bạn đã vào — tin nhắn được mã hóa.",
|
onboardingMember3: "Bạn đã vào — tin nhắn được mã hóa.",
|
||||||
encryptedNote: "Chỉ thành viên crew mới đọc được; điều phối viên chỉ chuyển tiếp tin nhắn đã mã hóa.",
|
encryptedNote: "Chỉ thành viên crew mới đọc được; điều phối viên chỉ chuyển tiếp tin nhắn đã mã hóa.",
|
||||||
advanced: "Nâng cao",
|
advanced: "Nâng cao",
|
||||||
|
compareTitle: "Trò chuyện và trò chuyện riêng tư",
|
||||||
|
compareChat: "Trò chuyện là một DM nhóm NIP-17 tiêu chuẩn. Nó hoạt động với các ứng dụng Nostr khác và không có người điều phối.",
|
||||||
|
comparePrivate: "Trò chuyện riêng tư dùng MLS: kiểm soát thành viên chặt chẽ hơn và bảo mật chuyển tiếp. Người điều phối thấy siêu dữ liệu nhóm và bản mã, không bao giờ thấy nội dung tin nhắn.",
|
||||||
coordinator: "Điều phối viên",
|
coordinator: "Điều phối viên",
|
||||||
|
lastChecked: "Kiểm tra lần cuối",
|
||||||
keyPackage: "Gói khóa",
|
keyPackage: "Gói khóa",
|
||||||
keyPackageReady: "Sẵn sàng trên thiết bị này",
|
keyPackageReady: "Sẵn sàng trên thiết bị này",
|
||||||
keyPackageMissing: "Chưa thiết lập trên thiết bị này",
|
keyPackageMissing: "Chưa thiết lập trên thiết bị này",
|
||||||
|
|
@ -370,6 +375,7 @@ const driftStrings = {
|
||||||
qrTitle: "Quét để tham gia chat riêng tư",
|
qrTitle: "Quét để tham gia chat riêng tư",
|
||||||
qrHint: "Thành viên crew quét mã bằng camera điện thoại. Liên kết mở tab Chat riêng tư.",
|
qrHint: "Thành viên crew quét mã bằng camera điện thoại. Liên kết mở tab Chat riêng tư.",
|
||||||
sendFailed: "Không gửi được — nhấn Thử lại.",
|
sendFailed: "Không gửi được — nhấn Thử lại.",
|
||||||
|
queued: "Đang xếp hàng trên thiết bị này",
|
||||||
sending: "Đang gửi…",
|
sending: "Đang gửi…",
|
||||||
sent: "Đã gửi",
|
sent: "Đã gửi",
|
||||||
retry: "Thử lại",
|
retry: "Thử lại",
|
||||||
|
|
@ -405,7 +411,7 @@ const driftStrings = {
|
||||||
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”.",
|
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ờ",
|
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.",
|
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.",
|
botMissing: "Bot cộng đồng hiện không có trong phòng này, nên quản trị viên sẽ xem xét các yêu cầu tham gia.",
|
||||||
secondDeviceHint: "Đã là thành viên trên thiết bị khác? Dùng “Liên kết thiết bị này” để chuyển giao trực tiếp, hoặc yêu cầu tham gia lại bằng thiết bị này.",
|
secondDeviceHint: "Đã là thành viên trên thiết bị khác? Dùng “Liên kết thiết bị này” để chuyển giao trực tiếp, hoặc yêu cầu tham gia lại bằng thiết bị này.",
|
||||||
create: "Tạo phòng cộng đồng",
|
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.",
|
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.",
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ const drift: Dict = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
|
privateRooms: '私密房间',
|
||||||
crews: 'Crew',
|
crews: 'Crew',
|
||||||
crewFounder: '创始人',
|
crewFounder: '创始人',
|
||||||
crewMember: '成员',
|
crewMember: '成员',
|
||||||
|
|
@ -319,10 +320,10 @@ const drift: Dict = {
|
||||||
noGroupMember: '你还没有加入此 Crew 的私密聊天。',
|
noGroupMember: '你还没有加入此 Crew 的私密聊天。',
|
||||||
create: '创建私密聊天',
|
create: '创建私密聊天',
|
||||||
requestJoin: '请求加入',
|
requestJoin: '请求加入',
|
||||||
requestAgain: "Request again",
|
requestAgain: "再次请求",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "请求已处理",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "复制 Cordn 链接",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn 网页版登录后可能会卡住;在应用内加入更可靠。",
|
||||||
checkInvite: '检查邀请',
|
checkInvite: '检查邀请',
|
||||||
requested: '请求已发送——Crew 管理员会批准。',
|
requested: '请求已发送——Crew 管理员会批准。',
|
||||||
noInvite: '还没有等待中的邀请。请管理员批准你的请求。',
|
noInvite: '还没有等待中的邀请。请管理员批准你的请求。',
|
||||||
|
|
@ -362,7 +363,11 @@ const drift: Dict = {
|
||||||
onboardingMember3: "你已加入——消息已加密。",
|
onboardingMember3: "你已加入——消息已加密。",
|
||||||
encryptedNote: "只有 Crew 成员可以阅读;协调器只转发加密消息。",
|
encryptedNote: "只有 Crew 成员可以阅读;协调器只转发加密消息。",
|
||||||
advanced: "高级",
|
advanced: "高级",
|
||||||
|
compareTitle: "聊天与私密聊天",
|
||||||
|
compareChat: "聊天是标准的 NIP-17 群组私信。它可与其他 Nostr 客户端互通,且没有协调者。",
|
||||||
|
comparePrivate: "私密聊天使用 MLS:更严格的成员控制和前向保密。协调者只能看到群组元数据和密文,永远看不到消息正文。",
|
||||||
coordinator: "协调器",
|
coordinator: "协调器",
|
||||||
|
lastChecked: "上次检查",
|
||||||
keyPackage: "密钥包",
|
keyPackage: "密钥包",
|
||||||
keyPackageReady: "已在此设备上就绪",
|
keyPackageReady: "已在此设备上就绪",
|
||||||
keyPackageMissing: "尚未在此设备上设置",
|
keyPackageMissing: "尚未在此设备上设置",
|
||||||
|
|
@ -386,6 +391,7 @@ const drift: Dict = {
|
||||||
qrTitle: "扫描加入私密聊天",
|
qrTitle: "扫描加入私密聊天",
|
||||||
qrHint: "Crew 成员用手机摄像头扫描。链接会打开“私密聊天”标签页。",
|
qrHint: "Crew 成员用手机摄像头扫描。链接会打开“私密聊天”标签页。",
|
||||||
sendFailed: "发送失败——点击重试。",
|
sendFailed: "发送失败——点击重试。",
|
||||||
|
queued: "已在此设备上排队",
|
||||||
sending: "发送中…",
|
sending: "发送中…",
|
||||||
sent: "已发送",
|
sent: "已发送",
|
||||||
retry: "重试",
|
retry: "重试",
|
||||||
|
|
@ -421,7 +427,7 @@ const drift: Dict = {
|
||||||
noInvite: "还没有邀请。机器人通常在一分钟内批准——之后请点按“检查邀请”。",
|
noInvite: "还没有邀请。机器人通常在一分钟内批准——之后请点按“检查邀请”。",
|
||||||
pendingRequests: "{{count}} 个加入请求正在等待",
|
pendingRequests: "{{count}} 个加入请求正在等待",
|
||||||
pendingRequestsDesc: "机器人通常会自动批准。如果卡住了,请打开下面的房间并点按“检查请求”。",
|
pendingRequestsDesc: "机器人通常会自动批准。如果卡住了,请打开下面的房间并点按“检查请求”。",
|
||||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
botMissing: "社区机器人目前不在这个房间,因此管理员会审核加入请求。",
|
||||||
secondDeviceHint: "已在其他设备上加入?使用“关联此设备”直接交接,或用此设备再次申请加入。",
|
secondDeviceHint: "已在其他设备上加入?使用“关联此设备”直接交接,或用此设备再次申请加入。",
|
||||||
create: "创建社区房间",
|
create: "创建社区房间",
|
||||||
setupDeviceBody: "这会为此设备提供社区房间的密钥,管理员或机器人会批准一次。",
|
setupDeviceBody: "这会为此设备提供社区房间的密钥,管理员或机器人会批准一次。",
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ const drift: Dict = {
|
||||||
},
|
},
|
||||||
dms: {
|
dms: {
|
||||||
refresh: '重新整理',
|
refresh: '重新整理',
|
||||||
|
privateRooms: '私密房間',
|
||||||
crews: 'Crew',
|
crews: 'Crew',
|
||||||
crewFounder: '創始人',
|
crewFounder: '創始人',
|
||||||
crewMember: '成員',
|
crewMember: '成員',
|
||||||
|
|
@ -319,10 +320,10 @@ const drift: Dict = {
|
||||||
noGroupMember: '你還沒有加入此 Crew 的私密聊天。',
|
noGroupMember: '你還沒有加入此 Crew 的私密聊天。',
|
||||||
create: '建立私密聊天',
|
create: '建立私密聊天',
|
||||||
requestJoin: '要求加入',
|
requestJoin: '要求加入',
|
||||||
requestAgain: "Request again",
|
requestAgain: "再次請求",
|
||||||
requestHandled: "Request handled",
|
requestHandled: "請求已處理",
|
||||||
copyCordnLink: "Copy Cordn link",
|
copyCordnLink: "複製 Cordn 連結",
|
||||||
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
|
cordnWebCaveat: "Cordn 網頁版登入後可能會卡住;在應用程式內加入更可靠。",
|
||||||
checkInvite: '檢查邀請',
|
checkInvite: '檢查邀請',
|
||||||
requested: '要求已送出——Crew 管理員會批准。',
|
requested: '要求已送出——Crew 管理員會批准。',
|
||||||
noInvite: '還沒有等待中的邀請。請管理員批准你的要求。',
|
noInvite: '還沒有等待中的邀請。請管理員批准你的要求。',
|
||||||
|
|
@ -362,7 +363,11 @@ const drift: Dict = {
|
||||||
onboardingMember3: "你已加入——訊息已加密。",
|
onboardingMember3: "你已加入——訊息已加密。",
|
||||||
encryptedNote: "只有 Crew 成員可以閱讀;協調器只轉送加密訊息。",
|
encryptedNote: "只有 Crew 成員可以閱讀;協調器只轉送加密訊息。",
|
||||||
advanced: "進階",
|
advanced: "進階",
|
||||||
|
compareTitle: "聊天與私密聊天",
|
||||||
|
compareChat: "聊天是標準的 NIP-17 群組私訊。可與其他 Nostr 用戶端互通,且沒有協調者。",
|
||||||
|
comparePrivate: "私密聊天使用 MLS:更嚴格的成員控制與前向保密。協調者只能看到群組中繼資料與密文,永遠看不到訊息內容。",
|
||||||
coordinator: "協調器",
|
coordinator: "協調器",
|
||||||
|
lastChecked: "上次檢查",
|
||||||
keyPackage: "金鑰包",
|
keyPackage: "金鑰包",
|
||||||
keyPackageReady: "已在此裝置上就緒",
|
keyPackageReady: "已在此裝置上就緒",
|
||||||
keyPackageMissing: "尚未在此裝置上設定",
|
keyPackageMissing: "尚未在此裝置上設定",
|
||||||
|
|
@ -386,6 +391,7 @@ const drift: Dict = {
|
||||||
qrTitle: "掃描加入私密聊天",
|
qrTitle: "掃描加入私密聊天",
|
||||||
qrHint: "Crew 成員用手機相機掃描。連結會開啟「私密聊天」分頁。",
|
qrHint: "Crew 成員用手機相機掃描。連結會開啟「私密聊天」分頁。",
|
||||||
sendFailed: "傳送失敗——點重試。",
|
sendFailed: "傳送失敗——點重試。",
|
||||||
|
queued: "已在此裝置上排隊",
|
||||||
sending: "傳送中…",
|
sending: "傳送中…",
|
||||||
sent: "已傳送",
|
sent: "已傳送",
|
||||||
retry: "重試",
|
retry: "重試",
|
||||||
|
|
@ -421,7 +427,7 @@ const drift: Dict = {
|
||||||
noInvite: "還沒有邀請。機器人通常在一分鐘內核准——之後請點按「檢查邀請」。",
|
noInvite: "還沒有邀請。機器人通常在一分鐘內核准——之後請點按「檢查邀請」。",
|
||||||
pendingRequests: "{{count}} 個加入要求正在等待",
|
pendingRequests: "{{count}} 個加入要求正在等待",
|
||||||
pendingRequestsDesc: "機器人通常會自動核准。如果卡住了,請開啟下面的房間並點按「檢查要求」。",
|
pendingRequestsDesc: "機器人通常會自動核准。如果卡住了,請開啟下面的房間並點按「檢查要求」。",
|
||||||
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
|
botMissing: "社群機器人目前不在這個房間,因此管理員會審核加入請求。",
|
||||||
secondDeviceHint: "已在其他裝置上加入?使用「連結此裝置」直接交接,或用此裝置再次申請加入。",
|
secondDeviceHint: "已在其他裝置上加入?使用「連結此裝置」直接交接,或用此裝置再次申請加入。",
|
||||||
create: "建立社群房間",
|
create: "建立社群房間",
|
||||||
setupDeviceBody: "這會為此裝置提供社群房間的金鑰,管理員或機器人會核准一次。",
|
setupDeviceBody: "這會為此裝置提供社群房間的金鑰,管理員或機器人會核准一次。",
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,47 @@
|
||||||
@keyframes splashSpin {
|
@keyframes splashSpin {
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
@keyframes splashLogoRollIn {
|
||||||
|
0% { opacity: 0; transform: translateX(-58px) rotate(-210deg) scale(0.82); }
|
||||||
|
62% { opacity: 1; transform: translateX(5px) rotate(14deg) scale(1.03); }
|
||||||
|
100% { opacity: 1; transform: translateX(0) rotate(0deg) scale(1); }
|
||||||
|
}
|
||||||
|
@keyframes splashWheelRoll {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes splashHover {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-4px); }
|
||||||
|
}
|
||||||
|
@keyframes splashDust {
|
||||||
|
0%, 100% { opacity: 0.28; transform: translateX(-50%) scaleX(1); }
|
||||||
|
50% { opacity: 0.5; transform: translateX(-50%) scaleX(1.25); }
|
||||||
|
}
|
||||||
|
@keyframes splashSpark {
|
||||||
|
0% { opacity: 0; transform: translate(-50%, -50%) translate(var(--sx), var(--sy)) scale(0.45); }
|
||||||
|
12% { opacity: 1; }
|
||||||
|
100% { opacity: 0; transform: translate(-50%, -50%) translate(var(--ex), var(--ey)) scale(0); }
|
||||||
|
}
|
||||||
|
.splash-spark {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: #ffb347;
|
||||||
|
box-shadow: 0 0 10px 3px rgba(255, 106, 0, 0.85);
|
||||||
|
animation: splashSpark 1.15s ease-out infinite;
|
||||||
|
}
|
||||||
|
.splash-spark:nth-child(3n) {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background: #ccff00;
|
||||||
|
box-shadow: 0 0 9px 3px rgba(204, 255, 0, 0.55);
|
||||||
|
}
|
||||||
|
.splash-spark:nth-child(3n + 1) {
|
||||||
|
background: #ffd166;
|
||||||
|
}
|
||||||
@keyframes splashRise {
|
@keyframes splashRise {
|
||||||
from { opacity: 0; transform: translateY(14px); }
|
from { opacity: 0; transform: translateY(14px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
|
|
||||||
117
src/lib/cordnOutbox.ts
Normal file
117
src/lib/cordnOutbox.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
/**
|
||||||
|
* Device-local outbox for Cordn (MLS) messages that failed to send.
|
||||||
|
*
|
||||||
|
* The encrypted room state lives in IndexedDB, but a failed MLS send otherwise
|
||||||
|
* disappears when the panel unmounts. This keeps the plaintext draft locally
|
||||||
|
* (same trust boundary as the composer) until it is sent or explicitly
|
||||||
|
* discarded. It is deliberately NOT published or synced anywhere.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const KEY_PREFIX = 'skatespots:cordn-outbox:';
|
||||||
|
const MAX_ITEMS = 50;
|
||||||
|
|
||||||
|
export interface CordnOutboxItem {
|
||||||
|
id: string;
|
||||||
|
gid: string;
|
||||||
|
text: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
createdAt: number;
|
||||||
|
attempts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function key(pubkey: string, gid: string): string {
|
||||||
|
return `${KEY_PREFIX}${pubkey}:${gid}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeId(): string {
|
||||||
|
try {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
} catch {
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(pubkey: string, gid: string, items: CordnOutboxItem[]): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key(pubkey, gid), JSON.stringify(items.slice(-MAX_ITEMS)));
|
||||||
|
} catch {
|
||||||
|
// private mode / quota — the in-memory pending bubble still works
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readCordnOutbox(pubkey: string | undefined, gid: string | undefined): CordnOutboxItem[] {
|
||||||
|
if (!pubkey || !gid) return [];
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key(pubkey, gid));
|
||||||
|
if (!raw) return [];
|
||||||
|
const parsed = JSON.parse(raw) as unknown;
|
||||||
|
if (!Array.isArray(parsed)) return [];
|
||||||
|
return parsed
|
||||||
|
.filter((item): item is CordnOutboxItem => {
|
||||||
|
if (!item || typeof item !== 'object') return false;
|
||||||
|
const value = item as Partial<CordnOutboxItem>;
|
||||||
|
return (
|
||||||
|
typeof value.id === 'string' &&
|
||||||
|
typeof value.gid === 'string' &&
|
||||||
|
typeof value.text === 'string' &&
|
||||||
|
Number.isFinite(value.createdAt) &&
|
||||||
|
Number.isFinite(value.attempts)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.createdAt - b.createdAt);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enqueueCordnOutbox(
|
||||||
|
pubkey: string | undefined,
|
||||||
|
gid: string | undefined,
|
||||||
|
text: string,
|
||||||
|
imageUrl?: string,
|
||||||
|
): CordnOutboxItem | null {
|
||||||
|
if (!pubkey || !gid || (!text.trim() && !imageUrl)) return null;
|
||||||
|
const items = readCordnOutbox(pubkey, gid);
|
||||||
|
const item: CordnOutboxItem = {
|
||||||
|
id: makeId(),
|
||||||
|
gid,
|
||||||
|
text,
|
||||||
|
imageUrl,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
attempts: 1,
|
||||||
|
};
|
||||||
|
items.push(item);
|
||||||
|
write(pubkey, gid, items);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markCordnOutboxAttempt(
|
||||||
|
pubkey: string | undefined,
|
||||||
|
gid: string | undefined,
|
||||||
|
id: string,
|
||||||
|
): void {
|
||||||
|
if (!pubkey || !gid) return;
|
||||||
|
const items = readCordnOutbox(pubkey, gid);
|
||||||
|
const next = items.map((item) =>
|
||||||
|
item.id === id ? { ...item, attempts: item.attempts + 1 } : item,
|
||||||
|
);
|
||||||
|
write(pubkey, gid, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCordnOutbox(
|
||||||
|
pubkey: string | undefined,
|
||||||
|
gid: string | undefined,
|
||||||
|
id: string,
|
||||||
|
): void {
|
||||||
|
if (!pubkey || !gid) return;
|
||||||
|
write(pubkey, gid, readCordnOutbox(pubkey, gid).filter((item) => item.id !== id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCordnOutbox(pubkey: string | undefined, gid: string | undefined): void {
|
||||||
|
if (!pubkey || !gid) return;
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(key(pubkey, gid));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/lib/cordnRoomRead.ts
Normal file
93
src/lib/cordnRoomRead.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
/**
|
||||||
|
* Device-local read state for Cordn rooms.
|
||||||
|
*
|
||||||
|
* This is intentionally lightweight: the panel that actually holds the MLS
|
||||||
|
* group records the newest inbound message and, while the room is visible,
|
||||||
|
* marks it seen. The Messages hub can then show an honest unread dot without
|
||||||
|
* decrypting anything in the background. A room that has never been opened
|
||||||
|
* has no entry (and therefore no invented badge).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const KEY_PREFIX = 'skatespots:cordn-room-read:';
|
||||||
|
const EVENT = 'skatespots:cordn-room-read-change';
|
||||||
|
|
||||||
|
export interface CordnRoomRead {
|
||||||
|
/** Newest inbound message timestamp (seconds). */
|
||||||
|
latestAt: number;
|
||||||
|
/** Newest timestamp the viewer has acknowledged. */
|
||||||
|
seenAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CordnRoomReadMap = Record<string, CordnRoomRead>;
|
||||||
|
|
||||||
|
function storageKey(pubkey: string): string {
|
||||||
|
return `${KEY_PREFIX}${pubkey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRaw(pubkey: string | undefined): CordnRoomReadMap {
|
||||||
|
if (!pubkey) return {};
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(storageKey(pubkey));
|
||||||
|
if (!raw) return {};
|
||||||
|
const parsed = JSON.parse(raw) as unknown;
|
||||||
|
if (!parsed || typeof parsed !== 'object') return {};
|
||||||
|
const result: CordnRoomReadMap = {};
|
||||||
|
for (const [gid, value] of Object.entries(parsed as Record<string, unknown>)) {
|
||||||
|
if (!value || typeof value !== 'object') continue;
|
||||||
|
const latestAt = Number((value as { latestAt?: unknown }).latestAt);
|
||||||
|
const seenAt = Number((value as { seenAt?: unknown }).seenAt);
|
||||||
|
if (!Number.isFinite(latestAt) || !Number.isFinite(seenAt)) continue;
|
||||||
|
result[gid] = { latestAt: Math.max(0, latestAt), seenAt: Math.max(0, seenAt) };
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRaw(pubkey: string, reads: CordnRoomReadMap): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey(pubkey), JSON.stringify(reads));
|
||||||
|
window.dispatchEvent(new CustomEvent(EVENT, { detail: { pubkey, reads } }));
|
||||||
|
} catch {
|
||||||
|
// private mode / quota — badges simply won't update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read every known room state for one account. */
|
||||||
|
export function readCordnRoomReads(pubkey: string | undefined): CordnRoomReadMap {
|
||||||
|
return readRaw(pubkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Record a new inbound message without marking it read. */
|
||||||
|
export function markCordnRoomActive(
|
||||||
|
pubkey: string | undefined,
|
||||||
|
gid: string,
|
||||||
|
latestAt: number,
|
||||||
|
): void {
|
||||||
|
if (!pubkey || !gid || latestAt <= 0) return;
|
||||||
|
const reads = readRaw(pubkey);
|
||||||
|
const previous = reads[gid] ?? { latestAt: 0, seenAt: 0 };
|
||||||
|
if (latestAt <= previous.latestAt) return;
|
||||||
|
reads[gid] = { ...previous, latestAt };
|
||||||
|
writeRaw(pubkey, reads);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mark the room seen up to its newest known inbound message. */
|
||||||
|
export function markCordnRoomSeen(
|
||||||
|
pubkey: string | undefined,
|
||||||
|
gid: string,
|
||||||
|
latestAt: number,
|
||||||
|
): void {
|
||||||
|
if (!pubkey || !gid || latestAt <= 0) return;
|
||||||
|
const reads = readRaw(pubkey);
|
||||||
|
const previous = reads[gid] ?? { latestAt: 0, seenAt: 0 };
|
||||||
|
const nextLatest = Math.max(previous.latestAt, latestAt);
|
||||||
|
if (nextLatest <= previous.seenAt) return;
|
||||||
|
reads[gid] = { latestAt: nextLatest, seenAt: nextLatest };
|
||||||
|
writeRaw(pubkey, reads);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cordnRoomReadEventName(): string {
|
||||||
|
return EVENT;
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,8 @@ import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities';
|
||||||
import { useConversations, useGroupConversations, type Conversation } from '@/hooks/useDirectMessages';
|
import { useConversations, useGroupConversations, type Conversation } from '@/hooks/useDirectMessages';
|
||||||
import { useUserCrews } from '@/hooks/useCrews';
|
import { useUserCrews } from '@/hooks/useCrews';
|
||||||
import { useCordnGroupGids } from '@/hooks/useCordnNative';
|
import { useCordnGroupGids } from '@/hooks/useCordnNative';
|
||||||
|
import { useCordnRoomRead } from '@/hooks/useCordnRoomRead';
|
||||||
|
import { COMMUNITY_ROOM } from '@/lib/cordn/community';
|
||||||
import { SKATE_CREW_KIND } from '@/lib/types';
|
import { SKATE_CREW_KIND } from '@/lib/types';
|
||||||
import { useDMRead } from '@/hooks/useDMRead';
|
import { useDMRead } from '@/hooks/useDMRead';
|
||||||
import { useDmUnreadSummary } from '@/hooks/useDmUnreadSummary';
|
import { useDmUnreadSummary } from '@/hooks/useDmUnreadSummary';
|
||||||
|
|
@ -33,6 +35,7 @@ import {
|
||||||
X,
|
X,
|
||||||
Users,
|
Users,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
MessagesSquare,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
|
@ -221,6 +224,55 @@ function ConversationSkeleton() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CordnRoomRow({
|
||||||
|
to,
|
||||||
|
name,
|
||||||
|
subtitle,
|
||||||
|
hasGroup,
|
||||||
|
unread,
|
||||||
|
icon: Icon,
|
||||||
|
}: {
|
||||||
|
to: string;
|
||||||
|
name: string;
|
||||||
|
subtitle: string;
|
||||||
|
hasGroup: boolean;
|
||||||
|
unread: boolean;
|
||||||
|
icon: typeof Lock;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Link to={to}>
|
||||||
|
<Card className="hover:bg-gray-900/50 transition-colors cursor-pointer border-gray-800 hover:border-teal-500/50 active:scale-[0.99]">
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full bg-teal-500/15 text-teal-400">
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
{unread && (
|
||||||
|
<span
|
||||||
|
className="absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full border-2 border-black bg-[#ccff00]"
|
||||||
|
aria-label={t('socialUi.dms.unreadMessages')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="truncate font-bold text-white">{name}</h3>
|
||||||
|
<p className="mt-0.5 truncate text-xs text-gray-500">{subtitle}</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide',
|
||||||
|
hasGroup ? 'bg-teal-500/15 text-teal-300' : 'bg-white/10 text-gray-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{hasGroup ? t('socialUi.dms.crewOpen') : t('socialUi.dms.crewSetUp')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function DirectMessagesPage() {
|
export default function DirectMessagesPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
|
|
@ -232,6 +284,7 @@ export default function DirectMessagesPage() {
|
||||||
const { getLastRead } = useDMRead();
|
const { getLastRead } = useDMRead();
|
||||||
const { markSeen: markDmEnvelopesSeen } = useDmUnreadSummary(!!user);
|
const { markSeen: markDmEnvelopesSeen } = useDmUnreadSummary(!!user);
|
||||||
const { isMuted } = useDmMute();
|
const { isMuted } = useDmMute();
|
||||||
|
const { isUnread: isCordnUnread } = useCordnRoomRead(user?.pubkey);
|
||||||
// The viewer's crews — surfaced here as entry points to their private (MLS)
|
// The viewer's crews — surfaced here as entry points to their private (MLS)
|
||||||
// chat. `cordnGids` is a local IndexedDB read (no signer prompt) used to mark
|
// chat. `cordnGids` is a local IndexedDB read (no signer prompt) used to mark
|
||||||
// which crews already have a chat on this device.
|
// which crews already have a chat on this device.
|
||||||
|
|
@ -377,46 +430,40 @@ export default function DirectMessagesPage() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Crews — entry points into each crew's private (MLS) chat */}
|
{/* Cordn private rooms — community + each crew's MLS chat */}
|
||||||
{myCrews.length > 0 && (
|
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<h2 className="mb-2 flex items-center gap-1.5 text-xs font-bold uppercase tracking-widest text-gray-500">
|
<h2 className="mb-2 flex items-center gap-1.5 text-xs font-bold uppercase tracking-widest text-gray-500">
|
||||||
<Lock className="h-3.5 w-3.5" /> {t('socialUi.dms.crews')}
|
<Lock className="h-3.5 w-3.5" /> {t('socialUi.dms.privateRooms')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<CordnRoomRow
|
||||||
|
to="/community"
|
||||||
|
name={t('communityUi.title')}
|
||||||
|
subtitle={t('communityUi.subtitle')}
|
||||||
|
hasGroup={cordnGids?.has(COMMUNITY_ROOM.gid ?? 'skatespots:community') ?? false}
|
||||||
|
unread={
|
||||||
|
isCordnUnread(COMMUNITY_ROOM.gid) &&
|
||||||
|
!isMuted(`cordn:${COMMUNITY_ROOM.gid}`)
|
||||||
|
}
|
||||||
|
icon={MessagesSquare}
|
||||||
|
/>
|
||||||
{myCrews.map(({ crew, role }) => {
|
{myCrews.map(({ crew, role }) => {
|
||||||
const hasGroup = cordnGids?.has(`${SKATE_CREW_KIND}:${crew.pubkey}:${crew.identifier}`) ?? false;
|
const gid = `${SKATE_CREW_KIND}:${crew.pubkey}:${crew.identifier}`;
|
||||||
|
const hasGroup = cordnGids?.has(gid) ?? false;
|
||||||
return (
|
return (
|
||||||
<Link key={crew.pubkey} to={`/crew/${crew.pubkey}/${crew.identifier}?tab=mls`}>
|
<CordnRoomRow
|
||||||
<Card className="hover:bg-gray-900/50 transition-colors cursor-pointer border-gray-800 hover:border-teal-500/50 active:scale-[0.99]">
|
key={crew.pubkey}
|
||||||
<CardContent className="p-3">
|
to={`/crew/${crew.pubkey}/${crew.identifier}?tab=mls`}
|
||||||
<div className="flex items-center gap-3">
|
name={crew.name}
|
||||||
<div className="flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full bg-teal-500/15 text-teal-400">
|
subtitle={role === 'founder' ? t('socialUi.dms.crewFounder') : t('socialUi.dms.crewMember')}
|
||||||
<Lock className="h-5 w-5" />
|
hasGroup={hasGroup}
|
||||||
</div>
|
unread={isCordnUnread(gid) && !isMuted(`cordn:${gid}`)}
|
||||||
<div className="min-w-0 flex-1">
|
icon={Lock}
|
||||||
<h3 className="truncate font-bold text-white">{crew.name}</h3>
|
/>
|
||||||
<p className="mt-0.5 truncate text-xs text-gray-500">
|
|
||||||
{role === 'founder' ? t('socialUi.dms.crewFounder') : t('socialUi.dms.crewMember')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide',
|
|
||||||
hasGroup ? 'bg-teal-500/15 text-teal-300' : 'bg-white/10 text-gray-400',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{hasGroup ? t('socialUi.dms.crewOpen') : t('socialUi.dms.crewSetUp')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Group conversations (NIP-17 group DMs) */}
|
{/* Group conversations (NIP-17 group DMs) */}
|
||||||
{groups && groups.length > 0 && (
|
{groups && groups.length > 0 && (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue