diff --git a/src/emojipicker/recent.js b/src/emojipicker/recent.js deleted file mode 100644 index 1d2106fbfb..0000000000 --- a/src/emojipicker/recent.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2019 Tulir Asokan - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -const REACTION_COUNT = JSON.parse(window.localStorage.mx_reaction_count || '{}'); -let sorted = null; - -export function add(emoji) { - const [count] = REACTION_COUNT[emoji] || [0]; - REACTION_COUNT[emoji] = [count + 1, Date.now()]; - window.localStorage.mx_reaction_count = JSON.stringify(REACTION_COUNT); - sorted = null; -} - -export function get(limit = 24) { - if (sorted === null) { - sorted = Object.entries(REACTION_COUNT) - .sort(([, [count1, date1]], [, [count2, date2]]) => - count2 === count1 ? date2 - date1 : count2 - count1) - .map(([emoji, count]) => emoji); - } - return sorted.slice(0, limit); -} diff --git a/src/emojipicker/recent.ts b/src/emojipicker/recent.ts new file mode 100644 index 0000000000..e3977faadd --- /dev/null +++ b/src/emojipicker/recent.ts @@ -0,0 +1,73 @@ +/* +Copyright 2019 Tulir Asokan +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; +import {sortBy} from "lodash"; + +interface ILegacyFormat { + [emoji: string]: [number, number]; // [count, date] +} + +// New format tries to be more space efficient for synchronization. Ordered by Date descending. +type Format = [string, number][]; // [emoji, count] + +const SETTING_NAME = "recent_emoji"; + +// we store more recents than we typically query but this lets us sort by weighted usage +// 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() { + 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(): Format { + return SettingsStore.getValue(SETTING_NAME) || []; +} + +export function add(emoji: string) { + const recents = getRecentEmoji(); + const i = recents.findIndex(([e]) => e === emoji); + + let newEntry; + 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 + } else { + newEntry = [emoji, 1]; + } + + SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, [newEntry, ...recents].slice(0, STORAGE_LIMIT)); +} + +export function get(limit = 24) { + let recents = getRecentEmoji(); + + if (recents.length < 1) { + migrate(); + recents = getRecentEmoji(); + } + + // perform a stable sort on `count` to keep the recent (date) order as a secondary sort factor + const sorted = sortBy(recents, "1"); + return sorted.slice(0, limit).map(([emoji]) => emoji); +} diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 3b1218c0d3..16c5a27f79 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -351,6 +351,12 @@ export const SETTINGS = { default: "en", }, "breadcrumb_rooms": { + // not really a setting + supportedLevels: ['account'], + default: [], + }, + "recent_emoji": { + // not really a setting supportedLevels: ['account'], default: [], }, diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index 732ce6c550..4048d8ddea 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -23,6 +23,7 @@ import {objectClone, objectKeyChanges} from "../../utils/objects"; 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 INTEG_PROVISIONING_EVENT_TYPE = "im.vector.setting.integration_provisioning"; @@ -69,6 +70,9 @@ 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']; + this._watchers.notifyUpdate("recent_emoji", null, SettingLevel.ACCOUNT, val); } } @@ -95,6 +99,12 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa return content && content['recent_rooms'] ? content['recent_rooms'] : []; } + // Special case recent emoji + if (settingName === "recent_emoji") { + const content = this._getSettings(RECENT_EMOJI_EVENT_TYPE); + return content ? content["recent_emoji"] : null; + } + // Special case integration manager provisioning if (settingName === "integrationProvisioning") { const content = this._getSettings(INTEG_PROVISIONING_EVENT_TYPE); @@ -135,6 +145,13 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa return MatrixClientPeg.get().setAccountData(BREADCRUMBS_EVENT_TYPE, content); } + // Special case recent emoji + if (settingName === "recent_emoji") { + const content = this._getSettings(RECENT_EMOJI_EVENT_TYPE) || {}; + content["recent_emoji"] = newValue; + return MatrixClientPeg.get().setAccountData(RECENT_EMOJI_EVENT_TYPE, content); + } + // Special case integration manager provisioning if (settingName === "integrationProvisioning") { const content = this._getSettings(INTEG_PROVISIONING_EVENT_TYPE) || {};