diff --git a/nobilis b/nobilis index af11ded..89b180e 160000 --- a/nobilis +++ b/nobilis @@ -1 +1 @@ -Subproject commit af11ded9236ce9923e4b72eefb072eaa0a132088 +Subproject commit 89b180ebd656b7425397b15a851dd1ef0c2dc7aa diff --git a/src/renderer/src/app.css b/src/renderer/src/app.css index 0707182..c322a99 100644 --- a/src/renderer/src/app.css +++ b/src/renderer/src/app.css @@ -2893,6 +2893,23 @@ img.smilie { background: var(--surface-hover); } +/* A request is a row you answer rather than open, so its buttons sit in it. */ +.friend-row.request { + display: flex; + align-items: center; + gap: var(--spacing-s); + cursor: default; +} + +.friend-row.request .button { + flex: none; + margin-left: auto; +} + +.friend-row.request .button + .button { + margin-left: 0; +} + .friend-row .ellipsis:first-of-type { flex: 1; } diff --git a/src/renderer/src/components/Composer.tsx b/src/renderer/src/components/Composer.tsx index f76bdbe..f50a17a 100644 --- a/src/renderer/src/components/Composer.tsx +++ b/src/renderer/src/components/Composer.tsx @@ -181,6 +181,36 @@ export function Composer(): JSX.Element | null { [mention, mentionTargets] ) + // What has been typed as a command, if anything: a slash at the very start + // and the word after it. Not mid-message - "and/or" is not a command. + const typedCommand = service === 'discord' && /^\/\S*$/.test(text) ? text.slice(1) : null + + useEffect(() => { + if (typedCommand === null || !buffer) { + setCommands([]) + return + } + const timer = setTimeout(() => { + void window.moho + .rpc<{ commands: DiscordCommand[] }>('listDiscordCommands', { + bufferId: buffer.id, + query: typedCommand + }) + .then((answer) => { + setCommands(answer.commands || []) + setCommandIndex(0) + }) + .catch(() => { + // A channel with no bots answers nothing useful, and a list that + // could not be read is a list that stays closed rather than a + // complaint about typing a slash. + setCommands([]) + }) + }, 200) + return () => clearTimeout(timer) + }, [typedCommand, buffer?.id]) + + if (!buffer) return null /** @@ -292,35 +322,6 @@ export function Composer(): JSX.Element | null { }) } - // What has been typed as a command, if anything: a slash at the very start - // and the word after it. Not mid-message - "and/or" is not a command. - const typedCommand = service === 'discord' && /^\/\S*$/.test(text) ? text.slice(1) : null - - useEffect(() => { - if (typedCommand === null) { - setCommands([]) - return - } - const timer = setTimeout(() => { - void window.moho - .rpc<{ commands: DiscordCommand[] }>('listDiscordCommands', { - bufferId: buffer!.id, - query: typedCommand - }) - .then((answer) => { - setCommands(answer.commands || []) - setCommandIndex(0) - }) - .catch(() => { - // A channel with no bots answers nothing useful, and a list that - // could not be read is a list that stays closed rather than a - // complaint about typing a slash. - setCommands([]) - }) - }, 200) - return () => clearTimeout(timer) - }, [typedCommand, buffer?.id]) - /** * Runs the chosen command, with whatever was typed after it as its * arguments, in the order the command declares them. diff --git a/src/renderer/src/components/JoinPanel.tsx b/src/renderer/src/components/JoinPanel.tsx index ab8be6b..f55929c 100644 --- a/src/renderer/src/components/JoinPanel.tsx +++ b/src/renderer/src/components/JoinPanel.tsx @@ -664,7 +664,28 @@ function DiscordJoin({ account }: { account: Account }): JSX.Element { useEffect(refreshFriends, [account.id]) - const shown = onlineOnly ? friends.filter((f) => f.status && f.status !== 'offline') : friends + // And whatever has changed since this was asked. A request that arrives + // while this panel is open is exactly the one somebody is waiting for, so + // it appears rather than waiting for the refresh button. + const live = useChat((s) => s.discordFriends)[account.id] + useEffect(() => { + if (live) setFriends(live) + }, [live]) + + // Requests are their own list above the friends. They are not people you + // can message and they are the thing somebody opened this panel to deal + // with, so filtering them by whether they happen to be online - which is + // what the toggle below does - would hide the ones that matter most. + const waiting = friends.filter((f) => f.kind === 'incoming' || f.kind === 'outgoing') + const settled = friends.filter((f) => !f.kind || f.kind === 'friend') + const shown = onlineOnly ? settled.filter((f) => f.status && f.status !== 'offline') : settled + + const answer = (userId: string, accept: boolean): void => { + void window.moho + .rpc('answerDiscordFriendRequest', { accountId: account.id, userId, accept }) + .then(refreshFriends) + .catch((e: Error) => store.toast('error', e.message)) + } return (
@@ -703,6 +724,37 @@ function DiscordJoin({ account }: { account: Account }): JSX.Element {
+ {waiting.length > 0 && ( + <> +
+ Requests +
+ {waiting.map((f) => ( +
+ {f.avatarUrl ? ( + + ) : ( + {(f.globalName || f.username).slice(0, 1)} + )} + {f.globalName || f.username} + + {f.kind === 'incoming' ? 'wants to be friends' : 'asked, waiting'} + + {f.kind === 'incoming' && ( + + )} + {/* Refusing one and taking back your own are the same call, and + read differently enough to be worth different words. */} + +
+ ))} + + )} +
Friends