diff --git a/apps/web/src/@types/matrix-js-sdk.d.ts b/apps/web/src/@types/matrix-js-sdk.d.ts index 69f75fa891..59c5710284 100644 --- a/apps/web/src/@types/matrix-js-sdk.d.ts +++ b/apps/web/src/@types/matrix-js-sdk.d.ts @@ -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; diff --git a/apps/web/src/emojipicker/recent.ts b/apps/web/src/emojipicker/recent.ts index 05c1321716..09d88b0aa3 100644 --- a/apps/web/src/emojipicker/recent.ts +++ b/apps/web/src/emojipicker/recent.ts @@ -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, + ), + ); +} + 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); } diff --git a/apps/web/src/settings/handlers/AccountSettingsHandler.ts b/apps/web/src/settings/handlers/AccountSettingsHandler.ts index 4145fc1bb6..5ac1302af4 100644 --- a/apps/web/src/settings/handlers/AccountSettingsHandler.ts +++ b/apps/web/src/settings/handlers/AccountSettingsHandler.ts @@ -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; + } }