Add new-chat compose flow to /messages with inline recipient autocomplete
Adds a 'New message' button to the conversation list (and the empty-pane prompt) that opens a fresh compose pane in the right column instead of a modal. The pane has a 'To:' field that drives debounced profile search via useSearchProfiles (name / NIP-05 / npub / nprofile), rendering suggestions inline beneath the field — no popover. Arrow-key navigation + Enter select a recipient; choosing one opens the normal thread (existing conversation or a fresh blank thread). Adds the supporting locale strings across all locales.
This commit is contained in:
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "لا توجد رسائل من الأشخاص الذين تتابعهم.",
|
||||
"translateDialogTitle": "هل تريد ترجمة الرسائل الخاصة؟",
|
||||
"translateDialogDescription": "سيتم إرسال الرسائل المستلمة إلى خدمة الترجمة التي قمت بتكوينها. لن تتم ترجمة الرسائل المرسلة. سيتذكر Agora هذا الاختيار على هذا الجهاز.",
|
||||
"confirmTranslate": "ترجمة الرسائل"
|
||||
"confirmTranslate": "ترجمة الرسائل",
|
||||
"newMessage": "رسالة جديدة",
|
||||
"newMessageTitle": "رسالة جديدة",
|
||||
"recipientLabel": "إلى",
|
||||
"recipientPlaceholder": "ابحث بالاسم أو NIP-05 أو npub…",
|
||||
"recipientHint": "ابحث عن شخص لبدء محادثة جديدة.",
|
||||
"recipientSearching": "جارٍ البحث…",
|
||||
"noRecipientResults": "لم يُعثر على أشخاص. جرّب اسمًا أو NIP-05 أو npub مختلفًا.",
|
||||
"recipientFollowed": "أنت تتابع هذا الشخص"
|
||||
},
|
||||
"auth": {
|
||||
"join": "انضمام",
|
||||
|
||||
+9
-1
@@ -123,7 +123,15 @@
|
||||
"friendsEmpty": "No messages from people you follow.",
|
||||
"translateDialogTitle": "Translate private messages?",
|
||||
"translateDialogDescription": "Received messages will be sent to your configured translation service. Sent messages will not be translated. Agora will remember this choice on this device.",
|
||||
"confirmTranslate": "Translate messages"
|
||||
"confirmTranslate": "Translate messages",
|
||||
"newMessage": "New message",
|
||||
"newMessageTitle": "New message",
|
||||
"recipientLabel": "To",
|
||||
"recipientPlaceholder": "Search by name, NIP-05, or npub…",
|
||||
"recipientHint": "Search for someone to start a new conversation.",
|
||||
"recipientSearching": "Searching…",
|
||||
"noRecipientResults": "No people found. Try a different name, NIP-05, or npub.",
|
||||
"recipientFollowed": "You follow this person"
|
||||
},
|
||||
"auth": {
|
||||
"join": "Join",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "No hay mensajes de personas que sigues.",
|
||||
"translateDialogTitle": "¿Traducir mensajes privados?",
|
||||
"translateDialogDescription": "Los mensajes recibidos se enviarán al servicio de traducción que configuraste. Los mensajes enviados no se traducirán. Agora recordará esta elección en este dispositivo.",
|
||||
"confirmTranslate": "Traducir mensajes"
|
||||
"confirmTranslate": "Traducir mensajes",
|
||||
"newMessage": "Mensaje nuevo",
|
||||
"newMessageTitle": "Mensaje nuevo",
|
||||
"recipientLabel": "Para",
|
||||
"recipientPlaceholder": "Busca por nombre, NIP-05 o npub…",
|
||||
"recipientHint": "Busca a alguien para iniciar una nueva conversación.",
|
||||
"recipientSearching": "Buscando…",
|
||||
"noRecipientResults": "No se encontraron personas. Prueba con otro nombre, NIP-05 o npub.",
|
||||
"recipientFollowed": "Sigues a esta persona"
|
||||
},
|
||||
"auth": {
|
||||
"join": "Unirse",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "پیامی از افرادی که دنبال میکنید وجود ندارد.",
|
||||
"translateDialogTitle": "پیامهای خصوصی ترجمه شوند؟",
|
||||
"translateDialogDescription": "پیامهای دریافتی به سرویس ترجمه تنظیمشده شما ارسال میشوند. پیامهای ارسالی ترجمه نخواهند شد. Agora این انتخاب را در این دستگاه به خاطر میسپارد.",
|
||||
"confirmTranslate": "ترجمه پیامها"
|
||||
"confirmTranslate": "ترجمه پیامها",
|
||||
"newMessage": "پیام جدید",
|
||||
"newMessageTitle": "پیام جدید",
|
||||
"recipientLabel": "به",
|
||||
"recipientPlaceholder": "جستجو بر اساس نام، NIP-05 یا npub…",
|
||||
"recipientHint": "برای شروع یک گفتگوی جدید، کسی را جستجو کنید.",
|
||||
"recipientSearching": "در حال جستجو…",
|
||||
"noRecipientResults": "کسی یافت نشد. نام، NIP-05 یا npub دیگری را امتحان کنید.",
|
||||
"recipientFollowed": "شما این فرد را دنبال میکنید"
|
||||
},
|
||||
"auth": {
|
||||
"join": "پیوستن",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "Aucun message des personnes que vous suivez.",
|
||||
"translateDialogTitle": "Traduire les messages privés ?",
|
||||
"translateDialogDescription": "Les messages reçus seront envoyés au service de traduction configuré. Les messages envoyés ne seront pas traduits. Agora mémorisera ce choix sur cet appareil.",
|
||||
"confirmTranslate": "Traduire les messages"
|
||||
"confirmTranslate": "Traduire les messages",
|
||||
"newMessage": "Nouveau message",
|
||||
"newMessageTitle": "Nouveau message",
|
||||
"recipientLabel": "À",
|
||||
"recipientPlaceholder": "Rechercher par nom, NIP-05 ou npub…",
|
||||
"recipientHint": "Recherchez quelqu'un pour démarrer une nouvelle conversation.",
|
||||
"recipientSearching": "Recherche en cours…",
|
||||
"noRecipientResults": "Aucune personne trouvée. Essayez un autre nom, NIP-05 ou npub.",
|
||||
"recipientFollowed": "Vous suivez cette personne"
|
||||
},
|
||||
"auth": {
|
||||
"join": "Rejoindre",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "जिन लोगों को आप फ़ॉलो करते हैं उनसे कोई संदेश नहीं।",
|
||||
"translateDialogTitle": "निजी संदेशों का अनुवाद करें?",
|
||||
"translateDialogDescription": "प्राप्त संदेश आपकी कॉन्फ़िगर की गई अनुवाद सेवा को भेजे जाएंगे। भेजे गए संदेशों का अनुवाद नहीं किया जाएगा। Agora इस डिवाइस पर यह विकल्प याद रखेगा।",
|
||||
"confirmTranslate": "संदेशों का अनुवाद करें"
|
||||
"confirmTranslate": "संदेशों का अनुवाद करें",
|
||||
"newMessage": "नया संदेश",
|
||||
"newMessageTitle": "नया संदेश",
|
||||
"recipientLabel": "को",
|
||||
"recipientPlaceholder": "नाम, NIP-05, या npub से खोजें…",
|
||||
"recipientHint": "नई बातचीत शुरू करने के लिए किसी को खोजें।",
|
||||
"recipientSearching": "खोज रहे हैं…",
|
||||
"noRecipientResults": "कोई व्यक्ति नहीं मिला। कोई दूसरा नाम, NIP-05, या npub आज़माएं।",
|
||||
"recipientFollowed": "आप इस व्यक्ति को फ़ॉलो करते हैं"
|
||||
},
|
||||
"auth": {
|
||||
"join": "जुड़ें",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "Tidak ada pesan dari orang yang Anda ikuti.",
|
||||
"translateDialogTitle": "Terjemahkan pesan pribadi?",
|
||||
"translateDialogDescription": "Pesan yang diterima akan dikirim ke layanan terjemahan yang Anda konfigurasi. Pesan terkirim tidak akan diterjemahkan. Agora akan mengingat pilihan ini di perangkat ini.",
|
||||
"confirmTranslate": "Terjemahkan pesan"
|
||||
"confirmTranslate": "Terjemahkan pesan",
|
||||
"newMessage": "Pesan baru",
|
||||
"newMessageTitle": "Pesan baru",
|
||||
"recipientLabel": "Kepada",
|
||||
"recipientPlaceholder": "Cari berdasarkan nama, NIP-05, atau npub…",
|
||||
"recipientHint": "Cari seseorang untuk memulai percakapan baru.",
|
||||
"recipientSearching": "Mencari…",
|
||||
"noRecipientResults": "Tidak ada orang ditemukan. Coba nama, NIP-05, atau npub yang berbeda.",
|
||||
"recipientFollowed": "Anda mengikuti orang ini"
|
||||
},
|
||||
"auth": {
|
||||
"join": "Gabung",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "គ្មានសារពីមនុស្សដែលអ្នកតាមដានទេ។",
|
||||
"translateDialogTitle": "បកប្រែសារឯកជនឬ?",
|
||||
"translateDialogDescription": "សារដែលបានទទួលនឹងត្រូវបានផ្ញើទៅសេវាបកប្រែដែលអ្នកបានកំណត់។ សារដែលបានផ្ញើនឹងមិនត្រូវបានបកប្រែទេ។ Agora នឹងចងចាំជម្រើសនេះនៅលើឧបករណ៍នេះ។",
|
||||
"confirmTranslate": "បកប្រែសារ"
|
||||
"confirmTranslate": "បកប្រែសារ",
|
||||
"newMessage": "សារថ្មី",
|
||||
"newMessageTitle": "សារថ្មី",
|
||||
"recipientLabel": "ទៅ",
|
||||
"recipientPlaceholder": "ស្វែងរកតាមឈ្មោះ, NIP-05, ឬ npub…",
|
||||
"recipientHint": "ស្វែងរកនរណាម្នាក់ដើម្បីចាប់ផ្តើមការសន្ទនាថ្មី។",
|
||||
"recipientSearching": "កំពុងស្វែងរក…",
|
||||
"noRecipientResults": "រកមិនឃើញនរណាម្នាក់ទេ។ សាកល្បងឈ្មោះ, NIP-05, ឬ npub ផ្សេង។",
|
||||
"recipientFollowed": "អ្នកតាមដានមនុស្សនេះ"
|
||||
},
|
||||
"auth": {
|
||||
"join": "ចូលរួម",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "د هغو خلکو څخه پیغامونه نشته چې تاسو یې تعقیبوئ.",
|
||||
"translateDialogTitle": "شخصي پیغامونه وژباړل شي؟",
|
||||
"translateDialogDescription": "ترلاسه شوي پیغامونه به ستاسو ټاکل شوي ژباړې خدمت ته واستول شي. لېږل شوي پیغامونه به نه ژباړل کېږي. Agora به دا انتخاب په دې وسیله کې په یاد وساتي.",
|
||||
"confirmTranslate": "پیغامونه وژباړئ"
|
||||
"confirmTranslate": "پیغامونه وژباړئ",
|
||||
"newMessage": "نوی پیغام",
|
||||
"newMessageTitle": "نوی پیغام",
|
||||
"recipientLabel": "ته",
|
||||
"recipientPlaceholder": "د نوم، NIP-05 یا npub له مخې لټون وکړئ…",
|
||||
"recipientHint": "د نوې خبرې اترې پیلولو لپاره څوک ولټوئ.",
|
||||
"recipientSearching": "په لټون کې…",
|
||||
"noRecipientResults": "هیڅ خلک ونه موندل شول. بل نوم، NIP-05 یا npub هڅه وکړئ.",
|
||||
"recipientFollowed": "تاسو دا کس تعقیبوئ"
|
||||
},
|
||||
"auth": {
|
||||
"join": "ګډون",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "Não há mensagens de pessoas que você segue.",
|
||||
"translateDialogTitle": "Traduzir mensagens privadas?",
|
||||
"translateDialogDescription": "As mensagens recebidas serão enviadas ao serviço de tradução configurado. As mensagens enviadas não serão traduzidas. O Agora lembrará esta escolha neste dispositivo.",
|
||||
"confirmTranslate": "Traduzir mensagens"
|
||||
"confirmTranslate": "Traduzir mensagens",
|
||||
"newMessage": "Nova mensagem",
|
||||
"newMessageTitle": "Nova mensagem",
|
||||
"recipientLabel": "Para",
|
||||
"recipientPlaceholder": "Pesquisar por nome, NIP-05 ou npub…",
|
||||
"recipientHint": "Pesquise alguém para iniciar uma nova conversa.",
|
||||
"recipientSearching": "Pesquisando…",
|
||||
"noRecipientResults": "Nenhuma pessoa encontrada. Tente um nome, NIP-05 ou npub diferente.",
|
||||
"recipientFollowed": "Você segue esta pessoa"
|
||||
},
|
||||
"auth": {
|
||||
"join": "Entrar",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "Нет сообщений от людей, на которых вы подписаны.",
|
||||
"translateDialogTitle": "Переводить личные сообщения?",
|
||||
"translateDialogDescription": "Полученные сообщения будут отправляться в настроенный вами сервис перевода. Отправленные сообщения переводиться не будут. Agora запомнит этот выбор на этом устройстве.",
|
||||
"confirmTranslate": "Переводить сообщения"
|
||||
"confirmTranslate": "Переводить сообщения",
|
||||
"newMessage": "Новое сообщение",
|
||||
"newMessageTitle": "Новое сообщение",
|
||||
"recipientLabel": "Кому",
|
||||
"recipientPlaceholder": "Поиск по имени, NIP-05 или npub…",
|
||||
"recipientHint": "Найдите кого-нибудь, чтобы начать новую беседу.",
|
||||
"recipientSearching": "Поиск…",
|
||||
"noRecipientResults": "Никто не найден. Попробуйте другое имя, NIP-05 или npub.",
|
||||
"recipientFollowed": "Вы подписаны на этого человека"
|
||||
},
|
||||
"auth": {
|
||||
"join": "Присоединиться",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "Hapana mameseji kubva kuvanhu vaunotevera.",
|
||||
"translateDialogTitle": "Dudzira mameseji epachivande?",
|
||||
"translateDialogDescription": "Mameseji agamuchirwa achaendeswa kusevhisi yako yekushandura yawakagadzirisa. Mameseji atumirwa haazodudzirwi. Agora icharangarira sarudzo iyi pachigadzirwa ichi.",
|
||||
"confirmTranslate": "Dudzira mameseji"
|
||||
"confirmTranslate": "Dudzira mameseji",
|
||||
"newMessage": "Meseji itsva",
|
||||
"newMessageTitle": "Meseji itsva",
|
||||
"recipientLabel": "Kuna",
|
||||
"recipientPlaceholder": "Tsvaga nezita, NIP-05, kana npub…",
|
||||
"recipientHint": "Tsvaga munhu kuti utange nhaurirano itsva.",
|
||||
"recipientSearching": "Kutsvaga…",
|
||||
"noRecipientResults": "Hapana vanhu vawanikwa. Edza rimwe zita, NIP-05, kana npub.",
|
||||
"recipientFollowed": "Unotevera munhu uyu"
|
||||
},
|
||||
"auth": {
|
||||
"join": "Joinha",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "Hakuna ujumbe kutoka kwa watu unaowafuata.",
|
||||
"translateDialogTitle": "Tafsiri ujumbe wa faragha?",
|
||||
"translateDialogDescription": "Ujumbe uliopokelewa utatumwa kwa huduma yako ya tafsiri uliyosanidi. Ujumbe uliotumwa hautatafsiriwa. Agora itakumbuka chaguo hili kwenye kifaa hiki.",
|
||||
"confirmTranslate": "Tafsiri ujumbe"
|
||||
"confirmTranslate": "Tafsiri ujumbe",
|
||||
"newMessage": "Ujumbe mpya",
|
||||
"newMessageTitle": "Ujumbe mpya",
|
||||
"recipientLabel": "Kwa",
|
||||
"recipientPlaceholder": "Tafuta kwa jina, NIP-05, au npub…",
|
||||
"recipientHint": "Tafuta mtu ili kuanza mazungumzo mapya.",
|
||||
"recipientSearching": "Inatafuta…",
|
||||
"noRecipientResults": "Hakuna watu waliopatikana. Jaribu jina, NIP-05, au npub tofauti.",
|
||||
"recipientFollowed": "Unamfuata mtu huyu"
|
||||
},
|
||||
"auth": {
|
||||
"join": "Jiunge",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "Takip ettiğiniz kişilerden mesaj yok.",
|
||||
"translateDialogTitle": "Özel mesajlar çevrilsin mi?",
|
||||
"translateDialogDescription": "Alınan mesajlar yapılandırdığınız çeviri hizmetine gönderilecek. Gönderilen mesajlar çevrilmeyecek. Agora bu tercihi bu cihazda hatırlayacak.",
|
||||
"confirmTranslate": "Mesajları çevir"
|
||||
"confirmTranslate": "Mesajları çevir",
|
||||
"newMessage": "Yeni mesaj",
|
||||
"newMessageTitle": "Yeni mesaj",
|
||||
"recipientLabel": "Kime",
|
||||
"recipientPlaceholder": "İsim, NIP-05 veya npub ile ara…",
|
||||
"recipientHint": "Yeni bir sohbet başlatmak için birini arayın.",
|
||||
"recipientSearching": "Aranıyor…",
|
||||
"noRecipientResults": "Kimse bulunamadı. Farklı bir isim, NIP-05 veya npub deneyin.",
|
||||
"recipientFollowed": "Bu kişiyi takip ediyorsunuz"
|
||||
},
|
||||
"auth": {
|
||||
"join": "Katıl",
|
||||
|
||||
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "沒有來自您追蹤的人的訊息。",
|
||||
"translateDialogTitle": "要翻譯私人訊息嗎?",
|
||||
"translateDialogDescription": "收到的訊息將會傳送至你設定的翻譯服務。已傳送的訊息不會被翻譯。Agora 會在此裝置上記住這個選擇。",
|
||||
"confirmTranslate": "翻譯訊息"
|
||||
"confirmTranslate": "翻譯訊息",
|
||||
"newMessage": "新訊息",
|
||||
"newMessageTitle": "新訊息",
|
||||
"recipientLabel": "收件人",
|
||||
"recipientPlaceholder": "按名稱、NIP-05 或 npub 搜尋…",
|
||||
"recipientHint": "搜尋某人以開始新的對話。",
|
||||
"recipientSearching": "正在搜尋…",
|
||||
"noRecipientResults": "找不到任何人。請嘗試其他名稱、NIP-05 或 npub。",
|
||||
"recipientFollowed": "你追蹤了此人"
|
||||
},
|
||||
"auth": {
|
||||
"join": "加入",
|
||||
|
||||
+9
-1
@@ -122,7 +122,15 @@
|
||||
"friendsEmpty": "没有来自您关注的人的消息。",
|
||||
"translateDialogTitle": "要翻译私信吗?",
|
||||
"translateDialogDescription": "收到的消息将发送到你配置的翻译服务。已发送的消息不会被翻译。Agora 会在此设备上记住此选择。",
|
||||
"confirmTranslate": "翻译消息"
|
||||
"confirmTranslate": "翻译消息",
|
||||
"newMessage": "新消息",
|
||||
"newMessageTitle": "新消息",
|
||||
"recipientLabel": "收件人",
|
||||
"recipientPlaceholder": "按名称、NIP-05 或 npub 搜索…",
|
||||
"recipientHint": "搜索某人以开始新的对话。",
|
||||
"recipientSearching": "正在搜索…",
|
||||
"noRecipientResults": "未找到任何人。请尝试其他名称、NIP-05 或 npub。",
|
||||
"recipientFollowed": "你关注了此人"
|
||||
},
|
||||
"auth": {
|
||||
"join": "加入",
|
||||
|
||||
+198
-4
@@ -3,7 +3,8 @@ import { useEffect, useMemo, useRef, useState, type FormEvent, type KeyboardEven
|
||||
import { Link, Navigate, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { ArrowLeft, ArrowUp, BellOff, Inbox, Loader2, Lock, MessageSquare, Search, UserCheck, X } from 'lucide-react';
|
||||
import { ArrowLeft, ArrowUp, BellOff, Inbox, Loader2, Lock, MessageSquare, PenSquare, Search, UserCheck, X } from 'lucide-react';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
import { useFollowList } from '@/hooks/useFollowActions';
|
||||
import { useMuteList } from '@/hooks/useMuteList';
|
||||
import { useProfileUrl } from '@/hooks/useProfileUrl';
|
||||
import { useSearchProfiles, type SearchProfile } from '@/hooks/useSearchProfiles';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useVerifierStatement } from '@/hooks/useVerifierStatement';
|
||||
import { getDisplayName } from '@/lib/genUserName';
|
||||
@@ -523,6 +525,164 @@ function MessageThread({
|
||||
);
|
||||
}
|
||||
|
||||
/** A single recipient suggestion row inside the new-message pane. */
|
||||
function RecipientSuggestion({
|
||||
profile,
|
||||
active,
|
||||
followed,
|
||||
onSelect,
|
||||
}: {
|
||||
profile: SearchProfile;
|
||||
active: boolean;
|
||||
followed: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const name = getDisplayName(profile.metadata, profile.pubkey);
|
||||
const picture = sanitizeUrl(profile.metadata.picture);
|
||||
const handle = profile.metadata.nip05 ?? nip19.npubEncode(profile.pubkey);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSelect}
|
||||
data-active={active}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-3 rounded-2xl border border-transparent p-3 text-left transition-all',
|
||||
'hover:border-border hover:bg-background/85 hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||
active && 'border-primary/20 bg-primary/10 shadow-sm',
|
||||
)}
|
||||
>
|
||||
<Avatar className="size-11 shrink-0 ring-2 ring-background">
|
||||
<AvatarImage src={picture} alt={name} />
|
||||
<AvatarFallback>{name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="truncate text-sm font-medium">{name}</p>
|
||||
{followed && (
|
||||
<UserCheck className="size-3.5 shrink-0 text-primary" aria-label={t('messages.recipientFollowed')} />
|
||||
)}
|
||||
</div>
|
||||
<p className="truncate text-xs text-muted-foreground">{handle}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The "start a new chat" pane. Renders in the right column in place of the
|
||||
* empty-state prompt. A "To:" field drives debounced profile autocomplete
|
||||
* (npub / nprofile / NIP-05 / name) and the suggestions render inline below
|
||||
* the field — no modal, no popover. Selecting a person opens a fresh thread.
|
||||
*/
|
||||
function NewMessagePane({
|
||||
onSelectRecipient,
|
||||
onCancel,
|
||||
}: {
|
||||
onSelectRecipient: (pubkey: string) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [query, setQuery] = useState('');
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const { data: profiles, isFetching, followedPubkeys } = useSearchProfiles(query);
|
||||
const trimmed = query.trim();
|
||||
const results = useMemo(() => profiles ?? [], [profiles]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveIndex(0);
|
||||
}, [results]);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (results.length === 0) return;
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setActiveIndex((i) => (i + 1) % results.length);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setActiveIndex((i) => (i - 1 + results.length) % results.length);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const chosen = results[activeIndex];
|
||||
if (chosen) onSelectRecipient(chosen.pubkey);
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col bg-gradient-to-b from-muted/30 via-background to-background">
|
||||
<div className="flex items-center gap-3 p-4">
|
||||
<Button type="button" variant="ghost" size="icon" className="md:hidden" onClick={onCancel}>
|
||||
<ArrowLeft className="size-4" />
|
||||
<span className="sr-only">{t('messages.conversations')}</span>
|
||||
</Button>
|
||||
<p className="min-w-0 flex-1 truncate font-semibold">{t('messages.newMessageTitle')}</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hidden shrink-0 md:inline-flex"
|
||||
onClick={onCancel}
|
||||
aria-label={t('common.cancel')}
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-3">
|
||||
<label htmlFor="dm-recipient" className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
||||
{t('messages.recipientLabel')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
id="dm-recipient"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('messages.recipientPlaceholder')}
|
||||
aria-label={t('messages.recipientLabel')}
|
||||
className="h-11 rounded-full border-0 bg-muted/40 pl-9 pr-9 text-base shadow-sm focus-visible:ring-2 focus-visible:ring-ring md:text-sm"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
{isFetching && (
|
||||
<Loader2 className="absolute right-3 top-1/2 size-4 -translate-y-1/2 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="min-h-0 flex-1 px-4">
|
||||
<div className="space-y-1.5 pb-5">
|
||||
{results.length > 0 ? (
|
||||
results.map((profile, index) => (
|
||||
<RecipientSuggestion
|
||||
key={profile.pubkey}
|
||||
profile={profile}
|
||||
active={index === activeIndex}
|
||||
followed={followedPubkeys.has(profile.pubkey)}
|
||||
onSelect={() => onSelectRecipient(profile.pubkey)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 px-6 py-12 text-center">
|
||||
<PenSquare className="size-9 text-primary" />
|
||||
<p className="mx-auto max-w-xs text-sm text-muted-foreground">
|
||||
{trimmed
|
||||
? (isFetching ? t('messages.recipientSearching') : t('messages.noRecipientResults'))
|
||||
: t('messages.recipientHint')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MessagesPage() {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -540,6 +700,7 @@ export function MessagesPage() {
|
||||
const { muteItems } = useMuteList();
|
||||
const requestedPeer = getValidPubkey(searchParams.get('to'));
|
||||
const [selectedPeer, setSelectedPeer] = useState<string | null>(() => requestedPeer);
|
||||
const [composing, setComposing] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [conversationFilter, setConversationFilter] = useState<'all' | 'friends'>('all');
|
||||
const [hiddenMutedPeers, setHiddenMutedPeers] = useState<Set<string>>(() => new Set());
|
||||
@@ -578,6 +739,7 @@ export function MessagesPage() {
|
||||
|
||||
const selectPeer = (peer: string) => {
|
||||
setSelectedPeer(peer);
|
||||
setComposing(false);
|
||||
setSearchParams((current) => {
|
||||
const next = new URLSearchParams(current);
|
||||
next.delete('to');
|
||||
@@ -587,6 +749,7 @@ export function MessagesPage() {
|
||||
|
||||
const clearSelectedPeer = () => {
|
||||
setSelectedPeer(null);
|
||||
setComposing(false);
|
||||
setSearchParams((current) => {
|
||||
const next = new URLSearchParams(current);
|
||||
next.delete('to');
|
||||
@@ -631,8 +794,8 @@ export function MessagesPage() {
|
||||
<div
|
||||
className={cn(
|
||||
'h-full min-h-0 flex-col bg-muted/40',
|
||||
selectedThread && 'hidden md:flex',
|
||||
!selectedThread && 'flex',
|
||||
(selectedThread || composing) && 'hidden md:flex',
|
||||
!(selectedThread || composing) && 'flex',
|
||||
)}
|
||||
>
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
@@ -659,6 +822,20 @@ export function MessagesPage() {
|
||||
>
|
||||
{conversationFilter === 'all' ? <Inbox className="size-4" /> : <UserCheck className="size-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="icon"
|
||||
className="size-10 shrink-0 rounded-full"
|
||||
onClick={() => {
|
||||
setSelectedPeer(null);
|
||||
setComposing(true);
|
||||
}}
|
||||
aria-label={t('messages.newMessage')}
|
||||
title={t('messages.newMessage')}
|
||||
>
|
||||
<PenSquare className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
@@ -697,9 +874,14 @@ export function MessagesPage() {
|
||||
</div>
|
||||
|
||||
{/* Thread */}
|
||||
<div className={cn('h-full min-h-0 min-w-0', !selectedThread && 'hidden md:block')}>
|
||||
<div className={cn('h-full min-h-0 min-w-0', !(selectedThread || composing) && 'hidden md:block')}>
|
||||
{selectedThread ? (
|
||||
<MessageThread conversation={selectedThread} onBack={clearSelectedPeer} onMuted={handleMuted} />
|
||||
) : composing ? (
|
||||
<NewMessagePane
|
||||
onSelectRecipient={selectPeer}
|
||||
onCancel={() => setComposing(false)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center bg-gradient-to-br from-background via-muted/20 to-primary/5 p-8 text-center">
|
||||
<div className="max-w-sm space-y-3">
|
||||
@@ -707,6 +889,18 @@ export function MessagesPage() {
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('messages.selectPrompt')}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
onClick={() => {
|
||||
setSelectedPeer(null);
|
||||
setComposing(true);
|
||||
}}
|
||||
>
|
||||
<PenSquare className="mr-2 size-4" />
|
||||
{t('messages.newMessage')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user