diff --git a/.changeset/add-close-menu-reactions.md b/.changeset/add-close-menu-reactions.md new file mode 100644 index 000000000..5c983229e --- /dev/null +++ b/.changeset/add-close-menu-reactions.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix right click menu not closing when viewing reactions. diff --git a/.changeset/add-file-extension-for-gifs.md b/.changeset/add-file-extension-for-gifs.md new file mode 100644 index 000000000..ffce0f2cb --- /dev/null +++ b/.changeset/add-file-extension-for-gifs.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Send gifs with proper file extensions in body. diff --git a/.changeset/fix-autocomplete-crash-edits.md b/.changeset/fix-autocomplete-crash-edits.md new file mode 100644 index 000000000..1c70897c5 --- /dev/null +++ b/.changeset/fix-autocomplete-crash-edits.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix crash in autocomplete menu when editing messages. diff --git a/.changeset/fix-call-button-reactivity.md b/.changeset/fix-call-button-reactivity.md new file mode 100644 index 000000000..8f4c124db --- /dev/null +++ b/.changeset/fix-call-button-reactivity.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Ensure call buttons appear/disappear when relevant state events occur. diff --git a/.changeset/fix-recent-emoji-format.md b/.changeset/fix-recent-emoji-format.md new file mode 100644 index 000000000..f007a65bf --- /dev/null +++ b/.changeset/fix-recent-emoji-format.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix format for recent emojis and related crash. diff --git a/.changeset/fix-sliding-sync-debug-display.md b/.changeset/fix-sliding-sync-debug-display.md new file mode 100644 index 000000000..db67b62e0 --- /dev/null +++ b/.changeset/fix-sliding-sync-debug-display.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix sliding sync debug saying sliding when it is in fact not sliding. diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 485283dca..2aae96460 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -552,6 +552,7 @@ export function EmojiBoard({ width, height, size: fullRes?.size || preview?.size || 0, + mimetype: 'image/gif', }; }, []); diff --git a/src/app/components/emoji-board/components/Item.tsx b/src/app/components/emoji-board/components/Item.tsx index 2fa0b6ad0..ea7022635 100644 --- a/src/app/components/emoji-board/components/Item.tsx +++ b/src/app/components/emoji-board/components/Item.tsx @@ -234,6 +234,7 @@ export function GifItem({ width: gif.width, height: gif.height, size: gif.size, + mimetype: gif.mimetype, }, ], }) diff --git a/src/app/components/emoji-board/types.ts b/src/app/components/emoji-board/types.ts index 775fe488d..0b08e8e91 100644 --- a/src/app/components/emoji-board/types.ts +++ b/src/app/components/emoji-board/types.ts @@ -26,4 +26,5 @@ export type GifData = { width?: number; height?: number; size?: number; + mimetype?: string; }; diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx index d7b6ac074..fd51775d8 100644 --- a/src/app/components/message/content/ImageContent.tsx +++ b/src/app/components/message/content/ImageContent.tsx @@ -442,6 +442,7 @@ export const ImageContent = as<'div', ImageContentProps>( width: imageW, height: imageH, size: info?.size, + mimetype: info?.mimetype, }, ], }) diff --git a/src/app/components/message/modals/MessageReactions.tsx b/src/app/components/message/modals/MessageReactions.tsx index d875acc42..cee9ac47f 100644 --- a/src/app/components/message/modals/MessageReactions.tsx +++ b/src/app/components/message/modals/MessageReactions.tsx @@ -7,7 +7,15 @@ import { modalAtom, ModalType } from '$state/modal'; import * as css from '$features/room/message/styles.css'; import { ReactionViewer } from '$features/room/reaction-viewer'; -export function MessageAllReactionItem({ room, relations }: { room: Room; relations: Relations }) { +export function MessageAllReactionItem({ + room, + relations, + closeMenu, +}: { + room: Room; + relations: Relations; + closeMenu: () => void; +}) { const setModal = useSetAtom(modalAtom); return ( @@ -18,6 +26,7 @@ export function MessageAllReactionItem({ room, relations }: { room: Room; relati onClick={(e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); + closeMenu(); setModal({ type: ModalType.Reactions, room, diff --git a/src/app/components/message/modals/Options.tsx b/src/app/components/message/modals/Options.tsx index da37d35bd..d6fadd979 100644 --- a/src/app/components/message/modals/Options.tsx +++ b/src/app/components/message/modals/Options.tsx @@ -656,7 +656,9 @@ export function OptionMenu({ )} - {relations && } + {relations && ( + + )} ( ); const handleCloseAutocomplete = useCallback(() => { - ReactEditor.focus(editor); - setAutocompleteQuery(undefined); + setAutocompleteQuery((prev) => { + if (prev !== undefined) { + focusEditor(editor); + } + return undefined; + }); }, [editor]); const handleEmoticonSelect = (key: string, shortcode: string) => { diff --git a/src/app/features/room/msgContent.ts b/src/app/features/room/msgContent.ts index d8da4262f..7b63518b9 100644 --- a/src/app/features/room/msgContent.ts +++ b/src/app/features/room/msgContent.ts @@ -18,6 +18,7 @@ import { getVideoInfo, mxcUrlToHttp, } from '$utils/matrix'; +import { mimeTypeToExt } from '$utils/mimeTypes'; import type { TUploadItem } from '$state/room/roomInputDrafts'; import type { GifData } from '$components/emoji-board/types'; import { encodeBlurHash } from '$utils/blurHash'; @@ -261,14 +262,17 @@ export const getGifMsgContent = async ( ); } + const mimetype = gif.mimetype ?? 'image/gif'; + const ext = mimeTypeToExt(mimetype); + const content: IContent = { msgtype: MsgType.Image, - body: gif.title, + body: gif.title.endsWith(`.${ext}`) ? gif.title : `${gif.title}.${ext}`, url: mxcUrl, info: { w: gif.width, h: gif.height, - mimetype: 'image/gif', + mimetype, }, }; diff --git a/src/app/hooks/useCallStartCapabilities.ts b/src/app/hooks/useCallStartCapabilities.ts index 8e2d55813..bad149ca5 100644 --- a/src/app/hooks/useCallStartCapabilities.ts +++ b/src/app/hooks/useCallStartCapabilities.ts @@ -1,8 +1,11 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import type { Room } from '$types/matrix-sdk'; +import { EventType } from '$types/matrix-sdk'; import { useCallEmbed } from '$hooks/useCallEmbed'; import { useLivekitSupport } from '$hooks/useLivekitSupport'; import { useMatrixClient } from '$hooks/useMatrixClient'; +import { useStateEventCallback } from '$hooks/useStateEventCallback'; +import { useForceUpdate } from '$hooks/useForceUpdate'; import { webRTCSupported } from '$utils/rtc'; import { evaluateCallStartCapabilities, @@ -15,16 +18,33 @@ export const useCallStartCapabilities = (room: Room): CallStartCapabilities => { const livekitSupported = useLivekitSupport(); const rtcSupported = webRTCSupported(); const myUserId = mx.getSafeUserId(); + const [updateCount, forceUpdate] = useForceUpdate(); - return useMemo( - () => - evaluateCallStartCapabilities({ - room, - myUserId, - activeCallRoomId: callEmbed?.roomId, - livekitSupported, - rtcSupported, - }), - [room, myUserId, callEmbed?.roomId, livekitSupported, rtcSupported] + useStateEventCallback( + mx, + useCallback( + (event) => { + if (event.getRoomId() !== room.roomId) return; + const eventType = event.getType(); + if ( + eventType === (EventType.RoomPowerLevels as string) || + (eventType === (EventType.RoomMember as string) && event.getStateKey() === myUserId) + ) { + forceUpdate(); + } + }, + [room.roomId, myUserId, forceUpdate] + ) ); + + return useMemo(() => { + void updateCount; + return evaluateCallStartCapabilities({ + room, + myUserId, + activeCallRoomId: callEmbed?.roomId, + livekitSupported, + rtcSupported, + }); + }, [room, myUserId, callEmbed?.roomId, livekitSupported, rtcSupported, updateCount]); }; diff --git a/src/app/plugins/recent-emoji.ts b/src/app/plugins/recent-emoji.ts index 84653a90a..580e02ef5 100644 --- a/src/app/plugins/recent-emoji.ts +++ b/src/app/plugins/recent-emoji.ts @@ -8,10 +8,40 @@ import { CustomAccountDataEvent } from '$types/matrix/accountData'; type EmojiUnicode = string; type EmojiUsageCount = number; +type RecentEmojiEntry = + | { emoji: EmojiUnicode; total: EmojiUsageCount } + | [EmojiUnicode, EmojiUsageCount]; + export type IRecentEmojiContent = { - recent_emoji?: [EmojiUnicode, EmojiUsageCount][]; + recent_emoji?: { emoji: EmojiUnicode; total: EmojiUsageCount }[]; }; +const isTuple = (entry: RecentEmojiEntry): entry is [EmojiUnicode, EmojiUsageCount] => + Array.isArray(entry) && + entry.length === 2 && + typeof entry[0] === 'string' && + typeof entry[1] === 'number'; + +const isValidEntry = ( + entry: RecentEmojiEntry +): entry is { emoji: EmojiUnicode; total: EmojiUsageCount } => + typeof entry === 'object' && + entry !== null && + !Array.isArray(entry) && + typeof (entry as { emoji?: unknown }).emoji === 'string' && + typeof (entry as { total?: unknown }).total === 'number'; + +const normalizeEntries = ( + raw: RecentEmojiEntry[] +): { emoji: EmojiUnicode; total: EmojiUsageCount }[] => + raw + .map((entry) => { + if (isTuple(entry)) return { emoji: entry[0], total: entry[1] }; + if (isValidEntry(entry)) return { emoji: entry.emoji, total: entry.total }; + return null; + }) + .filter((e): e is { emoji: EmojiUnicode; total: EmojiUsageCount } => e !== null); + export const getRecentEmojis = (mx: MatrixClient, limit?: number): IEmoji[] => { let recentEmojiEvent = getAccountData(mx, CustomAccountDataEvent.RecentEmoji); let isLegacy = false; @@ -19,20 +49,23 @@ export const getRecentEmojis = (mx: MatrixClient, limit?: number): IEmoji[] => { recentEmojiEvent = getAccountData(mx, CustomAccountDataEvent.LegacyElementRecentEmoji); isLegacy = true; } - const recentEmoji = recentEmojiEvent?.getContent().recent_emoji; + const raw = recentEmojiEvent?.getContent().recent_emoji; - if (isLegacy && Array.isArray(recentEmoji)) { + if (!Array.isArray(raw)) return []; + + const hasTuples = raw.some((e) => isTuple(e as RecentEmojiEntry)); + const entries = normalizeEntries(raw as RecentEmojiEntry[]); + + if ((isLegacy || hasTuples) && entries.length > 0) { mx.setAccountData(CustomAccountDataEvent.RecentEmoji, { - recent_emoji: recentEmoji, + recent_emoji: entries, }).catch(() => {}); } - if (!Array.isArray(recentEmoji)) return []; - - return recentEmoji - .toSorted((e1, e2) => e2[1] - e1[1]) + return entries + .toSorted((a, b) => b.total - a.total) .slice(0, limit) - .reduce((list, [unicode]) => { + .reduce((list, { emoji: unicode }) => { const emoji = emojis.find((e) => e.unicode === unicode); if (emoji) list.push(emoji); return list; @@ -44,22 +77,22 @@ export function addRecentEmoji(mx: MatrixClient, unicode: string) { if (!recentEmojiEvent) { recentEmojiEvent = getAccountData(mx, CustomAccountDataEvent.LegacyElementRecentEmoji); } - const recentEmojiContent = recentEmojiEvent?.getContent(); - const recentEmoji = - recentEmojiContent && Array.isArray(recentEmojiContent.recent_emoji) - ? structuredClone(recentEmojiContent.recent_emoji) - : []; + const raw = recentEmojiEvent?.getContent().recent_emoji; + const recentEmoji = Array.isArray(raw) ? normalizeEntries(raw as RecentEmojiEntry[]) : []; - const emojiIndex = recentEmoji.findIndex(([u]) => u === unicode); - let entry: [EmojiUnicode, EmojiUsageCount]; - if (emojiIndex < 0) { - entry = [unicode, 1]; + const existingIndex = recentEmoji.findIndex((e) => e.emoji === unicode); + if (existingIndex < 0) { + recentEmoji.unshift({ emoji: unicode, total: 1 }); } else { - const spliced = recentEmoji.splice(emojiIndex, 1); - entry = spliced[0] ?? [unicode, 1]; - entry[1] += 1; + const [entry] = recentEmoji.splice(existingIndex, 1); + if (entry) { + entry.total += 1; + recentEmoji.unshift(entry); + } else { + recentEmoji.unshift({ emoji: unicode, total: 1 }); + } } - recentEmoji.unshift(entry); + mx.setAccountData(CustomAccountDataEvent.RecentEmoji, { recent_emoji: recentEmoji.slice(0, 100), }); diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index 61766bc19..f7f9edd9e 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -373,7 +373,7 @@ export type StartClientConfig = { }; export type ClientSyncDiagnostics = { - transport: 'sliding'; + transport: 'sliding' | 'classic'; syncState: string | null; sliding?: SlidingSyncDiagnostics; }; @@ -468,10 +468,11 @@ export const clearCacheAndReload = async (mx: MatrixClient) => { }; export const getClientSyncDiagnostics = (mx: MatrixClient): ClientSyncDiagnostics => { + const slidingManager = slidingSyncByClient.get(mx); return { - transport: 'sliding', + transport: slidingManager ? 'sliding' : 'classic', syncState: mx.getSyncState(), - sliding: slidingSyncByClient.get(mx)?.getDiagnostics(), + sliding: slidingManager?.getDiagnostics(), }; };