Offer attachments on IRC, and a say in where they go
Issues #17 and #18. The attach button was dead on IRC, which is the service where sending a picture most needs help: it carries text and nothing else, so a file means uploading it somewhere and sending the link. That now happens on the way out, with the caption on the same line rather than as a second message that could arrive out of order or be split by somebody else talking. Where files go is a setting, listed by the daemon that performs the upload rather than spelled out again here. Read at send time rather than held in state, since it is a preference somebody may change between one message and the next. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+1
-1
Submodule nobilis updated: caf56c7e73...2f9c2bc7ff
@@ -70,10 +70,12 @@ export function Composer(): JSX.Element | null {
|
||||
|
||||
const account = buffer && accounts.find((a) => a.id === buffer.accountId)
|
||||
const service = account?.service
|
||||
// Discord, Sneedchat and Matrix each have a real upload path; IRC has no
|
||||
// attachment concept at all, and XMPP/Slack aren't implemented yet.
|
||||
// Discord, Sneedchat and Matrix each carry a file natively. IRC has no idea
|
||||
// of an attachment at all, so a file there is uploaded and the link sent -
|
||||
// which is what people do by hand on IRC anyway, and is why it is offered
|
||||
// here rather than left as the one service where the button is dead.
|
||||
const supportsAttachments =
|
||||
service === 'discord' || service === 'sockchat' || service === 'matrix'
|
||||
service === 'discord' || service === 'sockchat' || service === 'matrix' || service === 'irc'
|
||||
|
||||
// Refocus on buffer switch so typing works immediately after clicking a
|
||||
// channel, without a second click into the field.
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
StringSetting,
|
||||
ToggleSetting
|
||||
} from './controls'
|
||||
import { useChat, useStore } from '../../state/hooks'
|
||||
import { useChat, usePref, useStore } from '../../state/hooks'
|
||||
import { Icon } from '../Icon'
|
||||
import type { Account } from '../../../../shared/wire'
|
||||
|
||||
@@ -28,6 +28,43 @@ const CATEGORIES = [
|
||||
{ id: 'slack', label: 'Slack', available: false }
|
||||
]
|
||||
|
||||
/**
|
||||
* Where files go on the services that cannot carry them.
|
||||
*
|
||||
* IRC has no idea of an attachment and Sneedchat's own uploader takes images
|
||||
* only, so sharing anything on either means putting it somewhere and sending
|
||||
* the link - which is what people do by hand there anyway. The list comes from
|
||||
* the daemon rather than being spelled out here, since the daemon is what
|
||||
* performs the upload and a client drawing the menu should not hold a second,
|
||||
* separately-maintained copy of the answer.
|
||||
*/
|
||||
function UploadHostSetting(): JSX.Element {
|
||||
const [host, setHost] = usePref<string>('uploads.host', 'catbox')
|
||||
const [hosts, setHosts] = useState<{ id: string; label: string }[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
void window.moho
|
||||
.rpc<{ id: string; label: string }[]>('listUploadHosts')
|
||||
.then(setHosts)
|
||||
// An older daemon has no such method; the stored choice still applies.
|
||||
.catch(() => setHosts([]))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ChoiceSetting
|
||||
label="Upload files to"
|
||||
description="Used when sending a file on a service that cannot carry one itself - IRC, and anything Sneedchat's own uploader refuses. The file is uploaded anonymously and the link is sent."
|
||||
value={host}
|
||||
options={
|
||||
hosts.length
|
||||
? hosts.map((h) => ({ label: h.label, value: h.id }))
|
||||
: [{ label: host, value: host }]
|
||||
}
|
||||
onChange={setHost}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsPanel(): JSX.Element {
|
||||
const [selected, setSelected] = useState('general')
|
||||
|
||||
@@ -180,6 +217,10 @@ function GeneralSettings(): JSX.Element {
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Uploads">
|
||||
<UploadHostSetting />
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Downloads">
|
||||
<DirectorySetting
|
||||
settingKey="downloads.directory"
|
||||
|
||||
@@ -206,6 +206,16 @@ function tidyDetail(detail: string): string {
|
||||
return detail.replace(/(https?:\/\/[^\s)]+?)\?[^\s)]*/g, '$1')
|
||||
}
|
||||
|
||||
/** The upload host the person chose, or the default if they never did. */
|
||||
async function uploadHost(): Promise<string> {
|
||||
try {
|
||||
const prefs = await window.moho.prefs.getAll()
|
||||
return (prefs['uploads.host'] as string) || 'catbox'
|
||||
} catch {
|
||||
return 'catbox'
|
||||
}
|
||||
}
|
||||
|
||||
function bestEffort(work: Promise<unknown>, what: string): void {
|
||||
void work.catch((e: Error) => console.debug(`[moho] ${what}:`, e.message))
|
||||
}
|
||||
@@ -1202,7 +1212,13 @@ export class ChatStore {
|
||||
await window.moho.rpc('sendMessage', {
|
||||
bufferId,
|
||||
body,
|
||||
...(attachmentPath ? { attachmentPath } : {}),
|
||||
// Where a file goes on a service that cannot carry one. Read at send
|
||||
// time rather than held in state: it is a preference somebody may
|
||||
// change between one message and the next, and the daemon falls back
|
||||
// to its own default if this is absent or unknown to it.
|
||||
...(attachmentPath
|
||||
? { attachmentPath, uploadHost: await uploadHost() }
|
||||
: {}),
|
||||
...(replyToId ? { replyToId } : {})
|
||||
})
|
||||
// Success alone doesn't resolve the echo - only the real message event
|
||||
|
||||
Reference in New Issue
Block a user