diff --git a/meshchatx/src/backend/lxmf_utils.py b/meshchatx/src/backend/lxmf_utils.py index a59207c..9256b3e 100644 --- a/meshchatx/src/backend/lxmf_utils.py +++ b/meshchatx/src/backend/lxmf_utils.py @@ -152,7 +152,7 @@ def lxmf_sidebar_preview_for_conversation_latest_row( local_hash: str, peer_display_name: str, ) -> str: - """Single-line preview for conversation list APIs (reactions have empty body).""" + """Single-line preview for conversation list APIs (reactions and some media have empty body).""" content = row.get("content") if content is not None and str(content).strip(): return str(content) @@ -202,6 +202,29 @@ def lxmf_sidebar_preview_for_conversation_latest_row( return f"{actor} requested your location" return f"{actor} sent a location request" + image = fields.get("image") + if isinstance(image, dict) and image: + if actor == "You": + return "You sent an image" + return f"{actor} sent an image" + + audio = fields.get("audio") + if isinstance(audio, dict) and audio: + if actor == "You": + return "You sent a voice note" + return f"{actor} sent a voice note" + + file_attachments = fields.get("file_attachments") + if isinstance(file_attachments, list) and len(file_attachments) > 0: + n = len(file_attachments) + if n == 1: + if actor == "You": + return "You sent a file" + return f"{actor} sent a file" + if actor == "You": + return f"You sent {n} files" + return f"{actor} sent {n} files" + return str(content or "") diff --git a/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue b/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue index a5df386..f2c6cbf 100644 --- a/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue +++ b/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue @@ -48,7 +48,8 @@ v-if=" selectedPeerPath || selectedPeerSignalMetrics?.snr != null || - selectedPeerLxmfStampInfo?.stamp_cost + selectedPeerLxmfStampInfo?.stamp_cost || + lxmfHasOutboundTicket " class="flex items-center gap-2 min-w-0" > @@ -77,9 +78,30 @@ > - + + import Utils from "../../js/Utils"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; import MaterialDesignIcon from "../MaterialDesignIcon.vue"; import IconButton from "../IconButton.vue"; import LxmfUserIcon from "../LxmfUserIcon.vue"; import ConversationDropDownMenu from "./ConversationDropDownMenu.vue"; +dayjs.extend(relativeTime); + export default { name: "ConversationPeerHeader", components: { @@ -176,6 +202,34 @@ export default { destinationDisplay() { return Utils.formatDestinationHash(this.selectedPeer?.destination_hash); }, + lxmfHasOutboundTicket() { + return this.selectedPeerLxmfStampInfo?.outbound_ticket_expiry != null; + }, + lxmfStampTicketExpiryMs() { + const e = this.selectedPeerLxmfStampInfo?.outbound_ticket_expiry; + if (e == null) { + return null; + } + const n = Number(e); + if (!Number.isFinite(n)) { + return null; + } + return n * 1000; + }, + lxmfStampTicketValid() { + const ms = this.lxmfStampTicketExpiryMs; + if (ms == null) { + return false; + } + return ms > Date.now(); + }, + lxmfStampTicketExpiresRelative() { + const ms = this.lxmfStampTicketExpiryMs; + if (ms == null) { + return ""; + } + return dayjs(ms).fromNow(); + }, }, }; diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue index 1eacb80..f55ce80 100644 --- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue +++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue @@ -3130,9 +3130,7 @@ export default { } ); - // do nothing if response is for a previous request if (seq !== this.lxmfMessagesRequestSequence) { - console.log("ignoring response for previous lxmf messages request"); return; } @@ -4153,12 +4151,35 @@ export default { lxmfImageUrl(hash) { return `/api/v1/lxmf-messages/attachment/${hash}/image`; }, + lxmfDataUrlFromOutboundJobImage(img) { + if (!img?.image_bytes || typeof img.image_bytes !== "string") { + return null; + } + const raw = (img.image_type || "png").toLowerCase().replace(/^image\//, ""); + let mime = "image/png"; + if (raw === "jpg" || raw === "jpeg") { + mime = "image/jpeg"; + } else if (raw === "png" || raw === "gif" || raw === "webp" || raw === "bmp") { + mime = `image/${raw}`; + } else if (raw === "webm") { + mime = "video/webm"; + } else if (raw === "svg" || raw === "svg+xml") { + mime = "image/svg+xml"; + } else { + mime = `image/${raw}`; + } + return `data:${mime};base64,${img.image_bytes}`; + }, pendingOutboundImageSrc(chatItem) { const prev = chatItem.lxmf_message?.fields?.image?._preview_url; if (prev) { return prev; } - return this.lxmfImageUrl(chatItem.lxmf_message.hash); + const h = chatItem.lxmf_message?.hash; + if (typeof h === "string" && h.startsWith("pending-")) { + return ""; + } + return this.lxmfImageUrl(h); }, removePendingOutboundPlaceholder(hash) { if (!hash) { @@ -4836,10 +4857,12 @@ export default { job.pendingHash = pendingHash; const pendingFields = {}; if (job.images.length > 0) { + const previewUrl = + job.imagePreviewUrls[0] || this.lxmfDataUrlFromOutboundJobImage(job.images[0]); pendingFields.image = { image_type: job.images[0].image_type, image_size: job.images[0].image_size, - _preview_url: job.imagePreviewUrls[0], + _preview_url: previewUrl, }; } this.chatItems.push({ diff --git a/meshchatx/src/frontend/js/lxmfConversationPreview.js b/meshchatx/src/frontend/js/lxmfConversationPreview.js index 360d074..ff96c57 100644 --- a/meshchatx/src/frontend/js/lxmfConversationPreview.js +++ b/meshchatx/src/frontend/js/lxmfConversationPreview.js @@ -1,7 +1,8 @@ /** * One-line preview text for the conversation list / sidebar (LXMF latest row). - * Handles plain content, Columba reactions, location/telemetry fields, and - * Sideband location-request commands. Pairs with the Python helper + * Handles plain content, Columba reactions, location/telemetry fields, + * Sideband location-request commands, and media-only payloads (image, audio, + * file attachments). Pairs with the Python helper * lxmf_sidebar_preview_for_conversation_latest_row. */ @@ -102,5 +103,41 @@ export function lxmfConversationListPreview(msg, { myLxmfAddressHash, peerDispla return msg?.is_incoming ? `${name} requested your location` : `${name} sent a location request`; } + const imageField = fields?.image; + if (imageField && typeof imageField === "object" && Object.keys(imageField).length > 0) { + const fromSelf = isOutboundFromSelf(msg, myLxmfAddressHash); + if (typeof t === "function") { + return fromSelf ? t("messages.conversation_image_you") : t("messages.conversation_image_other", { name }); + } + return fromSelf ? "You sent an image" : `${name} sent an image`; + } + + const audioField = fields?.audio; + if (audioField && typeof audioField === "object" && Object.keys(audioField).length > 0) { + const fromSelf = isOutboundFromSelf(msg, myLxmfAddressHash); + if (typeof t === "function") { + return fromSelf ? t("messages.conversation_voice_you") : t("messages.conversation_voice_other", { name }); + } + return fromSelf ? "You sent a voice note" : `${name} sent a voice note`; + } + + const files = fields?.file_attachments; + if (Array.isArray(files) && files.length > 0) { + const fromSelf = isOutboundFromSelf(msg, myLxmfAddressHash); + const n = files.length; + if (typeof t === "function") { + if (n === 1) { + return fromSelf ? t("messages.conversation_file_you") : t("messages.conversation_file_other", { name }); + } + return fromSelf + ? t("messages.conversation_files_you", { count: n }) + : t("messages.conversation_files_other", { name, count: n }); + } + if (n === 1) { + return fromSelf ? "You sent a file" : `${name} sent a file`; + } + return fromSelf ? `You sent ${n} files` : `${name} sent ${n} files`; + } + return raw ?? ""; } diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json index a6bab0b..272c565 100644 --- a/meshchatx/src/frontend/locales/de.json +++ b/meshchatx/src/frontend/locales/de.json @@ -1175,6 +1175,8 @@ "hops_away": "{count} Hops entfernt", "snr": "SNR {snr}", "stamp_cost": "Stempelkosten {cost}", + "stamp_ticket_valid": "Ausgehendes Stempel-Ticket aktiv (sofortiger Versand). Läuft ab {expires}.", + "stamp_ticket_expired": "Stempel-Ticket abgelaufen; die nächste Nachricht kann einen Proof-of-Work erfordern.", "pop_out_chat": "Chat auslagern", "custom_display_name": "Benutzerdefinierter Anzeigename", "stranger_banner_text": "Dieser Peer ist nicht in Ihren Kontakten. Anhänge von Fremden werden blockiert.", @@ -1379,6 +1381,14 @@ "conversation_telemetry_stream_preview": "{name} hat einen Telemetrie-Datenstrom gesendet", "conversation_location_request_in_preview": "{name} hat den Standort angefordert", "conversation_location_request_out_preview": "{name} hat eine Standortanfrage gesendet", + "conversation_image_you": "Du hast ein Bild gesendet", + "conversation_image_other": "{name} hat ein Bild gesendet", + "conversation_voice_you": "Du hast eine Sprachnachricht gesendet", + "conversation_voice_other": "{name} hat eine Sprachnachricht gesendet", + "conversation_file_you": "Du hast eine Datei gesendet", + "conversation_file_other": "{name} hat eine Datei gesendet", + "conversation_files_you": "Du hast {count} Dateien gesendet", + "conversation_files_other": "{name} hat {count} Dateien gesendet", "message_not_found_in_cache": "Nachricht nicht im Cache gefunden" }, "nomadnet": { diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json index e49ece2..ceca817 100644 --- a/meshchatx/src/frontend/locales/en.json +++ b/meshchatx/src/frontend/locales/en.json @@ -1123,6 +1123,8 @@ "hops_away": "{count} hops away", "snr": "SNR {snr}", "stamp_cost": "Stamp Cost {cost}", + "stamp_ticket_valid": "Outbound stamp ticket active (instant send). Expires {expires}.", + "stamp_ticket_expired": "Stamp ticket expired; the next message may require proof-of-work.", "pop_out_chat": "Pop out chat", "more_actions": "More actions", "retry_failed": "Retry failed messages", @@ -1327,6 +1329,14 @@ "conversation_telemetry_stream_preview": "{name} sent a telemetry stream", "conversation_location_request_in_preview": "{name} requested your location", "conversation_location_request_out_preview": "{name} sent a location request", + "conversation_image_you": "You sent an image", + "conversation_image_other": "{name} sent an image", + "conversation_voice_you": "You sent a voice note", + "conversation_voice_other": "{name} sent a voice note", + "conversation_file_you": "You sent a file", + "conversation_file_other": "{name} sent a file", + "conversation_files_you": "You sent {count} files", + "conversation_files_other": "{name} sent {count} files", "message_not_found_in_cache": "Message not found in cache" }, "settings": { diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json index 1a378a4..a65b863 100644 --- a/meshchatx/src/frontend/locales/es.json +++ b/meshchatx/src/frontend/locales/es.json @@ -1123,6 +1123,8 @@ "hops_away": "{count}salta lejos", "snr": "SNR{snr}", "stamp_cost": "Costo de sello{cost}", + "stamp_ticket_valid": "Boleto de sello saliente activo (envío instantáneo). Caduca {expires}.", + "stamp_ticket_expired": "Boleto de sello caducado; el próximo mensaje puede requerir prueba de trabajo.", "pop_out_chat": "Apágalo.", "more_actions": "Más acciones", "retry_failed": "Retry falló mensajes", @@ -1327,6 +1329,14 @@ "conversation_telemetry_stream_preview": "{name} envió un flujo de telemetría", "conversation_location_request_in_preview": "{name} solicitó tu ubicación", "conversation_location_request_out_preview": "{name} envió una solicitud de ubicación", + "conversation_image_you": "Enviaste una imagen", + "conversation_image_other": "{name} envió una imagen", + "conversation_voice_you": "Enviaste una nota de voz", + "conversation_voice_other": "{name} envió una nota de voz", + "conversation_file_you": "Enviaste un archivo", + "conversation_file_other": "{name} envió un archivo", + "conversation_files_you": "Enviaste {count} archivos", + "conversation_files_other": "{name} envió {count} archivos", "message_not_found_in_cache": "Mensaje no encontrado en caché" }, "settings": { diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json index 4f8e03b..17b3a3c 100644 --- a/meshchatx/src/frontend/locales/fr.json +++ b/meshchatx/src/frontend/locales/fr.json @@ -1123,6 +1123,8 @@ "hops_away": "{count}houblon loin", "snr": "SNR{snr}", "stamp_cost": "Coût du timbre{cost}", + "stamp_ticket_valid": "Ticket de timbre sortant actif (envoi instantané). Expire {expires}.", + "stamp_ticket_expired": "Ticket de timbre expiré ; le prochain message peut nécessiter une preuve de travail.", "pop_out_chat": "Clavardage", "more_actions": "Autres actions", "retry_failed": "Réessayer les messages échoués", @@ -1327,6 +1329,14 @@ "conversation_telemetry_stream_preview": "{name} a envoyé un flux de télémétrie", "conversation_location_request_in_preview": "{name} a demandé votre position", "conversation_location_request_out_preview": "{name} a envoyé une demande de position", + "conversation_image_you": "Vous avez envoyé une image", + "conversation_image_other": "{name} a envoyé une image", + "conversation_voice_you": "Vous avez envoyé une note vocale", + "conversation_voice_other": "{name} a envoyé une note vocale", + "conversation_file_you": "Vous avez envoyé un fichier", + "conversation_file_other": "{name} a envoyé un fichier", + "conversation_files_you": "Vous avez envoyé {count} fichiers", + "conversation_files_other": "{name} a envoyé {count} fichiers", "message_not_found_in_cache": "Message non trouvé dans cache" }, "settings": { diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json index 33baf64..29ed0b4 100644 --- a/meshchatx/src/frontend/locales/it.json +++ b/meshchatx/src/frontend/locales/it.json @@ -1175,6 +1175,8 @@ "hops_away": "{count} salti di distanza", "snr": "SNR {snr}", "stamp_cost": "Costo Francobollo {cost}", + "stamp_ticket_valid": "Biglietto di francobollo in uscita attivo (invio istantaneo). Scade {expires}.", + "stamp_ticket_expired": "Biglietto scaduto; il prossimo messaggio potrebbe richiedere proof-of-work.", "pop_out_chat": "Estrai chat", "custom_display_name": "Nome Visualizzato Personalizzato", "stranger_banner_text": "Questo peer non è nei tuoi contatti. Gli allegati da sconosciuti sono bloccati.", @@ -1379,6 +1381,14 @@ "conversation_telemetry_stream_preview": "{name} ha inviato un flusso di telemetria", "conversation_location_request_in_preview": "{name} ha richiesto la tua posizione", "conversation_location_request_out_preview": "{name} ha inviato una richiesta di posizione", + "conversation_image_you": "Hai inviato un'immagine", + "conversation_image_other": "{name} ha inviato un'immagine", + "conversation_voice_you": "Hai inviato una nota vocale", + "conversation_voice_other": "{name} ha inviato una nota vocale", + "conversation_file_you": "Hai inviato un file", + "conversation_file_other": "{name} ha inviato un file", + "conversation_files_you": "Hai inviato {count} file", + "conversation_files_other": "{name} ha inviato {count} file", "message_not_found_in_cache": "Messaggio non trovato nella cache" }, "settings": { diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json index 21f3240..7495832 100644 --- a/meshchatx/src/frontend/locales/nl.json +++ b/meshchatx/src/frontend/locales/nl.json @@ -1123,6 +1123,8 @@ "hops_away": "{count}hop weg", "snr": "SNR{snr}", "stamp_cost": "Postzegelkosten{cost}", + "stamp_ticket_valid": "Uitgaand postzegelticket actief (direct verzenden). Verloopt {expires}.", + "stamp_ticket_expired": "Postzegelticket verlopen; het volgende bericht kan proof-of-work vereisen.", "pop_out_chat": "Pop out chat", "more_actions": "Meer acties", "retry_failed": "Opnieuw proberen van mislukte berichten", @@ -1327,6 +1329,14 @@ "conversation_telemetry_stream_preview": "{name} stuurde een telemetriestroom", "conversation_location_request_in_preview": "{name} vroeg om jouw locatie", "conversation_location_request_out_preview": "{name} stuurde een locatieverzoek", + "conversation_image_you": "Je hebt een afbeelding gestuurd", + "conversation_image_other": "{name} heeft een afbeelding gestuurd", + "conversation_voice_you": "Je hebt een spraakbericht gestuurd", + "conversation_voice_other": "{name} heeft een spraakbericht gestuurd", + "conversation_file_you": "Je hebt een bestand gestuurd", + "conversation_file_other": "{name} heeft een bestand gestuurd", + "conversation_files_you": "Je hebt {count} bestanden gestuurd", + "conversation_files_other": "{name} heeft {count} bestanden gestuurd", "message_not_found_in_cache": "Bericht niet gevonden in cache" }, "settings": { diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json index 72c5b96..7895b31 100644 --- a/meshchatx/src/frontend/locales/ru.json +++ b/meshchatx/src/frontend/locales/ru.json @@ -1175,6 +1175,8 @@ "hops_away": "в {count} прыжках", "snr": "SNR {snr}", "stamp_cost": "Стоимость штампа {cost}", + "stamp_ticket_valid": "Активный исходящий билет штампа (мгновенная отправка). Истекает {expires}.", + "stamp_ticket_expired": "Билет штампа истёк; следующее сообщение может потребовать proof-of-work.", "pop_out_chat": "Открыть в отдельном окне", "custom_display_name": "Свое имя для контакта", "stranger_banner_text": "Этот узел не в ваших контактах. Вложения от незнакомцев заблокированы.", @@ -1379,6 +1381,14 @@ "conversation_telemetry_stream_preview": "{name} отправил(а) поток телеметрии", "conversation_location_request_in_preview": "{name} запросил(а) ваше местоположение", "conversation_location_request_out_preview": "{name} отправил(а) запрос местоположения", + "conversation_image_you": "Вы отправили изображение", + "conversation_image_other": "{name} отправил(а) изображение", + "conversation_voice_you": "Вы отправили голосовое сообщение", + "conversation_voice_other": "{name} отправил(а) голосовое сообщение", + "conversation_file_you": "Вы отправили файл", + "conversation_file_other": "{name} отправил(а) файл", + "conversation_files_you": "Вы отправили файлов: {count}", + "conversation_files_other": "{name} отправил(а) файлов: {count}", "message_not_found_in_cache": "Сообщение не найдено в кэше" }, "nomadnet": { diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json index 0879588..5fbe046 100644 --- a/meshchatx/src/frontend/locales/zh.json +++ b/meshchatx/src/frontend/locales/zh.json @@ -1123,6 +1123,8 @@ "hops_away": "{count} 跳远", "snr": "SNR {snr} dB", "stamp_cost": "邮戳成本 {cost}", + "stamp_ticket_valid": "出站邮戳票据有效(即时发送)。将于 {expires} 过期。", + "stamp_ticket_expired": "邮戳票据已过期;下一条消息可能需要工作量证明。", "pop_out_chat": "弹出聊天", "more_actions": "更多操作", "retry_failed": "重试失败的消息", @@ -1327,6 +1329,14 @@ "conversation_telemetry_stream_preview": "{name} 发送了遥测流", "conversation_location_request_in_preview": "{name} 请求你的位置", "conversation_location_request_out_preview": "{name} 发送了位置请求", + "conversation_image_you": "你发送了一张图片", + "conversation_image_other": "{name} 发送了一张图片", + "conversation_voice_you": "你发送了一条语音", + "conversation_voice_other": "{name} 发送了一条语音", + "conversation_file_you": "你发送了一个文件", + "conversation_file_other": "{name} 发送了一个文件", + "conversation_files_you": "你发送了 {count} 个文件", + "conversation_files_other": "{name} 发送了 {count} 个文件", "message_not_found_in_cache": "缓存中未找到消息" }, "settings": { diff --git a/tests/backend/test_lxmf_utils_extended.py b/tests/backend/test_lxmf_utils_extended.py index a824626..eaee9a9 100644 --- a/tests/backend/test_lxmf_utils_extended.py +++ b/tests/backend/test_lxmf_utils_extended.py @@ -416,3 +416,71 @@ def test_sidebar_preview_telemetry_stream(): peer_display_name="Jordan", ) assert out == "Jordan sent a telemetry stream" + + +def test_sidebar_preview_image_incoming(): + row = { + "content": "", + "fields": json.dumps({"image": {"image_type": "png", "image_size": 12}}), + "is_incoming": 1, + "source_hash": "b" * 32, + } + out = lxmf_sidebar_preview_for_conversation_latest_row( + row, + local_hash="a" * 32, + peer_display_name="Quinn", + ) + assert out == "Quinn sent an image" + + +def test_sidebar_preview_image_outbound_you(): + me = "c" * 32 + row = { + "content": "", + "fields": json.dumps({"image": {"image_type": "jpeg", "image_size": 99}}), + "is_incoming": 0, + "source_hash": me, + } + out = lxmf_sidebar_preview_for_conversation_latest_row( + row, + local_hash=me, + peer_display_name="Pat", + ) + assert out == "You sent an image" + + +def test_sidebar_preview_audio_voice_note(): + row = { + "content": "", + "fields": json.dumps({"audio": {"audio_mode": "opus", "audio_size": 500}}), + "is_incoming": 1, + "source_hash": "b" * 32, + } + out = lxmf_sidebar_preview_for_conversation_latest_row( + row, + local_hash="a" * 32, + peer_display_name="Morgan", + ) + assert out == "Morgan sent a voice note" + + +def test_sidebar_preview_file_attachments_plural(): + row = { + "content": "", + "fields": json.dumps( + { + "file_attachments": [ + {"file_name": "a.txt", "file_size": 1}, + {"file_name": "b.txt", "file_size": 2}, + ], + }, + ), + "is_incoming": 0, + "source_hash": "d" * 32, + } + out = lxmf_sidebar_preview_for_conversation_latest_row( + row, + local_hash="d" * 32, + peer_display_name="Casey", + ) + assert out == "You sent 2 files" diff --git a/tests/backend/test_notification_user_facing_filter.py b/tests/backend/test_notification_user_facing_filter.py index 714a6a1..607499c 100644 --- a/tests/backend/test_notification_user_facing_filter.py +++ b/tests/backend/test_notification_user_facing_filter.py @@ -653,6 +653,10 @@ class TestNotificationsGetUserFacingFilter: body = await self._get(bell_app, unread="true", limit=10) assert body["unread_count"] == 1 assert len(body["notifications"]) == 1 + peer_label = f"peer-{PEER_HASH[:6]}" + assert body["notifications"][0]["latest_message_preview"] == ( + f"{peer_label} sent an image" + ) async def test_badge_count_matches_dropdown_items(self, bell_app): # Mix of user-facing and silent messages across multiple peers must diff --git a/tests/frontend/lxmfConversationPreview.test.js b/tests/frontend/lxmfConversationPreview.test.js index eae2133..6a4cfa0 100644 --- a/tests/frontend/lxmfConversationPreview.test.js +++ b/tests/frontend/lxmfConversationPreview.test.js @@ -96,4 +96,61 @@ describe("lxmfConversationListPreview", () => { ); expect(s).toBe("Riley requested your location"); }); + + it("shows image preview when body is empty", () => { + const s = lxmfConversationListPreview( + { + content: "", + is_incoming: true, + source_hash: peer, + fields: { image: { image_type: "png", image_size: 10 } }, + }, + { myLxmfAddressHash: me, peerDisplayName: "Jo" } + ); + expect(s).toBe("Jo sent an image"); + }); + + it("shows outbound image preview as You", () => { + const s = lxmfConversationListPreview( + { + content: "", + is_incoming: false, + source_hash: me, + fields: { image: { image_type: "webp", image_size: 20 } }, + }, + { myLxmfAddressHash: me, peerDisplayName: "Jo" } + ); + expect(s).toBe("You sent an image"); + }); + + it("shows voice note preview for audio field", () => { + const s = lxmfConversationListPreview( + { + content: "", + is_incoming: true, + source_hash: peer, + fields: { audio: { audio_mode: "opus", audio_size: 100 } }, + }, + { myLxmfAddressHash: me, peerDisplayName: "Max" } + ); + expect(s).toBe("Max sent a voice note"); + }); + + it("shows multiple file attachment preview", () => { + const s = lxmfConversationListPreview( + { + content: "", + is_incoming: false, + source_hash: me, + fields: { + file_attachments: [ + { file_name: "a.bin", file_size: 1 }, + { file_name: "b.bin", file_size: 2 }, + ], + }, + }, + { myLxmfAddressHash: me, peerDisplayName: "Jo" } + ); + expect(s).toBe("You sent 2 files"); + }); });