diff --git a/.changeset/fix-color-parser.md b/.changeset/fix-color-parser.md
new file mode 100644
index 000000000..0abdd8938
--- /dev/null
+++ b/.changeset/fix-color-parser.md
@@ -0,0 +1,5 @@
+---
+default: patch
+---
+
+Fix opacity rendering in name colors.
diff --git a/.changeset/fix-delated-file-uploads.md b/.changeset/fix-delated-file-uploads.md
new file mode 100644
index 000000000..c3d5f936d
--- /dev/null
+++ b/.changeset/fix-delated-file-uploads.md
@@ -0,0 +1,5 @@
+---
+default: patch
+---
+
+Fix sending scheduled file attachments.
diff --git a/.changeset/fix-reply-rendering.md b/.changeset/fix-reply-rendering.md
new file mode 100644
index 000000000..413388960
--- /dev/null
+++ b/.changeset/fix-reply-rendering.md
@@ -0,0 +1,5 @@
+---
+default: patch
+---
+
+Fix replies rendering new lines when messages have lists.
diff --git a/.changeset/fix-thread-replies.md b/.changeset/fix-thread-replies.md
new file mode 100644
index 000000000..d10cf9d9f
--- /dev/null
+++ b/.changeset/fix-thread-replies.md
@@ -0,0 +1,5 @@
+---
+default: patch
+---
+
+Fix threads rendering fallback replies.
diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx
index 2afd2aae3..1e047e9e9 100644
--- a/src/app/components/message/Reply.tsx
+++ b/src/app/components/message/Reply.tsx
@@ -122,6 +122,8 @@ export const Reply = as<'div', ReplyProps>(
.replaceAll(/
/gi, ' ')
.replaceAll(/<\/p>\s*
]*>/gi, ' ')
.replaceAll(/<\/?p[^>]*>/gi, '')
+ .replaceAll(/<\/li>\s*
]*>/gi, ' ')
+ .replaceAll(/<\/?(ul|ol|li|blockquote|h[1-6]|pre|div)[^>]*>/gi, '')
.replaceAll(/(?:\r\n|\r|\n)/g, ' ');
const parserOpts = getReactCustomHtmlParser(mx, room.roomId, {
linkifyOpts: LINKIFY_OPTS,
diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx
index ad9d63141..60ee8d20b 100644
--- a/src/app/features/room/RoomInput.tsx
+++ b/src/app/features/room/RoomInput.tsx
@@ -512,28 +512,74 @@ export const RoomInput = forwardRef(
}
}
- await Promise.all(
- contents.map((content) =>
- mx
- .sendMessage(roomId, threadRootId ?? null, content as any)
- .then((res) => {
- debugLog.info('message', 'Uploaded file message sent', {
- roomId,
- eventId: res.event_id,
- msgtype: content.msgtype,
- });
- return res;
- })
- .catch((error: unknown) => {
- debugLog.error('message', 'Failed to send uploaded file message', {
- roomId,
- error: error instanceof Error ? error.message : String(error),
- });
- log.error('failed to send uploaded message', { roomId }, error);
- throw error;
+ const invalidate = () =>
+ queryClient.invalidateQueries({ queryKey: ['delayedEvents', roomId] });
+
+ if (scheduledTime) {
+ try {
+ const delayMs = computeDelayMs(scheduledTime);
+ if (editingScheduledDelayId) {
+ await cancelDelayedEvent(mx, editingScheduledDelayId);
+ }
+
+ await Promise.all(
+ contents.map((content) => {
+ if (isEncrypted) {
+ return sendDelayedMessageE2EE(mx, roomId, room, content, delayMs);
+ }
+ return sendDelayedMessage(mx, roomId, content, delayMs);
})
- )
- );
+ );
+
+ invalidate();
+ setEditingScheduledDelayId(null);
+ setScheduledTime(null);
+ } catch (error) {
+ debugLog.error('message', 'Failed to schedule uploaded file message', {
+ roomId,
+ error: error instanceof Error ? error.message : String(error),
+ });
+ log.error('failed to schedule uploaded message', { roomId }, error);
+ throw error;
+ }
+ } else {
+ if (editingScheduledDelayId) {
+ try {
+ await cancelDelayedEvent(mx, editingScheduledDelayId);
+ invalidate();
+ setEditingScheduledDelayId(null);
+ } catch {
+ debugLog.error(
+ 'message',
+ 'Failed to cancel scheduled event before immediate file send',
+ { roomId }
+ );
+ }
+ }
+
+ await Promise.all(
+ contents.map((content) =>
+ mx
+ .sendMessage(roomId, threadRootId ?? null, content as any)
+ .then((res) => {
+ debugLog.info('message', 'Uploaded file message sent', {
+ roomId,
+ eventId: res.event_id,
+ msgtype: content.msgtype,
+ });
+ return res;
+ })
+ .catch((error: unknown) => {
+ debugLog.error('message', 'Failed to send uploaded file message', {
+ roomId,
+ error: error instanceof Error ? error.message : String(error),
+ });
+ log.error('failed to send uploaded message', { roomId }, error);
+ throw error;
+ })
+ )
+ );
+ }
};
const handleCloseAutocomplete = useCallback(() => {
diff --git a/src/app/features/room/ThreadDrawer.tsx b/src/app/features/room/ThreadDrawer.tsx
index fd7075bfd..00957bc34 100644
--- a/src/app/features/room/ThreadDrawer.tsx
+++ b/src/app/features/room/ThreadDrawer.tsx
@@ -180,6 +180,11 @@ function ThreadMessage({
const { replyEventId } = mEvent;
+ const relation = mEvent.getRelation();
+ const contentRelatesTo = mEvent.getContent()?.['m.relates_to'];
+ const isFallback =
+ relation?.is_falling_back === true || contentRelatesTo?.is_falling_back === true;
+
return (
{
if (typeof c !== 'string') return undefined;
// silly tuwunel smh
const cleaned = c.replaceAll(/["']/g, '').trim();
- return /^#([0-9A-F]{3,6})$/i.test(cleaned) ? cleaned : undefined;
+ // Strictly allow only 3 or 6 digit hex codes, aka no opacity
+ return /^#([0-9A-F]{3}|[0-9A-F]{6})$/i.test(cleaned) ? cleaned : undefined;
};
const sanitizeFont = (f: string) => f.replaceAll(/[;{}<>]/g, '').slice(0, 32);