feat(crew-chat): shareable 'join this crew's chat' invite
The MLS private-chat panel gets an admin 'Invite' action that shares the crew's private-chat link (…?tab=mls) via the OS share sheet or clipboard, landing the invitee on the Ask-to-join flow. Added cordnUi.shareInvite/shareInviteText/ inviteCopied to en/es and backfilled them into all 20 locale drift fragments so i18n stays at 100%.
This commit is contained in:
parent
9a5b706b1a
commit
a9669c17b8
22 changed files with 104 additions and 10 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { KeyRound, Lock, Plus, RefreshCw, Send, UserPlus, Copy, Check, ImagePlus, Loader2 } from 'lucide-react';
|
||||
import { KeyRound, Lock, Plus, RefreshCw, Send, UserPlus, Copy, Check, ImagePlus, Loader2, Share2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
|
@ -89,6 +89,24 @@ export function CordnNativePanel({ crew, canManage, isMember }: CordnNativePanel
|
|||
if (!joined) setNotice(t('cordnUi.noInvite'));
|
||||
}, [native, t]);
|
||||
|
||||
// "Join our crew chat" invite. The chat needs the person to be a crew member
|
||||
// first, so the link just drops them on this crew's private-chat tab with the
|
||||
// "Ask to join" flow ready. Uses the OS share sheet where available.
|
||||
const shareInvite = useCallback(async () => {
|
||||
const url = `${window.location.origin}/#/crew/${crew.pubkey}/${crew.identifier}?tab=mls`;
|
||||
try {
|
||||
if (navigator.share) {
|
||||
await navigator.share({ title: crew.name, text: t('cordnUi.shareInviteText', { crew: crew.name }), url });
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(url);
|
||||
setNotice(t('cordnUi.inviteCopied'));
|
||||
window.setTimeout(() => setNotice(null), 2500);
|
||||
} catch {
|
||||
// share cancelled or clipboard blocked — nothing to do
|
||||
}
|
||||
}, [crew, t]);
|
||||
|
||||
if (!user) {
|
||||
return <Notice icon={<Lock className="h-5 w-5" />} text={t('cordnUi.notLoggedIn')} />;
|
||||
}
|
||||
|
|
@ -293,15 +311,25 @@ export function CordnNativePanel({ crew, canManage, isMember }: CordnNativePanel
|
|||
<div className="space-y-2 rounded-md border border-white/10 bg-black/20 p-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-300">{t('cordnUi.requests')}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={native.busy}
|
||||
onClick={() => void native.loadRequests()}
|
||||
className="h-7 gap-1 text-xs"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" /> {t('cordnUi.checkRequests')}
|
||||
</Button>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => void shareInvite()}
|
||||
className="h-7 gap-1 text-xs"
|
||||
>
|
||||
<Share2 className="h-3 w-3" /> {t('cordnUi.shareInvite')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={native.busy}
|
||||
onClick={() => void native.loadRequests()}
|
||||
className="h-7 gap-1 text-xs"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" /> {t('cordnUi.checkRequests')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{native.requests.length === 0 ? (
|
||||
<p className="px-1 text-[11px] text-gray-500">
|
||||
|
|
|
|||
|
|
@ -278,6 +278,9 @@ const drift = {
|
|||
"legendWeatherOnly": "Jen počasí"
|
||||
},
|
||||
"cordnUi": {
|
||||
"shareInvite": 'Pozvat',
|
||||
"shareInviteText": 'Přidej se k {{crew}} — soukromý chat crew',
|
||||
"inviteCopied": 'Odkaz s pozvánkou zkopírován',
|
||||
"title": "Soukromý chat",
|
||||
"subtitle": "End-to-end šifrované (MLS) mezi členy crew. Koordinátor přenáší pouze šifrovaný text a metadata — nikdy nevidí obsah zpráv.",
|
||||
"notLoggedIn": "Přihlas se, abys mohl použít soukromý chat crew.",
|
||||
|
|
|
|||
|
|
@ -280,6 +280,9 @@ const drift = {
|
|||
legendWeatherOnly: "Nur Wetter",
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Einladen',
|
||||
shareInviteText: 'Komm zu {{crew}} — privater Crew-Chat',
|
||||
inviteCopied: 'Einladungslink kopiert',
|
||||
title: "Privater Chat",
|
||||
subtitle: "Ende-zu-Ende-verschlüsselt (MLS) zwischen den Crew-Mitgliedern. Der Koordinator leitet nur Chiffretext und Metadaten weiter – er sieht nie den Inhalt der Nachrichten.",
|
||||
notLoggedIn: "Melde dich an, um den privaten Chat der Crew zu nutzen.",
|
||||
|
|
|
|||
|
|
@ -282,6 +282,9 @@ const drift = {
|
|||
legendWeatherOnly: "Μόνο καιρός",
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Πρόσκληση',
|
||||
shareInviteText: 'Μπες στο {{crew}} — ιδιωτική συνομιλία',
|
||||
inviteCopied: 'Ο σύνδεσμος πρόσκλησης αντιγράφηκε',
|
||||
title: "Ιδιωτική συνομιλία",
|
||||
subtitle: "End-to-end κρυπτογραφημένη (MLS) μεταξύ των μελών του crew. Ο συντονιστής δρομολογεί μόνο κρυπτογραφημένα δεδομένα και metadata — δεν βλέπει ποτέ το περιεχόμενο των μηνυμάτων.",
|
||||
notLoggedIn: "Συνδέσου για να χρησιμοποιήσεις την ιδιωτική συνομιλία του crew.",
|
||||
|
|
|
|||
|
|
@ -280,6 +280,9 @@ const drift = {
|
|||
legendWeatherOnly: "Météo uniquement",
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Inviter',
|
||||
shareInviteText: 'Rejoins {{crew}} — chat privé du crew',
|
||||
inviteCopied: 'Lien d’invitation copié',
|
||||
title: "Chat privé",
|
||||
subtitle: "Chiffré de bout en bout (MLS) entre les membres de la Crew. Le coordinateur ne route que du texte chiffré et des métadonnées — il ne voit jamais le contenu des messages.",
|
||||
notLoggedIn: "Connectez-vous pour utiliser le chat privé de la Crew.",
|
||||
|
|
|
|||
|
|
@ -274,6 +274,9 @@ const driftStrings = {
|
|||
legendWeatherOnly: 'Cuaca saja',
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Undang',
|
||||
shareInviteText: 'Gabung ke {{crew}} — obrolan pribadi kru',
|
||||
inviteCopied: 'Tautan undangan disalin',
|
||||
title: 'Chat privat',
|
||||
subtitle: 'Terenkripsi ujung-ke-ujung (MLS) antar anggota crew. Koordinator hanya merutekan ciphertext dan metadata — tidak pernah melihat isi pesan.',
|
||||
notLoggedIn: 'Masuk untuk menggunakan chat privat crew.',
|
||||
|
|
|
|||
|
|
@ -280,6 +280,9 @@ const drift = {
|
|||
legendWeatherOnly: "Solo meteo",
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Invita',
|
||||
shareInviteText: 'Unisciti a {{crew}} — chat privata del crew',
|
||||
inviteCopied: 'Link d’invito copiato',
|
||||
title: "Chat privata",
|
||||
subtitle: "Crittografata end-to-end (MLS) tra i membri della crew. Il coordinatore instrada solo testo cifrato e metadati — non vede mai il contenuto dei messaggi.",
|
||||
notLoggedIn: "Accedi per usare la chat privata della crew.",
|
||||
|
|
|
|||
|
|
@ -290,6 +290,9 @@ const drift: Dict = {
|
|||
legendWeatherOnly: '天気のみ',
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: '招待',
|
||||
shareInviteText: '{{crew}} に参加 — クルーのプライベートチャット',
|
||||
inviteCopied: '招待リンクをコピーしました',
|
||||
title: 'プライベートチャット',
|
||||
subtitle: 'クルーメンバー間のエンドツーエンド暗号化(MLS)。コーディネーターは暗号文とメタデータのみをルーティングし、メッセージの内容を見ることはありません。',
|
||||
notLoggedIn: 'クルーのプライベートチャットを使うにはログインしてください。',
|
||||
|
|
|
|||
|
|
@ -290,6 +290,9 @@ const drift: Dict = {
|
|||
legendWeatherOnly: '날씨만',
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: '초대',
|
||||
shareInviteText: '{{crew}} 참여 — 크루 비공개 채팅',
|
||||
inviteCopied: '초대 링크를 복사했습니다',
|
||||
title: '비공개 채팅',
|
||||
subtitle: '크루 멤버 간 종단 간 암호화(MLS). 코디네이터는 암호문과 메타데이터만 라우팅하며 메시지 내용은 절대 보지 않습니다.',
|
||||
notLoggedIn: '크루의 비공개 채팅을 사용하려면 로그인하세요.',
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ const cordnUi = {
|
|||
approve: 'Add',
|
||||
noRequests: 'No pending join requests.',
|
||||
inviteHint: 'Only you are in this chat so far — crew members can tap “Ask to join”, then you approve them here.',
|
||||
shareInvite: 'Invite',
|
||||
shareInviteText: 'Join {{crew}} — private crew chat',
|
||||
inviteCopied: 'Invite link copied',
|
||||
memberCount: '{{count}} in the group',
|
||||
you: 'You',
|
||||
groupRef: 'Group link',
|
||||
|
|
@ -66,6 +69,9 @@ const cordnUi = {
|
|||
approve: 'Añadir',
|
||||
noRequests: 'No hay solicitudes pendientes.',
|
||||
inviteHint: 'De momento solo estás tú en este chat: los miembros pueden pulsar «Pedir entrar» y tú los apruebas aquí.',
|
||||
shareInvite: 'Invitar',
|
||||
shareInviteText: 'Únete a {{crew}} — chat privado de la peña',
|
||||
inviteCopied: 'Enlace de invitación copiado',
|
||||
memberCount: '{{count}} en el grupo',
|
||||
you: 'Tú',
|
||||
groupRef: 'Enlace del grupo',
|
||||
|
|
|
|||
|
|
@ -278,6 +278,9 @@ const drift = {
|
|||
"legendWeatherOnly": "Alleen weer"
|
||||
},
|
||||
"cordnUi": {
|
||||
"shareInvite": 'Uitnodigen',
|
||||
"shareInviteText": 'Sluit je aan bij {{crew}} — privéchat van de crew',
|
||||
"inviteCopied": 'Uitnodigingslink gekopieerd',
|
||||
"title": "Privéchat",
|
||||
"subtitle": "End-to-end versleuteld (MLS) tussen crewleden. De coördinator routeert alleen ciphertext en metadata — hij ziet nooit de inhoud van berichten.",
|
||||
"notLoggedIn": "Log in om de privéchat van de crew te gebruiken.",
|
||||
|
|
|
|||
|
|
@ -278,6 +278,9 @@ const drift = {
|
|||
"legendWeatherOnly": "Tylko pogoda"
|
||||
},
|
||||
"cordnUi": {
|
||||
"shareInvite": 'Zaproś',
|
||||
"shareInviteText": 'Dołącz do {{crew}} — prywatny czat ekipy',
|
||||
"inviteCopied": 'Link zaproszenia skopiowany',
|
||||
"title": "Prywatny czat",
|
||||
"subtitle": "Szyfrowany end-to-end (MLS) między członkami crew. Koordynator przekazuje wyłącznie szyfrogram i metadane — nigdy nie widzi treści wiadomości.",
|
||||
"notLoggedIn": "Zaloguj się, aby korzystać z prywatnego czatu crew.",
|
||||
|
|
|
|||
|
|
@ -280,6 +280,9 @@ const drift = {
|
|||
legendWeatherOnly: "Apenas clima",
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Convidar',
|
||||
shareInviteText: 'Entre em {{crew}} — chat privado do crew',
|
||||
inviteCopied: 'Link de convite copiado',
|
||||
title: "Chat privado",
|
||||
subtitle: "Criptografado de ponta a ponta (MLS) entre os membros da crew. O coordenador roteia apenas texto cifrado e metadados — nunca vê o conteúdo das mensagens.",
|
||||
notLoggedIn: "Faça login para usar o chat privado da crew.",
|
||||
|
|
|
|||
|
|
@ -278,6 +278,9 @@ const drift = {
|
|||
"legendWeatherOnly": "Doar vremea"
|
||||
},
|
||||
"cordnUi": {
|
||||
"shareInvite": 'Invită',
|
||||
"shareInviteText": 'Alătură-te la {{crew}} — chat privat al echipei',
|
||||
"inviteCopied": 'Link de invitație copiat',
|
||||
"title": "Chat privat",
|
||||
"subtitle": "Criptat end-to-end (MLS) între membrii crew-ului. Coordonatorul transportă doar text cifrat și metadate — nu vede niciodată conținutul mesajelor.",
|
||||
"notLoggedIn": "Autentifică-te pentru a folosi chatul privat al crew-ului.",
|
||||
|
|
|
|||
|
|
@ -282,6 +282,9 @@ const drift = {
|
|||
legendWeatherOnly: "Только погода",
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Пригласить',
|
||||
shareInviteText: 'Присоединяйся к {{crew}} — приватный чат команды',
|
||||
inviteCopied: 'Ссылка-приглашение скопирована',
|
||||
title: "Приватный чат",
|
||||
subtitle: "Сквозное шифрование (MLS) между участниками Crew. Координатор маршрутизирует только шифротекст и метаданные — он никогда не видит содержимое сообщений.",
|
||||
notLoggedIn: "Войдите, чтобы использовать приватный чат Crew.",
|
||||
|
|
|
|||
|
|
@ -282,6 +282,9 @@ const drift = {
|
|||
legendWeatherOnly: "Endast väder",
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Bjud in',
|
||||
shareInviteText: 'Gå med i {{crew}} — privat crewchatt',
|
||||
inviteCopied: 'Inbjudningslänk kopierad',
|
||||
title: "Privat chatt",
|
||||
subtitle: "End-to-end-krypterad (MLS) mellan crewmedlemmar. Koordinatorn dirigerar bara chiffertext och metadata — den ser aldrig meddelandeinnehåll.",
|
||||
notLoggedIn: "Logga in för att använda crewets privata chatt.",
|
||||
|
|
|
|||
|
|
@ -274,6 +274,9 @@ const driftStrings = {
|
|||
legendWeatherOnly: 'เฉพาะสภาพอากาศ',
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'เชิญ',
|
||||
shareInviteText: 'เข้าร่วม {{crew}} — แชทส่วนตัวของ crew',
|
||||
inviteCopied: 'คัดลอกลิงก์เชิญแล้ว',
|
||||
title: 'แชทส่วนตัว',
|
||||
subtitle: 'เข้ารหัสต้นทาง-ปลายทาง (MLS) ระหว่างสมาชิก crew coordinator เพียงส่งต่อข้อความเข้ารหัสและ metadata — ไม่เคยเห็นเนื้อหาข้อความ',
|
||||
notLoggedIn: 'เข้าสู่ระบบเพื่อใช้แชทส่วนตัวของ crew',
|
||||
|
|
|
|||
|
|
@ -274,6 +274,9 @@ const driftStrings = {
|
|||
legendWeatherOnly: 'Yalnızca hava',
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Davet et',
|
||||
shareInviteText: '{{crew}} grubuna katıl — özel ekip sohbeti',
|
||||
inviteCopied: 'Davet bağlantısı kopyalandı',
|
||||
title: 'Özel sohbet',
|
||||
subtitle: 'Crew üyeleri arasında uçtan uca şifreli (MLS). Koordinatör yalnızca şifreli metni ve üst veriyi yönlendirir — mesaj içeriğini asla görmez.',
|
||||
notLoggedIn: "Crew'in özel sohbetini kullanmak için giriş yap.",
|
||||
|
|
|
|||
|
|
@ -282,6 +282,9 @@ const drift = {
|
|||
legendWeatherOnly: "Лише погода",
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Запросити',
|
||||
shareInviteText: 'Долучайся до {{crew}} — приватний чат команди',
|
||||
inviteCopied: 'Посилання-запрошення скопійовано',
|
||||
title: "Приватний чат",
|
||||
subtitle: "Наскрізне шифрування (MLS) між учасниками команди. Координатор маршрутизує лише шифротекст і метадані — він ніколи не бачить вміст повідомлень.",
|
||||
notLoggedIn: "Увійдіть, щоб користуватися приватним чатом команди.",
|
||||
|
|
|
|||
|
|
@ -274,6 +274,9 @@ const driftStrings = {
|
|||
legendWeatherOnly: 'Chỉ thời tiết',
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: 'Mời',
|
||||
shareInviteText: 'Tham gia {{crew}} — trò chuyện riêng của crew',
|
||||
inviteCopied: 'Đã sao chép liên kết mời',
|
||||
title: 'Chat riêng tư',
|
||||
subtitle: 'Mã hóa đầu-cuối (MLS) giữa các thành viên crew. Điều phối viên chỉ định tuyến bản mã và metadata — không bao giờ thấy nội dung tin nhắn.',
|
||||
notLoggedIn: 'Đăng nhập để dùng chat riêng tư của crew.',
|
||||
|
|
|
|||
|
|
@ -290,6 +290,9 @@ const drift: Dict = {
|
|||
legendWeatherOnly: '仅天气',
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: '邀请',
|
||||
shareInviteText: '加入 {{crew}} — 团队私密聊天',
|
||||
inviteCopied: '邀请链接已复制',
|
||||
title: '私密聊天',
|
||||
subtitle: 'Crew 成员之间的端到端加密(MLS)。协调器只路由密文和元数据——它永远看不到消息内容。',
|
||||
notLoggedIn: '登录以使用 Crew 的私密聊天。',
|
||||
|
|
|
|||
|
|
@ -290,6 +290,9 @@ const drift: Dict = {
|
|||
legendWeatherOnly: '僅天氣',
|
||||
},
|
||||
cordnUi: {
|
||||
shareInvite: '邀請',
|
||||
shareInviteText: '加入 {{crew}} — 團隊私密聊天',
|
||||
inviteCopied: '邀請連結已複製',
|
||||
title: '私密聊天',
|
||||
subtitle: 'Crew 成員之間的端到端加密(MLS)。協調器只路由密文和中繼資料——它永遠看不到訊息內容。',
|
||||
notLoggedIn: '登入以使用 Crew 的私密聊天。',
|
||||
|
|
|
|||
Loading…
Reference in a new issue