From 93b4ea5be43eda0d70963c54af80721cfa27b2ac Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 Aug 2020 21:57:37 +0100 Subject: [PATCH 1/5] Create Maps utility comparison functions --- src/utils/maps.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/utils/maps.ts diff --git a/src/utils/maps.ts b/src/utils/maps.ts new file mode 100644 index 0000000000..0ac931a06e --- /dev/null +++ b/src/utils/maps.ts @@ -0,0 +1,49 @@ +/* +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 { arrayDiff, arrayMerge, arrayUnion } from "./arrays"; + +/** + * Determines the keys added, changed, and removed between two Maps. + * For changes, simple triple equal comparisons are done, not in-depth + * tree checking. + * @param a The first object. Must be defined. + * @param b The second object. Must be defined. + * @returns The difference between the keys of each object. + */ +export function mapDiff(a: Map, b: Map): { changed: K[], added: K[], removed: K[] } { + const aKeys = [...a.keys()]; + const bKeys = [...b.keys()]; + const keyDiff = arrayDiff(aKeys, bKeys); + const possibleChanges = arrayUnion(aKeys, bKeys); + const changes = possibleChanges.filter(k => a.get(k) !== b.get(k)); + + return {changed: changes, added: keyDiff.added, removed: keyDiff.removed}; +} + +/** + * Gets all the key changes (added, removed, or value difference) between + * two Maps. Triple equals is used to compare values, not in-depth tree + * checking. + * @param a The first object. Must be defined. + * @param b The second object. Must be defined. + * @returns The keys which have been added, removed, or changed between the + * two objects. + */ +export function mapKeyChanges(a: Map, b: Map): K[] { + const diff = mapDiff(a, b); + return arrayMerge(diff.removed, diff.added, diff.changed); +} From ed4a4272877432f8574a444b05848c8e05e78688 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 Aug 2020 22:00:05 +0100 Subject: [PATCH 2/5] Convert hooks to Typescript --- src/hooks/{useAsyncMemo.js => useAsyncMemo.ts} | 6 ++++-- src/hooks/{useEventEmitter.js => useEventEmitter.ts} | 7 +++++-- src/hooks/{useSettings.js => useSettings.ts} | 4 ++-- src/hooks/{useStateToggle.js => useStateToggle.ts} | 6 +++--- 4 files changed, 14 insertions(+), 9 deletions(-) rename src/hooks/{useAsyncMemo.js => useAsyncMemo.ts} (81%) rename src/hooks/{useEventEmitter.js => useEventEmitter.ts} (86%) rename src/hooks/{useSettings.js => useSettings.ts} (90%) rename src/hooks/{useStateToggle.js => useStateToggle.ts} (85%) diff --git a/src/hooks/useAsyncMemo.js b/src/hooks/useAsyncMemo.ts similarity index 81% rename from src/hooks/useAsyncMemo.js rename to src/hooks/useAsyncMemo.ts index ef7d256b04..11c7aca7f1 100644 --- a/src/hooks/useAsyncMemo.js +++ b/src/hooks/useAsyncMemo.ts @@ -14,9 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useState, useEffect } from 'react'; +import {useState, useEffect, DependencyList} from 'react'; -export const useAsyncMemo = (fn, deps, initialValue) => { +type Fn = () => Promise; + +export const useAsyncMemo = (fn: Fn, deps: DependencyList, initialValue?: T) => { const [value, setValue] = useState(initialValue); useEffect(() => { fn().then(setValue); diff --git a/src/hooks/useEventEmitter.js b/src/hooks/useEventEmitter.ts similarity index 86% rename from src/hooks/useEventEmitter.js rename to src/hooks/useEventEmitter.ts index 6a758fb108..6b5693ef95 100644 --- a/src/hooks/useEventEmitter.js +++ b/src/hooks/useEventEmitter.ts @@ -15,11 +15,14 @@ limitations under the License. */ import {useRef, useEffect} from "react"; +import type {EventEmitter} from "events"; + +type Handler = (...args: any[]) => void; // Hook to wrap event emitter on and removeListener in hook lifecycle -export const useEventEmitter = (emitter, eventName, handler) => { +export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbol, handler: Handler) => { // Create a ref that stores handler - const savedHandler = useRef(); + const savedHandler = useRef(handler); // Update ref.current value if handler changes. useEffect(() => { diff --git a/src/hooks/useSettings.js b/src/hooks/useSettings.ts similarity index 90% rename from src/hooks/useSettings.js rename to src/hooks/useSettings.ts index 151a6369de..4d1e1d5bad 100644 --- a/src/hooks/useSettings.js +++ b/src/hooks/useSettings.ts @@ -18,7 +18,7 @@ import {useEffect, useState} from "react"; import SettingsStore from '../settings/SettingsStore'; // Hook to fetch the value of a setting and dynamically update when it changes -export const useSettingValue = (settingName, roomId = null, excludeDefault = false) => { +export const useSettingValue = (settingName: string, roomId: string = null, excludeDefault = false) => { const [value, setValue] = useState(SettingsStore.getValue(settingName, roomId, excludeDefault)); useEffect(() => { @@ -35,7 +35,7 @@ export const useSettingValue = (settingName, roomId = null, excludeDefault = fal }; // Hook to fetch whether a feature is enabled and dynamically update when that changes -export const useFeatureEnabled = (featureName, roomId = null) => { +export const useFeatureEnabled = (featureName: string, roomId: string = null) => { const [enabled, setEnabled] = useState(SettingsStore.isFeatureEnabled(featureName, roomId)); useEffect(() => { diff --git a/src/hooks/useStateToggle.js b/src/hooks/useStateToggle.ts similarity index 85% rename from src/hooks/useStateToggle.js rename to src/hooks/useStateToggle.ts index 58cf123bfb..85441df328 100644 --- a/src/hooks/useStateToggle.js +++ b/src/hooks/useStateToggle.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {useState} from 'react'; +import {useState} from "react"; // Hook to simplify toggling of a boolean state value // Returns value, method to toggle boolean value and method to set the boolean value -export const useStateToggle = (initialValue) => { - const [value, setValue] = useState(Boolean(initialValue)); +export const useStateToggle = (initialValue: boolean) => { + const [value, setValue] = useState(initialValue); const toggleValue = () => { setValue(!value); }; From f5d75bc078582e55a0b92b85c4881843d83e0248 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 Aug 2020 22:03:30 +0100 Subject: [PATCH 3/5] fix comments --- src/utils/maps.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/utils/maps.ts b/src/utils/maps.ts index 0ac931a06e..96832094f0 100644 --- a/src/utils/maps.ts +++ b/src/utils/maps.ts @@ -18,11 +18,10 @@ import { arrayDiff, arrayMerge, arrayUnion } from "./arrays"; /** * Determines the keys added, changed, and removed between two Maps. - * For changes, simple triple equal comparisons are done, not in-depth - * tree checking. - * @param a The first object. Must be defined. - * @param b The second object. Must be defined. - * @returns The difference between the keys of each object. + * For changes, simple triple equal comparisons are done, not in-depth tree checking. + * @param a The first Map. Must be defined. + * @param b The second Map. Must be defined. + * @returns The difference between the keys of each Map. */ export function mapDiff(a: Map, b: Map): { changed: K[], added: K[], removed: K[] } { const aKeys = [...a.keys()]; @@ -35,13 +34,11 @@ export function mapDiff(a: Map, b: Map): { changed: K[], added } /** - * Gets all the key changes (added, removed, or value difference) between - * two Maps. Triple equals is used to compare values, not in-depth tree - * checking. - * @param a The first object. Must be defined. - * @param b The second object. Must be defined. - * @returns The keys which have been added, removed, or changed between the - * two objects. + * Gets all the key changes (added, removed, or value difference) between two Maps. + * Triple equals is used to compare values, not in-depth tree checking. + * @param a The first Map. Must be defined. + * @param b The second Map. Must be defined. + * @returns The keys which have been added, removed, or changed between the two Maps. */ export function mapKeyChanges(a: Map, b: Map): K[] { const diff = mapDiff(a, b); From cd29edb442b801cde1ccbaba042d5b62263c1bf7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 Aug 2020 09:13:01 +0100 Subject: [PATCH 4/5] Write more typescript defs --- src/components/views/rooms/RoomList.tsx | 1 + .../handlers/AccountSettingsHandler.ts | 2 +- .../handlers/RoomAccountSettingsHandler.ts | 2 +- src/settings/handlers/RoomSettingsHandler.ts | 5 +-- src/utils/objects.ts | 34 +++++++++++-------- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 09fdbf0864..90270452fd 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -245,6 +245,7 @@ export default class RoomList extends React.PureComponent { if (doUpdate) { // We have to break our reference to the room list store if we want to be able to // diff the object for changes, so do that. + // @ts-ignore - ITagMap is ts-ignored so this will have to be too const newSublists = objectWithOnly(newLists, newListIds); const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v)); diff --git a/src/settings/handlers/AccountSettingsHandler.ts b/src/settings/handlers/AccountSettingsHandler.ts index 53180aeba8..d609fd3231 100644 --- a/src/settings/handlers/AccountSettingsHandler.ts +++ b/src/settings/handlers/AccountSettingsHandler.ts @@ -59,7 +59,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa } else if (event.getType() === "im.vector.web.settings") { // Figure out what changed and fire those updates const prevContent = prevEvent ? prevEvent.getContent() : {}; - const changedSettings = objectKeyChanges(prevContent, event.getContent()); + const changedSettings = objectKeyChanges>(prevContent, event.getContent()); for (const settingName of changedSettings) { const val = event.getContent()[settingName]; this.watchers.notifyUpdate(settingName, null, SettingLevel.ACCOUNT, val); diff --git a/src/settings/handlers/RoomAccountSettingsHandler.ts b/src/settings/handlers/RoomAccountSettingsHandler.ts index e3449e76c3..53e29653f8 100644 --- a/src/settings/handlers/RoomAccountSettingsHandler.ts +++ b/src/settings/handlers/RoomAccountSettingsHandler.ts @@ -59,7 +59,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin } else if (event.getType() === "im.vector.web.settings") { // Figure out what changed and fire those updates const prevContent = prevEvent ? prevEvent.getContent() : {}; - const changedSettings = objectKeyChanges(prevContent, event.getContent()); + const changedSettings = objectKeyChanges>(prevContent, event.getContent()); for (const settingName of changedSettings) { const val = event.getContent()[settingName]; this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_ACCOUNT, val); diff --git a/src/settings/handlers/RoomSettingsHandler.ts b/src/settings/handlers/RoomSettingsHandler.ts index 4b4d4c4ad9..3315e40a65 100644 --- a/src/settings/handlers/RoomSettingsHandler.ts +++ b/src/settings/handlers/RoomSettingsHandler.ts @@ -65,9 +65,10 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl } else if (event.getType() === "im.vector.web.settings") { // Figure out what changed and fire those updates const prevContent = prevEvent ? prevEvent.getContent() : {}; - const changedSettings = objectKeyChanges(prevContent, event.getContent()); + const changedSettings = objectKeyChanges>(prevContent, event.getContent()); for (const settingName of changedSettings) { - this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM, event.getContent()[settingName]); + this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM, + event.getContent()[settingName]); } } }; diff --git a/src/utils/objects.ts b/src/utils/objects.ts index ddd9830832..658491188d 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -16,15 +16,17 @@ limitations under the License. import { arrayDiff, arrayHasDiff, arrayMerge, arrayUnion } from "./arrays"; +type ObjectExcluding = {[k in Exclude]: O[k]}; + /** * Gets a new object which represents the provided object, excluding some properties. * @param a The object to strip properties of. Must be defined. * @param props The property names to remove. * @returns The new object without the provided properties. */ -export function objectExcluding(a: any, props: string[]): any { +export function objectExcluding(a: O, props: P): ObjectExcluding { // We use a Map to avoid hammering the `delete` keyword, which is slow and painful. - const tempMap = new Map(Object.entries(a)); + const tempMap = new Map(Object.entries(a) as [keyof O, any][]); for (const prop of props) { tempMap.delete(prop); } @@ -33,7 +35,7 @@ export function objectExcluding(a: any, props: string[]): any { return Array.from(tempMap.entries()).reduce((c, [k, v]) => { c[k] = v; return c; - }, {}); + }, {} as O); } /** @@ -43,13 +45,13 @@ export function objectExcluding(a: any, props: string[]): any { * @param props The property names to keep. * @returns The new object with only the provided properties. */ -export function objectWithOnly(a: any, props: string[]): any { - const existingProps = Object.keys(a); +export function objectWithOnly(a: O, props: P): {[k in P[number]]: O[k]} { + const existingProps = Object.keys(a) as (keyof O)[]; const diff = arrayDiff(existingProps, props); if (diff.removed.length === 0) { return objectShallowClone(a); } else { - return objectExcluding(a, diff.removed); + return objectExcluding(a, diff.removed) as {[k in P[number]]: O[k]}; } } @@ -64,9 +66,9 @@ export function objectWithOnly(a: any, props: string[]): any { * First argument is the property key with the second being the current value. * @returns A cloned object. */ -export function objectShallowClone(a: any, propertyCloner?: (k: string, v: any) => any): any { - const newObj = {}; - for (const [k, v] of Object.entries(a)) { +export function objectShallowClone(a: O, propertyCloner?: (k: keyof O, v: O[keyof O]) => any): O { + const newObj = {} as O; + for (const [k, v] of Object.entries(a) as [keyof O, O[keyof O]][]) { newObj[k] = v; if (propertyCloner) { newObj[k] = propertyCloner(k, v); @@ -83,7 +85,7 @@ export function objectShallowClone(a: any, propertyCloner?: (k: string, v: any) * @param b The second object. Must be defined. * @returns True if there's a difference between the objects, false otherwise */ -export function objectHasDiff(a: any, b: any): boolean { +export function objectHasDiff(a: O, b: O): boolean { const aKeys = Object.keys(a); const bKeys = Object.keys(b); if (arrayHasDiff(aKeys, bKeys)) return true; @@ -92,6 +94,8 @@ export function objectHasDiff(a: any, b: any): boolean { return possibleChanges.some(k => a[k] !== b[k]); } +type Diff = { changed: K[], added: K[], removed: K[] }; + /** * Determines the keys added, changed, and removed between two objects. * For changes, simple triple equal comparisons are done, not in-depth @@ -100,9 +104,9 @@ export function objectHasDiff(a: any, b: any): boolean { * @param b The second object. Must be defined. * @returns The difference between the keys of each object. */ -export function objectDiff(a: any, b: any): { changed: string[], added: string[], removed: string[] } { - const aKeys = Object.keys(a); - const bKeys = Object.keys(b); +export function objectDiff(a: O, b: O): Diff { + const aKeys = Object.keys(a) as (keyof O)[]; + const bKeys = Object.keys(b) as (keyof O)[]; const keyDiff = arrayDiff(aKeys, bKeys); const possibleChanges = arrayUnion(aKeys, bKeys); const changes = possibleChanges.filter(k => a[k] !== b[k]); @@ -119,7 +123,7 @@ export function objectDiff(a: any, b: any): { changed: string[], added: string[] * @returns The keys which have been added, removed, or changed between the * two objects. */ -export function objectKeyChanges(a: any, b: any): string[] { +export function objectKeyChanges(a: O, b: O): (keyof O)[] { const diff = objectDiff(a, b); return arrayMerge(diff.removed, diff.added, diff.changed); } @@ -131,6 +135,6 @@ export function objectKeyChanges(a: any, b: any): string[] { * @param obj The object to clone. * @returns The cloned object */ -export function objectClone(obj: any): any { +export function objectClone(obj: O): O { return JSON.parse(JSON.stringify(obj)); } From 6220f503601126f03e26dfe3755e04b7ab1ca251 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 Aug 2020 09:28:02 +0100 Subject: [PATCH 5/5] delint --- src/utils/objects.ts | 4 ++-- src/widgets/WidgetApi.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/objects.ts b/src/utils/objects.ts index 658491188d..bc74ab9ee0 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -24,7 +24,7 @@ type ObjectExcluding = {[k in Exclude(a: O, props: P): ObjectExcluding { +export function objectExcluding>(a: O, props: P): ObjectExcluding { // We use a Map to avoid hammering the `delete` keyword, which is slow and painful. const tempMap = new Map(Object.entries(a) as [keyof O, any][]); for (const prop of props) { @@ -45,7 +45,7 @@ export function objectExcluding(a: O, props * @param props The property names to keep. * @returns The new object with only the provided properties. */ -export function objectWithOnly(a: O, props: P): {[k in P[number]]: O[k]} { +export function objectWithOnly>(a: O, props: P): {[k in P[number]]: O[k]} { const existingProps = Object.keys(a) as (keyof O)[]; const diff = arrayDiff(existingProps, props); if (diff.removed.length === 0) { diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index 4775c30c95..039f72ce0f 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -141,7 +141,7 @@ export class WidgetApi extends EventEmitter { private replyToRequest(payload: ToWidgetRequest, reply: any) { if (!window.parent) return; - const request = objectClone(payload); + const request: ToWidgetRequest & {response?: any} = objectClone(payload); request.response = reply; window.parent.postMessage(request, this.origin);