Add support for m.recent_emoji account data event

Maintains read-compatibility with `io.element.recent_emoji`
This commit is contained in:
Michael Telatynski 2026-04-16 12:18:29 +01:00
parent 7537d33817
commit f7a3977f19
No known key found for this signature in database
GPG Key ID: A2B008A5F49F5D0D
3 changed files with 56 additions and 31 deletions

View File

@ -16,6 +16,7 @@ import type { DeviceClientInformation } from "../utils/device/types.ts";
import type { UserWidget } from "../utils/WidgetUtils-types.ts";
import { type MediaPreviewConfig } from "./media_preview.ts";
import { type INVITE_RULES_ACCOUNT_DATA_TYPE, type InviteConfigAccountData } from "./invite-rules.ts";
import { type LegacyRecentEmojiData } from "../emojipicker/recent.ts";
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
declare module "matrix-js-sdk/src/types" {
@ -71,7 +72,7 @@ declare module "matrix-js-sdk/src/types" {
[key: `io.element.matrix_client_information.${string}`]: DeviceClientInformation;
// Element settings account data events
"im.vector.setting.breadcrumbs": { recent_rooms: string[] };
"io.element.recent_emoji": { recent_emoji: string[] };
"io.element.recent_emoji": { recent_emoji: LegacyRecentEmojiData };
"im.vector.setting.integration_provisioning": { enabled: boolean };
"im.vector.riot.breadcrumb_rooms": { rooms: string[] };
"im.vector.web.settings": Record<string, any>;

View File

@ -8,16 +8,13 @@ Please see LICENSE files in the repository root for full details.
*/
import { orderBy } from "lodash";
import { type AccountDataEvents } from "matrix-js-sdk/src/matrix";
import SettingsStore from "../settings/SettingsStore";
import { SettingLevel } from "../settings/SettingLevel";
interface ILegacyFormat {
[emoji: string]: [number, number]; // [count, date]
}
// New format tries to be more space efficient for synchronization. Ordered by Date descending.
export type RecentEmojiData = [emoji: string, count: number][];
export type RecentEmojiData = AccountDataEvents["m.recent_emoji"]["recent_emoji"];
export type LegacyRecentEmojiData = [emoji: string, count: number][];
const SETTING_NAME = "recent_emoji";
@ -25,43 +22,57 @@ const SETTING_NAME = "recent_emoji";
// even if you haven't used your typically favourite emoji for a little while.
const STORAGE_LIMIT = 100;
// TODO remove this after some time
function migrate(): void {
const data: ILegacyFormat = JSON.parse(window.localStorage.mx_reaction_count || "{}");
const sorted = Object.entries(data).sort(([, [count1, date1]], [, [count2, date2]]) => date2 - date1);
const newFormat = sorted.map(([emoji, [count, date]]) => [emoji, count]);
SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, newFormat.slice(0, STORAGE_LIMIT));
}
function getRecentEmoji(): RecentEmojiData {
return SettingsStore.getValue(SETTING_NAME) || [];
}
export function translateLegacyEmojiData(legacyData: LegacyRecentEmojiData): RecentEmojiData {
return legacyData.map(([emoji, total]) => ({
emoji,
total,
}));
}
export function mergeEmojiData(data1: RecentEmojiData, data2?: RecentEmojiData): RecentEmojiData {
if (!data2) return data1;
return Object.values(
[...data1, ...data2].reduce(
(acc, item) => {
const existing = acc[item.emoji];
// If it doesn't exist or the current total is higher, update it
if (!existing || item.total > existing.total) {
acc[item.emoji] = item;
}
return acc;
},
{} as Record<string, RecentEmojiData[number]>,
),
);
}
export function add(emoji: string): void {
const recents = getRecentEmoji();
const i = recents.findIndex(([e]) => e === emoji);
const i = recents.findIndex((entry) => entry.emoji === emoji);
let newEntry;
let newEntry: RecentEmojiData[number];
if (i >= 0) {
// first remove the existing tuple so that we can increment it and push it to the front
[newEntry] = recents.splice(i, 1);
newEntry[1]++; // increment the usage count
newEntry.total++; // increment the usage count
} else {
newEntry = [emoji, 1];
newEntry = { emoji, total: 1 };
}
SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, [newEntry, ...recents].slice(0, STORAGE_LIMIT));
}
export function get(limit = 24): string[] {
let recents = getRecentEmoji();
if (recents.length < 1) {
migrate();
recents = getRecentEmoji();
}
const recents = getRecentEmoji();
// perform a stable sort on `count` to keep the recent (date) order as a secondary sort factor
const sorted = orderBy(recents, "1", "desc");
return sorted.slice(0, limit).map(([emoji]) => emoji);
return sorted.slice(0, limit).map(({ emoji }) => emoji);
}

View File

@ -16,11 +16,13 @@ import { SettingLevel } from "../SettingLevel";
import { type WatchManager } from "../WatchManager";
import { MEDIA_PREVIEW_ACCOUNT_DATA_TYPE } from "../../@types/media_preview";
import { type SettingKey, type Settings } from "../Settings.tsx";
import { mergeEmojiData, type RecentEmojiData, translateLegacyEmojiData } from "../../emojipicker/recent.ts";
const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms";
const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs";
const BREADCRUMBS_EVENT_TYPES = [BREADCRUMBS_LEGACY_EVENT_TYPE, BREADCRUMBS_EVENT_TYPE];
const RECENT_EMOJI_EVENT_TYPE = "io.element.recent_emoji";
const LEGACY_RECENT_EMOJI_EVENT_TYPE = "io.element.recent_emoji";
const RECENT_EMOJI_EVENT_TYPE = "m.recent_emoji";
const INTEG_PROVISIONING_EVENT_TYPE = "im.vector.setting.integration_provisioning";
const ANALYTICS_EVENT_TYPE = "im.vector.analytics";
const DEFAULT_SETTINGS_EVENT_TYPE = "im.vector.web.settings";
@ -66,8 +68,8 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
} else if (event.getType() === INTEG_PROVISIONING_EVENT_TYPE) {
const val = event.getContent()["enabled"];
this.watchers.notifyUpdate("integrationProvisioning", null, SettingLevel.ACCOUNT, val);
} else if (event.getType() === RECENT_EMOJI_EVENT_TYPE) {
const val = event.getContent()["enabled"];
} else if (event.getType() === RECENT_EMOJI_EVENT_TYPE || event.getType() === LEGACY_RECENT_EMOJI_EVENT_TYPE) {
const val = this.getRecentEmoji();
this.watchers.notifyUpdate("recent_emoji", null, SettingLevel.ACCOUNT, val);
} else if (event.getType() === MEDIA_PREVIEW_ACCOUNT_DATA_TYPE) {
this.watchers.notifyUpdate("mediaPreviewConfig", null, SettingLevel.ROOM_ACCOUNT, event.getContent());
@ -102,8 +104,8 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
// Special case recent emoji
if (settingName === "recent_emoji") {
const content = this.getSettings(RECENT_EMOJI_EVENT_TYPE);
return content ? content["recent_emoji"] : null;
const val = this.getRecentEmoji();
return val ?? null;
}
// Special case integration manager provisioning
@ -265,4 +267,15 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
}
this.watchers.notifyUpdate("breadcrumb_rooms", null, SettingLevel.ACCOUNT, val || []);
}
private getRecentEmoji(): RecentEmojiData {
let val = this.getSettings(RECENT_EMOJI_EVENT_TYPE)?.recent_emoji || [];
const legacyVal = this.getSettings(LEGACY_RECENT_EMOJI_EVENT_TYPE)?.recent_emoji;
if (legacyVal) {
val = mergeEmojiData(val, translateLegacyEmojiData(legacyVal));
}
return val;
}
}