From 09d441afa9bc5d6d35be3fa456081875ea85681a Mon Sep 17 00:00:00 2001 From: Natalie Rose Date: Mon, 28 Sep 2026 10:23:57 +1000 Subject: [PATCH] Improve composing emails --- app/components/inbox/createTab.tsx | 168 +++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 8 deletions(-) diff --git a/app/components/inbox/createTab.tsx b/app/components/inbox/createTab.tsx index 94a71c36..9315e9ed 100644 --- a/app/components/inbox/createTab.tsx +++ b/app/components/inbox/createTab.tsx @@ -1,26 +1,54 @@ import React, { useState } from 'react'; import { ComposeDraft } from 'app/interfaces'; import { useSendMessageMutation } from 'app/queries/inbox'; +import { useFindEmpireQuery } from 'app/queries/empire'; +import MessageBody from 'app/components/inbox/messageBody'; +import EmpireRPCStore from 'app/stores/rpc/empire'; type Props = { draft?: ComposeDraft; }; +type Recipient = { name: string }; + +const parseRecipients = (value?: string): Recipient[] => + (value ?? '') + .split(',') + .map((name) => name.trim()) + .filter(Boolean) + .map((name) => ({ name })); + // // Mounted fresh (via a key on the draft) whenever Reply/Reply All/Forward prefill a new draft, so // local state can just be initialized from props rather than synced with an effect. // const CreateTab: React.FunctionComponent = ({ draft }) => { - const [recipients, setRecipients] = useState(draft?.recipients ?? ''); + const [recipients, setRecipients] = useState(() => + parseRecipients(draft?.recipients) + ); + const [search, setSearch] = useState(''); const [subject, setSubject] = useState(draft?.subject ?? ''); const [body, setBody] = useState(draft?.body ?? ''); + const [showPreview, setShowPreview] = useState(false); const send = useSendMessageMutation(); + const { data: empires = [], isFetching } = useFindEmpireQuery(search); + const candidates = empires.filter((empire) => !recipients.some((r) => r.name === empire.name)); + + const addRecipient = (empire: { id: number; name: string }) => { + setRecipients((current) => [...current, empire]); + setSearch(''); + }; + + const removeRecipient = (index: number) => { + setRecipients((current) => current.filter((_, i) => i !== index)); + }; + const onSend = () => { send.mutate( { - recipients, + recipients: recipients.map((r) => r.name).join(','), subject, body, options: { @@ -30,9 +58,11 @@ const CreateTab: React.FunctionComponent = ({ draft }) => { }, { onSuccess: () => { - setRecipients(''); + setRecipients([]); + setSearch(''); setSubject(''); setBody(''); + setShowPreview(false); }, } ); @@ -42,15 +72,55 @@ const CreateTab: React.FunctionComponent = ({ draft }) => {
+ + {recipients.length > 0 && ( +
+
+ {recipients.map((recipient, index) => ( + + {recipient.name} +
+
+ )} +
setRecipients(e.target.value)} + placeholder='At least three characters' + value={search} + onChange={(e) => setSearch(e.target.value)} />
+ + {search.length >= 3 && ( +
+ {isFetching &&

Searching...

} + {!isFetching && candidates.length === 0 &&

No empires found.

} + +
+ {candidates.map((empire) => ( + + ))} +
+
+ )}
@@ -77,19 +147,101 @@ const CreateTab: React.FunctionComponent = ({ draft }) => {
+
+ Formatting Tips + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ *text* + Bold text
+ {'{food}'}, {'{water}'}, {'{ore}'},{' '} + {'{energy}'}, {'{waste}'}, {'{happiness}'},{' '} + {'{time}'}, {'{essentia}'}, {'{plots}'},{' '} + {'{build}'} + Resource icon
+ [http://...] + Clickable link to a URL
+ {'{Starmap x y label}'} + Link that jumps the reader's starmap to a location
+ {'{Empire id label}'} + Link to an empire's profile
+ {'{Alliance id label}'} + Link to an alliance's profile
+
+ + {showPreview && ( +
+ + + + + + + + + + + + + + + +
+ From + {EmpireRPCStore.name}
+ To + {recipients.map((r) => r.name).join(', ')}
+ Subject + {subject}
+
+ +
+
+ )} + {send.isSuccess &&
Message sent.
} -
+
+
+ +
); -- 2.51.2