From 1d4c65ca7ef40df865c3d6c75e6c880655aebc78 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 21 Aug 2026 12:49:41 +0200 Subject: [PATCH 01/14] Chunksc --- src/components/page/channel/TextChannel.tsx | 41 ++++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index e613936..67a2cc1 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -4,7 +4,7 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { StoredMessage } from "@/lib/types"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; export default function TextChannel({ appRef, @@ -14,17 +14,37 @@ export default function TextChannel({ const [currentMessage, setCurrentMessage] = useState(""); const channel = appRef.current?.page?.channel; + const intersectionRef = useRef(null); + const chunkRef = useRef(0); + useEffect(() => { - if (!channel) return; + if (!intersectionRef.current) return; - appRef.current?.server?.websocket?.send({ - method: "GetMessages", - channel_id: channel.id, - chunk: 0, - }); + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + console.log("Element is in view!"); + if (!channel) return; - appRef.current?.forceRender(); - }, [channel]); + appRef.current?.server?.websocket?.send({ + method: "GetMessages", + channel_id: channel.id, + chunk: chunkRef.current, + }); + + appRef.current?.forceRender(); + chunkRef.current += 1; + } + }, + { + threshold: 0.5, + }, + ); + + observer.observe(intersectionRef.current); + + return () => observer.disconnect(); + }, []); if (!channel) return null; @@ -34,12 +54,13 @@ export default function TextChannel({ }; return ( -
+

{channel.name}

+
{appRef.current?.server?.messages[channel.id] && Object.entries(appRef.current?.server?.messages[channel.id]).map( ([_, message]) => ( From dc7f4d2f6116c187bb10682834428af11c90f954 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 21 Aug 2026 13:14:52 +0200 Subject: [PATCH 02/14] Current user author --- src/components/page/channel/TextChannel.tsx | 30 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index 67a2cc1..54c2e93 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -1,10 +1,12 @@ import Enclave from "@/app/app"; import { ChannelPageProps } from "@/components/page/PageView"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { StoredMessage } from "@/lib/types"; +import { ClientMeta, StoredMessage } from "@/lib/types"; +import { base58 } from "@scure/base"; import { useEffect, useRef, useState } from "react"; +import * as ed from "@noble/ed25519"; export default function TextChannel({ appRef, @@ -90,6 +92,7 @@ export default function TextChannel({ } export function TextMessage({ + appRef, message, }: { appRef: React.RefObject | null>; @@ -100,18 +103,37 @@ export function TextMessage({ minute: "2-digit", }); + const [author, setAuthor] = useState(null); + + useEffect(() => { + const currentAccount = appRef.current?.getAccount(); + + if (currentAccount) { + const pubKey = base58.encode( + ed.getPublicKey(base58.decode(currentAccount.privateKey)), + ); + + if (pubKey === message.author) { + setAuthor(currentAccount.meta); + } + } + }, []); + return (
+ + - {message.author.slice(0, 2).toUpperCase()} + {author?.displayName.slice(0, 1).toUpperCase() || + message.author.slice(0, 2).toUpperCase()}
- {message.author.slice(0, 8)} + {author?.displayName || message.author.slice(0, 8)} {time}
From ff36a84a23dcba0c5caaafe5b59e49b457ca2879 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 21 Aug 2026 15:06:20 +0200 Subject: [PATCH 03/14] idk[ --- src/components/page/channel/TextChannel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index 54c2e93..17c4ce5 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -56,7 +56,7 @@ export default function TextChannel({ }; return ( -
+

{channel.name}

@@ -131,7 +131,7 @@ export function TextMessage({
-
+
{author?.displayName || message.author.slice(0, 8)} From 8b80ecb8a80d6cde72c352ed1570de449d4d6d2a Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 22 Aug 2026 10:35:34 +0200 Subject: [PATCH 04/14] Auto scroll --- src/components/page/channel/TextChannel.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index 17c4ce5..f20e413 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -50,6 +50,8 @@ export default function TextChannel({ if (!channel) return null; + const messages = appRef.current?.server?.messages[channel.id]; + const sendMessage = () => { appRef.current?.sendMessage(currentMessage, channel.id); setCurrentMessage(""); @@ -61,14 +63,14 @@ export default function TextChannel({

{channel.name}

-
+
- {appRef.current?.server?.messages[channel.id] && - Object.entries(appRef.current?.server?.messages[channel.id]).map( - ([_, message]) => ( + {messages && + Object.entries(messages) + .sort((a, b) => b[1].timestamp - a[1].timestamp) + .map(([_, message]) => ( - ), - )} + ))}
From 21261098dc25f20af5b402e4add6f3f973b61360 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 22 Aug 2026 10:58:34 +0200 Subject: [PATCH 05/14] Message verification --- src/components/page/channel/TextChannel.tsx | 35 ++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index f20e413..33638e2 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -7,6 +7,7 @@ import { ClientMeta, StoredMessage } from "@/lib/types"; import { base58 } from "@scure/base"; import { useEffect, useRef, useState } from "react"; import * as ed from "@noble/ed25519"; +import { CircleXIcon } from "lucide-react"; export default function TextChannel({ appRef, @@ -64,13 +65,13 @@ export default function TextChannel({
-
{messages && Object.entries(messages) .sort((a, b) => b[1].timestamp - a[1].timestamp) .map(([_, message]) => ( ))} +
@@ -107,15 +108,32 @@ export function TextMessage({ const [author, setAuthor] = useState(null); + const [verified, setVerified] = useState(true); + useEffect(() => { + const serverPubKey = + appRef.current?.server?.serverPublicKey && + base58.encode(appRef.current?.server?.serverPublicKey); + + if (!serverPubKey) return; + + const authorPubKey = base58.decode(message.author); + + setVerified( + ed.verify( + base58.decode(message.signature), + new TextEncoder().encode( + `${message.timestamp}@${serverPubKey}@${message.content}`, + ), + authorPubKey, + ), + ); + const currentAccount = appRef.current?.getAccount(); - if (currentAccount) { - const pubKey = base58.encode( - ed.getPublicKey(base58.decode(currentAccount.privateKey)), - ); + const pubKey = ed.getPublicKey(base58.decode(currentAccount.privateKey)); - if (pubKey === message.author) { + if (pubKey === authorPubKey) { setAuthor(currentAccount.meta); } } @@ -134,7 +152,10 @@ export function TextMessage({
- + + {!verified && ( + + )} {author?.displayName || message.author.slice(0, 8)} {time} From a9cf2d4c70885c9f2adafdf09283e7dc3a6a8279 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 22 Aug 2026 11:06:10 +0200 Subject: [PATCH 06/14] Fix current account --- src/components/page/channel/TextChannel.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index 33638e2..fd2d0b4 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -131,9 +131,11 @@ export function TextMessage({ const currentAccount = appRef.current?.getAccount(); if (currentAccount) { - const pubKey = ed.getPublicKey(base58.decode(currentAccount.privateKey)); + const accountPubKey = ed.getPublicKey( + base58.decode(currentAccount.privateKey), + ); - if (pubKey === authorPubKey) { + if (base58.encode(accountPubKey) === message.author) { setAuthor(currentAccount.meta); } } From 78f5544952d3db50503e8426978ed524a836dccf Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 22 Aug 2026 11:14:51 +0200 Subject: [PATCH 07/14] Improved serverlist --- src/components/view/ServerList.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/view/ServerList.tsx b/src/components/view/ServerList.tsx index ae84df4..2e1da81 100644 --- a/src/components/view/ServerList.tsx +++ b/src/components/view/ServerList.tsx @@ -1,6 +1,7 @@ import Enclave from "@/app/app"; import { getHTTPUrl, saveServerList } from "@/lib/serverList"; import { AddServerDialog } from "../dialog/AddServerDialog"; +import { cn } from "@/lib/utils"; export default function ServerList({ appRef, @@ -16,11 +17,11 @@ export default function ServerList({ { if (!appRef.current) { From 546670b9d2c059f3962b5e8e9f25fe7508ca4c9a Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 22 Aug 2026 12:14:11 +0200 Subject: [PATCH 08/14] Get users --- src/app/app.ts | 30 +++++++++++++++++++++++------- src/app/protocol.ts | 8 ++++++++ src/app/server.ts | 14 +++++++++++++- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/app/app.ts b/src/app/app.ts index b79a940..ca8c339 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -165,14 +165,30 @@ export default class Enclave

{ return; case "Messages": - Object.entries(msg.messages).forEach(([channelId, messages]) => { - if (!server.messages[channelId]) { - server.messages[channelId] = {}; - } + const authors = Object.entries(msg.messages).flatMap( + ([channelId, messages]) => { + if (!server.messages[channelId]) { + server.messages[channelId] = {}; + } - messages.forEach((message) => { - server.messages[channelId][message.id] = message; - }); + return messages.map((message) => { + server.messages[channelId][message.id] = message; + return message.author; + }); + }, + ); + + const dedupedAuthors = Array.from(new Set(authors)); + + this.server?.getUsers(dedupedAuthors); + + this.forceRender(); + + return; + + case "Users": + Object.entries(msg.users).forEach(([pubKey, user]) => { + server.users[pubKey] = user; }); this.forceRender(); diff --git a/src/app/protocol.ts b/src/app/protocol.ts index ed48003..69b5777 100644 --- a/src/app/protocol.ts +++ b/src/app/protocol.ts @@ -15,6 +15,10 @@ export type ClientMethod = | { method: "Messages"; messages: Record; + } + | { + method: "Users"; + users: Record; }; export type ServerMethod = @@ -41,4 +45,8 @@ export type ServerMethod = method: "GetMessages"; channel_id: string; chunk: number; + } + | { + method: "GetUsers"; + pubkeys: string[]; }; diff --git a/src/app/server.ts b/src/app/server.ts index be0d8fe..37ed228 100644 --- a/src/app/server.ts +++ b/src/app/server.ts @@ -3,7 +3,7 @@ import * as ed from "@noble/ed25519"; import EnclaveWebSocket from "./ws"; import { sha512 } from "@noble/hashes/sha2.js"; import { getWSUrl } from "@/lib/serverList"; -import { ServerMeta, StoredMessage } from "@/lib/types"; +import { ClientMeta, ServerMeta, StoredMessage } from "@/lib/types"; ed.hashes.sha512 = sha512; @@ -30,11 +30,13 @@ export default class EnclaveServer { public websocket?: EnclaveWebSocket; public meta?: ServerMeta; public messages: Record>; + public users: Record; public constructor(hostname: string, isSecure: boolean) { this.hostname = hostname; this.isSecure = isSecure; this.messages = {}; + this.users = {}; } public disconnect() { @@ -94,4 +96,14 @@ export default class EnclaveServer { throw Error("Invalid signature"); } } + + public getUsers(pubkeys: string[]) { + const filteredPubkeys = pubkeys.filter((pubkey) => { + const exists = this.users[pubkey] !== undefined; + if (!exists) this.users[pubkey] = null; + return !exists; + }); + + this.websocket?.send({ method: "GetUsers", pubkeys: filteredPubkeys }); + } } From b4f9210486ea411a5d5faceb0ec8082fc155d6b9 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 22 Aug 2026 12:19:06 +0200 Subject: [PATCH 09/14] Author --- src/components/page/channel/TextChannel.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index fd2d0b4..af8c0c4 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -106,7 +106,7 @@ export function TextMessage({ minute: "2-digit", }); - const [author, setAuthor] = useState(null); + const author = appRef.current?.server?.users[message.author]; const [verified, setVerified] = useState(true); @@ -128,17 +128,6 @@ export function TextMessage({ authorPubKey, ), ); - - const currentAccount = appRef.current?.getAccount(); - if (currentAccount) { - const accountPubKey = ed.getPublicKey( - base58.decode(currentAccount.privateKey), - ); - - if (base58.encode(accountPubKey) === message.author) { - setAuthor(currentAccount.meta); - } - } }, []); return ( From 0dc30233e944ca4de32ce3e13b39e2a483c6aa65 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 22 Aug 2026 12:39:23 +0200 Subject: [PATCH 10/14] Msg delete and edit protocol and events --- src/app/app.ts | 10 ++++++++++ src/app/protocol.ts | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/app/app.ts b/src/app/app.ts index ca8c339..1150284 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -194,6 +194,16 @@ export default class Enclave

{ this.forceRender(); return; + + case "MessageDeleted": + delete server.messages[msg.channel_id][msg.message_id]; + this.forceRender(); + return; + + case "MessageEdited": + server.messages[msg.channel_id][msg.message.id] = msg.message; + this.forceRender(); + return; } } } diff --git a/src/app/protocol.ts b/src/app/protocol.ts index 69b5777..0d1c204 100644 --- a/src/app/protocol.ts +++ b/src/app/protocol.ts @@ -19,6 +19,16 @@ export type ClientMethod = | { method: "Users"; users: Record; + } + | { + method: "MessageEdited"; + channel_id: string; + message: StoredMessage; + } + | { + method: "MessageDeleted"; + channel_id: string; + message_id: string; }; export type ServerMethod = @@ -49,4 +59,16 @@ export type ServerMethod = | { method: "GetUsers"; pubkeys: string[]; + } + | { + method: "EditMessage"; + message_id: string; + channel_id: string; + content: string; + signature: string; + } + | { + method: "DeleteMessage"; + channel_id: string; + message_id: string; }; From e5ffc51189715d7866ebdf45afd6e0a5d6bb3a8a Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 22 Aug 2026 12:54:38 +0200 Subject: [PATCH 11/14] Msg delete --- src/components/page/channel/TextChannel.tsx | 72 +++++++++++++++++---- src/lib/types.ts | 1 + 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index af8c0c4..55ccff4 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -3,11 +3,17 @@ import { ChannelPageProps } from "@/components/page/PageView"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { ClientMeta, StoredMessage } from "@/lib/types"; +import { Channel, StoredMessage } from "@/lib/types"; import { base58 } from "@scure/base"; import { useEffect, useRef, useState } from "react"; import * as ed from "@noble/ed25519"; -import { CircleXIcon } from "lucide-react"; +import { CircleXIcon, PencilIcon, Trash2Icon } from "lucide-react"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; export default function TextChannel({ appRef, @@ -20,6 +26,13 @@ export default function TextChannel({ const intersectionRef = useRef(null); const chunkRef = useRef(0); + const account = appRef.current?.getAccount(); + + const userPubKey = + (account && + base58.encode(ed.getPublicKey(base58.decode(account.privateKey)))) || + null; + useEffect(() => { if (!intersectionRef.current) return; @@ -69,7 +82,13 @@ export default function TextChannel({ Object.entries(messages) .sort((a, b) => b[1].timestamp - a[1].timestamp) .map(([_, message]) => ( - + ))}

@@ -97,28 +116,28 @@ export default function TextChannel({ export function TextMessage({ appRef, message, + + channel, + userPubKey, }: { appRef: React.RefObject | null>; message: StoredMessage; + channel: Channel; + userPubKey: string | null; }) { const time = new Date(message.timestamp).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", }); - const author = appRef.current?.server?.users[message.author]; - const [verified, setVerified] = useState(true); useEffect(() => { const serverPubKey = appRef.current?.server?.serverPublicKey && base58.encode(appRef.current?.server?.serverPublicKey); - if (!serverPubKey) return; - const authorPubKey = base58.decode(message.author); - setVerified( ed.verify( base58.decode(message.signature), @@ -130,17 +149,25 @@ export function TextMessage({ ); }, []); - return ( + const isOwnMessage = message.author === userPubKey; + + function handleDelete() { + appRef.current?.server?.websocket?.send({ + method: "DeleteMessage", + message_id: message.id, + channel_id: channel.id, + }); + } + + const content = (
- {author?.displayName.slice(0, 1).toUpperCase() || message.author.slice(0, 2).toUpperCase()} -
@@ -150,6 +177,9 @@ export function TextMessage({ {author?.displayName || message.author.slice(0, 8)} {time} + {message.is_edited && ( + (edited) + )}

{message.content} @@ -157,4 +187,24 @@ export function TextMessage({

); + + if (!isOwnMessage) { + return content; + } + + return ( + + {content} + + {}}> + + Edit + + + + Delete + + + + ); } diff --git a/src/lib/types.ts b/src/lib/types.ts index 1c77ea6..647e08e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -18,6 +18,7 @@ export interface MessageData { export interface StoredMessage extends MessageData { id: string; author: string; + is_edited: boolean; } export type Channel = { id: string; name: string } & ChannelKind; From f7b7b5b637be0712b0157b3dfe49094c4c6b5eb7 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 22 Aug 2026 13:03:25 +0200 Subject: [PATCH 12/14] Msg edit --- src/components/page/channel/TextChannel.tsx | 116 ++++++++++++++++++-- 1 file changed, 104 insertions(+), 12 deletions(-) diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index 55ccff4..27c2151 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -14,6 +14,7 @@ import { ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu"; +import { Textarea } from "@/components/ui/textarea"; export default function TextChannel({ appRef, @@ -28,10 +29,9 @@ export default function TextChannel({ const account = appRef.current?.getAccount(); - const userPubKey = - (account && - base58.encode(ed.getPublicKey(base58.decode(account.privateKey)))) || - null; + const secretKey = (account && base58.decode(account.privateKey)) || null; + + const userPubKey = secretKey && base58.encode(ed.getPublicKey(secretKey)); useEffect(() => { if (!intersectionRef.current) return; @@ -88,6 +88,7 @@ export default function TextChannel({ message={message} channel={channel} userPubKey={userPubKey} + secretKey={secretKey} /> ))}
@@ -116,28 +117,36 @@ export default function TextChannel({ export function TextMessage({ appRef, message, - channel, userPubKey, + secretKey, }: { appRef: React.RefObject | null>; message: StoredMessage; channel: Channel; userPubKey: string | null; + secretKey: Uint8Array | null; }) { const time = new Date(message.timestamp).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", }); + const author = appRef.current?.server?.users[message.author]; + const [verified, setVerified] = useState(true); + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(message.content); useEffect(() => { const serverPubKey = appRef.current?.server?.serverPublicKey && - base58.encode(appRef.current?.server?.serverPublicKey); + base58.encode(appRef.current.server.serverPublicKey); + if (!serverPubKey) return; + const authorPubKey = base58.decode(message.author); + setVerified( ed.verify( base58.decode(message.signature), @@ -159,6 +168,56 @@ export function TextMessage({ }); } + function startEditing() { + setDraft(message.content); + setEditing(true); + } + + function cancelEditing() { + setDraft(message.content); + setEditing(false); + } + + function handleSave() { + const content = draft.trim(); + + if ( + !content || + content === message.content || + !secretKey || + !appRef.current?.server?.serverPublicKey + ) { + cancelEditing(); + return; + } + + const signature = new TextEncoder().encode( + `${message.timestamp}@${base58.encode(appRef.current.server?.serverPublicKey)}@${content}`, + ); + + appRef.current?.server?.websocket?.send({ + method: "EditMessage", + message_id: message.id, + channel_id: channel.id, + signature: base58.encode(ed.sign(signature, secretKey)), + content, + }); + + setEditing(false); + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Escape") { + e.preventDefault(); + cancelEditing(); + } + + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSave(); + } + } + const content = (
@@ -168,6 +227,7 @@ export function TextMessage({ message.author.slice(0, 2).toUpperCase()} +
@@ -176,30 +236,62 @@ export function TextMessage({ )} {author?.displayName || message.author.slice(0, 8)} + {time} - {message.is_edited && ( + + {message.is_edited && !editing && ( (edited) )}
-

- {message.content} -

+ + {editing ? ( +
+