Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-close-menu-reactions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Fix right click menu not closing when viewing reactions.
5 changes: 5 additions & 0 deletions .changeset/add-file-extension-for-gifs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Send gifs with proper file extensions in body.
5 changes: 5 additions & 0 deletions .changeset/fix-autocomplete-crash-edits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Fix crash in autocomplete menu when editing messages.
5 changes: 5 additions & 0 deletions .changeset/fix-call-button-reactivity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Ensure call buttons appear/disappear when relevant state events occur.
5 changes: 5 additions & 0 deletions .changeset/fix-recent-emoji-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Fix format for recent emojis and related crash.
5 changes: 5 additions & 0 deletions .changeset/fix-sliding-sync-debug-display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Fix sliding sync debug saying sliding when it is in fact not sliding.
1 change: 1 addition & 0 deletions src/app/components/emoji-board/EmojiBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ export function EmojiBoard({
width,
height,
size: fullRes?.size || preview?.size || 0,
mimetype: 'image/gif',
};
}, []);

Expand Down
1 change: 1 addition & 0 deletions src/app/components/emoji-board/components/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export function GifItem({
width: gif.width,
height: gif.height,
size: gif.size,
mimetype: gif.mimetype,
},
],
})
Expand Down
1 change: 1 addition & 0 deletions src/app/components/emoji-board/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export type GifData = {
width?: number;
height?: number;
size?: number;
mimetype?: string;
};
1 change: 1 addition & 0 deletions src/app/components/message/content/ImageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ export const ImageContent = as<'div', ImageContentProps>(
width: imageW,
height: imageH,
size: info?.size,
mimetype: info?.mimetype,
},
],
})
Expand Down
11 changes: 10 additions & 1 deletion src/app/components/message/modals/MessageReactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion src/app/components/message/modals/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,9 @@ export function OptionMenu({
</Text>
</MenuItem>
)}
{relations && <MessageAllReactionItem room={room} relations={relations} />}
{relations && (
<MessageAllReactionItem room={room} relations={relations} closeMenu={closeMenu} />
)}
<MenuItem
size="300"
after={menuIcon(ArrowBendUpLeftIcon)}
Expand Down
9 changes: 7 additions & 2 deletions src/app/features/room/message/MessageEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
RoomMentionAutocomplete,
UserMentionAutocomplete,
createEmoticonElement,
focusEditor,
customHtmlEqualsPlainText,
getAutocompleteQuery,
getPrevWorldRange,
Expand Down Expand Up @@ -365,8 +366,12 @@ export const MessageEditor = as<'div', MessageEditorProps>(
);

const handleCloseAutocomplete = useCallback(() => {
ReactEditor.focus(editor);
setAutocompleteQuery(undefined);
setAutocompleteQuery((prev) => {
if (prev !== undefined) {
focusEditor(editor);
}
return undefined;
});
}, [editor]);

const handleEmoticonSelect = (key: string, shortcode: string) => {
Expand Down
8 changes: 6 additions & 2 deletions src/app/features/room/msgContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
},
};

Expand Down
42 changes: 31 additions & 11 deletions src/app/hooks/useCallStartCapabilities.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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]);
};
77 changes: 55 additions & 22 deletions src/app/plugins/recent-emoji.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,64 @@ 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;
if (!recentEmojiEvent) {
recentEmojiEvent = getAccountData(mx, CustomAccountDataEvent.LegacyElementRecentEmoji);
isLegacy = true;
}
const recentEmoji = recentEmojiEvent?.getContent<IRecentEmojiContent>().recent_emoji;
const raw = recentEmojiEvent?.getContent<IRecentEmojiContent>().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<IEmoji[]>((list, [unicode]) => {
.reduce<IEmoji[]>((list, { emoji: unicode }) => {
const emoji = emojis.find((e) => e.unicode === unicode);
if (emoji) list.push(emoji);
return list;
Expand All @@ -44,22 +77,22 @@ export function addRecentEmoji(mx: MatrixClient, unicode: string) {
if (!recentEmojiEvent) {
recentEmojiEvent = getAccountData(mx, CustomAccountDataEvent.LegacyElementRecentEmoji);
}
const recentEmojiContent = recentEmojiEvent?.getContent<IRecentEmojiContent>();
const recentEmoji =
recentEmojiContent && Array.isArray(recentEmojiContent.recent_emoji)
? structuredClone(recentEmojiContent.recent_emoji)
: [];
const raw = recentEmojiEvent?.getContent<IRecentEmojiContent>().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),
});
Expand Down
7 changes: 4 additions & 3 deletions src/client/initMatrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ export type StartClientConfig = {
};

export type ClientSyncDiagnostics = {
transport: 'sliding';
transport: 'sliding' | 'classic';
syncState: string | null;
sliding?: SlidingSyncDiagnostics;
};
Expand Down Expand Up @@ -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(),
};
};

Expand Down
Loading