atagen

malice

public
Star0
Watch0

Commit 6c8fd192fb8c

tarball

atagen <boss@atagen.co>2026-10-24 08:21 UTC

1 file changed+38-6
web: set room and global avatars from an attached image

With no argument now meaning "drop the override", /roomavatar had no way to upload a file, so both avatar commands take a single staged image instead. Other commands refuse attachments rather than silently sending the command text as a caption.
Modified file: web/src/App.svelte+38 additions−6 deletions
@@ -28,6 +28,7 @@
2828 sendMessage,
2929 uploadAttachment,
3030 uploadAvatar,
31+ uploadRoomAvatar,
3132 editMessage
3233 } from './lib/api';
3334 import { isKnownCommand, parseCommand, splitTarget, unescapeCommand } from './lib/commands';
@@ -601,11 +602,21 @@
601602 return;
602603 }
603604 if (attachments.length > 0 && uploadBusy) return;
604- const command = attachments.length === 0 ? parseCommand(body) : null;
605+ const command = parseCommand(body);
605606 if (command && !isKnownCommand(command.name)) {
606607 pushBanner(`Unknown command /${command.name}`);
607608 return;
608609 }
610+ if (command && attachments.length > 0) {
611+ if (command.name !== 'roomavatar' && command.name !== 'globalavatar') {
612+ pushBanner(`/${command.name} doesn't take attachments`);
613+ return;
614+ }
615+ if (command.args || attachments.length !== 1 || !attachments[0].file.type.startsWith('image/')) {
616+ pushBanner(`/${command.name} takes a single attached image`);
617+ return;
618+ }
619+ }
609620 composer = '';
610621 if (roomId in drafts) delete drafts[roomId];
611622 stickToBottom = true;
@@ -616,7 +627,14 @@
616627 pickedMentions = { ...pickedMentions, [roomId]: [] };
617628 mentionQuery = null;
618629 if (command) {
619- void execute(roomId, command, reply, mentions);
630+ const image = attachments[0] ?? null;
631+ if (image) {
632+ const { [roomId]: _used, ...rest } = staged;
633+ staged = rest;
634+ }
635+ void execute(roomId, command, reply, mentions, image?.file ?? null).finally(() => {
636+ if (image?.previewUrl) URL.revokeObjectURL(image.previewUrl);
637+ });
620638 } else if (attachments.length === 0) {
621639 queueSend(roomId, text, reply, mentions, $markdownEnabled);
622640 } else {
@@ -637,7 +655,13 @@
637655 return null;
638656 }
639657
640- async function execute(roomId: string, command: ParsedCommand, reply: ReplyReference | null, mentions: Mention[]): Promise<void> {
658+ async function execute(
659+ roomId: string,
660+ command: ParsedCommand,
661+ reply: ReplyReference | null,
662+ mentions: Mention[],
663+ image: File | null
664+ ): Promise<void> {
641665 const { name, args } = command;
642666 const target = () => {
643667 const found = splitTarget(args, Object.values(people));
@@ -699,8 +723,13 @@
699723 await runCommand({ op: 'set_room_display_name', room_id: roomId, name: args || null });
700724 break;
701725 case 'roomavatar':
702- if (args && !args.startsWith('mxc://')) throw new Error('/roomavatar takes an mxc:// link, or nothing to drop the override');
703- await runCommand({ op: 'set_room_avatar', room_id: roomId, mxc: args || null });
726+ if (image) {
727+ await uploadRoomAvatar(roomId, image);
728+ } else if (args && !args.startsWith('mxc://')) {
729+ throw new Error('/roomavatar takes an mxc:// link, an attached image, or nothing to drop the override');
730+ } else {
731+ await runCommand({ op: 'set_room_avatar', room_id: roomId, mxc: args || null });
732+ }
704733 break;
705734 case 'delete': {
706735 const eventId = reply?.event_id ?? lastOwnEventId();
@@ -713,7 +742,10 @@
713742 await loadProfile();
714743 break;
715744 case 'globalavatar':
716- if (!args) {
745+ if (image) {
746+ await uploadAvatar(image);
747+ await loadProfile();
748+ } else if (!args) {
717749 avatarInput?.click();
718750 } else if (args.startsWith('mxc://')) {
719751 await runCommand({ op: 'set_avatar', mxc: args });