Sync recently used reactions list across sessions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/21833/head
Michael Telatynski 2020-07-16 04:15:32 +01:00
parent 113a0f1b5c
commit a7f92f35f5
4 changed files with 96 additions and 35 deletions

View File

@ -1,35 +0,0 @@
/*
Copyright 2019 Tulir Asokan <tulir@maunium.net>
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);
}

73
src/emojipicker/recent.ts Normal file
View File

@ -0,0 +1,73 @@
/*
Copyright 2019 Tulir Asokan <tulir@maunium.net>
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);
}

View File

@ -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: [],
},

View File

@ -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) || {};