From 6eee39c153f4f303c775866f42bd0366c194077a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 May 2020 12:04:13 +0100 Subject: [PATCH 001/179] Fix /op Slash Command Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/SlashCommands.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index fbb9e2eb0e..6fbf56a518 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -729,9 +729,9 @@ export const Commands = [ const cli = MatrixClientPeg.get(); const room = cli.getRoom(roomId); if (!room) return reject(_t("Command failed")); - + const member = room.getMember(args); + if (!member) return reject(_t("Could not find user in room")); const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); - if (!powerLevelEvent.getContent().users[args]) return reject(_t("Could not find user in room")); return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent)); } } From b4e2e54dc1bc096759ea00156b868ac922ef18cc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 May 2020 12:06:20 +0100 Subject: [PATCH 002/179] make test more specific Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/SlashCommands.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 6fbf56a518..cea780e361 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -730,7 +730,7 @@ export const Commands = [ const room = cli.getRoom(roomId); if (!room) return reject(_t("Command failed")); const member = room.getMember(args); - if (!member) return reject(_t("Could not find user in room")); + if (!member || member.membership !== "join") return reject(_t("Could not find user in room")); const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent)); } From 7699aafcaf27c51950306451a82648d4d376be71 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 23 Jun 2020 16:41:36 +0100 Subject: [PATCH 003/179] Use new eslint package- fix lint issues in ts and js --- .eslintrc.js | 126 +---- package.json | 4 +- src/@types/global.d.ts | 8 +- src/ContentMessages.tsx | 6 +- src/DeviceListener.ts | 10 +- src/actions/RoomListActions.ts | 4 +- src/actions/TagOrderActions.ts | 1 - src/autocomplete/QueryMatcher.ts | 2 +- src/components/views/elements/AppTile.js | 1 + .../views/messages/DateSeparator.js | 2 +- .../views/room_settings/UrlPreviewSettings.js | 2 +- src/dispatcher/payloads/ViewTooltipPayload.ts | 2 +- src/editor/serialize.ts | 3 +- src/hooks/useDispatcher.ts | 2 +- src/indexing/BaseEventIndexManager.ts | 28 +- src/settings/watchers/Watcher.ts | 2 +- src/stores/BreadcrumbsStore.ts | 1 - src/stores/CustomRoomTagStore.js | 3 +- src/stores/MessagePreviewStore.ts | 2 +- src/stores/ToastStore.ts | 4 +- src/stores/room-list/RoomListStore2.ts | 6 +- src/stores/room-list/algorithms/Algorithm.ts | 8 +- .../list-ordering/ImportanceAlgorithm.ts | 1 + .../list-ordering/NaturalAlgorithm.ts | 7 +- .../algorithms/list-ordering/index.ts | 6 +- .../tag-sorting/AlphabeticAlgorithm.ts | 2 - .../algorithms/tag-sorting/RecentAlgorithm.ts | 2 +- src/utils/FormattingUtils.ts | 12 +- src/utils/ShieldUtils.ts | 2 +- src/widgets/WidgetApi.ts | 2 +- yarn.lock | 473 +++++++++++------- 31 files changed, 387 insertions(+), 347 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6a0576c58a..0613121df0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,111 +11,35 @@ const path = require('path'); const matrixJsSdkPath = path.join(path.dirname(require.resolve('matrix-js-sdk')), '..'); module.exports = { + extends: ["matrix-org", "matrix-org/react-legacy"], parser: "babel-eslint", - extends: [matrixJsSdkPath + "/.eslintrc.js"], - plugins: [ - "react", - "react-hooks", - "flowtype", - "babel" - ], + + env: { + browser: true, + node: true, + }, globals: { LANGUAGES_FILE: "readonly", }, - env: { - es6: true, - }, - parserOptions: { - ecmaFeatures: { - jsx: true, - legacyDecorators: true, - } - }, rules: { - // eslint's built in no-invalid-this rule breaks with class properties - "no-invalid-this": "off", - // so we replace it with a version that is class property aware - "babel/no-invalid-this": "error", - - // We appear to follow this most of the time, so let's enforce it instead - // of occasionally following it (or catching it in review) - "keyword-spacing": "error", - - /** react **/ - // This just uses the react plugin to help eslint known when - // variables have been used in JSX - "react/jsx-uses-vars": "error", - // Don't mark React as unused if we're using JSX - "react/jsx-uses-react": "error", - - // bind or arrow function in props causes performance issues - // (but we currently use them in some places) - // It's disabled here, but we should using it sparingly. - "react/jsx-no-bind": "off", - "react/jsx-key": ["error"], - - // Components in JSX should always be defined. - "react/jsx-no-undef": "error", - - // Assert no spacing in JSX curly brackets - // - // - // https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-curly-spacing.md - // - // Disabled for now - if anything we'd like to *enforce* spacing in JSX - // curly brackets for legibility, but in practice it's not clear that the - // consistency particularly improves legibility here. --Matthew - // - // "react/jsx-curly-spacing": ["error", {"when": "never", "children": {"when": "always"}}], - - // Assert spacing before self-closing JSX tags, and no spacing before or - // after the closing slash, and no spacing after the opening bracket of - // the opening tag or closing tag. - // - // https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-tag-spacing.md - "react/jsx-tag-spacing": ["error"], - - /** flowtype **/ - "flowtype/require-parameter-type": ["warn", { - "excludeArrowFunctions": true, - }], - "flowtype/define-flow-type": "warn", - "flowtype/require-return-type": ["warn", - "always", - { - "annotateUndefined": "never", - "excludeArrowFunctions": true, - } - ], - "flowtype/space-after-type-colon": ["warn", "always"], - "flowtype/space-before-type-colon": ["warn", "never"], - - /* - * things that are errors in the js-sdk config that the current - * code does not adhere to, turned down to warn - */ - "max-len": ["warn", { - // apparently people believe the length limit shouldn't apply - // to JSX. - ignorePattern: '^\\s*<', - ignoreComments: true, - ignoreRegExpLiterals: true, - code: 120, - }], - "valid-jsdoc": ["warn"], - "new-cap": ["warn"], - "key-spacing": ["warn"], - "prefer-const": ["warn"], - - // crashes currently: https://github.com/eslint/eslint/issues/6274 - "generator-star-spacing": "off", - - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - }, - settings: { - flowtype: { - onlyFilesWithFlowAnnotation: true - }, + // Things we do that break the ideal style + "no-constant-condition": "off", + "prefer-promise-reject-errors": "off", + "no-async-promise-executor": "off", + "quotes": "off", + "indent": "off", }, + + overrides: [{ + files: ["src/**/*.{ts, tsx}"], + "extends": ["matrix-org/ts"], + "rules": { + // We disable this while we're transitioning + "@typescript-eslint/no-explicit-any": "off", + // We'd rather not do this but we do + "@typescript-eslint/ban-ts-comment": "off", + + "quotes": "off", + } + }], }; diff --git a/package.json b/package.json index 5f9b7dde1f..99b826cbd6 100644 --- a/package.json +++ b/package.json @@ -129,13 +129,15 @@ "@types/react-dom": "^16.9.8", "@types/react-transition-group": "^4.4.0", "@types/zxcvbn": "^4.4.0", + "@typescript-eslint/eslint-plugin": "^3.4.0", + "@typescript-eslint/parser": "^3.4.0", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", "chokidar": "^3.3.1", "concurrently": "^4.0.1", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.15.1", - "eslint": "^5.12.0", + "eslint": "7.3.1", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^5.2.1", "eslint-plugin-flowtype": "^2.30.0", diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index ffd3277892..e2a59e83ab 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -29,10 +29,10 @@ declare global { init: () => Promise; }; - mx_ContentMessages: ContentMessages; - mx_ToastStore: ToastStore; - mx_DeviceListener: DeviceListener; - mx_RoomListStore2: RoomListStore2; + mxContentMessages: ContentMessages; + mxToastStore: ToastStore; + mxDeviceListener: DeviceListener; + mxRoomListStore2: RoomListStore2; } // workaround for https://github.com/microsoft/TypeScript/issues/30933 diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 25445b1c74..e0597f5e59 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -621,9 +621,9 @@ export default class ContentMessages { } static sharedInstance() { - if (window.mx_ContentMessages === undefined) { - window.mx_ContentMessages = new ContentMessages(); + if (window.mxContentMessages === undefined) { + window.mxContentMessages = new ContentMessages(); } - return window.mx_ContentMessages; + return window.mxContentMessages; } } diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index cfec2890d2..a37521118f 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -17,16 +17,16 @@ limitations under the License. import {MatrixClientPeg} from './MatrixClientPeg'; import { hideToast as hideBulkUnverifiedSessionsToast, - showToast as showBulkUnverifiedSessionsToast + showToast as showBulkUnverifiedSessionsToast, } from "./toasts/BulkUnverifiedSessionsToast"; import { hideToast as hideSetupEncryptionToast, Kind as SetupKind, - showToast as showSetupEncryptionToast + showToast as showSetupEncryptionToast, } from "./toasts/SetupEncryptionToast"; import { hideToast as hideUnverifiedSessionsToast, - showToast as showUnverifiedSessionsToast + showToast as showUnverifiedSessionsToast, } from "./toasts/UnverifiedSessionToast"; import {privateShouldBeEncrypted} from "./createRoom"; @@ -48,8 +48,8 @@ export default class DeviceListener { private displayingToastsForDeviceIds = new Set(); static sharedInstance() { - if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener(); - return window.mx_DeviceListener; + if (!window.mxDeviceListener) window.mxDeviceListener = new DeviceListener(); + return window.mxDeviceListener; } start() { diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index e15e1b0c65..1936c22b8d 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -107,7 +107,7 @@ export default class RoomListActions { ) { const promiseToDelete = matrixClient.deleteRoomTag( roomId, oldTag, - ).catch(function (err) { + ).catch(function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to remove tag " + oldTag + " from room: " + err); Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { @@ -127,7 +127,7 @@ export default class RoomListActions { // at least be an empty object. metaData = metaData || {}; - const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function (err) { + const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to add tag " + newTag + " to room: " + err); Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { diff --git a/src/actions/TagOrderActions.ts b/src/actions/TagOrderActions.ts index 75097952c0..c203172874 100644 --- a/src/actions/TagOrderActions.ts +++ b/src/actions/TagOrderActions.ts @@ -22,7 +22,6 @@ import { AsyncActionPayload } from "../dispatcher/payloads"; import { MatrixClient } from "matrix-js-sdk/src/client"; export default class TagOrderActions { - /** * Creates an action thunk that will do an asynchronous request to * move a tag in TagOrderStore to destinationIx. diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 7a0219e264..d80f0df909 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -118,7 +118,7 @@ export default class QueryMatcher { const index = resultKey.indexOf(query); if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) { matches.push( - ...candidates.map((candidate) => ({index, ...candidate})) + ...candidates.map((candidate) => ({index, ...candidate})), ); } } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 9129b8fe48..ef0fa83fb6 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -704,6 +704,7 @@ export default class AppTile extends React.Component { _onReloadWidgetClick() { // Reload iframe in this way to avoid cross-origin restrictions + // eslint-disable-next-line no-self-assign this._appFrame.current.src = this._appFrame.current.src; } diff --git a/src/components/views/messages/DateSeparator.js b/src/components/views/messages/DateSeparator.js index 56faa670b2..ef4b5d16d1 100644 --- a/src/components/views/messages/DateSeparator.js +++ b/src/components/views/messages/DateSeparator.js @@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler'; import {formatFullDateNoTime} from '../../../DateUtils'; function getdaysArray() { - return [ + return [ _t('Sunday'), _t('Monday'), _t('Tuesday'), diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.js index cd00e5048c..51f6954975 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.js +++ b/src/components/views/room_settings/UrlPreviewSettings.js @@ -58,7 +58,7 @@ export default createReactClass({ 'a': (sub)=>{ sub }, }) ); - } else if (accountEnabled) { + } else { previewsForAccount = ( _t("You have disabled URL previews by default.", {}, { 'a': (sub)=>{ sub }, diff --git a/src/dispatcher/payloads/ViewTooltipPayload.ts b/src/dispatcher/payloads/ViewTooltipPayload.ts index 069e3a619a..780dad23cf 100644 --- a/src/dispatcher/payloads/ViewTooltipPayload.ts +++ b/src/dispatcher/payloads/ViewTooltipPayload.ts @@ -32,4 +32,4 @@ export interface ViewTooltipPayload extends ActionPayload { * the parent type. */ parent: null | Element; -} \ No newline at end of file +} diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 7e8f4a3bfc..c550f54291 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -31,7 +31,8 @@ export function mdSerialize(model: EditorModel) { return html + part.text; case "room-pill": case "user-pill": - return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`; + return html + + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`; } }, ""); } diff --git a/src/hooks/useDispatcher.ts b/src/hooks/useDispatcher.ts index 004b15fcef..f21eb8922b 100644 --- a/src/hooks/useDispatcher.ts +++ b/src/hooks/useDispatcher.ts @@ -22,7 +22,7 @@ import {Dispatcher} from "flux"; // Hook to simplify listening to flux dispatches export const useDispatcher = (dispatcher: Dispatcher, handler: (payload: ActionPayload) => void) => { // Create a ref that stores handler - const savedHandler = useRef((payload: ActionPayload) => {}); + const savedHandler = useRef((_: ActionPayload) => {}); // Update ref.current value if handler changes. useEffect(() => { diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts index be7b89be37..38c1a52e05 100644 --- a/src/indexing/BaseEventIndexManager.ts +++ b/src/indexing/BaseEventIndexManager.ts @@ -18,14 +18,14 @@ export interface MatrixEvent { type: string; sender: string; content: {}; - event_id: string; - origin_server_ts: number; + eventId: string; + originServerTs: number; unsigned?: {}; - room_id: string; + roomId: string; } export interface MatrixProfile { - avatar_url: string; + avatarUrl: string; displayname: string; } @@ -37,9 +37,9 @@ export interface CrawlerCheckpoint { } export interface ResultContext { - events_before: [MatrixEvent]; - events_after: [MatrixEvent]; - profile_info: Map; + eventsBefore: [MatrixEvent]; + eventsAfter: [MatrixEvent]; + profileInfo: Map; } export interface ResultsElement { @@ -55,11 +55,11 @@ export interface SearchResult { } export interface SearchArgs { - search_term: string; - before_limit: number; - after_limit: number; - order_by_recency: boolean; - room_id?: string; + searchTerm: string; + beforeLimit: number; + afterLimit: number; + orderByRecency: boolean; + roomId?: string; } export interface EventAndProfile { @@ -76,8 +76,8 @@ export interface LoadArgs { export interface IndexStats { size: number; - event_count: number; - room_count: number; + eventCount: number; + roomCount: number; } /** diff --git a/src/settings/watchers/Watcher.ts b/src/settings/watchers/Watcher.ts index 94a14faa27..031c2738fa 100644 --- a/src/settings/watchers/Watcher.ts +++ b/src/settings/watchers/Watcher.ts @@ -17,4 +17,4 @@ limitations under the License. export default interface IWatcher { start(): void; stop(): void; -} \ No newline at end of file +} diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 332fa7fe2e..7cd46222e0 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -162,5 +162,4 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds); } } - } diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index 48c80294b4..ad5bdd86bf 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -20,7 +20,8 @@ import { throttle } from "lodash"; import SettingsStore from "../settings/SettingsStore"; import {RoomListStoreTempProxy} from "./room-list/RoomListStoreTempProxy"; -const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; +const STANDARD_TAGS_REGEX = + /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; function commonPrefix(a, b) { const len = Math.min(a.length, b.length); diff --git a/src/stores/MessagePreviewStore.ts b/src/stores/MessagePreviewStore.ts index 64d65a72f3..3dad643ae6 100644 --- a/src/stores/MessagePreviewStore.ts +++ b/src/stores/MessagePreviewStore.ts @@ -61,7 +61,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient { /** * Gets the pre-translated preview for a given room * @param room The room to get the preview for. - * @returns The preview, or null if none present. + * @returns {string} The preview, or null if none present. */ public getPreviewForRoom(room: Room): string { if (!room) return null; // invalid room, just return nothing diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index 55c48c3937..65f6ba91af 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -37,8 +37,8 @@ export default class ToastStore extends EventEmitter { private countSeen = 0; static sharedInstance() { - if (!window.mx_ToastStore) window.mx_ToastStore = new ToastStore(); - return window.mx_ToastStore; + if (!window.mxToastStore) window.mxToastStore = new ToastStore(); + return window.mxToastStore; } reset() { diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 9684e338f8..1081fb26ec 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -180,7 +180,9 @@ export class RoomListStore2 extends AsyncStore { const roomId = eventPayload.event.getRoomId(); const room = this.matrixClient.getRoom(roomId); const tryUpdate = async (updatedRoom: Room) => { - console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`); + console.log( + `[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`, + ); if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') { console.log(`[RoomListDebug] Got tombstone event - regenerating room list`); // TODO: We could probably be smarter about this @@ -380,4 +382,4 @@ export default class RoomListStore { } } -window.mx_RoomListStore2 = RoomListStore.instance; +window.mxRoomListStore2 = RoomListStore.instance; diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 052c58bb83..06c6df1703 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -27,7 +27,7 @@ import { ITagMap, ITagSortingMap, ListAlgorithm, - SortAlgorithm + SortAlgorithm, } from "./models"; import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition"; import { EffectiveMembership, splitRoomsByMembership } from "../membership"; @@ -305,7 +305,7 @@ export class Algorithm extends EventEmitter { if (!this._stickyRoom) { // If there's no sticky room, just do nothing useful. - if (!!this._cachedStickyRooms) { + if (this._cachedStickyRooms) { // Clear the cache if we won't be needing it this._cachedStickyRooms = null; this.emit(LIST_UPDATED_EVENT); @@ -518,13 +518,12 @@ export class Algorithm extends EventEmitter { } } - let tags = this.roomIdsToTags[room.roomId]; + const tags = this.roomIdsToTags[room.roomId]; if (!tags) { console.warn(`No tags known for "${room.name}" (${room.roomId})`); return false; } - let changed = false; for (const tag of tags) { const algorithm: OrderingAlgorithm = this.algorithms[tag]; if (!algorithm) throw new Error(`No algorithm for ${tag}`); @@ -535,7 +534,6 @@ export class Algorithm extends EventEmitter { // Flag that we've done something this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed - changed = true; } return true; diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 15fa00c302..294bcdd35a 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -290,6 +290,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { if (indices[lastCat] > indices[thisCat]) { // "should never happen" disclaimer goes here + // eslint-disable-next-line max-len console.warn(`!! Room list index corruption: ${lastCat} (i:${indices[lastCat]}) is greater than ${thisCat} (i:${indices[thisCat]}) - category indices are likely desynced from reality`); // TODO: Regenerate index when this happens diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index 96a3f58d2c..21bd54e0f0 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -25,7 +25,6 @@ import { Room } from "matrix-js-sdk/src/models/room"; * additional behavioural changes are present. */ export class NaturalAlgorithm extends OrderingAlgorithm { - public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { super(tagId, initialSortingAlgorithm); console.log(`[RoomListDebug] Constructed a NaturalAlgorithm for ${tagId}`); @@ -51,7 +50,11 @@ export class NaturalAlgorithm extends OrderingAlgorithm { // TODO: Optimize this to avoid useless operations // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags - this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm); + this.cachedOrderedRooms = await sortRoomsWithAlgorithm( + this.cachedOrderedRooms, + this.tagId, + this.sortingAlgorithm, + ); return true; } diff --git a/src/stores/room-list/algorithms/list-ordering/index.ts b/src/stores/room-list/algorithms/list-ordering/index.ts index 8156c3a250..c002dac421 100644 --- a/src/stores/room-list/algorithms/list-ordering/index.ts +++ b/src/stores/room-list/algorithms/list-ordering/index.ts @@ -36,7 +36,11 @@ const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: AlgorithmFactory } = * @param {SortAlgorithm} initSort The initial sorting algorithm for the ordering algorithm. * @returns {Algorithm} The algorithm instance. */ -export function getListAlgorithmInstance(algorithm: ListAlgorithm, tagId: TagID, initSort: SortAlgorithm): OrderingAlgorithm { +export function getListAlgorithmInstance( + algorithm: ListAlgorithm, + tagId: TagID, + initSort: SortAlgorithm, +): OrderingAlgorithm { if (!ALGORITHM_FACTORIES[algorithm]) { throw new Error(`${algorithm} is not a known algorithm`); } diff --git a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts index 8d74ebd11e..d909fb6288 100644 --- a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts @@ -17,8 +17,6 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { TagID } from "../../models"; import { IAlgorithm } from "./IAlgorithm"; -import { MatrixClientPeg } from "../../../../MatrixClientPeg"; -import * as Unread from "../../../../Unread"; /** * Sorts rooms according to the browser's determination of alphabetic. diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts index 4e4df6c9d6..e41d1c4c8a 100644 --- a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts @@ -25,7 +25,7 @@ import * as Unread from "../../../../Unread"; * useful to the user. */ export class RecentAlgorithm implements IAlgorithm { - public async sortRooms(rooms: Room[], tagId: TagID): Promise { + public async sortRooms(rooms: Room[], _: TagID): Promise { // We cache the timestamp lookup to avoid iterating forever on the timeline // of events. This cache only survives a single sort though. // We wouldn't need this if `.sort()` didn't constantly try and compare all diff --git a/src/utils/FormattingUtils.ts b/src/utils/FormattingUtils.ts index f82500de71..52f8bfcfa2 100644 --- a/src/utils/FormattingUtils.ts +++ b/src/utils/FormattingUtils.ts @@ -22,12 +22,12 @@ import { _t } from '../languageHandler'; * e.g: 999, 9.9K, 99K, 0.9M, 9.9M, 99M, 0.9B, 9.9B */ export function formatCount(count: number): string { - if (count < 1000) return count.toString(); - if (count < 10000) return (count / 1000).toFixed(1) + "K"; - if (count < 100000) return (count / 1000).toFixed(0) + "K"; - if (count < 10000000) return (count / 1000000).toFixed(1) + "M"; - if (count < 100000000) return (count / 1000000).toFixed(0) + "M"; - return (count / 1000000000).toFixed(1) + "B"; // 10B is enough for anyone, right? :S + if (count < 1000) return count.toString(); + if (count < 10000) return (count / 1000).toFixed(1) + "K"; + if (count < 100000) return (count / 1000).toFixed(0) + "K"; + if (count < 10000000) return (count / 1000000).toFixed(1) + "M"; + if (count < 100000000) return (count / 1000000).toFixed(0) + "M"; + return (count / 1000000000).toFixed(1) + "B"; // 10B is enough for anyone, right? :S } /** diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts index 3f8cf06815..878ed3959c 100644 --- a/src/utils/ShieldUtils.ts +++ b/src/utils/ShieldUtils.ts @@ -27,7 +27,7 @@ export async function shieldStatusForRoom(client: Client, room: Room): Promise userId !== client.getUserId()) .forEach((userId) => { (client.checkUserTrust(userId).isCrossSigningVerified() ? - verified : unverified).push(userId); + verified : unverified).push(userId); }); /* Alarm if any unverified users were verified before. */ diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index 795c6648ef..39a4554a81 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -112,7 +112,7 @@ export class WidgetApi extends EventEmitter { // Finalization needs to be async, so postpone with a promise let finalizePromise = Promise.resolve(); const wait = (promise) => { - finalizePromise = finalizePromise.then(value => promise); + finalizePromise = finalizePromise.then(() => promise); }; this.emit('terminate', wait); Promise.resolve(finalizePromise).then(() => { diff --git a/yarn.lock b/yarn.lock index d2d53692b5..4d709c4282 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,7 +18,14 @@ optionalDependencies: chokidar "^2.1.8" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1", "@babel/code-frame@^7.5.5": +"@babel/code-frame@^7.0.0": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.3.tgz#324bcfd8d35cd3d47dae18cde63d752086435e9a" + integrity sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg== + dependencies: + "@babel/highlight" "^7.10.3" + +"@babel/code-frame@^7.10.1", "@babel/code-frame@^7.5.5": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff" integrity sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw== @@ -252,10 +259,10 @@ dependencies: "@babel/types" "^7.10.1" -"@babel/helper-validator-identifier@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz#5770b0c1a826c4f53f5ede5e153163e0318e94b5" - integrity sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw== +"@babel/helper-validator-identifier@^7.10.1", "@babel/helper-validator-identifier@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz#60d9847f98c4cea1b279e005fdb7c28be5412d15" + integrity sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw== "@babel/helper-wrap-function@^7.10.1": version "7.10.1" @@ -276,12 +283,12 @@ "@babel/traverse" "^7.10.1" "@babel/types" "^7.10.1" -"@babel/highlight@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.1.tgz#841d098ba613ba1a427a2b383d79e35552c38ae0" - integrity sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg== +"@babel/highlight@^7.10.1", "@babel/highlight@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.3.tgz#c633bb34adf07c5c13156692f5922c81ec53f28d" + integrity sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw== dependencies: - "@babel/helper-validator-identifier" "^7.10.1" + "@babel/helper-validator-identifier" "^7.10.3" chalk "^2.0.0" js-tokens "^4.0.0" @@ -1257,6 +1264,16 @@ resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999" integrity sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ== +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== + "@types/fbemitter@*": version "2.0.32" resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c" @@ -1410,6 +1427,28 @@ resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.0.tgz#fbc1d941cc6d9d37d18405c513ba6b294f89b609" integrity sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg== +"@typescript-eslint/eslint-plugin@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.4.0.tgz#8378062e6be8a1d049259bdbcf27ce5dfbeee62b" + integrity sha512-wfkpiqaEVhZIuQRmudDszc01jC/YR7gMSxa6ulhggAe/Hs0KVIuo9wzvFiDbG3JD5pRFQoqnf4m7REDsUvBnMQ== + dependencies: + "@typescript-eslint/experimental-utils" "3.4.0" + debug "^4.1.1" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.4.0.tgz#8a44dfc6fb7f1d071937b390fe27608ebda122b8" + integrity sha512-rHPOjL43lOH1Opte4+dhC0a/+ks+8gOBwxXnyrZ/K4OTAChpSjP76fbI8Cglj7V5GouwVAGaK+xVwzqTyE/TPw== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "3.4.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + "@typescript-eslint/experimental-utils@^2.5.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" @@ -1420,6 +1459,16 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" +"@typescript-eslint/parser@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.4.0.tgz#fe52b68c5cb3bba3f5d875bd17adb70420d49d8d" + integrity sha512-ZUGI/de44L5x87uX5zM14UYcbn79HSXUR+kzcqU42gH0AgpdB/TjuJy3m4ezI7Q/jk3wTQd755mxSDLhQP79KA== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "3.4.0" + "@typescript-eslint/typescript-estree" "3.4.0" + eslint-visitor-keys "^1.1.0" + "@typescript-eslint/typescript-estree@2.34.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" @@ -1433,6 +1482,19 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.4.0.tgz#6a787eb70b48969e4cd1ea67b057083f96dfee29" + integrity sha512-zKwLiybtt4uJb4mkG5q2t6+W7BuYx2IISiDNV+IY68VfoGwErDx/RfVI7SWL4gnZ2t1A1ytQQwZ+YOJbHHJ2rw== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -1601,7 +1663,7 @@ acorn-globals@^4.1.0: acorn "^6.0.1" acorn-walk "^6.0.1" -acorn-jsx@^5.0.0: +acorn-jsx@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== @@ -1616,11 +1678,16 @@ acorn@^5.5.3: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== -acorn@^6.0.1, acorn@^6.0.7, acorn@^6.4.1: +acorn@^6.0.1, acorn@^6.4.1: version "6.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== +acorn@^7.2.0: + version "7.3.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" + integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== + agent-base@4, agent-base@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" @@ -1668,7 +1735,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== -ajv@^6.1.0, ajv@^6.10.2, ajv@^6.5.5, ajv@^6.9.1: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: version "6.12.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== @@ -1683,7 +1750,12 @@ another-json@^0.2.0: resolved "https://registry.yarnpkg.com/another-json/-/another-json-0.2.0.tgz#b5f4019c973b6dd5c6506a2d93469cb6d32aeedc" integrity sha1-tfQBnJc7bdXGUGotk0acttMq7tw= -ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: +ansi-colors@^3.2.1: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-escapes@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== @@ -1703,6 +1775,11 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -1710,6 +1787,14 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -2379,7 +2464,7 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw== -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2388,6 +2473,14 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4. escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + character-entities-html4@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125" @@ -2408,11 +2501,6 @@ character-reference-invalid@^1.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - cheerio@^1.0.0-rc.3: version "1.0.0-rc.3" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" @@ -2499,18 +2587,6 @@ classnames@^2.1.2, classnames@^2.2.5: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= - dependencies: - restore-cursor "^2.0.0" - -cli-width@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" - integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== - cliui@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" @@ -2567,6 +2643,13 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-convert@~0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" @@ -2577,6 +2660,11 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2777,7 +2865,7 @@ create-react-class@^15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" -cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@6.0.5, cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -2788,6 +2876,15 @@ cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -2936,7 +3033,7 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@~0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= @@ -3251,6 +3348,13 @@ enhanced-resolve@^4.1.0: memory-fs "^0.5.0" tapable "^1.0.0" +enquirer@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" + integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA== + dependencies: + ansi-colors "^3.2.1" + entities@^1.1.1, "entities@~ 1.1.1", entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -3477,7 +3581,7 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-scope@^5.0.0: +eslint-scope@^5.0.0, eslint-scope@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== @@ -3485,13 +3589,6 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.3.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== - dependencies: - eslint-visitor-keys "^1.1.0" - eslint-utils@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" @@ -3499,68 +3596,68 @@ eslint-utils@^2.0.0: dependencies: eslint-visitor-keys "^1.1.0" -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz#74415ac884874495f78ec2a97349525344c981fa" - integrity sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ== +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== -eslint@^5.12.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" - integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg== +eslint@7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.3.1.tgz#76392bd7e44468d046149ba128d1566c59acbe19" + integrity sha512-cQC/xj9bhWUcyi/RuMbRtC3I0eW8MH0jhRELSvpKYkWep3C6YZ2OkvcvJVUeO6gcunABmzptbXBuDoXsjHmfTA== dependencies: "@babel/code-frame" "^7.0.0" - ajv "^6.9.1" - chalk "^2.1.0" - cross-spawn "^6.0.5" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" - eslint-scope "^4.0.3" - eslint-utils "^1.3.1" - eslint-visitor-keys "^1.0.0" - espree "^5.0.1" - esquery "^1.0.1" + enquirer "^2.3.5" + eslint-scope "^5.1.0" + eslint-utils "^2.0.0" + eslint-visitor-keys "^1.2.0" + espree "^7.1.0" + esquery "^1.2.0" esutils "^2.0.2" file-entry-cache "^5.0.1" functional-red-black-tree "^1.0.1" - glob "^7.1.2" - globals "^11.7.0" + glob-parent "^5.0.0" + globals "^12.1.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^6.2.2" - js-yaml "^3.13.0" + is-glob "^4.0.0" + js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.11" + levn "^0.4.1" + lodash "^4.17.14" minimatch "^3.0.4" - mkdirp "^0.5.1" natural-compare "^1.4.0" - optionator "^0.8.2" - path-is-inside "^1.0.2" + optionator "^0.9.1" progress "^2.0.0" - regexpp "^2.0.1" - semver "^5.5.1" - strip-ansi "^4.0.0" - strip-json-comments "^2.0.1" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" table "^5.2.3" text-table "^0.2.0" + v8-compile-cache "^2.0.3" -espree@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a" - integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A== +espree@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.1.0.tgz#a9c7f18a752056735bf1ba14cb1b70adc3a5ce1c" + integrity sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw== dependencies: - acorn "^6.0.7" - acorn-jsx "^5.0.0" - eslint-visitor-keys "^1.0.0" + acorn "^7.2.0" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.2.0" esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.0.1: +esquery@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== @@ -3714,15 +3811,6 @@ extend@^3.0.0, extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -3769,7 +3857,7 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -3815,13 +3903,6 @@ figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" - file-entry-cache@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-4.0.0.tgz#633567d15364aefe0b299e1e217735e8f3a9f6e8" @@ -4116,7 +4197,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@~5.1.0: +glob-parent@^5.0.0, glob-parent@~5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== @@ -4192,11 +4273,18 @@ global-prefix@^3.0.0: kind-of "^6.0.2" which "^1.3.1" -globals@^11.1.0, globals@^11.7.0: +globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + dependencies: + type-fest "^0.8.1" + globby@^9.0.0: version "9.2.0" resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" @@ -4256,6 +4344,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.0, has-symbols@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" @@ -4449,7 +4542,7 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -4563,25 +4656,6 @@ ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@^6.2.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - internal-slot@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3" @@ -5424,7 +5498,7 @@ jest@^24.9.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.0, js-yaml@^3.13.1: +js-yaml@^3.13.1: version "3.14.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== @@ -5599,7 +5673,15 @@ levenary@^1.1.1: dependencies: leven "^3.1.0" -levn@^0.3.0, levn@~0.3.0: +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= @@ -5693,7 +5775,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1: +lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -5962,11 +6044,6 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.44.0" -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - mimic-fn@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -6065,11 +6142,6 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= - nan@^2.12.1: version "2.14.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" @@ -6356,14 +6428,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= - dependencies: - mimic-fn "^1.0.0" - -optionator@^0.8.1, optionator@^0.8.2: +optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== @@ -6375,6 +6440,18 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" word-wrap "~1.2.3" +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" @@ -6389,11 +6466,6 @@ os-locale@^3.0.0, os-locale@^3.1.0: lcid "^2.0.0" mem "^4.0.0" -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -6578,16 +6650,16 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" @@ -6797,6 +6869,11 @@ postcss@^7.0.1, postcss@^7.0.13, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.2 source-map "^0.6.1" supports-color "^6.1.0" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -7323,10 +7400,10 @@ regexp.prototype.flags@^1.3.0: define-properties "^1.1.3" es-abstract "^1.17.0-next.1" -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== +regexpp@^3.0.0, regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== regexpu-core@^4.7.0: version "4.7.0" @@ -7539,14 +7616,6 @@ resolve@^1.10.0, resolve@^1.12.0, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.8. dependencies: path-parse "^1.0.6" -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= - dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" - ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -7610,11 +7679,6 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== -run-async@^2.2.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -7622,7 +7686,7 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@^6.4.0, rxjs@^6.5.2: +rxjs@^6.5.2: version "6.5.5" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== @@ -7715,7 +7779,7 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^7.3.2: +semver@^7.2.1, semver@^7.3.2: version "7.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== @@ -7767,11 +7831,23 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" @@ -8068,7 +8144,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -8190,6 +8266,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -8205,7 +8288,12 @@ strip-indent@^2.0.0: resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= -strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: +strip-json-comments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" + integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== + +strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= @@ -8319,6 +8407,13 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + svg-tags@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" @@ -8406,11 +8501,6 @@ through2@^2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" -through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - timers-browserify@^2.0.4: version "2.0.11" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" @@ -8423,13 +8513,6 @@ tmatch@^2.0.1: resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf" integrity sha1-DFYkbzPzDaG409colauvFmYPOM8= -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -8572,6 +8655,13 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -8584,6 +8674,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -8829,6 +8924,11 @@ v8-compile-cache@2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== +v8-compile-cache@^2.0.3: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -9073,7 +9173,14 @@ which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: dependencies: isexe "^2.0.0" -word-wrap@~1.2.3: +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== From 0a2934203f0f2e1b7e873eb3aef3cdb163322cd2 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 23 Jun 2020 17:01:40 +0100 Subject: [PATCH 004/179] Remove tslint --- package.json | 4 +--- yarn.lock | 44 ++++---------------------------------------- 2 files changed, 5 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 99b826cbd6..dbde4a3cb7 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,8 @@ "start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all", "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", - "lint": "yarn lint:types && yarn lint:ts && yarn lint:js && yarn lint:style", + "lint": "yarn lint:types && yarn lint:js && yarn lint:style", "lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", - "lint:ts": "tslint --project ./tsconfig.json -t stylish", "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", @@ -159,7 +158,6 @@ "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", "stylelint-scss": "^3.9.0", - "tslint": "^5.20.1", "typescript": "^3.7.3", "walk": "^2.3.9", "webpack": "^4.20.2", diff --git a/yarn.lock b/yarn.lock index 4d709c4282..64dd36aaaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2348,11 +2348,6 @@ buffer@^5.4.3: base64-js "^1.0.2" ieee754 "^1.1.4" -builtin-modules@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= - builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -2464,7 +2459,7 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw== -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2672,7 +2667,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.12.1, commander@^2.15.1, commander@^2.19.0, commander@^2.20.0: +commander@^2.15.1, commander@^2.19.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3113,11 +3108,6 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -7764,7 +7754,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -8600,37 +8590,11 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== -tslib@^1.10.0, tslib@^1.11.1, tslib@^1.11.2, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.10.0, tslib@^1.11.1, tslib@^1.11.2, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.13.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== -tslint@^5.20.1: - version "5.20.1" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" - integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== - dependencies: - "@babel/code-frame" "^7.0.0" - builtin-modules "^1.1.1" - chalk "^2.3.0" - commander "^2.12.1" - diff "^4.0.1" - glob "^7.1.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - mkdirp "^0.5.1" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.8.0" - tsutils "^2.29.0" - -tsutils@^2.29.0: - version "2.29.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" - integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== - dependencies: - tslib "^1.8.1" - tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" From 497ab0f2129b3cb20cf66f45f254f8c419e0f2fc Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Jun 2020 14:52:59 +0100 Subject: [PATCH 005/179] Hopefully ake cancel dialog a bit less weird There's no design on how to fix this so I've switched the buttons and made the primary not a danger button. We could also try some different wording, eg. 'abort' rather than 'cancel' because with 'ancel' it's not clear if you're cancelling whatever you were trying to do or the dialog asking you if you want to cancel... Ideal might be to make the cancel button red but that means making it a separate button or adding support for doing so to DialogButtons, so not going to do that unless we're sure that's what we want. Fixes https://github.com/vector-im/riot-web/issues/14140 --- src/CrossSigningManager.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index a80c91a59a..105f47c832 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -54,11 +54,11 @@ async function confirmToDismiss(name) { const [sure] = await Modal.createDialog(QuestionDialog, { title: _t("Cancel entering passphrase?"), description, - danger: true, - cancelButton: _t("Enter passphrase"), - button: _t("Cancel"), + danger: false, + button: _t("Enter passphrase"), + cancelButton: _t("Cancel"), }).finished; - return sure; + return !sure; } async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { From ac771f6a60ceed53a57a88cc742a903f20607846 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 6 Jul 2020 15:26:40 +0100 Subject: [PATCH 006/179] New copy on passphrase cancel dialog --- src/CrossSigningManager.js | 17 ++++------------- src/i18n/strings/en_EN.json | 8 +++----- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 105f47c832..a584a69d35 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -40,22 +40,13 @@ export class AccessCancelledError extends Error { } } -async function confirmToDismiss(name) { - let description; - if (name === "m.cross_signing.user_signing") { - description = _t("If you cancel now, you won't complete verifying the other user."); - } else if (name === "m.cross_signing.self_signing") { - description = _t("If you cancel now, you won't complete verifying your other session."); - } else { - description = _t("If you cancel now, you won't complete your operation."); - } - +async function confirmToDismiss() { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const [sure] = await Modal.createDialog(QuestionDialog, { title: _t("Cancel entering passphrase?"), - description, + description: _t("Are you sure you want to cancel entering passphrase?"), danger: false, - button: _t("Enter passphrase"), + button: _t("Go Back"), cancelButton: _t("Cancel"), }).finished; return !sure; @@ -102,7 +93,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { /* options= */ { onBeforeClose: async (reason) => { if (reason === "backgroundClick") { - return confirmToDismiss(ssssItemName); + return confirmToDismiss(); } return true; }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b23264a297..e38a7f28fc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -62,11 +62,9 @@ "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", "The server does not support the room version specified.": "The server does not support the room version specified.", "Failure to create room": "Failure to create room", - "If you cancel now, you won't complete verifying the other user.": "If you cancel now, you won't complete verifying the other user.", - "If you cancel now, you won't complete verifying your other session.": "If you cancel now, you won't complete verifying your other session.", - "If you cancel now, you won't complete your operation.": "If you cancel now, you won't complete your operation.", "Cancel entering passphrase?": "Cancel entering passphrase?", - "Enter passphrase": "Enter passphrase", + "Are you sure you want to cancel entering passphrase?": "Are you sure you want to cancel entering passphrase?", + "Go Back": "Go Back", "Cancel": "Cancel", "Setting up keys": "Setting up keys", "Sun": "Sun", @@ -1851,7 +1849,6 @@ "Enter your Security Phrase or to continue.": "Enter your Security Phrase or to continue.", "Security Key": "Security Key", "Use your Security Key to continue.": "Use your Security Key to continue.", - "Go Back": "Go Back", "Restoring keys from backup": "Restoring keys from backup", "Fetching keys from server...": "Fetching keys from server...", "%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored", @@ -2224,6 +2221,7 @@ "Export room keys": "Export room keys", "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.", "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", + "Enter passphrase": "Enter passphrase", "Confirm passphrase": "Confirm passphrase", "Export": "Export", "Import room keys": "Import room keys", From 545f11d742db110325a579c673de6f3ebcb08393 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 8 Jul 2020 22:23:51 -0600 Subject: [PATCH 007/179] Convert devtools dialog to use new room state format --- src/components/views/dialogs/DevtoolsDialog.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 08817cdfee..b3f9ef4537 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -412,13 +412,13 @@ class RoomStateExplorer extends React.PureComponent { if (this.state.eventType === null) { list = { - Object.keys(this.roomStateEvents).map((evType) => { - const stateGroup = this.roomStateEvents[evType]; - const stateKeys = Object.keys(stateGroup); + Array.from(this.roomStateEvents.keys()).map((evType) => { + const stateGroup = this.roomStateEvents.get(evType); + const stateKeys = Array.from(stateGroup.keys()); let onClickFn; if (stateKeys.length === 1 && stateKeys[0] === '') { - onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]); + onClickFn = this.onViewSourceClick(stateGroup.get(stateKeys[0])); } else { onClickFn = this.browseEventType(evType); } @@ -430,12 +430,12 @@ class RoomStateExplorer extends React.PureComponent { } ; } else { - const stateGroup = this.roomStateEvents[this.state.eventType]; + const stateGroup = this.roomStateEvents.get(this.state.eventType); list = { - Object.keys(stateGroup).map((stateKey) => { - const ev = stateGroup[stateKey]; + Array.from(stateGroup.keys()).map((stateKey) => { + const ev = stateGroup.get(stateKey); return ; From b868617ba3a7dc736f28bc55cfb246aecd69d7e1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 12 Jul 2020 21:13:28 +0100 Subject: [PATCH 008/179] Convert Modal to TypeScript Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/@types/global.d.ts | 2 + src/{Modal.js => Modal.tsx} | 251 ++++++++++++++++++++++-------------- 2 files changed, 157 insertions(+), 96 deletions(-) rename src/{Modal.js => Modal.tsx} (52%) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 3f970ea8c3..63c2c54138 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -22,6 +22,7 @@ import DeviceListener from "../DeviceListener"; import { RoomListStore2 } from "../stores/room-list/RoomListStore2"; import { PlatformPeg } from "../PlatformPeg"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; +import {ModalManager} from "../Modal"; declare global { interface Window { @@ -37,6 +38,7 @@ declare global { mx_RoomListStore2: RoomListStore2; mx_RoomListLayoutStore: RoomListLayoutStore; mxPlatformPeg: PlatformPeg; + singletonModalManager: ModalManager; // TODO: Remove flag before launch: https://github.com/vector-im/riot-web/issues/14231 mx_QuietRoomListLogging: boolean; diff --git a/src/Modal.js b/src/Modal.tsx similarity index 52% rename from src/Modal.js rename to src/Modal.tsx index 9b9f190d58..3243867555 100644 --- a/src/Modal.js +++ b/src/Modal.tsx @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +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. @@ -17,6 +18,8 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; +import classNames from 'classnames'; + import Analytics from './Analytics'; import dis from './dispatcher/dispatcher'; import {defer} from './utils/promise'; @@ -25,36 +28,52 @@ import AsyncWrapper from './AsyncWrapper'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; -class ModalManager { - constructor() { - this._counter = 0; +interface IModal { + elem: React.ReactNode; + className?: string; + beforeClosePromise?: Promise; + closeReason?: string; + onBeforeClose?(reason?: string): Promise; + onFinished(...args: T): void; + close(...args: T): void; +} - // The modal to prioritise over all others. If this is set, only show - // this modal. Remove all other modals from the stack when this modal - // is closed. - this._priorityModal = null; - // The modal to keep open underneath other modals if possible. Useful - // for cases like Settings where the modal should remain open while the - // user is prompted for more information/errors. - this._staticModal = null; - // A list of the modals we have stacked up, with the most recent at [0] - // Neither the static nor priority modal will be in this list. - this._modals = [ - /* { - elem: React component for this dialog - onFinished: caller-supplied onFinished callback - className: CSS class for the dialog wrapper div - } */ - ]; +interface IHandle { + finished: Promise; + close(...args: T): void; +} - this.onBackgroundClick = this.onBackgroundClick.bind(this); - } +interface IProps { + onFinished(...args: T): void; +} - hasDialogs() { - return this._priorityModal || this._staticModal || this._modals.length > 0; - } +interface IOptions { + onBeforeClose?: IModal["onBeforeClose"]; +} - getOrCreateContainer() { +type ParametersWithoutFirst any> = T extends (a: any, ...args: infer P) => any ? P : never; + +export class ModalManager { + private counter = 0; + // The modal to prioritise over all others. If this is set, only show + // this modal. Remove all other modals from the stack when this modal + // is closed. + private priorityModal: IModal = null; + // The modal to keep open underneath other modals if possible. Useful + // for cases like Settings where the modal should remain open while the + // user is prompted for more information/errors. + private staticModal: IModal = null; + // A list of the modals we have stacked up, with the most recent at [0] + // Neither the static nor priority modal will be in this list. + private modals: IModal[] = [ + /* { + elem: React component for this dialog + onFinished: caller-supplied onFinished callback + className: CSS class for the dialog wrapper div + } */ + ]; + + private static getOrCreateContainer() { let container = document.getElementById(DIALOG_CONTAINER_ID); if (!container) { @@ -66,7 +85,7 @@ class ModalManager { return container; } - getOrCreateStaticContainer() { + private static getOrCreateStaticContainer() { let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID); if (!container) { @@ -78,63 +97,93 @@ class ModalManager { return container; } - createTrackedDialog(analyticsAction, analyticsInfo, ...rest) { + public hasDialogs() { + return this.priorityModal || this.staticModal || this.modals.length > 0; + } + + public createTrackedDialog( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); return this.createDialog(...rest); } - appendTrackedDialog(analyticsAction, analyticsInfo, ...rest) { + public appendTrackedDialog( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); return this.appendDialog(...rest); } - createDialog(Element, ...rest) { + public createDialog(Element: React.ComponentType, ...rest: ParametersWithoutFirst) { return this.createDialogAsync(Promise.resolve(Element), ...rest); } - appendDialog(Element, ...rest) { + public appendDialog(Element: React.ComponentType, ...rest: ParametersWithoutFirst) { return this.appendDialogAsync(Promise.resolve(Element), ...rest); } - createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { + public createTrackedDialogAsync( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); return this.createDialogAsync(...rest); } - appendTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { + public appendTrackedDialogAsync( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); return this.appendDialogAsync(...rest); } - _buildModal(prom, props, className, options) { - const modal = {}; + private buildModal( + prom: Promise, + props?: IProps, + className?: string, + options?: IOptions + ) { + const modal: IModal = { + onFinished: props ? props.onFinished : null, + onBeforeClose: options.onBeforeClose, + beforeClosePromise: null, + closeReason: null, + className, + + // these will be set below but we need an object reference to pass to getCloseFn before we can do that + elem: null, + close: null, + }; // never call this from onFinished() otherwise it will loop - const [closeDialog, onFinishedProm] = this._getCloseFn(modal, props); + const [closeDialog, onFinishedProm] = this.getCloseFn(modal, props); // don't attempt to reuse the same AsyncWrapper for different dialogs, // otherwise we'll get confused. - const modalCount = this._counter++; + const modalCount = this.counter++; // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished // property set here so you can't close the dialog from a button click! - modal.elem = ( - - ); - modal.onFinished = props ? props.onFinished : null; - modal.className = className; - modal.onBeforeClose = options.onBeforeClose; - modal.beforeClosePromise = null; + modal.elem = ; modal.close = closeDialog; - modal.closeReason = null; return {modal, closeDialog, onFinishedProm}; } - _getCloseFn(modal, props) { - const deferred = defer(); - return [async (...args) => { + private getCloseFn( + modal: IModal, + props: IProps + ): [IHandle["close"], IHandle["finished"]] { + const deferred = defer(); + return [async (...args: T) => { if (modal.beforeClosePromise) { await modal.beforeClosePromise; } else if (modal.onBeforeClose) { @@ -147,26 +196,26 @@ class ModalManager { } deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); - const i = this._modals.indexOf(modal); + const i = this.modals.indexOf(modal); if (i >= 0) { - this._modals.splice(i, 1); + this.modals.splice(i, 1); } - if (this._priorityModal === modal) { - this._priorityModal = null; + if (this.priorityModal === modal) { + this.priorityModal = null; // XXX: This is destructive - this._modals = []; + this.modals = []; } - if (this._staticModal === modal) { - this._staticModal = null; + if (this.staticModal === modal) { + this.staticModal = null; // XXX: This is destructive - this._modals = []; + this.modals = []; } - this._reRender(); + this.reRender(); }, deferred.promise]; } @@ -207,38 +256,49 @@ class ModalManager { * @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog * @returns {object} Object with 'close' parameter being a function that will close the dialog */ - createDialogAsync(prom, props, className, isPriorityModal, isStaticModal, options = {}) { - const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, options); + private createDialogAsync( + prom: Promise, + props?: IProps & React.ComponentProps, + className?: string, + isPriorityModal = false, + isStaticModal = false, + options: IOptions = {} + ): IHandle { + const {modal, closeDialog, onFinishedProm} = this.buildModal(prom, props, className, options); if (isPriorityModal) { // XXX: This is destructive - this._priorityModal = modal; + this.priorityModal = modal; } else if (isStaticModal) { // This is intentionally destructive - this._staticModal = modal; + this.staticModal = modal; } else { - this._modals.unshift(modal); + this.modals.unshift(modal); } - this._reRender(); + this.reRender(); return { close: closeDialog, finished: onFinishedProm, }; } - appendDialogAsync(prom, props, className) { - const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, {}); + private appendDialogAsync( + prom: Promise, + props?: IProps, + className?: string + ): IHandle { + const {modal, closeDialog, onFinishedProm} = this.buildModal(prom, props, className, {}); - this._modals.push(modal); - this._reRender(); + this.modals.push(modal); + this.reRender(); return { close: closeDialog, finished: onFinishedProm, }; } - onBackgroundClick() { - const modal = this._getCurrentModal(); + private onBackgroundClick = () => { + const modal = this.getCurrentModal(); if (!modal) { return; } @@ -249,21 +309,21 @@ class ModalManager { modal.closeReason = "backgroundClick"; modal.close(); modal.closeReason = null; + }; + + private getCurrentModal(): IModal { + return this.priorityModal ? this.priorityModal : (this.modals[0] || this.staticModal); } - _getCurrentModal() { - return this._priorityModal ? this._priorityModal : (this._modals[0] || this._staticModal); - } - - _reRender() { - if (this._modals.length === 0 && !this._priorityModal && !this._staticModal) { + private reRender() { + if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) { // If there is no modal to render, make all of Riot available // to screen reader users again dis.dispatch({ action: 'aria_unhide_main_app', }); - ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); - ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); return; } @@ -274,49 +334,48 @@ class ModalManager { action: 'aria_hide_main_app', }); - if (this._staticModal) { - const classes = "mx_Dialog_wrapper mx_Dialog_staticWrapper " - + (this._staticModal.className ? this._staticModal.className : ''); + if (this.staticModal) { + const classes = classNames("mx_Dialog_wrapper mx_Dialog_staticWrapper", this.staticModal.className); const staticDialog = (
- { this._staticModal.elem } + { this.staticModal.elem }
-
+
); - ReactDOM.render(staticDialog, this.getOrCreateStaticContainer()); + ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer()); } else { // This is safe to call repeatedly if we happen to do that - ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); } - const modal = this._getCurrentModal(); - if (modal !== this._staticModal) { - const classes = "mx_Dialog_wrapper " - + (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '') - + (modal.className ? modal.className : ''); + const modal = this.getCurrentModal(); + if (modal !== this.staticModal) { + const classes = classNames("mx_Dialog_wrapper", modal.className, { + mx_Dialog_wrapperWithStaticUnder: this.staticModal, + }); const dialog = (
{modal.elem}
-
+
); - ReactDOM.render(dialog, this.getOrCreateContainer()); + ReactDOM.render(dialog, ModalManager.getOrCreateContainer()); } else { // This is safe to call repeatedly if we happen to do that - ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); } } } -if (!global.singletonModalManager) { - global.singletonModalManager = new ModalManager(); +if (!window.singletonModalManager) { + window.singletonModalManager = new ModalManager(); } -export default global.singletonModalManager; +export default window.singletonModalManager; From 004d954a5b5ec625467feb02deed381fa6811f6d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 12 Jul 2020 21:17:51 +0100 Subject: [PATCH 009/179] remove redundant comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Modal.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Modal.tsx b/src/Modal.tsx index 3243867555..8e0bff03e7 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -65,13 +65,7 @@ export class ModalManager { private staticModal: IModal = null; // A list of the modals we have stacked up, with the most recent at [0] // Neither the static nor priority modal will be in this list. - private modals: IModal[] = [ - /* { - elem: React component for this dialog - onFinished: caller-supplied onFinished callback - className: CSS class for the dialog wrapper div - } */ - ]; + private modals: IModal[] = []; private static getOrCreateContainer() { let container = document.getElementById(DIALOG_CONTAINER_ID); From 209c35013232f3e430d3fa45d7b163179dfe9e2e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jul 2020 00:19:15 +0100 Subject: [PATCH 010/179] Fix typing Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/ContentMessages.tsx | 12 ++++++------ src/Modal.tsx | 40 ++++++++++++++++++++++++---------------- src/SlashCommands.tsx | 12 +++++++----- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 25445b1c74..afb3081448 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -386,7 +386,7 @@ export default class ContentMessages { const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); if (isQuoting) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const {finished} = Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, { + const {finished} = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, { title: _t('Replying With Files'), description: (
{_t( @@ -397,7 +397,7 @@ export default class ContentMessages { hasCancelButton: true, button: _t("Continue"), }); - const [shouldUpload]: [boolean] = await finished; + const [shouldUpload] = await finished; if (!shouldUpload) return; } @@ -420,12 +420,12 @@ export default class ContentMessages { if (tooBigFiles.length > 0) { const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); - const {finished} = Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, { + const {finished} = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, { badFiles: tooBigFiles, totalFiles: files.length, contentMessages: this, }); - const [shouldContinue]: [boolean] = await finished; + const [shouldContinue] = await finished; if (!shouldContinue) return; } @@ -437,12 +437,12 @@ export default class ContentMessages { for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { - const {finished} = Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { + const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, { file, currentIndex: i, totalFiles: okFiles.length, }); - const [shouldContinue, shouldUploadAll]: [boolean, boolean] = await finished; + const [shouldContinue, shouldUploadAll] = await finished; if (!shouldContinue) break; if (shouldUploadAll) { uploadAll = true; diff --git a/src/Modal.tsx b/src/Modal.tsx index 8e0bff03e7..b744dbacf4 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -44,7 +44,9 @@ interface IHandle { } interface IProps { - onFinished(...args: T): void; + onFinished?(...args: T): void; + // TODO improve typing here once all Modals are TS and we can exhaustively check the props + [key: string]: any; } interface IOptions { @@ -95,48 +97,54 @@ export class ModalManager { return this.priorityModal || this.staticModal || this.modals.length > 0; } - public createTrackedDialog( + public createTrackedDialog( analyticsAction: string, analyticsInfo: string, ...rest: Parameters ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.createDialog(...rest); + return this.createDialog(...rest); } - public appendTrackedDialog( + public appendTrackedDialog( analyticsAction: string, analyticsInfo: string, ...rest: Parameters ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.appendDialog(...rest); + return this.appendDialog(...rest); } - public createDialog(Element: React.ComponentType, ...rest: ParametersWithoutFirst) { - return this.createDialogAsync(Promise.resolve(Element), ...rest); + public createDialog( + Element: React.ComponentType, + ...rest: ParametersWithoutFirst + ) { + return this.createDialogAsync(Promise.resolve(Element), ...rest); } - public appendDialog(Element: React.ComponentType, ...rest: ParametersWithoutFirst) { - return this.appendDialogAsync(Promise.resolve(Element), ...rest); + public appendDialog( + Element: React.ComponentType, + ...rest: ParametersWithoutFirst + ) { + return this.appendDialogAsync(Promise.resolve(Element), ...rest); } - public createTrackedDialogAsync( + public createTrackedDialogAsync( analyticsAction: string, analyticsInfo: string, ...rest: Parameters ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.createDialogAsync(...rest); + return this.createDialogAsync(...rest); } - public appendTrackedDialogAsync( + public appendTrackedDialogAsync( analyticsAction: string, analyticsInfo: string, ...rest: Parameters ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.appendDialogAsync(...rest); + return this.appendDialogAsync(...rest); } private buildModal( @@ -250,9 +258,9 @@ export class ModalManager { * @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog * @returns {object} Object with 'close' parameter being a function that will close the dialog */ - private createDialogAsync( - prom: Promise, - props?: IProps & React.ComponentProps, + private createDialogAsync( + prom: Promise, + props?: IProps, className?: string, isPriorityModal = false, isStaticModal = false, diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index f667c47b3c..f45c3b5471 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -400,14 +400,16 @@ export const Commands = [ // If we need an identity server but don't have one, things // get a bit more complex here, but we try to show something // meaningful. - let finished = Promise.resolve(); + let prom = Promise.resolve(); if ( getAddressType(address) === 'email' && !MatrixClientPeg.get().getIdentityServerUrl() ) { const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); if (defaultIdentityServerUrl) { - ({ finished } = Modal.createTrackedDialog('Slash Commands', 'Identity server', + const { finished } = Modal.createTrackedDialog<[boolean]>( + 'Slash Commands', + 'Identity server', QuestionDialog, { title: _t("Use an identity server"), description:

{_t( @@ -420,9 +422,9 @@ export const Commands = [ )}

, button: _t("Continue"), }, - )); + ); - finished = finished.then(([useDefault]: any) => { + prom = finished.then(([useDefault]) => { if (useDefault) { useDefaultIdentityServer(); return; @@ -434,7 +436,7 @@ export const Commands = [ } } const inviter = new MultiInviter(roomId); - return success(finished.then(() => { + return success(prom.then(() => { return inviter.invite([address]); }).then(() => { if (inviter.getCompletionState(address) !== "invited") { From 030586275ffea1a67e909765bf8f6d5f74e160b9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 14 Jul 2020 10:59:06 +0100 Subject: [PATCH 011/179] Fix /op command to accept only joined/invited users Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/SlashCommands.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index c593d3786f..ed69dd2204 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -43,6 +43,7 @@ import SdkConfig from "./SdkConfig"; import { ensureDMExists } from "./createRoom"; import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; import { Action } from "./dispatcher/actions"; +import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -731,7 +732,9 @@ export const Commands = [ const room = cli.getRoom(roomId); if (!room) return reject(_t("Command failed")); const member = room.getMember(args); - if (!member || member.membership !== "join") return reject(_t("Could not find user in room")); + if (!member || getEffectiveMembership(member.membership) === EffectiveMembership.Leave) { + return reject(_t("Could not find user in room")); + } const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent)); } From 8703bc1abc13f42d150197ffcea00ce6a93d34da Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 03:47:35 +0100 Subject: [PATCH 012/179] Create a generic ARIA toolbar component which works with existing roving tab index context Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/accessibility/RovingTabIndex.tsx | 6 +- src/accessibility/Toolbar.tsx | 69 +++++++++++++++++++ src/components/structures/ContextMenu.tsx | 3 + .../views/messages/MessageActionBar.js | 22 +++--- 4 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 src/accessibility/Toolbar.tsx diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 388d67d9f3..3e52f9fe2a 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -47,7 +47,7 @@ const DOCUMENT_POSITION_PRECEDING = 2; type Ref = RefObject; -interface IState { +export interface IState { activeRef: Ref; refs: Ref[]; } @@ -156,7 +156,7 @@ interface IProps { children(renderProps: { onKeyDownHandler(ev: React.KeyboardEvent); }); - onKeyDown?(ev: React.KeyboardEvent); + onKeyDown?(ev: React.KeyboardEvent, state: IState); } export const RovingTabIndexProvider: React.FC = ({children, handleHomeEnd, onKeyDown}) => { @@ -193,7 +193,7 @@ export const RovingTabIndexProvider: React.FC = ({children, handleHomeEn ev.preventDefault(); ev.stopPropagation(); } else if (onKeyDown) { - return onKeyDown(ev); + return onKeyDown(ev, state); } }, [context.state, onKeyDown, handleHomeEnd]); diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx new file mode 100644 index 0000000000..0e968461a8 --- /dev/null +++ b/src/accessibility/Toolbar.tsx @@ -0,0 +1,69 @@ +/* +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 React from "react"; + +import {IState, RovingTabIndexProvider} from "./RovingTabIndex"; +import {Key} from "../Keyboard"; + +interface IProps extends Omit, "onKeyDown"> { +} + +// This component implements the Toolbar design pattern from the WAI-ARIA Authoring Practices guidelines. +// https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar +// All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref` +const Toolbar: React.FC = ({children, ...props}) => { + const onKeyDown = (ev: React.KeyboardEvent, state: IState) => { + const target = ev.target as HTMLElement; + let handled = true; + + switch (ev.key) { + case Key.ARROW_UP: + case Key.ARROW_DOWN: + if (target.hasAttribute('aria-haspopup')) { + target.click(); + } + break; + + case Key.ARROW_LEFT: + case Key.ARROW_RIGHT: + if (state.refs.length > 0) { + const i = state.refs.findIndex(r => r === state.activeRef); + const delta = ev.key === Key.ARROW_RIGHT ? 1 : -1; + state.refs.slice((i + delta) % state.refs.length)[0].current.focus(); + } + break; + + // HOME and END are handled by RovingTabIndexProvider + + default: + handled = false; + } + + if (handled) { + ev.preventDefault(); + ev.stopPropagation(); + } + }; + + return + {({onKeyDownHandler}) =>
+ { children } +
} +
; +}; + +export default Toolbar; diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index cb1349da4b..62964c5799 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -233,6 +233,9 @@ export class ContextMenu extends React.PureComponent { switch (ev.key) { case Key.TAB: case Key.ESCAPE: + // close on left and right arrows too for when it is a context menu on a + case Key.ARROW_LEFT: + case Key.ARROW_RIGHT: this.props.onFinished(); break; case Key.ARROW_UP: diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 95eb37b588..7959ad8a93 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -25,9 +25,12 @@ import dis from '../../../dispatcher/dispatcher'; import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import RoomContext from "../../../contexts/RoomContext"; +import Toolbar from "../../../accessibility/Toolbar"; +import {RovingAccessibleButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex"; const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { onFocusChange(menuDisplayed); }, [onFocusChange, menuDisplayed]); @@ -57,7 +60,9 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo label={_t("Options")} onClick={openMenu} isExpanded={menuDisplayed} - inputRef={button} + inputRef={ref} + onFocus={onFocus} + tabIndex={isActive ? 0 : -1} /> { contextMenu } @@ -66,6 +71,7 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo const ReactButton = ({mxEvent, reactions, onFocusChange}) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { onFocusChange(menuDisplayed); }, [onFocusChange, menuDisplayed]); @@ -85,7 +91,9 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => { label={_t("React")} onClick={openMenu} isExpanded={menuDisplayed} - inputRef={button} + inputRef={ref} + onFocus={onFocus} + tabIndex={isActive ? 0 : -1} /> { contextMenu } @@ -148,8 +156,6 @@ export default class MessageActionBar extends React.PureComponent { }; render() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let reactButton; let replyButton; let editButton; @@ -161,7 +167,7 @@ export default class MessageActionBar extends React.PureComponent { ); } if (this.context.canReply) { - replyButton = + return {reactButton} {replyButton} {editButton} @@ -188,6 +194,6 @@ export default class MessageActionBar extends React.PureComponent { permalinkCreator={this.props.permalinkCreator} onFocusChange={this.onFocusChange} /> -
; + ; } } From 793c3554dce954c7ddc656c667bbeec768af746f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 03:58:49 +0100 Subject: [PATCH 013/179] fix up type declaration Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/accessibility/RovingTabIndex.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 3e52f9fe2a..13b7285605 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -259,7 +259,7 @@ export const RovingTabIndexWrapper: React.FC = ({ch return children({onFocus, isActive, ref}); }; -interface IRovingAccessibleButtonProps extends React.ComponentProps { +interface IRovingAccessibleButtonProps extends Omit, "onFocus" | "inputRef" | "tabIndex"> { inputRef?: Ref; } From 1b08c1e9df908bb4d602da0c34f927da75c16f66 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 04:19:51 +0100 Subject: [PATCH 014/179] Fix AccessibleTooltipButton leaking tooltipclassname into the DOM Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/elements/AccessibleTooltipButton.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index f4d63136e1..4dbb8f42bf 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; -import classnames from 'classnames'; +import classNames from 'classnames'; import AccessibleButton from "./AccessibleButton"; import {IProps} from "./AccessibleButton"; @@ -52,15 +52,11 @@ export default class AccessibleTooltipButton extends React.PureComponent :
; return ( From 933945130eaee1ab957815ffff68d589d6b89525 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 04:22:19 +0100 Subject: [PATCH 015/179] Tidy up Roving Tab Index helpers and create one for RovingAccessibleTooltipButton Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/accessibility/RovingTabIndex.tsx | 36 +++--------------- .../roving/RovingAccessibleButton.tsx | 34 +++++++++++++++++ .../roving/RovingAccessibleTooltipButton.tsx | 34 +++++++++++++++++ .../roving/RovingTabIndexWrapper.tsx | 38 +++++++++++++++++++ src/accessibility/roving/types.ts | 21 ++++++++++ 5 files changed, 132 insertions(+), 31 deletions(-) create mode 100644 src/accessibility/roving/RovingAccessibleButton.tsx create mode 100644 src/accessibility/roving/RovingAccessibleTooltipButton.tsx create mode 100644 src/accessibility/roving/RovingTabIndexWrapper.tsx create mode 100644 src/accessibility/roving/types.ts diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 13b7285605..5a650d4b6e 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -23,12 +23,11 @@ import React, { useRef, useReducer, Reducer, - RefObject, Dispatch, } from "react"; import {Key} from "../Keyboard"; -import AccessibleButton from "../components/views/elements/AccessibleButton"; +import {FocusHandler, Ref} from "./roving/types"; /** * Module to simplify implementing the Roving TabIndex accessibility technique @@ -45,8 +44,6 @@ import AccessibleButton from "../components/views/elements/AccessibleButton"; const DOCUMENT_POSITION_PRECEDING = 2; -type Ref = RefObject; - export interface IState { activeRef: Ref; refs: Ref[]; @@ -202,8 +199,6 @@ export const RovingTabIndexProvider: React.FC = ({children, handleHomeEn ; }; -type FocusHandler = () => void; - // Hook to register a roving tab index // inputRef parameter specifies the ref to use // onFocus should be called when the index gained focus in any manner @@ -244,28 +239,7 @@ export const useRovingTabIndex = (inputRef: Ref): [FocusHandler, boolean, Ref] = return [onFocus, isActive, ref]; }; -interface IRovingTabIndexWrapperProps { - inputRef?: Ref; - children(renderProps: { - onFocus: FocusHandler; - isActive: boolean; - ref: Ref; - }); -} - -// Wrapper to allow use of useRovingTabIndex outside of React Functional Components. -export const RovingTabIndexWrapper: React.FC = ({children, inputRef}) => { - const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); - return children({onFocus, isActive, ref}); -}; - -interface IRovingAccessibleButtonProps extends Omit, "onFocus" | "inputRef" | "tabIndex"> { - inputRef?: Ref; -} - -// Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components. -export const RovingAccessibleButton: React.FC = ({inputRef, ...props}) => { - const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); - return ; -}; - +// re-export the semantic helper components for simplicity +export {RovingTabIndexWrapper} from "./roving/RovingTabIndexWrapper"; +export {RovingAccessibleButton} from "./roving/RovingAccessibleButton"; +export {RovingAccessibleTooltipButton} from "./roving/RovingAccessibleTooltipButton"; diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx new file mode 100644 index 0000000000..78572954ed --- /dev/null +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -0,0 +1,34 @@ +/* + * + * Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + * + * 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 React from "react"; + +import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import {useRovingTabIndex} from "../RovingTabIndex"; +import {Ref} from "./types"; + +interface IProps extends Omit, "onFocus" | "inputRef" | "tabIndex"> { + inputRef?: Ref; +} + +// Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components. +export const RovingAccessibleButton: React.FC = ({inputRef, ...props}) => { + const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); + return ; +}; + diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx new file mode 100644 index 0000000000..0390f4d343 --- /dev/null +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -0,0 +1,34 @@ +/* + * + * Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + * + * 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 React from "react"; + +import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; +import {useRovingTabIndex} from "../RovingTabIndex"; +import {Ref} from "./types"; + +interface IProps extends Omit, "onFocus" | "inputRef" | "tabIndex"> { + inputRef?: Ref; +} + +// Wrapper to allow use of useRovingTabIndex for simple AccessibleTooltipButtons outside of React Functional Components. +export const RovingAccessibleTooltipButton: React.FC = ({inputRef, ...props}) => { + const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); + return ; +}; + diff --git a/src/accessibility/roving/RovingTabIndexWrapper.tsx b/src/accessibility/roving/RovingTabIndexWrapper.tsx new file mode 100644 index 0000000000..ce45027023 --- /dev/null +++ b/src/accessibility/roving/RovingTabIndexWrapper.tsx @@ -0,0 +1,38 @@ +/* + * + * Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + * + * 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 React from "react"; + +import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import {useRovingTabIndex} from "../RovingTabIndex"; +import {FocusHandler, Ref} from "./types"; + +interface IProps { + inputRef?: Ref; + children(renderProps: { + onFocus: FocusHandler; + isActive: boolean; + ref: Ref; + }); +} + +// Wrapper to allow use of useRovingTabIndex outside of React Functional Components. +export const RovingTabIndexWrapper: React.FC = ({children, inputRef}) => { + const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); + return children({onFocus, isActive, ref}); +}; diff --git a/src/accessibility/roving/types.ts b/src/accessibility/roving/types.ts new file mode 100644 index 0000000000..f0a43e5fb8 --- /dev/null +++ b/src/accessibility/roving/types.ts @@ -0,0 +1,21 @@ +/* +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 {RefObject} from "react"; + +export type Ref = RefObject; + +export type FocusHandler = () => void; From 2a683354a8052a72fc377e20c7a412900a1eb446 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 04:22:37 +0100 Subject: [PATCH 016/179] Wire up new room list breadcrums as an ARIA Toolbar Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomBreadcrumbs2.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index 619ad6da4d..fde24524cd 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -25,7 +25,8 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { CSSTransition } from "react-transition-group"; import RoomListStore from "../../../stores/room-list/RoomListStore2"; import { DefaultTagID } from "../../../stores/room-list/models"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; +import Toolbar from "../../../accessibility/Toolbar"; // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367 @@ -86,7 +87,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent this.viewRoom(r, i)} @@ -101,7 +102,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent - + ); }); @@ -112,9 +113,9 @@ export default class RoomBreadcrumbs2 extends React.PureComponent -
+ {tiles.slice(this.state.skipFirst ? 1 : 0)} -
+ ); } else { From dd0bf17cec917795700e1c37f23f355639a5c1f3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 04:26:10 +0100 Subject: [PATCH 017/179] Fix copyrights Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../roving/RovingAccessibleButton.tsx | 30 +++++++++---------- .../roving/RovingAccessibleTooltipButton.tsx | 30 +++++++++---------- .../roving/RovingTabIndexWrapper.tsx | 30 +++++++++---------- 3 files changed, 42 insertions(+), 48 deletions(-) diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx index 78572954ed..3473ef1bc9 100644 --- a/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -1,20 +1,18 @@ /* - * - * Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - * - * 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. - * / - */ +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 React from "react"; diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx index 0390f4d343..cc824fef22 100644 --- a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -1,20 +1,18 @@ /* - * - * Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - * - * 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. - * / - */ +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 React from "react"; diff --git a/src/accessibility/roving/RovingTabIndexWrapper.tsx b/src/accessibility/roving/RovingTabIndexWrapper.tsx index ce45027023..c826b74497 100644 --- a/src/accessibility/roving/RovingTabIndexWrapper.tsx +++ b/src/accessibility/roving/RovingTabIndexWrapper.tsx @@ -1,20 +1,18 @@ /* - * - * Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - * - * 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. - * / - */ +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 React from "react"; From 31e0d74adc07655ec9cec100895e71e6c8f6e176 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 04:58:13 +0100 Subject: [PATCH 018/179] Query Matcher apply js-sdk's removeHiddenChars Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/QueryMatcher.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 7a0219e264..99814bf4d0 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -18,16 +18,15 @@ limitations under the License. import _at from 'lodash/at'; import _uniq from 'lodash/uniq'; - -function stripDiacritics(str: string): string { - return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); -} +import {removeHiddenChars} from "../../../matrix-js-sdk/src/utils"; interface IOptions { keys: Array; funcs?: Array<(T) => string>; shouldMatchWordsOnly?: boolean; shouldMatchPrefix?: boolean; + // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true + fuzzy?: boolean; } /** @@ -86,7 +85,7 @@ export default class QueryMatcher { for (const [index, keyValue] of Object.entries(keyValues)) { if (!keyValue) continue; // skip falsy keyValues - const key = stripDiacritics(keyValue).toLowerCase(); + const key = this.processQuery(keyValue); if (!this._items.has(key)) { this._items.set(key, []); } @@ -99,7 +98,7 @@ export default class QueryMatcher { } match(query: string): T[] { - query = stripDiacritics(query).toLowerCase(); + query = this.processQuery(query); if (this._options.shouldMatchWordsOnly) { query = query.replace(/[^\w]/g, ''); } @@ -142,4 +141,11 @@ export default class QueryMatcher { // Now map the keys to the result objects. Also remove any duplicates. return _uniq(matches.map((match) => match.object)); } + + private processQuery(query: string): string { + if (this._options.fuzzy !== false) { + return removeHiddenChars(query).toLowerCase(); + } + return query.toLowerCase(); + } } From 13775f897cc225ea2f71c28c17207f514cd60aeb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 04:59:35 +0100 Subject: [PATCH 019/179] consolidate properties Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/QueryMatcher.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 99814bf4d0..a61af06344 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -45,14 +45,10 @@ interface IOptions { */ export default class QueryMatcher { private _options: IOptions; - private _keys: IOptions["keys"]; - private _funcs: Required["funcs"]>; private _items: Map; constructor(objects: T[], options: IOptions = { keys: [] }) { this._options = options; - this._keys = options.keys; - this._funcs = options.funcs || []; this.setObjects(objects); @@ -77,10 +73,12 @@ export default class QueryMatcher { // type for their values. We assume that those values who's keys have // been specified will be string. Also, we cannot infer all the // types of the keys of the objects at compile. - const keyValues = _at(object, this._keys); + const keyValues = _at(object, this._options.keys); - for (const f of this._funcs) { - keyValues.push(f(object)); + if (this._options.funcs) { + for (const f of this._options.funcs) { + keyValues.push(f(object)); + } } for (const [index, keyValue] of Object.entries(keyValues)) { From 918683c232b4f9691713bb4e54ceec5cc74cbec1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 05:10:05 +0100 Subject: [PATCH 020/179] fix import. wtf webstorm Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/QueryMatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index a61af06344..2a44f20fe3 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -18,7 +18,7 @@ limitations under the License. import _at from 'lodash/at'; import _uniq from 'lodash/uniq'; -import {removeHiddenChars} from "../../../matrix-js-sdk/src/utils"; +import {removeHiddenChars} from "matrix-js-sdk/src/utils"; interface IOptions { keys: Array; From 8812f98b35359352b70bb10ad4ccfbc3cd4f4824 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 09:45:45 +0100 Subject: [PATCH 021/179] Convert editor to TypeScript Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/Autocomplete.tsx | 2 +- ...geComposer.js => BasicMessageComposer.tsx} | 391 ++++++++++-------- src/editor/autocomplete.js | 121 ------ src/editor/autocomplete.ts | 140 +++++++ src/editor/{caret.js => caret.ts} | 19 +- src/editor/deserialize.ts | 2 +- src/editor/{diff.js => diff.ts} | 14 +- src/editor/{dom.js => dom.ts} | 16 +- src/editor/{history.js => history.ts} | 108 ++--- src/editor/{model.js => model.ts} | 188 +++++---- src/editor/{offset.js => offset.ts} | 10 +- src/editor/{operations.js => operations.ts} | 25 +- src/editor/{parts.js => parts.ts} | 177 ++++---- src/editor/{position.js => position.ts} | 42 +- src/editor/{range.js => range.ts} | 29 +- src/editor/{render.js => render.ts} | 34 +- src/editor/serialize.ts | 3 +- 17 files changed, 721 insertions(+), 600 deletions(-) rename src/components/views/rooms/{BasicMessageComposer.js => BasicMessageComposer.tsx} (67%) delete mode 100644 src/editor/autocomplete.js create mode 100644 src/editor/autocomplete.ts rename src/editor/{caret.js => caret.ts} (86%) rename src/editor/{diff.js => diff.ts} (87%) rename src/editor/{dom.js => dom.ts} (91%) rename src/editor/{history.js => history.ts} (52%) rename src/editor/{model.js => model.ts} (69%) rename src/editor/{offset.js => offset.ts} (80%) rename src/editor/{operations.js => operations.ts} (89%) rename src/editor/{parts.js => parts.ts} (69%) rename src/editor/{position.js => position.ts} (79%) rename src/editor/{range.js => range.ts} (71%) rename src/editor/{render.js => render.ts} (84%) diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index f5cf1a981c..70f7556550 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React, {createRef, KeyboardEvent} from 'react'; import classNames from 'classnames'; import flatMap from 'lodash/flatMap'; import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter'; diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.tsx similarity index 67% rename from src/components/views/rooms/BasicMessageComposer.js rename to src/components/views/rooms/BasicMessageComposer.tsx index 82f61e0e1f..18ea75f8a9 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -16,11 +16,13 @@ limitations under the License. */ import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; +import React, {createRef, ClipboardEvent} from 'react'; +import {Room} from 'matrix-js-sdk/src/models/room'; +import EMOTICON_REGEX from 'emojibase-regex/emoticon'; + import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; -import {setSelection} from '../../../editor/caret'; +import {Caret, setSelection} from '../../../editor/caret'; import { formatRangeAsQuote, formatRangeAsCode, @@ -29,17 +31,21 @@ import { } from '../../../editor/operations'; import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; import Autocomplete, {generateCompletionDomId} from '../rooms/Autocomplete'; -import {autoCompleteCreator} from '../../../editor/parts'; +import {getAutoCompleteCreator} from '../../../editor/parts'; import {parsePlainTextMessage} from '../../../editor/deserialize'; import {renderModel} from '../../../editor/render'; -import {Room} from 'matrix-js-sdk'; import TypingStore from "../../../stores/TypingStore"; import SettingsStore from "../../../settings/SettingsStore"; -import EMOTICON_REGEX from 'emojibase-regex/emoticon'; -import * as sdk from '../../../index'; import {Key} from "../../../Keyboard"; import {EMOTICON_TO_EMOJI} from "../../../emoji"; import {CommandCategories, CommandMap, parseCommandString} from "../../../SlashCommands"; +import Range from "../../../editor/range"; +import MessageComposerFormatBar from "./MessageComposerFormatBar"; +import DocumentOffset from "../../../editor/offset"; +import {IDiff} from "../../../editor/diff"; +import AutocompleteWrapperModel from "../../../editor/autocomplete"; +import DocumentPosition from "../../../editor/position"; +import {ICompletion} from "../../../autocomplete/Autocompleter"; const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -49,7 +55,7 @@ function ctrlShortcutLabel(key) { return (IS_MAC ? "⌘" : "Ctrl") + "+" + key; } -function cloneSelection(selection) { +function cloneSelection(selection: Selection): Partial { return { anchorNode: selection.anchorNode, anchorOffset: selection.anchorOffset, @@ -61,7 +67,7 @@ function cloneSelection(selection) { }; } -function selectionEquals(a: Selection, b: Selection): boolean { +function selectionEquals(a: Partial, b: Selection): boolean { return a.anchorNode === b.anchorNode && a.anchorOffset === b.anchorOffset && a.focusNode === b.focusNode && @@ -71,45 +77,75 @@ function selectionEquals(a: Selection, b: Selection): boolean { a.type === b.type; } -export default class BasicMessageEditor extends React.Component { - static propTypes = { - onChange: PropTypes.func, - onPaste: PropTypes.func, // returns true if handled and should skip internal onPaste handler - model: PropTypes.instanceOf(EditorModel).isRequired, - room: PropTypes.instanceOf(Room).isRequired, - placeholder: PropTypes.string, - label: PropTypes.string, // the aria label - initialCaret: PropTypes.object, // See DocumentPosition in editor/model.js - }; +enum Formatting { + Bold = "bold", + Italics = "italics", + Strikethrough = "strikethrough", + Code = "code", + Quote = "quote", +} + +interface IProps { + model: EditorModel; + room: Room; + placeholder?: string; + label?: string; + initialCaret?: DocumentOffset; + + onChange(); + onPaste(event: ClipboardEvent, model: EditorModel): boolean; +} + +interface IState { + showPillAvatar: boolean; + query?: string; + showVisualBell?: boolean; + autoComplete?: AutocompleteWrapperModel; + completionIndex?: number; +} + +export default class BasicMessageEditor extends React.Component { + private editorRef = createRef(); + private autocompleteRef = createRef(); + private formatBarRef = createRef(); + + private modifiedFlag = false; + private isIMEComposing = false; + private hasTextSelected = false; + + private _isCaretAtEnd: boolean; + private lastCaret: DocumentOffset; + private lastSelection: ReturnType; + + private readonly emoticonSettingHandle: string; + private readonly shouldShowPillAvatarSettingHandle: string; + private readonly historyManager = new HistoryManager(); constructor(props) { super(props); this.state = { - autoComplete: null, showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"), }; - this._editorRef = null; - this._autocompleteRef = null; - this._formatBarRef = null; - this._modifiedFlag = false; - this._isIMEComposing = false; - this._hasTextSelected = false; - this._emoticonSettingHandle = null; - this._shouldShowPillAvatarSettingHandle = null; + + this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, + this.configureEmoticonAutoReplace); + this.configureEmoticonAutoReplace(); + this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null, + this.configureShouldShowPillAvatar); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: IProps) { if (this.props.placeholder !== prevProps.placeholder && this.props.placeholder) { const {isEmpty} = this.props.model; if (isEmpty) { - this._showPlaceholder(); + this.showPlaceholder(); } else { - this._hidePlaceholder(); + this.hidePlaceholder(); } } } - _replaceEmoticon = (caretPosition, inputType, diff) => { + private replaceEmoticon = (caretPosition: DocumentPosition) => { const {model} = this.props; const range = model.startRange(caretPosition); // expand range max 8 characters backwards from caretPosition, @@ -139,30 +175,30 @@ export default class BasicMessageEditor extends React.Component { return range.replace([partCreator.plain(data.unicode + " ")]); } } - } + }; - _updateEditorState = (selection, inputType, diff) => { - renderModel(this._editorRef, this.props.model); + private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff) => { + renderModel(this.editorRef.current, this.props.model); if (selection) { // set the caret/selection try { - setSelection(this._editorRef, this.props.model, selection); + setSelection(this.editorRef.current, this.props.model, selection); } catch (err) { console.error(err); } // if caret selection is a range, take the end position - const position = selection.end || selection; - this._setLastCaretFromPosition(position); + const position = selection instanceof Range ? selection.end : selection; + this.setLastCaretFromPosition(position); } const {isEmpty} = this.props.model; if (this.props.placeholder) { if (isEmpty) { - this._showPlaceholder(); + this.showPlaceholder(); } else { - this._hidePlaceholder(); + this.hidePlaceholder(); } } if (isEmpty) { - this._formatBarRef.hide(); + this.formatBarRef.current.hide(); } this.setState({autoComplete: this.props.model.autoComplete}); this.historyManager.tryPush(this.props.model, selection, inputType, diff); @@ -180,26 +216,26 @@ export default class BasicMessageEditor extends React.Component { if (this.props.onChange) { this.props.onChange(); } + }; + + private showPlaceholder() { + this.editorRef.current.style.setProperty("--placeholder", `'${this.props.placeholder}'`); + this.editorRef.current.classList.add("mx_BasicMessageComposer_inputEmpty"); } - _showPlaceholder() { - this._editorRef.style.setProperty("--placeholder", `'${this.props.placeholder}'`); - this._editorRef.classList.add("mx_BasicMessageComposer_inputEmpty"); + private hidePlaceholder() { + this.editorRef.current.classList.remove("mx_BasicMessageComposer_inputEmpty"); + this.editorRef.current.style.removeProperty("--placeholder"); } - _hidePlaceholder() { - this._editorRef.classList.remove("mx_BasicMessageComposer_inputEmpty"); - this._editorRef.style.removeProperty("--placeholder"); - } - - _onCompositionStart = (event) => { - this._isIMEComposing = true; + private onCompositionStart = () => { + this.isIMEComposing = true; // even if the model is empty, the composition text shouldn't be mixed with the placeholder - this._hidePlaceholder(); - } + this.hidePlaceholder(); + }; - _onCompositionEnd = (event) => { - this._isIMEComposing = false; + private onCompositionEnd = () => { + this.isIMEComposing = false; // some browsers (Chrome) don't fire an input event after ending a composition, // so trigger a model update after the composition is done by calling the input handler. @@ -213,48 +249,48 @@ export default class BasicMessageEditor extends React.Component { const isSafari = ua.includes('safari/') && !ua.includes('chrome/'); if (isSafari) { - this._onInput({inputType: "insertCompositionText"}); + this.onInput({inputType: "insertCompositionText"}); } else { Promise.resolve().then(() => { - this._onInput({inputType: "insertCompositionText"}); + this.onInput({inputType: "insertCompositionText"}); }); } - } + }; - isComposing(event) { + isComposing(event: React.KeyboardEvent) { // checking the event.isComposing flag just in case any browser out there // emits events related to the composition after compositionend // has been fired - return !!(this._isIMEComposing || (event.nativeEvent && event.nativeEvent.isComposing)); + return !!(this.isIMEComposing || (event.nativeEvent && event.nativeEvent.isComposing)); } - _onCutCopy = (event, type) => { + private onCutCopy = (event: ClipboardEvent, type: string) => { const selection = document.getSelection(); const text = selection.toString(); if (text) { const {model} = this.props; - const range = getRangeForSelection(this._editorRef, model, selection); + const range = getRangeForSelection(this.editorRef.current, model, selection); const selectedParts = range.parts.map(p => p.serialize()); event.clipboardData.setData("application/x-riot-composer", JSON.stringify(selectedParts)); event.clipboardData.setData("text/plain", text); // so plain copy/paste works if (type === "cut") { // Remove the text, updating the model as appropriate - this._modifiedFlag = true; + this.modifiedFlag = true; replaceRangeAndMoveCaret(range, []); } event.preventDefault(); } - } + }; - _onCopy = (event) => { - this._onCutCopy(event, "copy"); - } + private onCopy = (event: ClipboardEvent) => { + this.onCutCopy(event, "copy"); + }; - _onCut = (event) => { - this._onCutCopy(event, "cut"); - } + private onCut = (event: ClipboardEvent) => { + this.onCutCopy(event, "cut"); + }; - _onPaste = (event) => { + private onPaste = (event: ClipboardEvent) => { event.preventDefault(); // we always handle the paste ourselves if (this.props.onPaste && this.props.onPaste(event, this.props.model)) { // to prevent double handling, allow props.onPaste to skip internal onPaste @@ -273,28 +309,28 @@ export default class BasicMessageEditor extends React.Component { const text = event.clipboardData.getData("text/plain"); parts = parsePlainTextMessage(text, partCreator); } - this._modifiedFlag = true; - const range = getRangeForSelection(this._editorRef, model, document.getSelection()); + this.modifiedFlag = true; + const range = getRangeForSelection(this.editorRef.current, model, document.getSelection()); replaceRangeAndMoveCaret(range, parts); - } + }; - _onInput = (event) => { + private onInput = (event: Partial) => { // ignore any input while doing IME compositions - if (this._isIMEComposing) { + if (this.isIMEComposing) { return; } - this._modifiedFlag = true; + this.modifiedFlag = true; const sel = document.getSelection(); - const {caret, text} = getCaretOffsetAndText(this._editorRef, sel); + const {caret, text} = getCaretOffsetAndText(this.editorRef.current, sel); this.props.model.update(text, event.inputType, caret); - } + }; - _insertText(textToInsert, inputType = "insertText") { + private insertText(textToInsert: string, inputType = "insertText") { const sel = document.getSelection(); - const {caret, text} = getCaretOffsetAndText(this._editorRef, sel); + const {caret, text} = getCaretOffsetAndText(this.editorRef.current, sel); const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset); caret.offset += textToInsert.length; - this._modifiedFlag = true; + this.modifiedFlag = true; this.props.model.update(newText, inputType, caret); } @@ -303,28 +339,28 @@ export default class BasicMessageEditor extends React.Component { // we don't need to. But if the user is navigating the caret without input // we need to recalculate it, to be able to know where to insert content after // losing focus - _setLastCaretFromPosition(position) { + private setLastCaretFromPosition(position: DocumentPosition) { const {model} = this.props; this._isCaretAtEnd = position.isAtEnd(model); - this._lastCaret = position.asOffset(model); - this._lastSelection = cloneSelection(document.getSelection()); + this.lastCaret = position.asOffset(model); + this.lastSelection = cloneSelection(document.getSelection()); } - _refreshLastCaretIfNeeded() { + private refreshLastCaretIfNeeded() { // XXX: needed when going up and down in editing messages ... not sure why yet // because the editors should stop doing this when when blurred ... // maybe it's on focus and the _editorRef isn't available yet or something. - if (!this._editorRef) { + if (!this.editorRef.current) { return; } const selection = document.getSelection(); - if (!this._lastSelection || !selectionEquals(this._lastSelection, selection)) { - this._lastSelection = cloneSelection(selection); - const {caret, text} = getCaretOffsetAndText(this._editorRef, selection); - this._lastCaret = caret; + if (!this.lastSelection || !selectionEquals(this.lastSelection, selection)) { + this.lastSelection = cloneSelection(selection); + const {caret, text} = getCaretOffsetAndText(this.editorRef.current, selection); + this.lastCaret = caret; this._isCaretAtEnd = caret.offset === text.length; } - return this._lastCaret; + return this.lastCaret; } clearUndoHistory() { @@ -332,11 +368,11 @@ export default class BasicMessageEditor extends React.Component { } getCaret() { - return this._lastCaret; + return this.lastCaret; } isSelectionCollapsed() { - return !this._lastSelection || this._lastSelection.isCollapsed; + return !this.lastSelection || this.lastSelection.isCollapsed; } isCaretAtStart() { @@ -347,51 +383,51 @@ export default class BasicMessageEditor extends React.Component { return this._isCaretAtEnd; } - _onBlur = () => { - document.removeEventListener("selectionchange", this._onSelectionChange); - } + private onBlur = () => { + document.removeEventListener("selectionchange", this.onSelectionChange); + }; - _onFocus = () => { - document.addEventListener("selectionchange", this._onSelectionChange); + private onFocus = () => { + document.addEventListener("selectionchange", this.onSelectionChange); // force to recalculate - this._lastSelection = null; - this._refreshLastCaretIfNeeded(); - } + this.lastSelection = null; + this.refreshLastCaretIfNeeded(); + }; - _onSelectionChange = () => { + private onSelectionChange = () => { const {isEmpty} = this.props.model; - this._refreshLastCaretIfNeeded(); + this.refreshLastCaretIfNeeded(); const selection = document.getSelection(); - if (this._hasTextSelected && selection.isCollapsed) { - this._hasTextSelected = false; - if (this._formatBarRef) { - this._formatBarRef.hide(); + if (this.hasTextSelected && selection.isCollapsed) { + this.hasTextSelected = false; + if (this.formatBarRef.current) { + this.formatBarRef.current.hide(); } } else if (!selection.isCollapsed && !isEmpty) { - this._hasTextSelected = true; - if (this._formatBarRef) { + this.hasTextSelected = true; + if (this.formatBarRef.current) { const selectionRect = selection.getRangeAt(0).getBoundingClientRect(); - this._formatBarRef.showAt(selectionRect); + this.formatBarRef.current.showAt(selectionRect); } } - } + }; - _onKeyDown = (event) => { + private onKeyDown = (event: React.KeyboardEvent) => { const model = this.props.model; const modKey = IS_MAC ? event.metaKey : event.ctrlKey; let handled = false; // format bold if (modKey && event.key === Key.B) { - this._onFormatAction("bold"); + this.onFormatAction(Formatting.Bold); handled = true; // format italics } else if (modKey && event.key === Key.I) { - this._onFormatAction("italics"); + this.onFormatAction(Formatting.Italics); handled = true; // format quote } else if (modKey && event.key === Key.GREATER_THAN) { - this._onFormatAction("quote"); + this.onFormatAction(Formatting.Quote); handled = true; // redo } else if ((!IS_MAC && modKey && event.key === Key.Y) || @@ -414,18 +450,18 @@ export default class BasicMessageEditor extends React.Component { handled = true; // insert newline on Shift+Enter } else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) { - this._insertText("\n"); + this.insertText("\n"); handled = true; // move selection to start of composer } else if (modKey && event.key === Key.HOME && !event.shiftKey) { - setSelection(this._editorRef, model, { + setSelection(this.editorRef.current, model, { index: 0, offset: 0, }); handled = true; // move selection to end of composer } else if (modKey && event.key === Key.END && !event.shiftKey) { - setSelection(this._editorRef, model, { + setSelection(this.editorRef.current, model, { index: model.parts.length - 1, offset: model.parts[model.parts.length - 1].text.length, }); @@ -465,19 +501,19 @@ export default class BasicMessageEditor extends React.Component { return; // don't preventDefault on anything else } } else if (event.key === Key.TAB) { - this._tabCompleteName(); + this.tabCompleteName(event); handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { - this._formatBarRef.hide(); + this.formatBarRef.current.hide(); } } if (handled) { event.preventDefault(); event.stopPropagation(); } - } + }; - async _tabCompleteName() { + private async tabCompleteName(event: React.KeyboardEvent) { try { await new Promise(resolve => this.setState({showVisualBell: false}, resolve)); const {model} = this.props; @@ -500,7 +536,7 @@ export default class BasicMessageEditor extends React.Component { // Don't try to do things with the autocomplete if there is none shown if (model.autoComplete) { - await model.autoComplete.onTab(); + await model.autoComplete.onTab(event); if (!model.autoComplete.hasSelection()) { this.setState({showVisualBell: true}); model.autoComplete.close(); @@ -512,64 +548,58 @@ export default class BasicMessageEditor extends React.Component { } isModified() { - return this._modifiedFlag; + return this.modifiedFlag; } - _onAutoCompleteConfirm = (completion) => { + private onAutoCompleteConfirm = (completion: ICompletion) => { this.props.model.autoComplete.onComponentConfirm(completion); - } - - _onAutoCompleteSelectionChange = (completion, completionIndex) => { - this.props.model.autoComplete.onComponentSelectionChange(completion); - this.setState({completionIndex}); - } - - _configureEmoticonAutoReplace = () => { - const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji'); - this.props.model.setTransformCallback(shouldReplace ? this._replaceEmoticon : null); }; - _configureShouldShowPillAvatar = () => { + private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number) => { + this.props.model.autoComplete.onComponentSelectionChange(completion); + this.setState({completionIndex}); + }; + + private configureEmoticonAutoReplace = () => { + const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji'); + this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null); + }; + + private configureShouldShowPillAvatar = () => { const showPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); this.setState({ showPillAvatar }); }; componentWillUnmount() { - document.removeEventListener("selectionchange", this._onSelectionChange); - this._editorRef.removeEventListener("input", this._onInput, true); - this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true); - this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true); - SettingsStore.unwatchSetting(this._emoticonSettingHandle); - SettingsStore.unwatchSetting(this._shouldShowPillAvatarSettingHandle); + document.removeEventListener("selectionchange", this.onSelectionChange); + this.editorRef.current.removeEventListener("input", this.onInput, true); + this.editorRef.current.removeEventListener("compositionstart", this.onCompositionStart, true); + this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true); + SettingsStore.unwatchSetting(this.emoticonSettingHandle); + SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle); } componentDidMount() { const model = this.props.model; - model.setUpdateCallback(this._updateEditorState); - this._emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, - this._configureEmoticonAutoReplace); - this._configureEmoticonAutoReplace(); - this._shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null, - this._configureShouldShowPillAvatar); + model.setUpdateCallback(this.updateEditorState); const partCreator = model.partCreator; // TODO: does this allow us to get rid of EditorStateTransfer? // not really, but we could not serialize the parts, and just change the autoCompleter - partCreator.setAutoCompleteCreator(autoCompleteCreator( - () => this._autocompleteRef, + partCreator.setAutoCompleteCreator(getAutoCompleteCreator( + () => this.autocompleteRef.current, query => new Promise(resolve => this.setState({query}, resolve)), )); - this.historyManager = new HistoryManager(partCreator); // initial render of model - this._updateEditorState(this._getInitialCaretPosition()); + this.updateEditorState(this.getInitialCaretPosition()); // attach input listener by hand so React doesn't proxy the events, // as the proxied event doesn't support inputType, which we need. - this._editorRef.addEventListener("input", this._onInput, true); - this._editorRef.addEventListener("compositionstart", this._onCompositionStart, true); - this._editorRef.addEventListener("compositionend", this._onCompositionEnd, true); - this._editorRef.focus(); + this.editorRef.current.addEventListener("input", this.onInput, true); + this.editorRef.current.addEventListener("compositionstart", this.onCompositionStart, true); + this.editorRef.current.addEventListener("compositionend", this.onCompositionEnd, true); + this.editorRef.current.focus(); } - _getInitialCaretPosition() { + private getInitialCaretPosition() { let caretPosition; if (this.props.initialCaret) { // if restoring state from a previous editor, @@ -583,34 +613,34 @@ export default class BasicMessageEditor extends React.Component { return caretPosition; } - _onFormatAction = (action) => { + private onFormatAction = (action: Formatting) => { const range = getRangeForSelection( - this._editorRef, + this.editorRef.current, this.props.model, document.getSelection()); if (range.length === 0) { return; } this.historyManager.ensureLastChangesPushed(this.props.model); - this._modifiedFlag = true; + this.modifiedFlag = true; switch (action) { - case "bold": + case Formatting.Bold: toggleInlineFormat(range, "**"); break; - case "italics": + case Formatting.Italics: toggleInlineFormat(range, "_"); break; - case "strikethrough": + case Formatting.Strikethrough: toggleInlineFormat(range, "", ""); break; - case "code": + case Formatting.Code: formatRangeAsCode(range); break; - case "quote": + case Formatting.Quote: formatRangeAsQuote(range); break; } - } + }; render() { let autoComplete; @@ -619,10 +649,10 @@ export default class BasicMessageEditor extends React.Component { const queryLen = query.length; autoComplete = (
this._autocompleteRef = ref} + ref={this.autocompleteRef} query={query} - onConfirm={this._onAutoCompleteConfirm} - onSelectionChange={this._onAutoCompleteSelectionChange} + onConfirm={this.onAutoCompleteConfirm} + onSelectionChange={this.onAutoCompleteSelectionChange} selection={{beginning: true, end: queryLen, start: queryLen}} room={this.props.room} /> @@ -635,7 +665,6 @@ export default class BasicMessageEditor extends React.Component { "mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar, }); - const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar'); const shortcuts = { bold: ctrlShortcutLabel("B"), italics: ctrlShortcutLabel("I"), @@ -646,18 +675,18 @@ export default class BasicMessageEditor extends React.Component { return (
{ autoComplete } - this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} /> +
this._editorRef = ref} + tabIndex={0} + onBlur={this.onBlur} + onFocus={this.onFocus} + onCopy={this.onCopy} + onCut={this.onCut} + onPaste={this.onPaste} + onKeyDown={this.onKeyDown} + ref={this.editorRef} aria-label={this.props.label} role="textbox" aria-multiline="true" @@ -671,6 +700,6 @@ export default class BasicMessageEditor extends React.Component { } focus() { - this._editorRef.focus(); + this.editorRef.current.focus(); } } diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js deleted file mode 100644 index fcde6e0ce4..0000000000 --- a/src/editor/autocomplete.js +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright 2019 New Vector Ltd -Copyright 2019 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. -*/ - -export default class AutocompleteWrapperModel { - constructor(updateCallback, getAutocompleterComponent, updateQuery, partCreator) { - this._updateCallback = updateCallback; - this._getAutocompleterComponent = getAutocompleterComponent; - this._updateQuery = updateQuery; - this._partCreator = partCreator; - this._query = null; - } - - onEscape(e) { - this._getAutocompleterComponent().onEscape(e); - this._updateCallback({ - replaceParts: [this._partCreator.plain(this._queryPart.text)], - close: true, - }); - } - - close() { - this._updateCallback({close: true}); - } - - hasSelection() { - return this._getAutocompleterComponent().hasSelection(); - } - - hasCompletions() { - const ac = this._getAutocompleterComponent(); - return ac && ac.countCompletions() > 0; - } - - onEnter() { - this._updateCallback({close: true}); - } - - async onTab(e) { - const acComponent = this._getAutocompleterComponent(); - - if (acComponent.countCompletions() === 0) { - // Force completions to show for the text currently entered - await acComponent.forceComplete(); - // Select the first item by moving "down" - await acComponent.moveSelection(+1); - } else { - await acComponent.moveSelection(e.shiftKey ? -1 : +1); - } - } - - onUpArrow() { - this._getAutocompleterComponent().moveSelection(-1); - } - - onDownArrow() { - this._getAutocompleterComponent().moveSelection(+1); - } - - onPartUpdate(part, pos) { - // cache the typed value and caret here - // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) - this._queryPart = part; - this._partIndex = pos.index; - return this._updateQuery(part.text); - } - - onComponentSelectionChange(completion) { - if (!completion) { - this._updateCallback({ - replaceParts: [this._queryPart], - }); - } else { - this._updateCallback({ - replaceParts: this._partForCompletion(completion), - }); - } - } - - onComponentConfirm(completion) { - this._updateCallback({ - replaceParts: this._partForCompletion(completion), - close: true, - }); - } - - _partForCompletion(completion) { - const {completionId} = completion; - const text = completion.completion; - switch (completion.type) { - case "room": - return [this._partCreator.roomPill(text, completionId), this._partCreator.plain(completion.suffix)]; - case "at-room": - return [this._partCreator.atRoomPill(completionId), this._partCreator.plain(completion.suffix)]; - case "user": - // not using suffix here, because we also need to calculate - // the suffix when clicking a display name to insert a mention, - // which happens in createMentionParts - return this._partCreator.createMentionParts(this._partIndex, text, completionId); - case "command": - // command needs special handling for auto complete, but also renders as plain texts - return [this._partCreator.command(text)]; - default: - // used for emoji and other plain text completion replacement - return [this._partCreator.plain(text)]; - } - } -} diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts new file mode 100644 index 0000000000..5832557ae9 --- /dev/null +++ b/src/editor/autocomplete.ts @@ -0,0 +1,140 @@ +/* +Copyright 2019 New Vector Ltd +Copyright 2019 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 {KeyboardEvent} from "react"; + +import {BasePart, CommandPartCreator, PartCreator} from "./parts"; +import DocumentPosition from "./position"; +import {ICompletion} from "../autocomplete/Autocompleter"; +import Autocomplete from "../components/views/rooms/Autocomplete"; + +export interface ICallback { + replaceParts?: BasePart[]; + close?: boolean; +} + +export type UpdateCallback = (data: ICallback) => void; +export type GetAutocompleterComponent = () => Autocomplete; +export type UpdateQuery = (test: string) => Promise; + +export default class AutocompleteWrapperModel { + private queryPart: BasePart; + private partIndex: number; + + constructor( + private updateCallback: UpdateCallback, + private getAutocompleterComponent: GetAutocompleterComponent, + private updateQuery: UpdateQuery, + private partCreator: PartCreator | CommandPartCreator, + ) { + } + + onEscape(e: KeyboardEvent) { + this.getAutocompleterComponent().onEscape(e); + this.updateCallback({ + replaceParts: [this.partCreator.plain(this.queryPart.text)], + close: true, + }); + } + + close() { + this.updateCallback({close: true}); + } + + hasSelection() { + return this.getAutocompleterComponent().hasSelection(); + } + + hasCompletions() { + const ac = this.getAutocompleterComponent(); + return ac && ac.countCompletions() > 0; + } + + onEnter() { + this.updateCallback({close: true}); + } + + async onTab(e: KeyboardEvent) { + const acComponent = this.getAutocompleterComponent(); + + if (acComponent.countCompletions() === 0) { + // Force completions to show for the text currently entered + await acComponent.forceComplete(); + // Select the first item by moving "down" + await acComponent.moveSelection(+1); + } else { + await acComponent.moveSelection(e.shiftKey ? -1 : +1); + } + } + + onUpArrow(e: KeyboardEvent) { + this.getAutocompleterComponent().moveSelection(-1); + } + + onDownArrow(e: KeyboardEvent) { + this.getAutocompleterComponent().moveSelection(+1); + } + + onPartUpdate(part: BasePart, pos: DocumentPosition) { + // cache the typed value and caret here + // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) + this.queryPart = part; + this.partIndex = pos.index; + return this.updateQuery(part.text); + } + + onComponentSelectionChange(completion: ICompletion) { + if (!completion) { + this.updateCallback({ + replaceParts: [this.queryPart], + }); + } else { + this.updateCallback({ + replaceParts: this.partForCompletion(completion), + }); + } + } + + onComponentConfirm(completion: ICompletion) { + this.updateCallback({ + replaceParts: this.partForCompletion(completion), + close: true, + }); + } + + private partForCompletion(completion: ICompletion) { + const {completionId} = completion; + const text = completion.completion; + switch (completion.type) { + case "room": + return [this.partCreator.roomPill(text, completionId), this.partCreator.plain(completion.suffix)]; + case "at-room": + return [this.partCreator.atRoomPill(completionId), this.partCreator.plain(completion.suffix)]; + case "user": + // not using suffix here, because we also need to calculate + // the suffix when clicking a display name to insert a mention, + // which happens in createMentionParts + return this.partCreator.createMentionParts(this.partIndex, text, completionId); + case "command": + // command needs special handling for auto complete, but also renders as plain texts + return [(this.partCreator as CommandPartCreator).command(text)]; + default: + // used for emoji and other plain text completion replacement + return [this.partCreator.plain(text)]; + } + } +} diff --git a/src/editor/caret.js b/src/editor/caret.ts similarity index 86% rename from src/editor/caret.js rename to src/editor/caret.ts index 8c0090a6f1..cfbc701183 100644 --- a/src/editor/caret.js +++ b/src/editor/caret.ts @@ -17,8 +17,13 @@ limitations under the License. import {needsCaretNodeBefore, needsCaretNodeAfter} from "./render"; import Range from "./range"; +import EditorModel from "./model"; +import DocumentPosition, {IPosition} from "./position"; +import {BasePart} from "./parts"; -export function setSelection(editor, model, selection) { +export type Caret = Range | DocumentPosition; + +export function setSelection(editor: HTMLDivElement, model: EditorModel, selection: Range | IPosition) { if (selection instanceof Range) { setDocumentRangeSelection(editor, model, selection); } else { @@ -26,7 +31,7 @@ export function setSelection(editor, model, selection) { } } -function setDocumentRangeSelection(editor, model, range) { +function setDocumentRangeSelection(editor: HTMLDivElement, model: EditorModel, range: Range) { const sel = document.getSelection(); sel.removeAllRanges(); const selectionRange = document.createRange(); @@ -37,7 +42,7 @@ function setDocumentRangeSelection(editor, model, range) { sel.addRange(selectionRange); } -export function setCaretPosition(editor, model, caretPosition) { +export function setCaretPosition(editor: HTMLDivElement, model: EditorModel, caretPosition: IPosition) { const range = document.createRange(); const {node, offset} = getNodeAndOffsetForPosition(editor, model, caretPosition); range.setStart(node, offset); @@ -62,7 +67,7 @@ export function setCaretPosition(editor, model, caretPosition) { sel.addRange(range); } -function getNodeAndOffsetForPosition(editor, model, position) { +function getNodeAndOffsetForPosition(editor: HTMLDivElement, model: EditorModel, position: IPosition) { const {offset, lineIndex, nodeIndex} = getLineAndNodePosition(model, position); const lineNode = editor.childNodes[lineIndex]; @@ -80,7 +85,7 @@ function getNodeAndOffsetForPosition(editor, model, position) { return {node: focusNode, offset}; } -export function getLineAndNodePosition(model, caretPosition) { +export function getLineAndNodePosition(model: EditorModel, caretPosition: IPosition) { const {parts} = model; const partIndex = caretPosition.index; const lineResult = findNodeInLineForPart(parts, partIndex); @@ -99,7 +104,7 @@ export function getLineAndNodePosition(model, caretPosition) { return {lineIndex, nodeIndex, offset}; } -function findNodeInLineForPart(parts, partIndex) { +function findNodeInLineForPart(parts: BasePart[], partIndex: number) { let lineIndex = 0; let nodeIndex = -1; @@ -135,7 +140,7 @@ function findNodeInLineForPart(parts, partIndex) { return {lineIndex, nodeIndex}; } -function moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset) { +function moveOutOfUneditablePart(parts: BasePart[], partIndex: number, nodeIndex: number, offset: number) { // move caret before or after uneditable part const part = parts[partIndex]; if (part && !part.canEdit) { diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index 48d1d98ae4..46eb74f818 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -257,7 +257,7 @@ function parseHtmlMessage(html: string, partCreator: PartCreator, isQuotedMessag return parts; } -export function parsePlainTextMessage(body: string, partCreator: PartCreator, isQuotedMessage: boolean) { +export function parsePlainTextMessage(body: string, partCreator: PartCreator, isQuotedMessage?: boolean) { const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n return lines.reduce((parts, line, i) => { if (isQuotedMessage) { diff --git a/src/editor/diff.js b/src/editor/diff.ts similarity index 87% rename from src/editor/diff.js rename to src/editor/diff.ts index 27d10689b3..cda454306a 100644 --- a/src/editor/diff.js +++ b/src/editor/diff.ts @@ -15,7 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -function firstDiff(a, b) { +export interface IDiff { + removed?: string; + added?: string; + at?: number; +} + +function firstDiff(a: string, b: string) { const compareLen = Math.min(a.length, b.length); for (let i = 0; i < compareLen; ++i) { if (a[i] !== b[i]) { @@ -25,7 +31,7 @@ function firstDiff(a, b) { return compareLen; } -function diffStringsAtEnd(oldStr, newStr) { +function diffStringsAtEnd(oldStr: string, newStr: string): IDiff { const len = Math.min(oldStr.length, newStr.length); const startInCommon = oldStr.substr(0, len) === newStr.substr(0, len); if (startInCommon && oldStr.length > newStr.length) { @@ -43,7 +49,7 @@ function diffStringsAtEnd(oldStr, newStr) { } // assumes only characters have been deleted at one location in the string, and none added -export function diffDeletion(oldStr, newStr) { +export function diffDeletion(oldStr: string, newStr: string): IDiff { if (oldStr === newStr) { return {}; } @@ -61,7 +67,7 @@ export function diffDeletion(oldStr, newStr) { * `added` with the added string (if any), and * `removed` with the removed string (if any) */ -export function diffAtCaret(oldValue, newValue, caretPosition) { +export function diffAtCaret(oldValue: string, newValue: string, caretPosition: number): IDiff { const diffLen = newValue.length - oldValue.length; const caretPositionBeforeInput = caretPosition - diffLen; const oldValueBeforeCaret = oldValue.substr(0, caretPositionBeforeInput); diff --git a/src/editor/dom.js b/src/editor/dom.ts similarity index 91% rename from src/editor/dom.js rename to src/editor/dom.ts index 3efc64f1c9..6a43ffaac5 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.ts @@ -17,8 +17,12 @@ limitations under the License. import {CARET_NODE_CHAR, isCaretNode} from "./render"; import DocumentOffset from "./offset"; +import EditorModel from "./model"; -export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) { +type Predicate = (node: Node) => boolean; +type Callback = (node: Node) => void; + +export function walkDOMDepthFirst(rootNode: Node, enterNodeCallback: Predicate, leaveNodeCallback: Callback) { let node = rootNode.firstChild; while (node && node !== rootNode) { const shouldDescend = enterNodeCallback(node); @@ -40,12 +44,12 @@ export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback } } -export function getCaretOffsetAndText(editor, sel) { +export function getCaretOffsetAndText(editor: HTMLDivElement, sel: Selection) { const {offset, text} = getSelectionOffsetAndText(editor, sel.focusNode, sel.focusOffset); return {caret: offset, text}; } -function tryReduceSelectionToTextNode(selectionNode, selectionOffset) { +function tryReduceSelectionToTextNode(selectionNode: Node, selectionOffset: number) { // if selectionNode is an element, the selected location comes after the selectionOffset-th child node, // which can point past any childNode, in which case, the end of selectionNode is selected. // we try to simplify this to point at a text node with the offset being @@ -82,7 +86,7 @@ function tryReduceSelectionToTextNode(selectionNode, selectionOffset) { }; } -function getSelectionOffsetAndText(editor, selectionNode, selectionOffset) { +function getSelectionOffsetAndText(editor: HTMLDivElement, selectionNode: Node, selectionOffset: number) { const {node, characterOffset} = tryReduceSelectionToTextNode(selectionNode, selectionOffset); const {text, offsetToNode} = getTextAndOffsetToNode(editor, node); const offset = getCaret(node, offsetToNode, characterOffset); @@ -91,7 +95,7 @@ function getSelectionOffsetAndText(editor, selectionNode, selectionOffset) { // gets the caret position details, ignoring and adjusting to // the ZWS if you're typing in a caret node -function getCaret(node, offsetToNode, offsetWithinNode) { +function getCaret(node: Node, offsetToNode: number, offsetWithinNode: number) { // if no node is selected, return an offset at the start if (!node) { return new DocumentOffset(0, false); @@ -114,7 +118,7 @@ function getCaret(node, offsetToNode, offsetWithinNode) { // gets the text of the editor as a string, // and the offset in characters where the selectionNode starts in that string // all ZWS from caret nodes are filtered out -function getTextAndOffsetToNode(editor, selectionNode) { +function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) { let offsetToNode = 0; let foundNode = false; let text = ""; diff --git a/src/editor/history.js b/src/editor/history.ts similarity index 52% rename from src/editor/history.js rename to src/editor/history.ts index d66def4704..f0dc3c251b 100644 --- a/src/editor/history.js +++ b/src/editor/history.ts @@ -14,25 +14,41 @@ See the License for the specific language governing permissions and limitations under the License. */ +import EditorModel from "./model"; +import {IDiff} from "./diff"; +import {ISerializedPart} from "./parts"; +import Range from "./range"; +import {Caret} from "./caret"; + +interface IHistory { + parts: ISerializedPart[]; + caret: Caret; +} + export const MAX_STEP_LENGTH = 10; export default class HistoryManager { - constructor() { - this.clear(); - } + private stack: IHistory[] = []; + private newlyTypedCharCount = 0; + private currentIndex = -1; + private changedSinceLastPush = false; + private lastCaret: Caret = null; + private nonWordBoundarySinceLastPush = false; + private addedSinceLastPush = false; + private removedSinceLastPush = false; clear() { - this._stack = []; - this._newlyTypedCharCount = 0; - this._currentIndex = -1; - this._changedSinceLastPush = false; - this._lastCaret = null; - this._nonWordBoundarySinceLastPush = false; - this._addedSinceLastPush = false; - this._removedSinceLastPush = false; + this.stack = []; + this.newlyTypedCharCount = 0; + this.currentIndex = -1; + this.changedSinceLastPush = false; + this.lastCaret = null; + this.nonWordBoundarySinceLastPush = false; + this.addedSinceLastPush = false; + this.removedSinceLastPush = false; } - _shouldPush(inputType, diff) { + private shouldPush(inputType, diff) { // right now we can only push a step after // the input has been applied to the model, // so we can't push the state before something happened. @@ -43,24 +59,24 @@ export default class HistoryManager { inputType === "deleteContentBackward"; if (diff && isNonBulkInput) { if (diff.added) { - this._addedSinceLastPush = true; + this.addedSinceLastPush = true; } if (diff.removed) { - this._removedSinceLastPush = true; + this.removedSinceLastPush = true; } // as long as you've only been adding or removing since the last push - if (this._addedSinceLastPush !== this._removedSinceLastPush) { + if (this.addedSinceLastPush !== this.removedSinceLastPush) { // add steps by word boundary, up to MAX_STEP_LENGTH characters const str = diff.added ? diff.added : diff.removed; const isWordBoundary = str === " " || str === "\t" || str === "\n"; - if (this._nonWordBoundarySinceLastPush && isWordBoundary) { + if (this.nonWordBoundarySinceLastPush && isWordBoundary) { return true; } if (!isWordBoundary) { - this._nonWordBoundarySinceLastPush = true; + this.nonWordBoundarySinceLastPush = true; } - this._newlyTypedCharCount += str.length; - return this._newlyTypedCharCount > MAX_STEP_LENGTH; + this.newlyTypedCharCount += str.length; + return this.newlyTypedCharCount > MAX_STEP_LENGTH; } else { // if starting to remove while adding before, or the opposite, push return true; @@ -71,24 +87,24 @@ export default class HistoryManager { } } - _pushState(model, caret) { + private pushState(model: EditorModel, caret: Caret) { // remove all steps after current step - while (this._currentIndex < (this._stack.length - 1)) { - this._stack.pop(); + while (this.currentIndex < (this.stack.length - 1)) { + this.stack.pop(); } const parts = model.serializeParts(); - this._stack.push({parts, caret}); - this._currentIndex = this._stack.length - 1; - this._lastCaret = null; - this._changedSinceLastPush = false; - this._newlyTypedCharCount = 0; - this._nonWordBoundarySinceLastPush = false; - this._addedSinceLastPush = false; - this._removedSinceLastPush = false; + this.stack.push({parts, caret}); + this.currentIndex = this.stack.length - 1; + this.lastCaret = null; + this.changedSinceLastPush = false; + this.newlyTypedCharCount = 0; + this.nonWordBoundarySinceLastPush = false; + this.addedSinceLastPush = false; + this.removedSinceLastPush = false; } // needs to persist parts and caret position - tryPush(model, caret, inputType, diff) { + tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff) { // ignore state restoration echos. // these respect the inputType values of the input event, // but are actually passed in from MessageEditor calling model.reset() @@ -96,45 +112,45 @@ export default class HistoryManager { if (inputType === "historyUndo" || inputType === "historyRedo") { return false; } - const shouldPush = this._shouldPush(inputType, diff); + const shouldPush = this.shouldPush(inputType, diff); if (shouldPush) { - this._pushState(model, caret); + this.pushState(model, caret); } else { - this._lastCaret = caret; - this._changedSinceLastPush = true; + this.lastCaret = caret; + this.changedSinceLastPush = true; } return shouldPush; } - ensureLastChangesPushed(model) { - if (this._changedSinceLastPush) { - this._pushState(model, this._lastCaret); + ensureLastChangesPushed(model: EditorModel) { + if (this.changedSinceLastPush) { + this.pushState(model, this.lastCaret); } } canUndo() { - return this._currentIndex >= 1 || this._changedSinceLastPush; + return this.currentIndex >= 1 || this.changedSinceLastPush; } canRedo() { - return this._currentIndex < (this._stack.length - 1); + return this.currentIndex < (this.stack.length - 1); } // returns state that should be applied to model - undo(model) { + undo(model: EditorModel) { if (this.canUndo()) { this.ensureLastChangesPushed(model); - this._currentIndex -= 1; - return this._stack[this._currentIndex]; + this.currentIndex -= 1; + return this.stack[this.currentIndex]; } } // returns state that should be applied to model redo() { if (this.canRedo()) { - this._changedSinceLastPush = false; - this._currentIndex += 1; - return this._stack[this._currentIndex]; + this.changedSinceLastPush = false; + this.currentIndex += 1; + return this.stack[this.currentIndex]; } } } diff --git a/src/editor/model.js b/src/editor/model.ts similarity index 69% rename from src/editor/model.js rename to src/editor/model.ts index 5072c5b2c6..460f95ec0f 100644 --- a/src/editor/model.js +++ b/src/editor/model.ts @@ -15,9 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {diffAtCaret, diffDeletion} from "./diff"; -import DocumentPosition from "./position"; +import {diffAtCaret, diffDeletion, IDiff} from "./diff"; +import DocumentPosition, {IPosition} from "./position"; import Range from "./range"; +import {BasePart, ISerializedPart, PartCreator} from "./parts"; +import AutocompleteWrapperModel, {ICallback} from "./autocomplete"; +import DocumentOffset from "./offset"; /** * @callback ModelCallback @@ -40,16 +43,23 @@ import Range from "./range"; * @return the caret position */ +type TransformCallback = (caretPosition: IPosition, inputType: string, diff: IDiff) => number | void; +type UpdateCallback = (caret: Range | IPosition, inputType?: string, diff?: IDiff) => void; +type ManualTransformCallback = () => Range | DocumentPosition; + export default class EditorModel { - constructor(parts, partCreator, updateCallback = null) { + private _parts: BasePart[]; + private readonly _partCreator: PartCreator; + private activePartIdx: number = null; + private _autoComplete: AutocompleteWrapperModel = null; + private autoCompletePartIdx: number = null; + private autoCompletePartCount = 0; + private transformCallback: TransformCallback = null; + + constructor(parts: BasePart[], partCreator: PartCreator, private updateCallback: UpdateCallback = null) { this._parts = parts; this._partCreator = partCreator; - this._activePartIdx = null; - this._autoComplete = null; - this._autoCompletePartIdx = null; - this._autoCompletePartCount = 0; - this._transformCallback = null; - this.setUpdateCallback(updateCallback); + this.transformCallback = null; } /** @@ -59,16 +69,16 @@ export default class EditorModel { * on the model that can span multiple parts. Also see `startRange()`. * @param {TransformCallback} transformCallback */ - setTransformCallback(transformCallback) { - this._transformCallback = transformCallback; + setTransformCallback(transformCallback: TransformCallback) { + this.transformCallback = transformCallback; } /** * Set a callback for rerendering the model after it has been updated. * @param {ModelCallback} updateCallback */ - setUpdateCallback(updateCallback) { - this._updateCallback = updateCallback; + setUpdateCallback(updateCallback: UpdateCallback) { + this.updateCallback = updateCallback; } get partCreator() { @@ -80,34 +90,34 @@ export default class EditorModel { } clone() { - return new EditorModel(this._parts, this._partCreator, this._updateCallback); + return new EditorModel(this._parts, this._partCreator, this.updateCallback); } - _insertPart(index, part) { + private insertPart(index: number, part: BasePart) { this._parts.splice(index, 0, part); - if (this._activePartIdx >= index) { - ++this._activePartIdx; + if (this.activePartIdx >= index) { + ++this.activePartIdx; } - if (this._autoCompletePartIdx >= index) { - ++this._autoCompletePartIdx; + if (this.autoCompletePartIdx >= index) { + ++this.autoCompletePartIdx; } } - _removePart(index) { + private removePart(index: number) { this._parts.splice(index, 1); - if (index === this._activePartIdx) { - this._activePartIdx = null; - } else if (this._activePartIdx > index) { - --this._activePartIdx; + if (index === this.activePartIdx) { + this.activePartIdx = null; + } else if (this.activePartIdx > index) { + --this.activePartIdx; } - if (index === this._autoCompletePartIdx) { - this._autoCompletePartIdx = null; - } else if (this._autoCompletePartIdx > index) { - --this._autoCompletePartIdx; + if (index === this.autoCompletePartIdx) { + this.autoCompletePartIdx = null; + } else if (this.autoCompletePartIdx > index) { + --this.autoCompletePartIdx; } } - _replacePart(index, part) { + private replacePart(index: number, part: BasePart) { this._parts.splice(index, 1, part); } @@ -116,7 +126,7 @@ export default class EditorModel { } get autoComplete() { - if (this._activePartIdx === this._autoCompletePartIdx) { + if (this.activePartIdx === this.autoCompletePartIdx) { return this._autoComplete; } return null; @@ -137,7 +147,7 @@ export default class EditorModel { return this._parts.map(p => p.serialize()); } - _diff(newValue, inputType, caret) { + private diff(newValue: string, inputType: string, caret: DocumentOffset) { const previousValue = this.parts.reduce((text, p) => text + p.text, ""); // can't use caret position with drag and drop if (inputType === "deleteByDrag") { @@ -147,7 +157,7 @@ export default class EditorModel { } } - reset(serializedParts, caret, inputType) { + reset(serializedParts: ISerializedPart[], caret: Range | IPosition, inputType: string) { this._parts = serializedParts.map(p => this._partCreator.deserializePart(p)); if (!caret) { caret = this.getPositionAtEnd(); @@ -157,9 +167,9 @@ export default class EditorModel { // a message with the autocomplete still open if (this._autoComplete) { this._autoComplete = null; - this._autoCompletePartIdx = null; + this.autoCompletePartIdx = null; } - this._updateCallback(caret, inputType); + this.updateCallback(caret, inputType); } /** @@ -169,19 +179,19 @@ export default class EditorModel { * @param {DocumentPosition} position the position to start inserting at * @return {Number} the amount of characters added */ - insert(parts, position) { - const insertIndex = this._splitAt(position); + insert(parts: BasePart[], position: IPosition) { + const insertIndex = this.splitAt(position); let newTextLength = 0; for (let i = 0; i < parts.length; ++i) { const part = parts[i]; newTextLength += part.text.length; - this._insertPart(insertIndex + i, part); + this.insertPart(insertIndex + i, part); } return newTextLength; } - update(newValue, inputType, caret) { - const diff = this._diff(newValue, inputType, caret); + update(newValue: string, inputType: string, caret: DocumentOffset) { + const diff = this.diff(newValue, inputType, caret); const position = this.positionForOffset(diff.at, caret.atNodeEnd); let removedOffsetDecrease = 0; if (diff.removed) { @@ -189,40 +199,40 @@ export default class EditorModel { } let addedLen = 0; if (diff.added) { - addedLen = this._addText(position, diff.added, inputType); + addedLen = this.addText(position, diff.added, inputType); } - this._mergeAdjacentParts(); + this.mergeAdjacentParts(); const caretOffset = diff.at - removedOffsetDecrease + addedLen; let newPosition = this.positionForOffset(caretOffset, true); const canOpenAutoComplete = inputType !== "insertFromPaste" && inputType !== "insertFromDrop"; - const acPromise = this._setActivePart(newPosition, canOpenAutoComplete); - if (this._transformCallback) { - const transformAddedLen = this._transform(newPosition, inputType, diff); + const acPromise = this.setActivePart(newPosition, canOpenAutoComplete); + if (this.transformCallback) { + const transformAddedLen = this.getTransformAddedLen(newPosition, inputType, diff); newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); } - this._updateCallback(newPosition, inputType, diff); + this.updateCallback(newPosition, inputType, diff); return acPromise; } - _transform(newPosition, inputType, diff) { - const result = this._transformCallback(newPosition, inputType, diff); - return Number.isFinite(result) ? result : 0; + private getTransformAddedLen(newPosition: IPosition, inputType: string, diff: IDiff): number { + const result = this.transformCallback(newPosition, inputType, diff); + return Number.isFinite(result) ? result as number : 0; } - _setActivePart(pos, canOpenAutoComplete) { + private setActivePart(pos: DocumentPosition, canOpenAutoComplete: boolean) { const {index} = pos; const part = this._parts[index]; if (part) { - if (index !== this._activePartIdx) { - this._activePartIdx = index; - if (canOpenAutoComplete && this._activePartIdx !== this._autoCompletePartIdx) { + if (index !== this.activePartIdx) { + this.activePartIdx = index; + if (canOpenAutoComplete && this.activePartIdx !== this.autoCompletePartIdx) { // else try to create one - const ac = part.createAutoComplete(this._onAutoComplete); + const ac = part.createAutoComplete(this.onAutoComplete); if (ac) { // make sure that react picks up the difference between both acs this._autoComplete = ac; - this._autoCompletePartIdx = index; - this._autoCompletePartCount = 1; + this.autoCompletePartIdx = index; + this.autoCompletePartCount = 1; } } } @@ -231,35 +241,35 @@ export default class EditorModel { return this.autoComplete.onPartUpdate(part, pos); } } else { - this._activePartIdx = null; + this.activePartIdx = null; this._autoComplete = null; - this._autoCompletePartIdx = null; - this._autoCompletePartCount = 0; + this.autoCompletePartIdx = null; + this.autoCompletePartCount = 0; } return Promise.resolve(); } - _onAutoComplete = ({replaceParts, close}) => { + private onAutoComplete = ({replaceParts, close}: ICallback) => { let pos; if (replaceParts) { - this._parts.splice(this._autoCompletePartIdx, this._autoCompletePartCount, ...replaceParts); - this._autoCompletePartCount = replaceParts.length; + this._parts.splice(this.autoCompletePartIdx, this.autoCompletePartCount, ...replaceParts); + this.autoCompletePartCount = replaceParts.length; const lastPart = replaceParts[replaceParts.length - 1]; - const lastPartIndex = this._autoCompletePartIdx + replaceParts.length - 1; + const lastPartIndex = this.autoCompletePartIdx + replaceParts.length - 1; pos = new DocumentPosition(lastPartIndex, lastPart.text.length); } if (close) { this._autoComplete = null; - this._autoCompletePartIdx = null; - this._autoCompletePartCount = 0; + this.autoCompletePartIdx = null; + this.autoCompletePartCount = 0; } // rerender even if editor contents didn't change // to make sure the MessageEditor checks // model.autoComplete being empty and closes it - this._updateCallback(pos); - } + this.updateCallback(pos); + }; - _mergeAdjacentParts() { + private mergeAdjacentParts() { let prevPart; for (let i = 0; i < this._parts.length; ++i) { let part = this._parts[i]; @@ -268,7 +278,7 @@ export default class EditorModel { if (isEmpty || isMerged) { // remove empty or merged part part = prevPart; - this._removePart(i); + this.removePart(i); //repeat this index, as it's removed now --i; } @@ -283,7 +293,7 @@ export default class EditorModel { * @return {Number} how many characters before pos were also removed, * usually because of non-editable parts that can only be removed in their entirety. */ - removeText(pos, len) { + removeText(pos: IPosition, len: number) { let {index, offset} = pos; let removedOffsetDecrease = 0; while (len > 0) { @@ -295,18 +305,18 @@ export default class EditorModel { if (part.canEdit) { const replaceWith = part.remove(offset, amount); if (typeof replaceWith === "string") { - this._replacePart(index, this._partCreator.createDefaultPart(replaceWith)); + this.replacePart(index, this._partCreator.createDefaultPart(replaceWith)); } part = this._parts[index]; // remove empty part if (!part.text.length) { - this._removePart(index); + this.removePart(index); } else { index += 1; } } else { removedOffsetDecrease += offset; - this._removePart(index); + this.removePart(index); } } else { index += 1; @@ -316,8 +326,9 @@ export default class EditorModel { } return removedOffsetDecrease; } + // return part index where insertion will insert between at offset - _splitAt(pos) { + private splitAt(pos: IPosition) { if (pos.index === -1) { return 0; } @@ -330,7 +341,7 @@ export default class EditorModel { } const secondPart = part.split(pos.offset); - this._insertPart(pos.index + 1, secondPart); + this.insertPart(pos.index + 1, secondPart); return pos.index + 1; } @@ -344,7 +355,7 @@ export default class EditorModel { * @return {Number} how far from position (in characters) the insertion ended. * This can be more than the length of `str` when crossing non-editable parts, which are skipped. */ - _addText(pos, str, inputType) { + private addText(pos: IPosition, str: string, inputType: string) { let {index} = pos; const {offset} = pos; let addLen = str.length; @@ -356,7 +367,7 @@ export default class EditorModel { } else { const splitPart = part.split(offset); index += 1; - this._insertPart(index, splitPart); + this.insertPart(index, splitPart); } } else if (offset !== 0) { // not-editable part, caret is not at start, @@ -372,13 +383,13 @@ export default class EditorModel { while (str) { const newPart = this._partCreator.createPartForInput(str, index, inputType); str = newPart.appendUntilRejected(str, inputType); - this._insertPart(index, newPart); + this.insertPart(index, newPart); index += 1; } return addLen; } - positionForOffset(totalOffset, atPartEnd) { + positionForOffset(totalOffset: number, atPartEnd: boolean) { let currentOffset = 0; const index = this._parts.findIndex(part => { const partLen = part.text.length; @@ -404,28 +415,27 @@ export default class EditorModel { * @param {DocumentPosition?} positionB the other boundary of the range, optional * @return {Range} */ - startRange(positionA, positionB = positionA) { + startRange(positionA: DocumentPosition, positionB = positionA) { return new Range(this, positionA, positionB); } - // called from Range.replace - _replaceRange(startPosition, endPosition, parts) { + replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: BasePart[]) { // convert end position to offset, so it is independent of how the document is split into parts // which we'll change when splitting up at the start position const endOffset = endPosition.asOffset(this); - const newStartPartIndex = this._splitAt(startPosition); + const newStartPartIndex = this.splitAt(startPosition); // convert it back to position once split at start endPosition = endOffset.asPosition(this); - const newEndPartIndex = this._splitAt(endPosition); + const newEndPartIndex = this.splitAt(endPosition); for (let i = newEndPartIndex - 1; i >= newStartPartIndex; --i) { - this._removePart(i); + this.removePart(i); } let insertIdx = newStartPartIndex; for (const part of parts) { - this._insertPart(insertIdx, part); + this.insertPart(insertIdx, part); insertIdx += 1; } - this._mergeAdjacentParts(); + this.mergeAdjacentParts(); } /** @@ -434,15 +444,15 @@ export default class EditorModel { * @param {ManualTransformCallback} callback to run the transformations in * @return {Promise} a promise when auto-complete (if applicable) is done updating */ - transform(callback) { + transform(callback: ManualTransformCallback) { const pos = callback(); let acPromise = null; if (!(pos instanceof Range)) { - acPromise = this._setActivePart(pos, true); + acPromise = this.setActivePart(pos, true); } else { acPromise = Promise.resolve(); } - this._updateCallback(pos); + this.updateCallback(pos); return acPromise; } } diff --git a/src/editor/offset.js b/src/editor/offset.ts similarity index 80% rename from src/editor/offset.js rename to src/editor/offset.ts index 785f16bc6d..413a22c71b 100644 --- a/src/editor/offset.js +++ b/src/editor/offset.ts @@ -14,17 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ +import EditorModel from "./model"; + export default class DocumentOffset { - constructor(offset, atNodeEnd) { - this.offset = offset; - this.atNodeEnd = atNodeEnd; + constructor(public offset: number, public readonly atNodeEnd: boolean) { } - asPosition(model) { + asPosition(model: EditorModel) { return model.positionForOffset(this.offset, this.atNodeEnd); } - add(delta, atNodeEnd = false) { + add(delta: number, atNodeEnd = false) { return new DocumentOffset(this.offset + delta, atNodeEnd); } } diff --git a/src/editor/operations.js b/src/editor/operations.ts similarity index 89% rename from src/editor/operations.js rename to src/editor/operations.ts index d677d7016c..ee3aa04671 100644 --- a/src/editor/operations.js +++ b/src/editor/operations.ts @@ -14,11 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +import Range from "./range"; +import {BasePart} from "./parts"; + /** * Some common queries and transformations on the editor model */ -export function replaceRangeAndExpandSelection(range, newParts) { +export function replaceRangeAndExpandSelection(range: Range, newParts: BasePart[]) { const {model} = range; model.transform(() => { const oldLen = range.length; @@ -29,7 +32,7 @@ export function replaceRangeAndExpandSelection(range, newParts) { }); } -export function replaceRangeAndMoveCaret(range, newParts) { +export function replaceRangeAndMoveCaret(range: Range, newParts: BasePart[]) { const {model} = range; model.transform(() => { const oldLen = range.length; @@ -40,7 +43,7 @@ export function replaceRangeAndMoveCaret(range, newParts) { }); } -export function rangeStartsAtBeginningOfLine(range) { +export function rangeStartsAtBeginningOfLine(range: Range) { const {model} = range; const startsWithPartial = range.start.offset !== 0; const isFirstPart = range.start.index === 0; @@ -48,16 +51,16 @@ export function rangeStartsAtBeginningOfLine(range) { return !startsWithPartial && (isFirstPart || previousIsNewline); } -export function rangeEndsAtEndOfLine(range) { +export function rangeEndsAtEndOfLine(range: Range) { const {model} = range; const lastPart = model.parts[range.end.index]; - const endsWithPartial = range.end.offset !== lastPart.length; + const endsWithPartial = range.end.offset !== lastPart.text.length; const isLastPart = range.end.index === model.parts.length - 1; const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === "newline"; return !endsWithPartial && (isLastPart || nextIsNewline); } -export function formatRangeAsQuote(range) { +export function formatRangeAsQuote(range: Range) { const {model, parts} = range; const {partCreator} = model; for (let i = 0; i < parts.length; ++i) { @@ -78,7 +81,7 @@ export function formatRangeAsQuote(range) { replaceRangeAndExpandSelection(range, parts); } -export function formatRangeAsCode(range) { +export function formatRangeAsCode(range: Range) { const {model, parts} = range; const {partCreator} = model; const needsBlock = parts.some(p => p.type === "newline"); @@ -104,7 +107,7 @@ export function formatRangeAsCode(range) { const isBlank = part => !part.text || !/\S/.test(part.text); const isNL = part => part.type === "newline"; -export function toggleInlineFormat(range, prefix, suffix = prefix) { +export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix) { const {model, parts} = range; const {partCreator} = model; @@ -140,10 +143,10 @@ export function toggleInlineFormat(range, prefix, suffix = prefix) { // keep track of how many things we have inserted as an offset:=0 let offset = 0; - paragraphIndexes.forEach(([startIndex, endIndex]) => { + paragraphIndexes.forEach(([startIdx, endIdx]) => { // for each paragraph apply the same rule - const base = startIndex + offset; - const index = endIndex + offset; + const base = startIdx + offset; + const index = endIdx + offset; const isFormatted = (index - base > 0) && parts[base].text.startsWith(prefix) && diff --git a/src/editor/parts.js b/src/editor/parts.ts similarity index 69% rename from src/editor/parts.js rename to src/editor/parts.ts index 0adc5573ea..f90308a202 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.ts @@ -15,27 +15,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -import AutocompleteWrapperModel from "./autocomplete"; +import {MatrixClient} from "matrix-js-sdk/src/client"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import {Room} from "matrix-js-sdk/src/models/room"; + +import AutocompleteWrapperModel, {GetAutocompleterComponent, UpdateCallback, UpdateQuery} from "./autocomplete"; import * as Avatar from "../Avatar"; -class BasePart { +export interface ISerializedPart { + type: string; + text: string; +} + +export abstract class BasePart { + protected _text: string; + constructor(text = "") { this._text = text; } - acceptsInsertion(chr, offset, inputType) { + acceptsInsertion(chr: string, offset: number, inputType: string) { return true; } - acceptsRemoval(position, chr) { + acceptsRemoval(position: number, chr: string) { return true; } - merge(part) { + merge(part: BasePart) { return false; } - split(offset) { + split(offset: number) { const splitText = this.text.substr(offset); this._text = this.text.substr(0, offset); return new PlainPart(splitText); @@ -43,7 +54,7 @@ class BasePart { // removes len chars, or returns the plain text this part should be replaced with // if the part would become invalid if it removed everything. - remove(offset, len) { + remove(offset: number, len: number) { // validate const strWithRemoval = this.text.substr(0, offset) + this.text.substr(offset + len); for (let i = offset; i < (len + offset); ++i) { @@ -56,7 +67,7 @@ class BasePart { } // append str, returns the remaining string if a character was rejected. - appendUntilRejected(str, inputType) { + appendUntilRejected(str: string, inputType: string) { const offset = this.text.length; for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); @@ -70,7 +81,7 @@ class BasePart { // inserts str at offset if all the characters in str were accepted, otherwise don't do anything // return whether the str was accepted or not. - validateAndInsert(offset, str, inputType) { + validateAndInsert(offset: number, str: string, inputType: string) { for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); if (!this.acceptsInsertion(chr, offset + i, inputType)) { @@ -83,9 +94,9 @@ class BasePart { return true; } - createAutoComplete() {} + createAutoComplete(updateCallback): AutocompleteWrapperModel | void {} - trim(len) { + trim(len: number) { const remaining = this._text.substr(len); this._text = this._text.substr(0, len); return remaining; @@ -95,6 +106,8 @@ class BasePart { return this._text; } + abstract get type(): string; + get canEdit() { return true; } @@ -103,14 +116,18 @@ class BasePart { return `${this.type}(${this.text})`; } - serialize() { + serialize(): ISerializedPart { return {type: this.type, text: this.text}; } + + abstract updateDOMNode(node: Node); + abstract canUpdateDOMNode(node: Node); + abstract toDOMNode(): Node; } // exported for unit tests, should otherwise only be used through PartCreator export class PlainPart extends BasePart { - acceptsInsertion(chr, offset, inputType) { + acceptsInsertion(chr: string, offset: number, inputType: string) { if (chr === "\n") { return false; } @@ -137,28 +154,27 @@ export class PlainPart extends BasePart { return "plain"; } - updateDOMNode(node) { + updateDOMNode(node: Node) { if (node.textContent !== this.text) { node.textContent = this.text; } } - canUpdateDOMNode(node) { + canUpdateDOMNode(node: Node) { return node.nodeType === Node.TEXT_NODE; } } -class PillPart extends BasePart { - constructor(resourceId, label) { +export abstract class PillPart extends BasePart { + constructor(public resourceId: string, label) { super(label); - this.resourceId = resourceId; } - acceptsInsertion(chr) { + acceptsInsertion(chr: string) { return chr !== " "; } - acceptsRemoval(position, chr) { + acceptsRemoval(position: number, chr: string) { return position !== 0; //if you remove initial # or @, pill should become plain } @@ -171,7 +187,7 @@ class PillPart extends BasePart { return container; } - updateDOMNode(node) { + updateDOMNode(node: HTMLElement) { const textNode = node.childNodes[0]; if (textNode.textContent !== this.text) { textNode.textContent = this.text; @@ -182,7 +198,7 @@ class PillPart extends BasePart { this.setAvatar(node); } - canUpdateDOMNode(node) { + canUpdateDOMNode(node: HTMLElement) { return node.nodeType === Node.ELEMENT_NODE && node.nodeName === "SPAN" && node.childNodes.length === 1 && @@ -190,7 +206,7 @@ class PillPart extends BasePart { } // helper method for subclasses - _setAvatarVars(node, avatarUrl, initialLetter) { + _setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) { const avatarBackground = `url('${avatarUrl}')`; const avatarLetter = `'${initialLetter}'`; // check if the value is changing, @@ -206,14 +222,18 @@ class PillPart extends BasePart { get canEdit() { return false; } + + abstract get className(): string; + + abstract setAvatar(node: HTMLElement): void; } class NewlinePart extends BasePart { - acceptsInsertion(chr, offset) { + acceptsInsertion(chr: string, offset: number) { return offset === 0 && chr === "\n"; } - acceptsRemoval(position, chr) { + acceptsRemoval(position: number, chr: string) { return true; } @@ -227,7 +247,7 @@ class NewlinePart extends BasePart { updateDOMNode() {} - canUpdateDOMNode(node) { + canUpdateDOMNode(node: HTMLElement) { return node.tagName === "BR"; } @@ -245,21 +265,20 @@ class NewlinePart extends BasePart { } class RoomPillPart extends PillPart { - constructor(displayAlias, room) { + constructor(displayAlias, private room: Room) { super(displayAlias, displayAlias); - this._room = room; } - setAvatar(node) { + setAvatar(node: HTMLElement) { let initialLetter = ""; let avatarUrl = Avatar.avatarUrlForRoom( - this._room, + this.room, 16 * window.devicePixelRatio, 16 * window.devicePixelRatio, "crop"); if (!avatarUrl) { - initialLetter = Avatar.getInitialLetter(this._room ? this._room.name : this.resourceId); - avatarUrl = Avatar.defaultAvatarUrlForString(this._room ? this._room.roomId : this.resourceId); + initialLetter = Avatar.getInitialLetter(this.room ? this.room.name : this.resourceId); + avatarUrl = Avatar.defaultAvatarUrlForString(this.room ? this.room.roomId : this.resourceId); } this._setAvatarVars(node, avatarUrl, initialLetter); } @@ -280,19 +299,18 @@ class AtRoomPillPart extends RoomPillPart { } class UserPillPart extends PillPart { - constructor(userId, displayName, member) { + constructor(userId, displayName, private member: RoomMember) { super(userId, displayName); - this._member = member; } - setAvatar(node) { - if (!this._member) { + setAvatar(node: HTMLElement) { + if (!this.member) { return; } - const name = this._member.name || this._member.userId; - const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this._member.userId); + const name = this.member.name || this.member.userId; + const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this.member.userId); const avatarUrl = Avatar.avatarUrlForMember( - this._member, + this.member, 16 * window.devicePixelRatio, 16 * window.devicePixelRatio, "crop"); @@ -312,24 +330,23 @@ class UserPillPart extends PillPart { } serialize() { - const obj = super.serialize(); - obj.resourceId = this.resourceId; - return obj; + return { + ...super.serialize(), + resourceId: this.resourceId, + }; } } - class PillCandidatePart extends PlainPart { - constructor(text, autoCompleteCreator) { + constructor(text: string, private autoCompleteCreator: IAutocompleteCreator) { super(text); - this._autoCompleteCreator = autoCompleteCreator; } - createAutoComplete(updateCallback) { - return this._autoCompleteCreator.create(updateCallback); + createAutoComplete(updateCallback): AutocompleteWrapperModel { + return this.autoCompleteCreator.create(updateCallback); } - acceptsInsertion(chr, offset, inputType) { + acceptsInsertion(chr: string, offset: number, inputType: string) { if (offset === 0) { return true; } else { @@ -341,7 +358,7 @@ class PillCandidatePart extends PlainPart { return false; } - acceptsRemoval(position, chr) { + acceptsRemoval(position: number, chr: string) { return true; } @@ -350,9 +367,9 @@ class PillCandidatePart extends PlainPart { } } -export function autoCompleteCreator(getAutocompleterComponent, updateQuery) { - return (partCreator) => { - return (updateCallback) => { +export function getAutoCompleteCreator(getAutocompleterComponent: GetAutocompleterComponent, updateQuery: UpdateQuery) { + return (partCreator: PartCreator) => { + return (updateCallback: UpdateCallback) => { return new AutocompleteWrapperModel( updateCallback, getAutocompleterComponent, @@ -363,20 +380,26 @@ export function autoCompleteCreator(getAutocompleterComponent, updateQuery) { }; } +type AutoCompleteCreator = ReturnType; + +interface IAutocompleteCreator { + create(updateCallback: UpdateCallback): AutocompleteWrapperModel; +} + export class PartCreator { - constructor(room, client, autoCompleteCreator = null) { - this._room = room; - this._client = client; + protected readonly autoCompleteCreator: IAutocompleteCreator; + + constructor(private room: Room, private client: MatrixClient, autoCompleteCreator: AutoCompleteCreator = null) { // pre-create the creator as an object even without callback so it can already be passed // to PillCandidatePart (e.g. while deserializing) and set later on - this._autoCompleteCreator = {create: autoCompleteCreator && autoCompleteCreator(this)}; + this.autoCompleteCreator = {create: autoCompleteCreator && autoCompleteCreator(this)}; } - setAutoCompleteCreator(autoCompleteCreator) { - this._autoCompleteCreator.create = autoCompleteCreator(this); + setAutoCompleteCreator(autoCompleteCreator: AutoCompleteCreator) { + this.autoCompleteCreator.create = autoCompleteCreator(this); } - createPartForInput(input) { + createPartForInput(input: string, partIndex: number, inputType?: string): BasePart { switch (input[0]) { case "#": case "@": @@ -389,11 +412,11 @@ export class PartCreator { } } - createDefaultPart(text) { + createDefaultPart(text: string) { return this.plain(text); } - deserializePart(part) { + deserializePart(part: ISerializedPart) { switch (part.type) { case "plain": return this.plain(part.text); @@ -406,11 +429,11 @@ export class PartCreator { case "room-pill": return this.roomPill(part.text); case "user-pill": - return this.userPill(part.text, part.resourceId); + return this.userPill(part.text, (part as PillPart).resourceId); } } - plain(text) { + plain(text: string) { return new PlainPart(text); } @@ -418,16 +441,16 @@ export class PartCreator { return new NewlinePart("\n"); } - pillCandidate(text) { - return new PillCandidatePart(text, this._autoCompleteCreator); + pillCandidate(text: string) { + return new PillCandidatePart(text, this.autoCompleteCreator); } - roomPill(alias, roomId) { + roomPill(alias: string, roomId?: string) { let room; if (roomId || alias[0] !== "#") { - room = this._client.getRoom(roomId || alias); + room = this.client.getRoom(roomId || alias); } else { - room = this._client.getRooms().find((r) => { + room = this.client.getRooms().find((r) => { return r.getCanonicalAlias() === alias || r.getAltAliases().includes(alias); }); @@ -435,16 +458,16 @@ export class PartCreator { return new RoomPillPart(alias, room); } - atRoomPill(text) { - return new AtRoomPillPart(text, this._room); + atRoomPill(text: string) { + return new AtRoomPillPart(text, this.room); } - userPill(displayName, userId) { - const member = this._room.getMember(userId); + userPill(displayName: string, userId: string) { + const member = this.room.getMember(userId); return new UserPillPart(userId, displayName, member); } - createMentionParts(partIndex, displayName, userId) { + createMentionParts(partIndex: number, displayName: string, userId: string) { const pill = this.userPill(displayName, userId); const postfix = this.plain(partIndex === 0 ? ": " : " "); return [pill, postfix]; @@ -454,7 +477,7 @@ export class PartCreator { // part creator that support auto complete for /commands, // used in SendMessageComposer export class CommandPartCreator extends PartCreator { - createPartForInput(text, partIndex) { + createPartForInput(text: string, partIndex: number) { // at beginning and starts with /? create if (partIndex === 0 && text[0] === "/") { // text will be inserted by model, so pass empty string @@ -464,11 +487,11 @@ export class CommandPartCreator extends PartCreator { } } - command(text) { - return new CommandPart(text, this._autoCompleteCreator); + command(text: string) { + return new CommandPart(text, this.autoCompleteCreator); } - deserializePart(part) { + deserializePart(part: BasePart) { if (part.type === "command") { return this.command(part.text); } else { diff --git a/src/editor/position.js b/src/editor/position.ts similarity index 79% rename from src/editor/position.js rename to src/editor/position.ts index 726377ef48..9c12fff778 100644 --- a/src/editor/position.js +++ b/src/editor/position.ts @@ -15,30 +15,30 @@ limitations under the License. */ import DocumentOffset from "./offset"; +import EditorModel from "./model"; +import {BasePart} from "./parts"; -export default class DocumentPosition { - constructor(index, offset) { - this._index = index; - this._offset = offset; +export interface IPosition { + index: number; + offset: number; +} + +type Callback = (part: BasePart, startIdx: number, endIdx: number) => void; +type Predicate = (index: number, offset: number, part: BasePart) => boolean; + +export default class DocumentPosition implements IPosition { + constructor(public readonly index: number, public readonly offset: number) { } - get index() { - return this._index; - } - - get offset() { - return this._offset; - } - - compare(otherPos) { - if (this._index === otherPos._index) { - return this._offset - otherPos._offset; + compare(otherPos: DocumentPosition) { + if (this.index === otherPos.index) { + return this.offset - otherPos.offset; } else { - return this._index - otherPos._index; + return this.index - otherPos.index; } } - iteratePartsBetween(other, model, callback) { + iteratePartsBetween(other: DocumentPosition, model: EditorModel, callback: Callback) { if (this.index === -1 || other.index === -1) { return; } @@ -57,7 +57,7 @@ export default class DocumentPosition { } } - forwardsWhile(model, predicate) { + forwardsWhile(model: EditorModel, predicate: Predicate) { if (this.index === -1) { return this; } @@ -82,7 +82,7 @@ export default class DocumentPosition { } } - backwardsWhile(model, predicate) { + backwardsWhile(model: EditorModel, predicate: Predicate) { if (this.index === -1) { return this; } @@ -107,7 +107,7 @@ export default class DocumentPosition { } } - asOffset(model) { + asOffset(model: EditorModel) { if (this.index === -1) { return new DocumentOffset(0, true); } @@ -121,7 +121,7 @@ export default class DocumentPosition { return new DocumentOffset(offset, atEnd); } - isAtEnd(model) { + isAtEnd(model: EditorModel) { if (model.parts.length === 0) { return true; } diff --git a/src/editor/range.js b/src/editor/range.ts similarity index 71% rename from src/editor/range.js rename to src/editor/range.ts index 822c3b13a7..456509a855 100644 --- a/src/editor/range.js +++ b/src/editor/range.ts @@ -14,32 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ +import EditorModel from "./model"; +import DocumentPosition from "./position"; + export default class Range { - constructor(model, positionA, positionB = positionA) { - this._model = model; + private _start: DocumentPosition; + private _end: DocumentPosition; + + constructor(public readonly model: EditorModel, positionA: DocumentPosition, positionB = positionA) { const bIsLarger = positionA.compare(positionB) < 0; this._start = bIsLarger ? positionA : positionB; this._end = bIsLarger ? positionB : positionA; } moveStart(delta) { - this._start = this._start.forwardsWhile(this._model, () => { + this._start = this._start.forwardsWhile(this.model, () => { delta -= 1; return delta >= 0; }); } expandBackwardsWhile(predicate) { - this._start = this._start.backwardsWhile(this._model, predicate); - } - - get model() { - return this._model; + this._start = this._start.backwardsWhile(this.model, predicate); } get text() { let text = ""; - this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { const t = part.text.substring(startIdx, endIdx); text = text + t; }); @@ -55,10 +56,10 @@ export default class Range { replace(parts) { const newLength = parts.reduce((sum, part) => sum + part.text.length, 0); let oldLength = 0; - this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { oldLength += endIdx - startIdx; }); - this._model._replaceRange(this._start, this._end, parts); + this.model.replaceRange(this._start, this._end, parts); return newLength - oldLength; } @@ -68,10 +69,10 @@ export default class Range { */ get parts() { const parts = []; - this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { const serializedPart = part.serialize(); serializedPart.text = part.text.substring(startIdx, endIdx); - const newPart = this._model.partCreator.deserializePart(serializedPart); + const newPart = this.model.partCreator.deserializePart(serializedPart); parts.push(newPart); }); return parts; @@ -79,7 +80,7 @@ export default class Range { get length() { let len = 0; - this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { len += endIdx - startIdx; }); return len; diff --git a/src/editor/render.js b/src/editor/render.ts similarity index 84% rename from src/editor/render.js rename to src/editor/render.ts index 84e57c2a3f..a60fb19730 100644 --- a/src/editor/render.js +++ b/src/editor/render.ts @@ -15,16 +15,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function needsCaretNodeBefore(part, prevPart) { +import {BasePart} from "./parts"; +import EditorModel from "./model"; +import {instanceOf} from "prop-types"; + +export function needsCaretNodeBefore(part: BasePart, prevPart: BasePart) { const isFirst = !prevPart || prevPart.type === "newline"; return !part.canEdit && (isFirst || !prevPart.canEdit); } -export function needsCaretNodeAfter(part, isLastOfLine) { +export function needsCaretNodeAfter(part: BasePart, isLastOfLine: boolean) { return !part.canEdit && isLastOfLine; } -function insertAfter(node, nodeToInsert) { +function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement) { const next = node.nextSibling; if (next) { node.parentElement.insertBefore(nodeToInsert, next); @@ -48,18 +52,18 @@ function createCaretNode() { return span; } -function updateCaretNode(node) { +function updateCaretNode(node: HTMLElement) { // ensure the caret node contains only a zero-width space if (node.textContent !== CARET_NODE_CHAR) { node.textContent = CARET_NODE_CHAR; } } -export function isCaretNode(node) { +export function isCaretNode(node: HTMLElement) { return node && node.tagName === "SPAN" && node.className === "caretNode"; } -function removeNextSiblings(node) { +function removeNextSiblings(node: ChildNode) { if (!node) { return; } @@ -71,7 +75,7 @@ function removeNextSiblings(node) { } } -function removeChildren(parent) { +function removeChildren(parent: HTMLElement) { const firstChild = parent.firstChild; if (firstChild) { removeNextSiblings(firstChild); @@ -79,7 +83,7 @@ function removeChildren(parent) { } } -function reconcileLine(lineContainer, parts) { +function reconcileLine(lineContainer: ChildNode, parts: BasePart[]) { let currentNode; let prevPart; const lastPart = parts[parts.length - 1]; @@ -146,23 +150,23 @@ function reconcileEmptyLine(lineContainer) { } } -export function renderModel(editor, model) { - const lines = model.parts.reduce((lines, part) => { +export function renderModel(editor: HTMLDivElement, model: EditorModel) { + const lines = model.parts.reduce((linesArr, part) => { if (part.type === "newline") { - lines.push([]); + linesArr.push([]); } else { - const lastLine = lines[lines.length - 1]; + const lastLine = linesArr[linesArr.length - 1]; lastLine.push(part); } - return lines; + return linesArr; }, [[]]); lines.forEach((parts, i) => { // find first (and remove anything else) div without className // (as browsers insert these in contenteditable) line container - let lineContainer = editor.childNodes[i]; + let lineContainer = editor.children[i]; while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) { editor.removeChild(lineContainer); - lineContainer = editor.childNodes[i]; + lineContainer = editor.children[i]; } if (!lineContainer) { lineContainer = document.createElement("div"); diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 7e8f4a3bfc..8ee726e8a1 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -18,6 +18,7 @@ limitations under the License. import Markdown from '../Markdown'; import {makeGenericPermalink} from "../utils/permalinks/Permalinks"; import EditorModel from "./model"; +import {PillPart} from "./parts"; export function mdSerialize(model: EditorModel) { return model.parts.reduce((html, part) => { @@ -31,7 +32,7 @@ export function mdSerialize(model: EditorModel) { return html + part.text; case "room-pill": case "user-pill": - return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`; + return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink((part as PillPart).resourceId)})`; } }, ""); } From 2bf5e4b142401c92e9a50c98bc964a428d721807 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 09:49:02 +0100 Subject: [PATCH 022/179] clean up Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/editor/model.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/editor/model.ts b/src/editor/model.ts index 460f95ec0f..37a1ada543 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -21,6 +21,7 @@ import Range from "./range"; import {BasePart, ISerializedPart, PartCreator} from "./parts"; import AutocompleteWrapperModel, {ICallback} from "./autocomplete"; import DocumentOffset from "./offset"; +import {Caret} from "./caret"; /** * @callback ModelCallback @@ -43,9 +44,9 @@ import DocumentOffset from "./offset"; * @return the caret position */ -type TransformCallback = (caretPosition: IPosition, inputType: string, diff: IDiff) => number | void; -type UpdateCallback = (caret: Range | IPosition, inputType?: string, diff?: IDiff) => void; -type ManualTransformCallback = () => Range | DocumentPosition; +type TransformCallback = (caretPosition: DocumentPosition, inputType: string, diff: IDiff) => number | void; +type UpdateCallback = (caret: Caret, inputType?: string, diff?: IDiff) => void; +type ManualTransformCallback = () => Caret; export default class EditorModel { private _parts: BasePart[]; @@ -157,7 +158,7 @@ export default class EditorModel { } } - reset(serializedParts: ISerializedPart[], caret: Range | IPosition, inputType: string) { + reset(serializedParts: ISerializedPart[], caret: Caret, inputType: string) { this._parts = serializedParts.map(p => this._partCreator.deserializePart(p)); if (!caret) { caret = this.getPositionAtEnd(); @@ -214,7 +215,7 @@ export default class EditorModel { return acPromise; } - private getTransformAddedLen(newPosition: IPosition, inputType: string, diff: IDiff): number { + private getTransformAddedLen(newPosition: DocumentPosition, inputType: string, diff: IDiff): number { const result = this.transformCallback(newPosition, inputType, diff); return Number.isFinite(result) ? result as number : 0; } From 7d5f40876ee7d6f730d4592f34349cf522ede35e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Jul 2020 14:31:15 +0100 Subject: [PATCH 023/179] Update help link --- .../views/settings/tabs/user/HelpUserSettingsTab.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index 3a3e2923a4..f9da5d94f0 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -158,7 +158,7 @@ export default class HelpUserSettingsTab extends React.Component { }, { 'a': (sub) => @@ -177,7 +177,7 @@ export default class HelpUserSettingsTab extends React.Component { }, { 'a': (sub) => From ae17cd3ec6a3da53e8506b8109b8c8b7292b6a0a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 16:46:39 +0100 Subject: [PATCH 024/179] Add tooltip to Room Tile Icon Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTileIcon.tsx | 21 ++++++++++++++++++++- src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTileIcon.tsx b/src/components/views/rooms/RoomTileIcon.tsx index b0cf10e313..725ef570a0 100644 --- a/src/components/views/rooms/RoomTileIcon.tsx +++ b/src/components/views/rooms/RoomTileIcon.tsx @@ -22,6 +22,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import DMRoomMap from "../../../utils/DMRoomMap"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { isPresenceEnabled } from "../../../utils/presence"; +import { _t } from "../../../languageHandler"; +import TextWithTooltip from "../elements/TextWithTooltip"; enum Icon { // Note: the names here are used in CSS class names @@ -32,6 +34,19 @@ enum Icon { PresenceOffline = "OFFLINE", } +function tooltipText(variant: Icon) { + switch (variant) { + case Icon.Globe: + return _t("Public Room"); + case Icon.PresenceOnline: + return _t("Online"); + case Icon.PresenceAway: + return _t("Away"); + case Icon.PresenceOffline: + return _t("Offline"); + } +} + interface IProps { room: Room; tag: TagID; @@ -145,6 +160,10 @@ export default class RoomTileIcon extends React.Component { public render(): React.ReactElement { if (this.state.icon === Icon.None) return null; - return ; + return ; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 11115ac4fe..5085506601 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1239,6 +1239,8 @@ "Leave Room": "Leave Room", "Forget Room": "Forget Room", "Room options": "Room options", + "Public Room": "Public Room", + "Away": "Away", "Add a topic": "Add a topic", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", From 78da0c863a339d76aeeeaa21a77d48c2d2a1578d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jul 2020 16:52:10 +0100 Subject: [PATCH 025/179] Update copy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTileIcon.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTileIcon.tsx b/src/components/views/rooms/RoomTileIcon.tsx index 725ef570a0..cd7a18be7e 100644 --- a/src/components/views/rooms/RoomTileIcon.tsx +++ b/src/components/views/rooms/RoomTileIcon.tsx @@ -37,7 +37,7 @@ enum Icon { function tooltipText(variant: Icon) { switch (variant) { case Icon.Globe: - return _t("Public Room"); + return _t("This room is public"); case Icon.PresenceOnline: return _t("Online"); case Icon.PresenceAway: diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5085506601..a205b78f1e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1239,7 +1239,7 @@ "Leave Room": "Leave Room", "Forget Room": "Forget Room", "Room options": "Room options", - "Public Room": "Public Room", + "This room is public": "This room is public", "Away": "Away", "Add a topic": "Add a topic", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", From aa445a061d17b62d17ebb8841c220c24b8fbd5c9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 15 Jul 2020 12:25:10 -0400 Subject: [PATCH 026/179] check that encryptionInfo.sender is set --- src/components/views/rooms/EventTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 88c4ed2e7d..f6eaf5f003 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -333,7 +333,7 @@ export default createReactClass({ return; } - const eventSenderTrust = this.context.checkDeviceTrust( + const eventSenderTrust = encryptionInfo.sender && this.context.checkDeviceTrust( senderId, encryptionInfo.sender.deviceId, ); if (!eventSenderTrust) { From e873ba96084e4a527157c11538e73c950043a3db Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Jul 2020 02:52:16 +0100 Subject: [PATCH 027/179] ellipse senders for images and videos --- res/css/views/rooms/_IRCLayout.scss | 3 ++- src/components/views/messages/SenderProfile.js | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 94753f9473..a97de54134 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -181,7 +181,8 @@ $irc-line-height: $font-18px; > span { display: flex; - > .mx_SenderProfile_name { + > .mx_SenderProfile_name, + > .mx_SenderProfile_aux { overflow: hidden; text-overflow: ellipsis; } diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index d512b186e9..5064c4d2b2 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -125,8 +125,10 @@ export default createReactClass({ ; const content = this.props.text ? - - { _t(this.props.text, { senderName: () => nameElem }) } + + + { _t(this.props.text, { senderName: () => nameElem }) } + : nameFlair; return ( From bcbb3e8169da6a21e75e93cdc100dafa8eea0df1 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Jul 2020 03:14:20 +0100 Subject: [PATCH 028/179] fix name jump --- res/css/views/rooms/_IRCLayout.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 94753f9473..dd805805ab 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -184,6 +184,7 @@ $irc-line-height: $font-18px; > .mx_SenderProfile_name { overflow: hidden; text-overflow: ellipsis; + min-width: var(--name-width); } } } From 96f0681c2e68c220ff95d8330da45c98e47c7bcb Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Jul 2020 03:52:35 +0100 Subject: [PATCH 029/179] Move e2e icon --- res/css/views/rooms/_IRCLayout.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 94753f9473..c23fb2d539 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -97,7 +97,7 @@ $irc-line-height: $font-18px; } > .mx_EventTile_e2eIcon { - position: relative; + position: absolute; right: unset; left: unset; top: 0; From a7f92f35f5a27a53a5a030ea7c471be97751a67a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 04:15:32 +0100 Subject: [PATCH 030/179] Sync recently used reactions list across sessions Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/emojipicker/recent.js | 35 --------- src/emojipicker/recent.ts | 73 +++++++++++++++++++ src/settings/Settings.js | 6 ++ .../handlers/AccountSettingsHandler.js | 17 +++++ 4 files changed, 96 insertions(+), 35 deletions(-) delete mode 100644 src/emojipicker/recent.js create mode 100644 src/emojipicker/recent.ts 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) || {}; From 0097ba24a42ab1294a4228acaec6b0432482067c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 05:52:39 +0100 Subject: [PATCH 031/179] When removing a filter condition, try recalculate in case it wasn't the last one Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/stores/room-list/algorithms/Algorithm.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 6bc58ecd57..6f718c09b2 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -158,6 +158,7 @@ export class Algorithm extends EventEmitter { filterCondition.off(FILTER_CHANGED, this.handleFilterChange.bind(this)); if (this.allowedByFilter.has(filterCondition)) { this.allowedByFilter.delete(filterCondition); + this.recalculateFilteredRooms(); // If we removed the last filter, tell consumers that we've "updated" our filtered // view. This will trick them into getting the complete room list. From 48aa203b95869c2c65b426b71567b9c2e772383a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 06:05:53 +0100 Subject: [PATCH 032/179] Notify left panel of resizing when it is collapsed&expanded Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index c7f3c5266d..920b7e4dec 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -675,12 +675,16 @@ export default class MatrixChat extends React.PureComponent { case 'hide_left_panel': this.setState({ collapseLhs: true, + }, () => { + this.state.resizeNotifier.notifyLeftHandleResized(); }); break; case 'focus_room_filter': // for CtrlOrCmd+K to work by expanding the left panel first case 'show_left_panel': this.setState({ collapseLhs: false, + }, () => { + this.state.resizeNotifier.notifyLeftHandleResized(); }); break; case 'panel_disable': { From 2f959a974a8f607123dd5dda82b11d9abc369172 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 06:31:06 +0100 Subject: [PATCH 033/179] Fix enter in new room list filter breaking things Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanel2.tsx | 2 +- src/components/structures/RoomSearch.tsx | 8 +++++++- src/stores/room-list/RoomListStore2.ts | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 012b518093..7fe1c24062 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -269,7 +269,7 @@ export default class LeftPanel2 extends React.Component { const firstRoom = this.listContainerRef.current.querySelector(".mx_RoomTile2"); if (firstRoom) { firstRoom.click(); - this.onSearch(""); // clear the search field + return true; // to get the field to clear } }; diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 517a5f2580..e40c362ecb 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -107,7 +107,13 @@ export default class RoomSearch extends React.PureComponent { } else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) { this.props.onVerticalArrow(ev); } else if (ev.key === Key.ENTER) { - this.props.onEnter(ev); + const shouldClear = this.props.onEnter(ev); + if (shouldClear) { + // wrap in set immediate to delay it so that we don't clear the filter & then change room + setImmediate(() => { + this.clearInput(); + }); + } } }; diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index d66f7f9b05..4368120a2e 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -33,7 +33,6 @@ import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import RoomListLayoutStore from "./RoomListLayoutStore"; import { MarkedExecution } from "../../utils/MarkedExecution"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; interface IState { tagsEnabled?: boolean; From ed9d3a36a2d4dac1d9eda7e593f7b29f22481428 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 08:58:11 +0100 Subject: [PATCH 034/179] fix typescript definitions Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSearch.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index e40c362ecb..1451630c97 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -28,8 +28,8 @@ import { Action } from "../../dispatcher/actions"; interface IProps { onQueryUpdate: (newQuery: string) => void; isMinimized: boolean; - onVerticalArrow(ev: React.KeyboardEvent); - onEnter(ev: React.KeyboardEvent); + onVerticalArrow(ev: React.KeyboardEvent): void; + onEnter(ev: React.KeyboardEvent): boolean; } interface IState { From 34989228820b90dfcccaa5549274e649a72e5ba7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 09:13:23 +0100 Subject: [PATCH 035/179] Fix filtering by community not showing DM rooms with community members Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../room-list/filters/CommunityFilterCondition.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/stores/room-list/filters/CommunityFilterCondition.ts b/src/stores/room-list/filters/CommunityFilterCondition.ts index 45e65fb4f4..924a85e86a 100644 --- a/src/stores/room-list/filters/CommunityFilterCondition.ts +++ b/src/stores/room-list/filters/CommunityFilterCondition.ts @@ -21,6 +21,7 @@ import { EventEmitter } from "events"; import GroupStore from "../../GroupStore"; import { arrayHasDiff } from "../../../utils/arrays"; import { IDestroyable } from "../../../utils/IDestroyable"; +import DMRoomMap from "../../../utils/DMRoomMap"; /** * A filter condition for the room list which reveals rooms which @@ -28,6 +29,7 @@ import { IDestroyable } from "../../../utils/IDestroyable"; */ export class CommunityFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable { private roomIds: string[] = []; + private userIds: string[] = []; constructor(private community: Group) { super(); @@ -43,15 +45,19 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon } public isVisible(room: Room): boolean { - return this.roomIds.includes(room.roomId); + return this.roomIds.includes(room.roomId) || + this.userIds.includes(DMRoomMap.shared().getUserIdForRoomId(room.roomId)); } private onStoreUpdate = async (): Promise => { - // We don't actually know if the room list changed for the community, so just - // check it again. + // We don't actually know if the room list changed for the community, so just check it again. const beforeRoomIds = this.roomIds; this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId); - if (arrayHasDiff(beforeRoomIds, this.roomIds)) { + + const beforeUserIds = this.userIds; + this.userIds = (await GroupStore.getGroupMembers(this.community.groupId)).map(u => u.userId); + + if (arrayHasDiff(beforeRoomIds, this.roomIds) || arrayHasDiff(beforeUserIds, this.userIds)) { this.emit(FILTER_CHANGED); } }; From 4006ae670186746e9d2053327b9d5154aa2a02e7 Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Thu, 16 Jul 2020 10:30:31 +0100 Subject: [PATCH 036/179] Fix copy button in share dialog --- res/css/views/dialogs/_ShareDialog.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/dialogs/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss index e3d2ae8306..d2fe98e8f9 100644 --- a/res/css/views/dialogs/_ShareDialog.scss +++ b/res/css/views/dialogs/_ShareDialog.scss @@ -55,7 +55,7 @@ limitations under the License. margin-left: 5px; width: 20px; height: 20px; - background-repeat: none; + background-repeat: no-repeat; } .mx_ShareDialog_split { From c94c937b31c84f6a0ac6a345a42879f2dc447c63 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 12:33:05 +0100 Subject: [PATCH 037/179] Fix Room Tile Icon to not ignore DMs in other tags Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/avatars/DecoratedRoomAvatar.tsx | 2 +- src/components/views/rooms/RoomTileIcon.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 80bfac6787..bb737397dc 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -66,7 +66,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent - + {badge}
; } diff --git a/src/components/views/rooms/RoomTileIcon.tsx b/src/components/views/rooms/RoomTileIcon.tsx index cd7a18be7e..234840d28d 100644 --- a/src/components/views/rooms/RoomTileIcon.tsx +++ b/src/components/views/rooms/RoomTileIcon.tsx @@ -49,7 +49,6 @@ function tooltipText(variant: Icon) { interface IProps { room: Room; - tag: TagID; } interface IState { @@ -137,10 +136,11 @@ export default class RoomTileIcon extends React.Component { private calculateIcon(): Icon { let icon = Icon.None; - if (this.props.tag === DefaultTagID.DM && this.props.room.getJoinedMemberCount() === 2) { + // We look at the DMRoomMap and not the tag here so that we don't exclude DMs in Favourites + const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId); + if (otherUserId && this.props.room.getJoinedMemberCount() === 2) { // Track presence, if available if (isPresenceEnabled()) { - const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId); if (otherUserId) { this.dmUser = MatrixClientPeg.get().getUser(otherUserId); icon = this.getPresenceIcon(); From 15f94d37327f6574a1ea16070a0134b76247caa8 Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Thu, 16 Jul 2020 17:22:35 +0530 Subject: [PATCH 038/179] Update checkbox --- src/components/views/terms/InlineTermsAgreement.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js index 55719fe57f..0501c1066c 100644 --- a/src/components/views/terms/InlineTermsAgreement.js +++ b/src/components/views/terms/InlineTermsAgreement.js @@ -19,6 +19,7 @@ import PropTypes from "prop-types"; import {_t, pickBestLanguage} from "../../../languageHandler"; import * as sdk from "../../.."; import {objectClone} from "../../../utils/objects"; +import StyledCheckbox from "../elements/StyledCheckbox"; export default class InlineTermsAgreement extends React.Component { static propTypes = { @@ -90,8 +91,9 @@ export default class InlineTermsAgreement extends React.Component {
{introText}
- this._togglePolicy(i)} checked={policy.checked} /> + this._togglePolicy(i)} checked={policy.checked}> {_t("Accept")} +
, ); From 8de6863bf64b99bd64d45139deaa27dbe9b189dd Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Thu, 16 Jul 2020 18:12:11 +0530 Subject: [PATCH 039/179] Fix indentation --- src/components/views/terms/InlineTermsAgreement.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js index 0501c1066c..5f6e276976 100644 --- a/src/components/views/terms/InlineTermsAgreement.js +++ b/src/components/views/terms/InlineTermsAgreement.js @@ -91,9 +91,9 @@ export default class InlineTermsAgreement extends React.Component {
{introText}
- this._togglePolicy(i)} checked={policy.checked}> - {_t("Accept")} - + this._togglePolicy(i)} checked={policy.checked}> + {_t("Accept")} +
, ); From cb794af5f1099de3f9856ecbed16544ace33f0a9 Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Thu, 16 Jul 2020 19:07:15 +0530 Subject: [PATCH 040/179] Increase width for country code dropdown --- res/css/views/elements/_Field.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index cf5bc7ab41..fc529b140b 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -191,5 +191,5 @@ limitations under the License. } .mx_Field .mx_CountryDropdown { - width: 67px; + width: 78px; } From 2b260a69a010656fc1688b92f66856824f024e80 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 14:37:41 +0100 Subject: [PATCH 041/179] Change colour of unread count dot to primary fg Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_NotificationBadge.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/rooms/_NotificationBadge.scss b/res/css/views/rooms/_NotificationBadge.scss index 0e6d442cc1..dbc9006ca3 100644 --- a/res/css/views/rooms/_NotificationBadge.scss +++ b/res/css/views/rooms/_NotificationBadge.scss @@ -42,6 +42,8 @@ limitations under the License. // These are the 3 background types &.mx_NotificationBadge_dot { + background-color: $primary-fg-color; // increased visibility + width: 6px; height: 6px; border-radius: 6px; From f2104b59f07f05f15dc252c93f492168eb9cc662 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 14:38:04 +0100 Subject: [PATCH 042/179] Simplify room sublist context menu Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomSublist2.tsx | 10 +++------- src/i18n/strings/en_EN.json | 8 +++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index ec937dcad3..145bd8d68e 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -444,24 +444,20 @@ export default class RoomSublist2 extends React.Component {
-
{_t("Unread rooms")}
+
{_t("Appearance")}
- {_t("Always show first")} + {_t("Show rooms with unread messages first")} -
-
-
-
{_t("Show")}
- {_t("Message preview")} + {_t("Show previews of messages")}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a205b78f1e..25e8d24e82 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1210,10 +1210,9 @@ "Securely back up your keys to avoid losing them.
Learn more.": "Securely back up your keys to avoid losing them. Learn more.", "Not now": "Not now", "Don't ask me again": "Don't ask me again", - "Unread rooms": "Unread rooms", - "Always show first": "Always show first", - "Show": "Show", - "Message preview": "Message preview", + "Appearance": "Appearance", + "Show rooms with unread messages first": "Show rooms with unread messages first", + "Show previews of messages": "Show previews of messages", "Sort by": "Sort by", "Activity": "Activity", "A-Z": "A-Z", @@ -1855,7 +1854,6 @@ "Upload %(count)s other files|one": "Upload %(count)s other file", "Cancel All": "Cancel All", "Upload Error": "Upload Error", - "Appearance": "Appearance", "Verify other session": "Verify other session", "Verification Request": "Verification Request", "A widget would like to verify your identity": "A widget would like to verify your identity", From cb4f4c96b4aa8c8fd547fd0b8069f6b4c7433ab7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 14:38:23 +0100 Subject: [PATCH 043/179] Update colour of typing indicator in timeline to match topic Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_WhoIsTypingTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_WhoIsTypingTile.scss b/res/css/views/rooms/_WhoIsTypingTile.scss index 02d779a99c..1c0dabbeb5 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.scss +++ b/res/css/views/rooms/_WhoIsTypingTile.scss @@ -59,7 +59,7 @@ limitations under the License. flex: 1; font-size: $font-14px; font-weight: 600; - color: $composer-button-color; + color: $roomtopic-color; } .mx_WhoIsTypingTile_label > span { From a2b5e5a6a1f1e685d361d414b2d17aaa50803451 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 14:38:46 +0100 Subject: [PATCH 044/179] Fix top right header button layout and hover consistency Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/_HeaderButtons.scss | 2 +- res/css/structures/_RightPanel.scss | 8 ++++---- res/css/views/rooms/_RoomHeader.scss | 23 +++++++++++++++++------ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/res/css/structures/_HeaderButtons.scss b/res/css/structures/_HeaderButtons.scss index eef7653b24..9ef40e9d6a 100644 --- a/res/css/structures/_HeaderButtons.scss +++ b/res/css/structures/_HeaderButtons.scss @@ -22,7 +22,7 @@ limitations under the License. content: ""; background-color: $header-divider-color; opacity: 0.5; - margin: 0 15px; + margin: 6px 8px; border-radius: 1px; width: 1px; } diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 77114954eb..2fe7aac3b2 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -61,10 +61,10 @@ limitations under the License. &::before { content: ''; position: absolute; - top: 6px; // center with parent of 32px - left: 6px; // center with parent of 32px - height: 20px; - width: 20px; + top: 4px; // center with parent of 32px + left: 4px; // center with parent of 32px + height: 24px; + width: 24px; background-color: $rightpanel-button-color; mask-repeat: no-repeat; mask-size: contain; diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 46993bb644..ba46100ea6 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -75,7 +75,6 @@ limitations under the License. .mx_RoomHeader_buttons { display: flex; background-color: $primary-bg-color; - padding-right: 5px; } .mx_RoomHeader_info { @@ -209,20 +208,32 @@ limitations under the License. .mx_RoomHeader_button { position: relative; - margin-left: 10px; + margin-left: 1px; + margin-right: 1px; cursor: pointer; - height: 20px; - width: 20px; + height: 32px; + width: 32px; + border-radius: 100%; &::before { content: ''; position: absolute; - height: 20px; - width: 20px; + top: 4px; // center with parent of 32px + left: 4px; // center with parent of 32px + height: 24px; + width: 24px; background-color: $roomheader-button-color; mask-repeat: no-repeat; mask-size: contain; } + + &:hover { + background: rgba($accent-color, 0.1); + + &::before { + background-color: $accent-color; + } + } } .mx_RoomHeader_settingsButton::before { From b6cd8065f8b88b3f66235e751d37307199a93396 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 14:39:02 +0100 Subject: [PATCH 045/179] Re-export top right header button svg masks to match Figma Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/img/element-icons/community-members.svg | 10 +++++----- res/img/element-icons/community-rooms.svg | 4 ++-- res/img/element-icons/leave.svg | 8 ++++---- res/img/element-icons/notifications.svg | 8 ++++---- res/img/element-icons/room/files.svg | 4 ++-- res/img/element-icons/room/integrations.svg | 4 ++-- res/img/element-icons/room/members.svg | 8 ++++---- res/img/element-icons/room/pin.svg | 12 ++++++------ res/img/element-icons/room/search-inset.svg | 4 ++-- res/img/element-icons/room/share.svg | 4 ++-- res/img/element-icons/settings.svg | 4 ++-- 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/res/img/element-icons/community-members.svg b/res/img/element-icons/community-members.svg index 3131075c7b..0052fa6b5b 100644 --- a/res/img/element-icons/community-members.svg +++ b/res/img/element-icons/community-members.svg @@ -1,8 +1,8 @@ - + - + - - - + + + diff --git a/res/img/element-icons/community-rooms.svg b/res/img/element-icons/community-rooms.svg index c46cedc74f..8556cca131 100644 --- a/res/img/element-icons/community-rooms.svg +++ b/res/img/element-icons/community-rooms.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/element-icons/leave.svg b/res/img/element-icons/leave.svg index 8a96160afd..ae8029f5bb 100644 --- a/res/img/element-icons/leave.svg +++ b/res/img/element-icons/leave.svg @@ -1,7 +1,7 @@ - - + + - + - + diff --git a/res/img/element-icons/notifications.svg b/res/img/element-icons/notifications.svg index c86a7a3b98..b0efba27ca 100644 --- a/res/img/element-icons/notifications.svg +++ b/res/img/element-icons/notifications.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/res/img/element-icons/room/files.svg b/res/img/element-icons/room/files.svg index 6dfd6856d6..da07f07d98 100644 --- a/res/img/element-icons/room/files.svg +++ b/res/img/element-icons/room/files.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/element-icons/room/integrations.svg b/res/img/element-icons/room/integrations.svg index 57937cd929..f983c1e703 100644 --- a/res/img/element-icons/room/integrations.svg +++ b/res/img/element-icons/room/integrations.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/element-icons/room/members.svg b/res/img/element-icons/room/members.svg index e73834bfe5..c5ca577653 100644 --- a/res/img/element-icons/room/members.svg +++ b/res/img/element-icons/room/members.svg @@ -1,7 +1,7 @@ - + - + - - + + diff --git a/res/img/element-icons/room/pin.svg b/res/img/element-icons/room/pin.svg index d2e9a2c2eb..df8438ba69 100644 --- a/res/img/element-icons/room/pin.svg +++ b/res/img/element-icons/room/pin.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/res/img/element-icons/room/search-inset.svg b/res/img/element-icons/room/search-inset.svg index 2a837f5106..403f55dfbc 100644 --- a/res/img/element-icons/room/search-inset.svg +++ b/res/img/element-icons/room/search-inset.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/element-icons/room/share.svg b/res/img/element-icons/room/share.svg index 5accc0a849..ca567612ad 100644 --- a/res/img/element-icons/room/share.svg +++ b/res/img/element-icons/room/share.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/element-icons/settings.svg b/res/img/element-icons/settings.svg index e6e2aef54c..e9e74a8200 100644 --- a/res/img/element-icons/settings.svg +++ b/res/img/element-icons/settings.svg @@ -1,3 +1,3 @@ - - + + From ddb6104590d653ce987fc2ee4a0ce6619f0596b5 Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Thu, 16 Jul 2020 19:09:33 +0530 Subject: [PATCH 046/179] Revert "Increase width for country code dropdown" This reverts commit cb794af5f1099de3f9856ecbed16544ace33f0a9. --- res/css/views/elements/_Field.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index fc529b140b..cf5bc7ab41 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -191,5 +191,5 @@ limitations under the License. } .mx_Field .mx_CountryDropdown { - width: 78px; + width: 67px; } From 1195c09f5c8e9bd350b48bec96cc6c50b40773e0 Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Thu, 16 Jul 2020 19:11:52 +0530 Subject: [PATCH 047/179] Increase width for country code dropdown --- res/css/views/elements/_Field.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index cf5bc7ab41..fc529b140b 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -191,5 +191,5 @@ limitations under the License. } .mx_Field .mx_CountryDropdown { - width: 67px; + width: 78px; } From d5f2d4342996eceaf2f507e3a19ed044f3883641 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 15:15:00 +0100 Subject: [PATCH 048/179] Improve Tooltip font/layout consistency Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_components.scss | 1 - res/css/views/elements/_Tooltip.scss | 13 +++++++++++-- .../messages/_ReactionsRowButtonTooltip.scss | 19 ------------------- .../elements/AccessibleTooltipButton.tsx | 5 +++-- .../messages/ReactionsRowButtonTooltip.js | 4 ++-- src/components/views/messages/TextualBody.js | 10 ++++++++++ src/i18n/strings/en_EN.json | 2 ++ 7 files changed, 28 insertions(+), 26 deletions(-) delete mode 100644 res/css/views/messages/_ReactionsRowButtonTooltip.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 8f3c187ff8..d0432b2f23 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -146,7 +146,6 @@ @import "./views/messages/_MjolnirBody.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; -@import "./views/messages/_ReactionsRowButtonTooltip.scss"; @import "./views/messages/_RedactedBody.scss"; @import "./views/messages/_RoomAvatarEvent.scss"; @import "./views/messages/_SenderProfile.scss"; diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 7efea2c3f7..a3a90e2a4f 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -52,7 +52,7 @@ limitations under the License. display: none; position: fixed; border: 1px solid $menu-border-color; - border-radius: 4px; + border-radius: 8px; box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; background-color: $menu-bg-color; z-index: 6000; // Higher than context menu so tooltips can be used everywhere @@ -60,7 +60,7 @@ limitations under the License. pointer-events: none; line-height: $font-14px; font-size: $font-12px; - font-weight: 600; + font-weight: 500; color: $primary-fg-color; max-width: 200px; word-break: break-word; @@ -87,3 +87,12 @@ limitations under the License. } } } + +.mx_Tooltip_title { + font-weight: 600; +} + +.mx_Tooltip_sub { + opacity: 0.7; + margin-top: 4px; +} diff --git a/res/css/views/messages/_ReactionsRowButtonTooltip.scss b/res/css/views/messages/_ReactionsRowButtonTooltip.scss deleted file mode 100644 index cf4219fcec..0000000000 --- a/res/css/views/messages/_ReactionsRowButtonTooltip.scss +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2019 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. -*/ - -.mx_ReactionsRowButtonTooltip_reactedWith { - opacity: 0.7; -} diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index f4d63136e1..af4316225f 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -24,6 +24,7 @@ import Tooltip from './Tooltip'; interface ITooltipProps extends IProps { title: string; + tooltip?: React.ReactNode; tooltipClassName?: string; } @@ -52,7 +53,7 @@ export default class AccessibleTooltipButton extends React.PureComponent :
; return ( diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js index 59e9d2ad7f..3a87befdae 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.js +++ b/src/components/views/messages/ReactionsRowButtonTooltip.js @@ -55,7 +55,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent { }, { reactors: () => { - return
+ return
{formatCommaSeparatedList(senders, 6)}
; }, @@ -63,7 +63,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent { if (!shortName) { return null; } - return
+ return
{sub}
; }, diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 84126ed7fc..5784e36a8b 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -376,11 +376,21 @@ export default createReactClass({ const date = this.props.mxEvent.replacingEventDate(); const dateString = date && formatDate(date); + const tooltip =
+
+ {_t("Edited at %(date)s", {date: dateString})} +
+
+ {_t("Click to view edits")} +
+
; + return ( {`(${_t("edited")})`} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 25e8d24e82..d81c0c717b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1455,6 +1455,8 @@ "Failed to copy": "Failed to copy", "Add an Integration": "Add an Integration", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", + "Edited at %(date)s": "Edited at %(date)s", + "Click to view edits": "Click to view edits", "Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.", "edited": "edited", "Can't load this message": "Can't load this message", From 576294e7ce33bbb6985fddbdd248d973257edc41 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 15:26:25 +0100 Subject: [PATCH 049/179] Add shadow to the reply preview and autocomplete composer panes Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_Autocomplete.scss | 3 ++- res/css/views/rooms/_ReplyPreview.scss | 3 ++- res/themes/dark/css/_dark.scss | 2 ++ res/themes/legacy-dark/css/_legacy-dark.scss | 2 ++ res/themes/legacy-light/css/_legacy-light.scss | 2 ++ res/themes/light/css/_light.scss | 1 + 6 files changed, 11 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss index a4aebdb708..f8e0a382b1 100644 --- a/res/css/views/rooms/_Autocomplete.scss +++ b/res/css/views/rooms/_Autocomplete.scss @@ -6,9 +6,10 @@ border: 1px solid $primary-hairline-color; background: $primary-bg-color; border-bottom: none; - border-radius: 4px 4px 0 0; + border-radius: 8px 8px 0 0; max-height: 50vh; overflow: auto; + box-shadow: 0px -16px 32px $composer-shadow-color; } .mx_Autocomplete_ProviderSection { diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss index 4dc4cb2c40..9feb337042 100644 --- a/res/css/views/rooms/_ReplyPreview.scss +++ b/res/css/views/rooms/_ReplyPreview.scss @@ -22,9 +22,10 @@ limitations under the License. border: 1px solid $primary-hairline-color; background: $primary-bg-color; border-bottom: none; - border-radius: 4px 4px 0 0; + border-radius: 8px 8px 0 0; max-height: 50vh; overflow: auto; + box-shadow: 0px -16px 32px $composer-shadow-color; } .mx_ReplyPreview_section { diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 990debb0a4..370c30d4f2 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -211,6 +211,8 @@ $appearance-tab-border-color: $room-highlight-color; $roomlist-background-blur-amount: 60px; $tagpanel-background-blur-amount: 30px; +$composer-shadow-color: rgba(0, 0, 0, 0.28); + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index a6a85edfe1..6671afb929 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -203,6 +203,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color; // Appearance tab colors $appearance-tab-border-color: $room-highlight-color; +$composer-shadow-color: tranparent; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 991abdefff..822b65d57d 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -331,6 +331,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color; // FontSlider colors $appearance-tab-border-color: $input-darker-bg-color; +$composer-shadow-color: tranparent; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 3737d21a0f..657d3bfd13 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -335,6 +335,7 @@ $appearance-tab-border-color: $input-darker-bg-color; $roomlist-background-blur-amount: 40px; $tagpanel-background-blur-amount: 20px; +$composer-shadow-color: rgba(0, 0, 0, 0.04); // ***** Mixins! ***** From 007f63b5bb2c41544d76f3fc1dc974ae0d20d653 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 15:30:06 +0100 Subject: [PATCH 050/179] Update the SVGs with 24x24 fixed Figma ones Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/img/element-icons/community-members.svg | 10 +++++----- res/img/element-icons/community-rooms.svg | 4 ++-- res/img/element-icons/leave.svg | 8 ++++---- res/img/element-icons/notifications.svg | 8 ++++---- res/img/element-icons/room/files.svg | 4 ++-- res/img/element-icons/room/integrations.svg | 4 ++-- res/img/element-icons/room/members.svg | 8 ++++---- res/img/element-icons/room/pin.svg | 12 ++++++------ res/img/element-icons/room/search-inset.svg | 4 ++-- res/img/element-icons/room/share.svg | 4 ++-- res/img/element-icons/settings.svg | 4 ++-- 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/res/img/element-icons/community-members.svg b/res/img/element-icons/community-members.svg index 0052fa6b5b..553ba3b1af 100644 --- a/res/img/element-icons/community-members.svg +++ b/res/img/element-icons/community-members.svg @@ -1,8 +1,8 @@ - + - + - - - + + + diff --git a/res/img/element-icons/community-rooms.svg b/res/img/element-icons/community-rooms.svg index 8556cca131..570b45a488 100644 --- a/res/img/element-icons/community-rooms.svg +++ b/res/img/element-icons/community-rooms.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/element-icons/leave.svg b/res/img/element-icons/leave.svg index ae8029f5bb..773e27d4ce 100644 --- a/res/img/element-icons/leave.svg +++ b/res/img/element-icons/leave.svg @@ -1,7 +1,7 @@ - - + + - + - + diff --git a/res/img/element-icons/notifications.svg b/res/img/element-icons/notifications.svg index b0efba27ca..7002782129 100644 --- a/res/img/element-icons/notifications.svg +++ b/res/img/element-icons/notifications.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/res/img/element-icons/room/files.svg b/res/img/element-icons/room/files.svg index da07f07d98..6648ab00a5 100644 --- a/res/img/element-icons/room/files.svg +++ b/res/img/element-icons/room/files.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/element-icons/room/integrations.svg b/res/img/element-icons/room/integrations.svg index f983c1e703..3a39506411 100644 --- a/res/img/element-icons/room/integrations.svg +++ b/res/img/element-icons/room/integrations.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/element-icons/room/members.svg b/res/img/element-icons/room/members.svg index c5ca577653..03aba81ad4 100644 --- a/res/img/element-icons/room/members.svg +++ b/res/img/element-icons/room/members.svg @@ -1,7 +1,7 @@ - + - + - - + + diff --git a/res/img/element-icons/room/pin.svg b/res/img/element-icons/room/pin.svg index df8438ba69..16941b329b 100644 --- a/res/img/element-icons/room/pin.svg +++ b/res/img/element-icons/room/pin.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/res/img/element-icons/room/search-inset.svg b/res/img/element-icons/room/search-inset.svg index 403f55dfbc..699cdd1d00 100644 --- a/res/img/element-icons/room/search-inset.svg +++ b/res/img/element-icons/room/search-inset.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/element-icons/room/share.svg b/res/img/element-icons/room/share.svg index ca567612ad..dac35ae5a7 100644 --- a/res/img/element-icons/room/share.svg +++ b/res/img/element-icons/room/share.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/element-icons/settings.svg b/res/img/element-icons/settings.svg index e9e74a8200..05d640df27 100644 --- a/res/img/element-icons/settings.svg +++ b/res/img/element-icons/settings.svg @@ -1,3 +1,3 @@ - - + + From 9b13ef1446dbc1fde6251868de1f14ac7cb79e68 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 16 Jul 2020 15:31:41 +0100 Subject: [PATCH 051/179] Revert "Merge branch 'joriks/horizontal-resize-bars' into 'element'" This reverts commit eb0cb5c279b1ad5f31fff87cc43440adc7b7c4b3. --- res/css/structures/_MainSplit.scss | 20 -------------------- res/css/structures/_MatrixChat.scss | 20 -------------------- 2 files changed, 40 deletions(-) diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index 387879ea7b..25e1153fce 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -26,23 +26,3 @@ limitations under the License. margin: 0 -10px 0 0; padding: 0 10px 0 0; } - -.mx_MainSplit > .mx_ResizeHandle_horizontal:hover { - position: relative; - - &::before { - position: absolute; - left: 4px; - top: 50%; - transform: translate(0, -50%); - - height: 30%; - width: 4px; - border-radius: 4px; - - content: ' '; - - background-color: $primary-fg-color; - opacity: 0.8; - } -} diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index 926d10ee04..08ed9e5559 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -78,23 +78,3 @@ limitations under the License. */ height: 100%; } - -.mx_MatrixChat > .mx_ResizeHandle_horizontal:hover { - position: relative; - - &::before { - position: absolute; - left: -2px; - top: 50%; - transform: translate(0, -50%); - - height: 30%; - width: 4px; - border-radius: 4px; - - content: ' '; - - background-color: $primary-fg-color; - opacity: 0.8; - } -} From 1a9680b527d152f2521ef99df01a305b3475242b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 15:40:31 +0100 Subject: [PATCH 052/179] Update font size of member filter field Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_MemberList.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss index 718164589c..be58db43ae 100644 --- a/res/css/views/rooms/_MemberList.scss +++ b/res/css/views/rooms/_MemberList.scss @@ -63,6 +63,11 @@ limitations under the License. .mx_GroupMemberList_query, .mx_GroupRoomList_query { flex: 1 1 0; + + // stricter rule to override the one in _common.scss + &[type="text"] { + font-size: $font-12px; + } } From e6dff951b80ec42e57f28cdba826962599e2d8bd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 15:42:58 +0100 Subject: [PATCH 053/179] Fix composer text alignment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_MessageComposer.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 9f6d2ec590..ec95403262 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -20,7 +20,7 @@ limitations under the License. margin: auto; border-top: 1px solid $primary-hairline-color; position: relative; - padding-left: 83px; + padding-left: 82px; } .mx_MessageComposer_replaced_wrapper { From 0ddfd9ee8c47506d8f29630bc10a42bc5d819692 Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Thu, 16 Jul 2020 20:15:28 +0530 Subject: [PATCH 054/179] Use rem to guard against font scaling breakages --- res/css/_font-sizes.scss | 1 + res/css/views/elements/_Field.scss | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/res/css/_font-sizes.scss b/res/css/_font-sizes.scss index 5b876ab11d..caa3a452b0 100644 --- a/res/css/_font-sizes.scss +++ b/res/css/_font-sizes.scss @@ -68,5 +68,6 @@ $font-49px: 4.9rem; $font-50px: 5.0rem; $font-51px: 5.1rem; $font-52px: 5.2rem; +$font-78px: 7.8rem; $font-88px: 8.8rem; $font-400px: 40rem; diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index fc529b140b..7a6539748b 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -16,6 +16,8 @@ limitations under the License. /* TODO: Consider unifying with general input styles in _light.scss */ +@import "../../_font-sizes.scss"; + .mx_Field { display: flex; flex: 1; @@ -191,5 +193,5 @@ limitations under the License. } .mx_Field .mx_CountryDropdown { - width: 78px; + width: $font-78px; } From ebb9c4e814c8c65251adf1178dea015b9320975e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 15:46:06 +0100 Subject: [PATCH 055/179] Update text input placeholder active colour to 75% inactive Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_common.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 7fead4317e..fa91532540 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -174,7 +174,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=text]::placeholder, :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search]::placeholder, .mx_textinput input::placeholder { - color: $roomsublist-label-fg-color; + color: rgba($input-darker-fg-color, .75); } } From 82537562768f492550584e46dc2e9b1a858adc2f Mon Sep 17 00:00:00 2001 From: Swapnil Raj Date: Thu, 16 Jul 2020 20:28:19 +0530 Subject: [PATCH 056/179] Remove unnecessary import --- res/css/views/elements/_Field.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 7a6539748b..f67da6477b 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -16,8 +16,6 @@ limitations under the License. /* TODO: Consider unifying with general input styles in _light.scss */ -@import "../../_font-sizes.scss"; - .mx_Field { display: flex; flex: 1; From bc97f731fc5279bfe5515f99869c2b29c1baf1d4 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Thu, 16 Jul 2020 16:03:00 +0100 Subject: [PATCH 057/179] Upgrade matrix-js-sdk to 7.1.0 --- package.json | 2 +- yarn.lock | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c000466778..e2f6a975ee 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "7.1.0", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index f3dc163b00..ddf51c3fac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5844,9 +5844,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": +matrix-js-sdk@7.1.0: version "7.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2a688bdac828dc62916437d83c72cef1e525d5f9" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-7.1.0.tgz#b3e3304e890df45c827706831748935168ee839f" + integrity sha512-Y+MdOfsVQRGx0KcwSdNtwsFNGWUF7Zi7wPxUSa050J8eBlbkXUNFAyuSWviLJ5pUwzmp9XMEKD9Cdv+tGZG1ww== dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From f68ad8f19e0ef7fb822148b4f54afec15a43c597 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Thu, 16 Jul 2020 16:09:41 +0100 Subject: [PATCH 058/179] Prepare changelog for v2.10.1 --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ad99ca49..e08b2ad612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +Changes in [2.10.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.10.1) (2020-07-16) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.10.0...v2.10.1) + + * Post-launch Element Web polish + [\#5002](https://github.com/matrix-org/matrix-react-sdk/pull/5002) + * Move e2e icon + [\#4992](https://github.com/matrix-org/matrix-react-sdk/pull/4992) + * Wire up new room list breadcrumbs as an ARIA Toolbar + [\#4976](https://github.com/matrix-org/matrix-react-sdk/pull/4976) + * Fix Room Tile Icon to not ignore DMs in other tags + [\#4999](https://github.com/matrix-org/matrix-react-sdk/pull/4999) + * Fix filtering by community not showing DM rooms with community members + [\#4997](https://github.com/matrix-org/matrix-react-sdk/pull/4997) + * Fix enter in new room list filter breaking things + [\#4996](https://github.com/matrix-org/matrix-react-sdk/pull/4996) + * Notify left panel of resizing when it is collapsed&expanded + [\#4995](https://github.com/matrix-org/matrix-react-sdk/pull/4995) + * When removing a filter condition, try recalculate in case it wasn't last + [\#4994](https://github.com/matrix-org/matrix-react-sdk/pull/4994) + * Create a generic ARIA toolbar component + [\#4975](https://github.com/matrix-org/matrix-react-sdk/pull/4975) + * Fix /op Slash Command + [\#4604](https://github.com/matrix-org/matrix-react-sdk/pull/4604) + * Fix copy button in share dialog + [\#4998](https://github.com/matrix-org/matrix-react-sdk/pull/4998) + * Add tooltip to Room Tile Icon + [\#4987](https://github.com/matrix-org/matrix-react-sdk/pull/4987) + * Fix names jumping on hover in irc layout + [\#4991](https://github.com/matrix-org/matrix-react-sdk/pull/4991) + * check that encryptionInfo.sender is set + [\#4988](https://github.com/matrix-org/matrix-react-sdk/pull/4988) + * Update help link + [\#4986](https://github.com/matrix-org/matrix-react-sdk/pull/4986) + * Update cover photo link + [\#4985](https://github.com/matrix-org/matrix-react-sdk/pull/4985) + Changes in [2.10.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.10.0) (2020-07-15) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.9.0...v2.10.0) From 0c89b8d267fbfd560876a4aa98dfd61188a9293f Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Thu, 16 Jul 2020 16:09:41 +0100 Subject: [PATCH 059/179] v2.10.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2f6a975ee..d5175776ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.10.0", + "version": "2.10.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 2f9bfdbb697146d97330af2f95a3b4b39f314ad0 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Thu, 16 Jul 2020 16:11:59 +0100 Subject: [PATCH 060/179] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d5175776ef..57096532a5 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "7.1.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index ddf51c3fac..f3dc163b00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5844,10 +5844,9 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@7.1.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "7.1.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-7.1.0.tgz#b3e3304e890df45c827706831748935168ee839f" - integrity sha512-Y+MdOfsVQRGx0KcwSdNtwsFNGWUF7Zi7wPxUSa050J8eBlbkXUNFAyuSWviLJ5pUwzmp9XMEKD9Cdv+tGZG1ww== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2a688bdac828dc62916437d83c72cef1e525d5f9" dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From 4aedf857cbfc809955a59862cf7933a768b2d540 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 15:12:26 +0000 Subject: [PATCH 061/179] Bump lodash from 4.17.15 to 4.17.19 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index ddf51c3fac..cbe4be69ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5718,9 +5718,9 @@ lodash.sortby@^4.7.0: integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== log-symbols@^2.0.0, log-symbols@^2.2.0: version "2.2.0" From 4d61d6320a51987bf3b7df0ae373d36b8a7fa06a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 15:12:32 +0000 Subject: [PATCH 062/179] Bump lodash from 4.17.15 to 4.17.19 in /test/end-to-end-tests Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19) Signed-off-by: dependabot[bot] --- test/end-to-end-tests/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/end-to-end-tests/yarn.lock b/test/end-to-end-tests/yarn.lock index c26dde0f97..2f4d9979fb 100644 --- a/test/end-to-end-tests/yarn.lock +++ b/test/end-to-end-tests/yarn.lock @@ -425,9 +425,9 @@ jsprim@^1.2.2: verror "1.10.0" lodash@^4.15.0, lodash@^4.17.11: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== mime-db@~1.38.0: version "1.38.0" From 9c9ff281e7c700dd8a0db650c3ae541f0a4a9e1f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 16 Jul 2020 13:00:02 -0600 Subject: [PATCH 063/179] Use .entries() --- .../views/dialogs/DevtoolsDialog.js | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index b3f9ef4537..26ab71a873 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import SyntaxHighlight from '../elements/SyntaxHighlight'; import { _t } from '../../../languageHandler'; -import { Room } from "matrix-js-sdk"; +import { Room, MatrixEvent } from "matrix-js-sdk"; import Field from "../elements/Field"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; @@ -327,6 +327,8 @@ class RoomStateExplorer extends React.PureComponent { static contextType = MatrixClientContext; + roomStateEvents: Map>; + constructor(props) { super(props); @@ -412,19 +414,16 @@ class RoomStateExplorer extends React.PureComponent { if (this.state.eventType === null) { list = { - Array.from(this.roomStateEvents.keys()).map((evType) => { - const stateGroup = this.roomStateEvents.get(evType); - const stateKeys = Array.from(stateGroup.keys()); - + Array.from(this.roomStateEvents.entries()).map(([eventType, allStateKeys]) => { let onClickFn; - if (stateKeys.length === 1 && stateKeys[0] === '') { - onClickFn = this.onViewSourceClick(stateGroup.get(stateKeys[0])); + if (allStateKeys.size() === 1 && allStateKeys.has("")) { + onClickFn = this.onViewSourceClick(allStateKeys.get("")); } else { - onClickFn = this.browseEventType(evType); + onClickFn = this.browseEventType(eventType); } - return
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d81c0c717b..964207e79a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -492,6 +492,7 @@ "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Use the improved room list (will refresh to apply changes)": "Use the improved room list (will refresh to apply changes)", "Support adding custom themes": "Support adding custom themes", + "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", "Font size": "Font size", "Use custom size": "Use custom size", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 3b1218c0d3..b3f70f3c97 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -154,6 +154,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "advancedRoomListLogging": { + // TODO: Remove flag before launch: https://github.com/vector-im/riot-web/issues/14231 + displayName: _td("Enable advanced debugging for the room list"), + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: false, + }, "mjolnirRooms": { supportedLevels: ['account'], default: [], diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 4368120a2e..6f0fbb5afa 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -60,6 +60,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { private readonly watchedSettings = [ 'feature_custom_tags', + 'advancedRoomListLogging', // TODO: Remove watch: https://github.com/vector-im/riot-web/issues/14367 ]; constructor() { @@ -126,6 +127,9 @@ export class RoomListStore2 extends AsyncStoreWithClient { if (this.enabled) { console.log("⚡ new room list store engaged"); } + if (SettingsStore.getValue("advancedRoomListLogging")) { + console.warn("Advanced room list logging is enabled"); + } } private async readAndCacheSettingsFromStore() { @@ -154,7 +158,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`); await this.algorithm.setStickyRoom(null); } else if (activeRoom !== this.algorithm.stickyRoom) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`Changing sticky room to ${activeRoomId}`); } @@ -196,6 +200,14 @@ export class RoomListStore2 extends AsyncStoreWithClient { if (payload.action === 'setting_updated') { if (this.watchedSettings.includes(payload.settingName)) { + // TODO: Remove with https://github.com/vector-im/riot-web/issues/14367 + if (payload.settingName === "advancedRoomListLogging") { + // Log when the setting changes so we know when it was turned on in the rageshake + const enabled = SettingsStore.getValue("advancedRoomListLogging"); + console.warn("Advanced room list logging is enabled? " + enabled); + return; + } + console.log("Regenerating room lists: Settings changed"); await this.readAndCacheSettingsFromStore(); @@ -218,7 +230,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { console.warn(`Own read receipt was in unknown room ${room.roomId}`); return; } - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`); } @@ -228,7 +240,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { } } else if (payload.action === 'MatrixActions.Room.tags') { const roomPayload = (payload); // TODO: Type out the dispatcher types - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Got tag change in ${roomPayload.room.roomId}`); } @@ -243,13 +255,13 @@ export class RoomListStore2 extends AsyncStoreWithClient { const roomId = eventPayload.event.getRoomId(); const room = this.matrixClient.getRoom(roomId); const tryUpdate = async (updatedRoom: Room) => { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` + ` in ${updatedRoom.roomId}`); } if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`); } @@ -282,7 +294,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { console.warn(`Event ${eventPayload.event.getId()} was decrypted in an unknown room ${roomId}`); return; } - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`); } @@ -290,7 +302,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { this.updateFn.trigger(); } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') { const eventPayload = (payload); // TODO: Type out the dispatcher types - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Received updated DM map`); } @@ -317,7 +329,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { const oldMembership = getEffectiveMembership(membershipPayload.oldMembership); const newMembership = getEffectiveMembership(membershipPayload.membership); if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`); } @@ -326,7 +338,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { // the dead room in the list. const createEvent = membershipPayload.room.currentState.getStateEvents("m.room.create", ""); if (createEvent && createEvent.getContent()['predecessor']) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Room has a predecessor`); } @@ -334,7 +346,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { if (prevRoom) { const isSticky = this.algorithm.stickyRoom === prevRoom; if (isSticky) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`); } @@ -343,7 +355,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { // Note: we hit the algorithm instead of our handleRoomUpdate() function to // avoid redundant updates. - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Removing previous room from room list`); } @@ -351,7 +363,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { } } - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Adding new room to room list`); } @@ -361,7 +373,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { } if (oldMembership !== EffectiveMembership.Invite && newMembership === EffectiveMembership.Invite) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Handling invite to ${membershipPayload.room.roomId}`); } @@ -372,7 +384,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { // If it's not a join, it's transitioning into a different list (possibly historical) if (oldMembership !== newMembership) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Handling membership change in ${membershipPayload.room.roomId}`); } @@ -386,7 +398,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); if (shouldUpdate) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`); } @@ -509,7 +521,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { } private onAlgorithmListUpdated = () => { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log("Underlying algorithm has triggered a list update - marking"); } @@ -559,7 +571,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { } public addFilter(filter: IFilterCondition): void { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log("Adding filter condition:", filter); } @@ -571,7 +583,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { } public removeFilter(filter: IFilterCondition): void { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log("Removing filter condition:", filter); } diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 6f718c09b2..c6f42aa979 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -33,6 +33,7 @@ import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFi import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership"; import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { getListAlgorithmInstance } from "./list-ordering"; +import SettingsStore from "../../../settings/SettingsStore"; /** * Fired when the Algorithm has determined a list has been updated. @@ -321,7 +322,7 @@ export class Algorithm extends EventEmitter { } newMap[tagId] = allowedRoomsInThisTag; - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`); } @@ -336,7 +337,7 @@ export class Algorithm extends EventEmitter { protected recalculateFilteredRoomsForTag(tagId: TagID): void { if (!this.hasFilters) return; // don't bother doing work if there's nothing to do - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`Recalculating filtered rooms for ${tagId}`); } @@ -348,7 +349,7 @@ export class Algorithm extends EventEmitter { this.filteredRooms[tagId] = filteredRooms; } - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`); } @@ -390,7 +391,7 @@ export class Algorithm extends EventEmitter { } if (!this._cachedStickyRooms || !updatedTag) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`Generating clone of cached rooms for sticky room handling`); } @@ -404,7 +405,7 @@ export class Algorithm extends EventEmitter { if (updatedTag) { // Update the tag indicated by the caller, if possible. This is mostly to ensure // our cache is up to date. - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`Replacing cached sticky rooms for ${updatedTag}`); } @@ -416,7 +417,7 @@ export class Algorithm extends EventEmitter { // we might have updated from the cache is also our sticky room. const sticky = this._stickyRoom; if (!updatedTag || updatedTag === sticky.tag) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`); } @@ -644,7 +645,7 @@ export class Algorithm extends EventEmitter { * processing. */ public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`Handle room update for ${room.roomId} called with cause ${cause}`); } @@ -704,7 +705,7 @@ export class Algorithm extends EventEmitter { const diff = arrayDiff(oldTags, newTags); if (diff.removed.length > 0 || diff.added.length > 0) { for (const rmTag of diff.removed) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`Removing ${room.roomId} from ${rmTag}`); } @@ -714,7 +715,7 @@ export class Algorithm extends EventEmitter { this.cachedRooms[rmTag] = algorithm.orderedRooms; } for (const addTag of diff.added) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`Adding ${room.roomId} to ${addTag}`); } @@ -727,14 +728,14 @@ export class Algorithm extends EventEmitter { // Update the tag map so we don't regen it in a moment this.roomIdsToTags[room.roomId] = newTags; - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`); } cause = RoomUpdateCause.Timeline; didTagChange = true; } else { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`); } @@ -763,7 +764,7 @@ export class Algorithm extends EventEmitter { // as the sticky room relies on this. if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) { if (this.stickyRoom === room) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`); } @@ -773,14 +774,14 @@ export class Algorithm extends EventEmitter { if (!this.roomIdsToTags[room.roomId]) { if (CAUSES_REQUIRING_ROOM.includes(cause)) { - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`); } return false; } - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`); } @@ -794,13 +795,13 @@ export class Algorithm extends EventEmitter { this.roomIdsToTags[room.roomId] = roomTags; - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags); } } - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`); } @@ -825,7 +826,7 @@ export class Algorithm extends EventEmitter { changed = true; } - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`); } From a85af47b0a5ed701dfc5fa4d660a94e22ef91e5a Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 16 Jul 2020 17:46:49 -0400 Subject: [PATCH 065/179] use a proper HTML sanitizer to strip , rather than a regexp --- src/components/views/elements/ReplyThread.js | 31 ++++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index e96d9ced11..56b3c3ff8b 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -27,6 +27,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import escapeHtml from "escape-html"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {Action} from "../../../dispatcher/actions"; +import sanitizeHtml from "sanitize-html"; // This component does no cycle detection, simply because the only way to make such a cycle would be to // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would @@ -92,7 +93,21 @@ export default class ReplyThread extends React.Component { // Part of Replies fallback support static stripHTMLReply(html) { - return html.replace(/^[\s\S]+?<\/mx-reply>/, ''); + // Sanitize the original HTML for inclusion in . We allow + // any HTML, since the original sender could use special tags that we + // don't recognize, but want to pass along to any recipients who do + // recognize them -- recipients should be sanitizing before displaying + // anyways. However, we sanitize to 1) remove any mx-reply, so that we + // don't generate a nested mx-reply, and 2) make sure that the HTML is + // properly formatted (e.g. tags are closed where necessary) + return sanitizeHtml( + html, + { + allowedTags: false, // false means allow everything + allowedAttributes: false, + exclusiveFilter: (frame) => frame.tag === "mx-reply", + } + ); } // Part of Replies fallback support @@ -102,15 +117,19 @@ export default class ReplyThread extends React.Component { let {body, formatted_body: html} = ev.getContent(); if (this.getParentEventId(ev)) { if (body) body = this.stripPlainReply(body); - if (html) html = this.stripHTMLReply(html); } if (!body) body = ""; // Always ensure we have a body, for reasons. - // Escape the body to use as HTML below. - // We also run a nl2br over the result to fix the fallback representation. We do this - // after converting the text to safe HTML to avoid user-provided BR's from being converted. - if (!html) html = escapeHtml(body).replace(/\n/g, '
'); + if (html) { + // sanitize the HTML before we put it in an + html = this.stripHTMLReply(html); + } else { + // Escape the body to use as HTML below. + // We also run a nl2br over the result to fix the fallback representation. We do this + // after converting the text to safe HTML to avoid user-provided BR's from being converted. + html = escapeHtml(body).replace(/\n/g, '
'); + } // dev note: do not rely on `body` being safe for HTML usage below. From b05a19ef13d19b7525fefff01b74bdd61b681fc2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 16 Jul 2020 18:07:51 -0400 Subject: [PATCH 066/179] lint --- src/components/views/elements/ReplyThread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 56b3c3ff8b..409bf9e01f 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -106,7 +106,7 @@ export default class ReplyThread extends React.Component { allowedTags: false, // false means allow everything allowedAttributes: false, exclusiveFilter: (frame) => frame.tag === "mx-reply", - } + }, ); } From 6e5efd083948bfd905568adef4e794e3f1f746b2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 17 Jul 2020 00:35:41 +0100 Subject: [PATCH 067/179] stop Inter from clobbering Twemoji Fixes the bug where red heart emoji appear monochrome --- res/themes/light/css/_fonts.scss | 44 ++++++++++++-------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/res/themes/light/css/_fonts.scss b/res/themes/light/css/_fonts.scss index 4f133bb015..ba64830f15 100644 --- a/res/themes/light/css/_fonts.scss +++ b/res/themes/light/css/_fonts.scss @@ -1,25 +1,20 @@ -/* - * Nunito. - * Includes extended Latin and Vietnamese character sets - * Current URLs are taken from - * https://github.com/alexeiva/NunitoFont/releases/tag/v3.500 - * ...in order to include cyrillic. - * - * Previously, they were - * https://fonts.googleapis.com/css?family=Nunito:400,400i,600,600i,700,700i&subset=latin-ext,vietnamese - * - * We explicitly do not include Nunito's italic variants, as they are not italic enough - * and it's better to rely on the browser's built-in obliquing behaviour. - */ - /* the 'src' links are relative to the bundle.css, which is in a subdirectory. */ +/* Inter unexpectedly contains various codepoints which collide with emoji, even + when variation-16 is applied to request the emoji variant. From eyeballing + the emoji picker, these are: 20e3, 23cf, 24c2, 25a0-25c1, 2665, 2764, 2b06, 2b1c. + Therefore we define a unicode-range to load which excludes the glyphs + (to avoid having to maintain a fork of Inter). */ + +$inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-2664,U+2666-2763,U+2765-2b05,U+2b07-2b1b,U+2b1d-10FFFF; + @font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap; + unicode-range: $inter-unicode-range; src: url("$(res)/fonts/Inter/Inter-Regular.woff2?v=3.13") format("woff2"), url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.13") format("woff"); } @@ -28,6 +23,7 @@ font-style: italic; font-weight: 400; font-display: swap; + unicode-range: $inter-unicode-range; src: url("$(res)/fonts/Inter/Inter-Italic.woff2?v=3.13") format("woff2"), url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.13") format("woff"); } @@ -37,6 +33,7 @@ font-style: normal; font-weight: 500; font-display: swap; + unicode-range: $inter-unicode-range; src: url("$(res)/fonts/Inter/Inter-Medium.woff2?v=3.13") format("woff2"), url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.13") format("woff"); } @@ -45,6 +42,7 @@ font-style: italic; font-weight: 500; font-display: swap; + unicode-range: $inter-unicode-range; src: url("$(res)/fonts/Inter/Inter-MediumItalic.woff2?v=3.13") format("woff2"), url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.13") format("woff"); } @@ -54,6 +52,7 @@ font-style: normal; font-weight: 600; font-display: swap; + unicode-range: $inter-unicode-range; src: url("$(res)/fonts/Inter/Inter-SemiBold.woff2?v=3.13") format("woff2"), url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.13") format("woff"); } @@ -62,6 +61,7 @@ font-style: italic; font-weight: 600; font-display: swap; + unicode-range: $inter-unicode-range; src: url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff2?v=3.13") format("woff2"), url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.13") format("woff"); } @@ -71,6 +71,7 @@ font-style: normal; font-weight: 700; font-display: swap; + unicode-range: $inter-unicode-range; src: url("$(res)/fonts/Inter/Inter-Bold.woff2?v=3.13") format("woff2"), url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.13") format("woff"); } @@ -79,6 +80,7 @@ font-style: italic; font-weight: 700; font-display: swap; + unicode-range: $inter-unicode-range; src: url("$(res)/fonts/Inter/Inter-BoldItalic.woff2?v=3.13") format("woff2"), url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.13") format("woff"); } @@ -118,17 +120,3 @@ src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } - -/* a COLR/CPAL version of Twemoji used for consistent cross-browser emoji - * taken from https://github.com/mozilla/twemoji-colr - * using the fix from https://github.com/mozilla/twemoji-colr/issues/50 to - * work on macOS - */ -/* -// except we now load it dynamically via FontManager to handle browsers -// which can't render COLR/CPAL still -@font-face { - font-family: "Twemoji Mozilla"; - src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2'); -} -*/ From 87743fe0e84376d533673c33a49b134f7ab9a676 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 16 Jul 2020 18:34:04 -0600 Subject: [PATCH 068/179] Fix size call for devtools state events Fixes https://github.com/vector-im/riot-web/issues/14565 It's not a function --- src/components/views/dialogs/DevtoolsDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 26ab71a873..a0c5375843 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -416,7 +416,7 @@ class RoomStateExplorer extends React.PureComponent { { Array.from(this.roomStateEvents.entries()).map(([eventType, allStateKeys]) => { let onClickFn; - if (allStateKeys.size() === 1 && allStateKeys.has("")) { + if (allStateKeys.size === 1 && allStateKeys.has("")) { onClickFn = this.onViewSourceClick(allStateKeys.get("")); } else { onClickFn = this.browseEventType(eventType); From 5c8d12ab25e878d0960ea73ee178f5fe6f569b0c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 10:49:57 +0100 Subject: [PATCH 069/179] Null guard no e2ee for UserInfo Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/right_panel/UserInfo.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 1fd5221cdb..71e6266264 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1287,11 +1287,11 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { ); // only display the devices list if our client supports E2E - const _enableDevices = cli.isCryptoEnabled(); + const cryptoEnabled = cli.isCryptoEnabled(); let text; if (!isRoomEncrypted) { - if (!_enableDevices) { + if (!cryptoEnabled) { text = _t("This client does not support end-to-end encryption."); } else if (room) { text = _t("Messages in this room are not end-to-end encrypted."); @@ -1305,8 +1305,8 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { let verifyButton; const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli); - const userTrust = cli.checkUserTrust(member.userId); - const userVerified = userTrust.isCrossSigningVerified(); + const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId); + const userVerified = cryptoEnabled && userTrust.isCrossSigningVerified(); const isMe = member.userId === cli.getUserId(); const canVerify = homeserverSupportsCrossSigning && !userVerified && !isMe; @@ -1345,10 +1345,10 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {

{ _t("Security") }

{ text }

{ verifyButton } - + userId={member.userId} /> }
); From 3305ca26f0a7d4a9f42dd75d88f2abd3085da88f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 12:07:54 +0100 Subject: [PATCH 070/179] fix Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/right_panel/UserInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 71e6266264..719a64063d 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1308,7 +1308,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId); const userVerified = cryptoEnabled && userTrust.isCrossSigningVerified(); const isMe = member.userId === cli.getUserId(); - const canVerify = homeserverSupportsCrossSigning && !userVerified && !isMe; + const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe; const setUpdating = (updating) => { setPendingUpdateCount(count => count + (updating ? 1 : -1)); From 7453f8dd071b525b522bca525bc331d26866849b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 13:25:28 +0100 Subject: [PATCH 071/179] Fix `this` context in _setupHomeserverManagers for IntegrationManagers Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/integrations/IntegrationManagers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 5fd28d7c54..c2cdeb5209 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -78,7 +78,7 @@ export class IntegrationManagers { } } - async _setupHomeserverManagers(discoveryResponse) { + _setupHomeserverManagers = async (discoveryResponse) => { console.log("Updating homeserver-configured integration managers..."); if (discoveryResponse && discoveryResponse['m.integrations']) { let managers = discoveryResponse['m.integrations']['managers']; @@ -104,7 +104,7 @@ export class IntegrationManagers { } else { console.log("Homeserver has no integration managers"); } - } + }; _setupAccountManagers() { if (!this._client || !this._client.getUserId()) return; // not logged in From c0e78b56b15956c522cdb84079146f6dd42052b5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 13:38:08 +0100 Subject: [PATCH 072/179] convert integrations manager utils to Typescript Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/@types/global.d.ts | 4 +- ...tance.js => IntegrationManagerInstance.ts} | 33 ++--- ...tionManagers.js => IntegrationManagers.ts} | 123 +++++++++--------- 3 files changed, 82 insertions(+), 78 deletions(-) rename src/integrations/{IntegrationManagerInstance.js => IntegrationManagerInstance.ts} (82%) rename src/integrations/{IntegrationManagers.js => IntegrationManagers.ts} (63%) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 2d895e12eb..9424cdcd17 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -23,6 +23,7 @@ import RebrandListener from "../RebrandListener"; import { RoomListStore2 } from "../stores/room-list/RoomListStore2"; import { PlatformPeg } from "../PlatformPeg"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; +import {IntegrationManagers} from "../integrations/IntegrationManagers"; declare global { interface Window { @@ -39,11 +40,12 @@ declare global { mx_RoomListStore2: RoomListStore2; mx_RoomListLayoutStore: RoomListLayoutStore; mxPlatformPeg: PlatformPeg; + mxIntegrationManagers: typeof IntegrationManagers; } // workaround for https://github.com/microsoft/TypeScript/issues/30933 interface ObjectConstructor { - fromEntries?(xs: [string|number|symbol, any][]): object + fromEntries?(xs: [string|number|symbol, any][]): object; } interface Document { diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.ts similarity index 82% rename from src/integrations/IntegrationManagerInstance.js rename to src/integrations/IntegrationManagerInstance.ts index 3ffe1e5401..85748db3c2 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.ts @@ -14,32 +14,34 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type {Room} from "matrix-js-sdk/src/models/room"; + import ScalarAuthClient from "../ScalarAuthClient"; -import * as sdk from "../index"; import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms"; -import type {Room} from "matrix-js-sdk"; import Modal from '../Modal'; import url from 'url'; import SettingsStore from "../settings/SettingsStore"; +import IntegrationManager from "../components/views/settings/IntegrationManager"; import {IntegrationManagers} from "./IntegrationManagers"; -export const KIND_ACCOUNT = "account"; -export const KIND_CONFIG = "config"; -export const KIND_HOMESERVER = "homeserver"; +export enum Kind { + Account = "account", + Config = "config", + Homeserver = "homeserver", +} export class IntegrationManagerInstance { - apiUrl: string; - uiUrl: string; - kind: string; - id: string; // only applicable in some cases + public readonly apiUrl: string; + public readonly uiUrl: string; + public readonly kind: string; + public readonly id: string; // only applicable in some cases - constructor(kind: string, apiUrl: string, uiUrl: string) { + // Per the spec: UI URL is optional. + constructor(kind: string, apiUrl: string, uiUrl: string = apiUrl, id?: string) { this.kind = kind; this.apiUrl = apiUrl; this.uiUrl = uiUrl; - - // Per the spec: UI URL is optional. - if (!this.uiUrl) this.uiUrl = this.apiUrl; + this.id = id; } get name(): string { @@ -51,19 +53,18 @@ export class IntegrationManagerInstance { const parsed = url.parse(this.apiUrl); parsed.pathname = ''; parsed.path = ''; - return parsed.format(); + return url.format(parsed); } getScalarClient(): ScalarAuthClient { return new ScalarAuthClient(this.apiUrl, this.uiUrl); } - async open(room: Room = null, screen: string = null, integrationId: string = null): void { + async open(room: Room = null, screen: string = null, integrationId: string = null): Promise { if (!SettingsStore.getValue("integrationProvisioning")) { return IntegrationManagers.sharedInstance().showDisabledDialog(); } - const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); const dialog = Modal.createTrackedDialog( 'Integration Manager', '', IntegrationManager, {loading: true}, 'mx_IntegrationManager', diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.ts similarity index 63% rename from src/integrations/IntegrationManagers.js rename to src/integrations/IntegrationManagers.ts index c2cdeb5209..cf934521c0 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.ts @@ -14,71 +14,77 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type {MatrixClient} from "matrix-js-sdk/src/client"; +import type {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import type {Room} from "matrix-js-sdk/src/models/room"; + import SdkConfig from '../SdkConfig'; -import * as sdk from "../index"; import Modal from '../Modal'; -import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG, KIND_HOMESERVER} from "./IntegrationManagerInstance"; -import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk"; +import {IntegrationManagerInstance, Kind} from "./IntegrationManagerInstance"; +import IntegrationsImpossibleDialog from "../components/views/dialogs/IntegrationsImpossibleDialog"; +import TabbedIntegrationManagerDialog from "../components/views/dialogs/TabbedIntegrationManagerDialog"; +import IntegrationsDisabledDialog from "../components/views/dialogs/IntegrationsDisabledDialog"; import WidgetUtils from "../utils/WidgetUtils"; import {MatrixClientPeg} from "../MatrixClientPeg"; import SettingsStore from "../settings/SettingsStore"; +import url from 'url'; const KIND_PREFERENCE = [ // Ordered: first is most preferred, last is least preferred. - KIND_ACCOUNT, - KIND_HOMESERVER, - KIND_CONFIG, + Kind.Account, + Kind.Homeserver, + Kind.Config, ]; export class IntegrationManagers { - static _instance; + private static instance; - static sharedInstance(): IntegrationManagers { - if (!IntegrationManagers._instance) { - IntegrationManagers._instance = new IntegrationManagers(); + private managers: IntegrationManagerInstance[] = []; + private client: MatrixClient; + private primaryManager: IntegrationManagerInstance; + + public static sharedInstance(): IntegrationManagers { + if (!IntegrationManagers.instance) { + IntegrationManagers.instance = new IntegrationManagers(); } - return IntegrationManagers._instance; + return IntegrationManagers.instance; } - _managers: IntegrationManagerInstance[] = []; - _client: MatrixClient; - _primaryManager: IntegrationManagerInstance; - constructor() { - this._compileManagers(); + this.compileManagers(); } startWatching(): void { this.stopWatching(); - this._client = MatrixClientPeg.get(); - this._client.on("accountData", this._onAccountData); - this._client.on("WellKnown.client", this._setupHomeserverManagers); - this._compileManagers(); + this.client = MatrixClientPeg.get(); + this.client.on("accountData", this.onAccountData); + this.client.on("WellKnown.client", this.setupHomeserverManagers); + this.compileManagers(); } stopWatching(): void { - if (!this._client) return; - this._client.removeListener("accountData", this._onAccountData); - this._client.removeListener("WellKnown.client", this._setupHomeserverManagers); + if (!this.client) return; + this.client.removeListener("accountData", this.onAccountData); + this.client.removeListener("WellKnown.client", this.setupHomeserverManagers); } - _compileManagers() { - this._managers = []; - this._setupConfiguredManager(); - this._setupAccountManagers(); + private compileManagers() { + this.managers = []; + this.setupConfiguredManager(); + this.setupAccountManagers(); } - _setupConfiguredManager() { - const apiUrl = SdkConfig.get()['integrations_rest_url']; - const uiUrl = SdkConfig.get()['integrations_ui_url']; + private setupConfiguredManager() { + const apiUrl: string = SdkConfig.get()['integrations_rest_url']; + const uiUrl: string = SdkConfig.get()['integrations_ui_url']; if (apiUrl && uiUrl) { - this._managers.push(new IntegrationManagerInstance(KIND_CONFIG, apiUrl, uiUrl)); - this._primaryManager = null; // reset primary + this.managers.push(new IntegrationManagerInstance(Kind.Config, apiUrl, uiUrl)); + this.primaryManager = null; // reset primary } } - _setupHomeserverManagers = async (discoveryResponse) => { + private setupHomeserverManagers = async (discoveryResponse) => { console.log("Updating homeserver-configured integration managers..."); if (discoveryResponse && discoveryResponse['m.integrations']) { let managers = discoveryResponse['m.integrations']['managers']; @@ -88,26 +94,26 @@ export class IntegrationManagers { // Clear out any known managers for the homeserver // TODO: Log out of the scalar clients - this._managers = this._managers.filter(m => m.kind !== KIND_HOMESERVER); + this.managers = this.managers.filter(m => m.kind !== Kind.Homeserver); // Now add all the managers the homeserver wants us to have for (const hsManager of managers) { if (!hsManager["api_url"]) continue; - this._managers.push(new IntegrationManagerInstance( - KIND_HOMESERVER, + this.managers.push(new IntegrationManagerInstance( + Kind.Homeserver, hsManager["api_url"], hsManager["ui_url"], // optional )); } - this._primaryManager = null; // reset primary + this.primaryManager = null; // reset primary } else { console.log("Homeserver has no integration managers"); } }; - _setupAccountManagers() { - if (!this._client || !this._client.getUserId()) return; // not logged in + private setupAccountManagers() { + if (!this.client || !this.client.getUserId()) return; // not logged in const widgets = WidgetUtils.getIntegrationManagerWidgets(); widgets.forEach(w => { const data = w.content['data']; @@ -117,30 +123,29 @@ export class IntegrationManagers { const apiUrl = data['api_url']; if (!apiUrl || !uiUrl) return; - const manager = new IntegrationManagerInstance(KIND_ACCOUNT, apiUrl, uiUrl); - manager.id = w['id'] || w['state_key'] || ''; - this._managers.push(manager); + const manager = new IntegrationManagerInstance(Kind.Account, apiUrl, uiUrl, w['id'] || w['state_key'] || ''); + this.managers.push(manager); }); - this._primaryManager = null; // reset primary + this.primaryManager = null; // reset primary } - _onAccountData = (ev: MatrixEvent): void => { + private onAccountData = (ev: MatrixEvent): void => { if (ev.getType() === 'm.widgets') { - this._compileManagers(); + this.compileManagers(); } }; hasManager(): boolean { - return this._managers.length > 0; + return this.managers.length > 0; } getOrderedManagers(): IntegrationManagerInstance[] { const ordered = []; for (const kind of KIND_PREFERENCE) { - const managers = this._managers.filter(m => m.kind === kind); + const managers = this.managers.filter(m => m.kind === kind); if (!managers || !managers.length) continue; - if (kind === KIND_ACCOUNT) { + if (kind === Kind.Account) { // Order by state_keys (IDs) managers.sort((a, b) => a.id.localeCompare(b.id)); } @@ -152,17 +157,16 @@ export class IntegrationManagers { getPrimaryManager(): IntegrationManagerInstance { if (this.hasManager()) { - if (this._primaryManager) return this._primaryManager; + if (this.primaryManager) return this.primaryManager; - this._primaryManager = this.getOrderedManagers()[0]; - return this._primaryManager; + this.primaryManager = this.getOrderedManagers()[0]; + return this.primaryManager; } else { return null; } } openNoManagerDialog(): void { - const IntegrationsImpossibleDialog = sdk.getComponent("dialogs.IntegrationsImpossibleDialog"); Modal.createTrackedDialog('Integrations impossible', '', IntegrationsImpossibleDialog); } @@ -171,11 +175,10 @@ export class IntegrationManagers { return this.showDisabledDialog(); } - if (this._managers.length === 0) { + if (this.managers.length === 0) { return this.openNoManagerDialog(); } - const TabbedIntegrationManagerDialog = sdk.getComponent("views.dialogs.TabbedIntegrationManagerDialog"); Modal.createTrackedDialog( 'Tabbed Integration Manager', '', TabbedIntegrationManagerDialog, {room, screen, integrationId}, 'mx_TabbedIntegrationManagerDialog', @@ -183,7 +186,6 @@ export class IntegrationManagers { } showDisabledDialog(): void { - const IntegrationsDisabledDialog = sdk.getComponent("dialogs.IntegrationsDisabledDialog"); Modal.createTrackedDialog('Integrations disabled', '', IntegrationsDisabledDialog); } @@ -202,15 +204,14 @@ export class IntegrationManagers { * @returns {Promise} Resolves to an integration manager instance, * or null if none was found. */ - async tryDiscoverManager(domainName: string): IntegrationManagerInstance { + async tryDiscoverManager(domainName: string): Promise { console.log("Looking up integration manager via .well-known"); if (domainName.startsWith("http:") || domainName.startsWith("https:")) { // trim off the scheme and just use the domain - const url = url.parse(domainName); - domainName = url.host; + domainName = url.parse(domainName).host; } - let wkConfig; + let wkConfig: object; try { const result = await fetch(`https://${domainName}/.well-known/matrix/integrations`); wkConfig = await result.json(); @@ -232,7 +233,7 @@ export class IntegrationManagers { } // All discovered managers are per-user managers - const manager = new IntegrationManagerInstance(KIND_ACCOUNT, widget["data"]["api_url"], widget["url"]); + const manager = new IntegrationManagerInstance(Kind.Account, widget["data"]["api_url"], widget["url"]); console.log("Got an integration manager (untested)"); // We don't test the manager because the caller may need to do extra @@ -244,4 +245,4 @@ export class IntegrationManagers { } // For debugging -global.mxIntegrationManagers = IntegrationManagers; +window.mxIntegrationManagers = IntegrationManagers; From 43beeb1d08d50c4edf9de3e2c440ac7feac88211 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 16:03:07 +0100 Subject: [PATCH 073/179] Fix emoji filterString Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/emoji.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emoji.ts b/src/emoji.ts index 53625f3026..7caeb06d21 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -71,8 +71,8 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { DATA_BY_CATEGORY[categoryId].push(emoji); } // This is used as the string to match the query against when filtering emojis - emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` + - `${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`.toLowerCase(); + emoji.filterString = (`${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` + + `${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase(); // Add mapping from unicode to Emoji object // The 'unicode' field that we use in emojibase has either From 16b1dbc375a78f7d07d167ebae349d92ce04540e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 16:10:36 +0100 Subject: [PATCH 074/179] remove unused view_tooltip Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/Tooltip.tsx | 17 --------- src/components/views/rooms/RoomList.js | 15 -------- src/dispatcher/actions.ts | 5 --- src/dispatcher/payloads/ViewTooltipPayload.ts | 35 ------------------- 4 files changed, 72 deletions(-) delete mode 100644 src/dispatcher/payloads/ViewTooltipPayload.ts diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index 69ad5e256c..2aa3391af2 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -20,10 +20,7 @@ limitations under the License. import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import dis from '../../../dispatcher/dispatcher'; import classNames from 'classnames'; -import { ViewTooltipPayload } from '../../../dispatcher/payloads/ViewTooltipPayload'; -import { Action } from '../../../dispatcher/actions'; const MIN_TOOLTIP_HEIGHT = 25; @@ -68,12 +65,6 @@ export default class Tooltip extends React.Component { // Remove the wrapper element, as the tooltip has finished using it public componentWillUnmount() { - dis.dispatch({ - action: Action.ViewTooltip, - tooltip: null, - parent: null, - }); - ReactDOM.unmountComponentAtNode(this.tooltipContainer); document.body.removeChild(this.tooltipContainer); window.removeEventListener('scroll', this.renderTooltip, true); @@ -99,7 +90,6 @@ export default class Tooltip extends React.Component { // positioned, also taking into account any window zoom // NOTE: The additional 6 pixels for the left position, is to take account of the // tooltips chevron - const parent = ReactDOM.findDOMNode(this).parentNode as Element; const style = this.updatePosition({}); // Hide the entire container when not visible. This prevents flashing of the tooltip // if it is not meant to be visible on first mount. @@ -119,13 +109,6 @@ export default class Tooltip extends React.Component { // Render the tooltip manually, as we wish it not to be rendered within the parent this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer); - - // Tell the roomlist about us so it can manipulate us if it wishes - dis.dispatch({ - action: Action.ViewTooltip, - tooltip: this.tooltip, - parent: parent, - }); }; public render() { diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index e4290f87d9..dee4015003 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -230,9 +230,6 @@ export default createReactClass({ onAction: function(payload) { switch (payload.action) { - case 'view_tooltip': - this.tooltip = payload.tooltip; - break; case 'call_state': var call = CallHandler.getCall(payload.room_id); if (call && call.call_state === 'ringing') { @@ -589,18 +586,6 @@ export default createReactClass({ } }, - _whenScrolling: function(e) { - this._hideTooltip(e); - this._repositionIncomingCallBox(e, false); - }, - - _hideTooltip: function(e) { - // Hide tooltip when scrolling, as we'll no longer be over the one we were on - if (this.tooltip && this.tooltip.style.display !== "none") { - this.tooltip.style.display = "none"; - } - }, - _repositionIncomingCallBox: function(e, firstTime) { const incomingCallBox = document.getElementById("incomingCallBox"); if (incomingCallBox && incomingCallBox.parentElement) { diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 9be674b59e..519a799e67 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -45,11 +45,6 @@ export enum Action { */ ViewRoomDirectory = "view_room_directory", - /** - * Sets the current tooltip. Should be use with ViewTooltipPayload. - */ - ViewTooltip = "view_tooltip", - /** * Forces the theme to reload. No additional payload information required. */ diff --git a/src/dispatcher/payloads/ViewTooltipPayload.ts b/src/dispatcher/payloads/ViewTooltipPayload.ts deleted file mode 100644 index 069e3a619a..0000000000 --- a/src/dispatcher/payloads/ViewTooltipPayload.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* -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 { ActionPayload } from "../payloads"; -import { Action } from "../actions"; -import { Component } from "react"; - -export interface ViewTooltipPayload extends ActionPayload { - action: Action.ViewTooltip; - - /* - * The tooltip to render. If it's null the tooltip will not be rendered - * We need the void type because of typescript headaches. - */ - tooltip: null | void | Element | Component; - - /* - * The parent under which to render the tooltip. Can be null to remove - * the parent type. - */ - parent: null | Element; -} \ No newline at end of file From 9ec2ca447ccbc0825b1ec8c2a37b101767cbc0d5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 16:19:10 +0100 Subject: [PATCH 075/179] Update style of default tooltips to match the new style Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/elements/_Tooltip.scss | 25 +++++++------------ res/css/views/rooms/_RoomBreadcrumbs2.scss | 10 -------- .../messages/ReactionsRowButtonTooltip.js | 6 +---- src/components/views/messages/TextualBody.js | 1 - .../views/rooms/RoomBreadcrumbs2.tsx | 2 +- src/components/views/rooms/RoomTileIcon.tsx | 1 - 6 files changed, 11 insertions(+), 34 deletions(-) diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index a3a90e2a4f..45202ff5f9 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -51,21 +51,27 @@ limitations under the License. .mx_Tooltip { display: none; position: fixed; - border: 1px solid $menu-border-color; border-radius: 8px; box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; - background-color: $menu-bg-color; z-index: 6000; // Higher than context menu so tooltips can be used everywhere padding: 10px; pointer-events: none; line-height: $font-14px; font-size: $font-12px; font-weight: 500; - color: $primary-fg-color; max-width: 200px; word-break: break-word; margin-right: 50px; + background-color: $inverted-bg-color; + color: $accent-fg-color; + border: 0; + text-align: center; + + .mx_Tooltip_chevron { + display: none; + } + &.mx_Tooltip_visible { animation: mx_fadein 0.2s forwards; } @@ -75,19 +81,6 @@ limitations under the License. } } -.mx_Tooltip_timeline { - &.mx_Tooltip { - background-color: $inverted-bg-color; - color: $accent-fg-color; - border: 0; - text-align: center; - - .mx_Tooltip_chevron { - display: none; - } - } -} - .mx_Tooltip_title { font-weight: 600; } diff --git a/res/css/views/rooms/_RoomBreadcrumbs2.scss b/res/css/views/rooms/_RoomBreadcrumbs2.scss index fd050cfd7c..b4a957cd8d 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs2.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs2.scss @@ -55,14 +55,4 @@ limitations under the License. .mx_RoomBreadcrumbs2_Tooltip { margin-left: -42px; margin-top: -42px; - - &.mx_Tooltip { - background-color: $inverted-bg-color; - color: $accent-fg-color; - border: 0; - - .mx_Tooltip_chevron { - display: none; - } - } } diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js index 3a87befdae..2b90175722 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.js +++ b/src/components/views/messages/ReactionsRowButtonTooltip.js @@ -73,11 +73,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent { let tooltip; if (tooltipLabel) { - tooltip = ; + tooltip = ; } return tooltip; diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 5784e36a8b..87df1b8a87 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -391,7 +391,6 @@ export default createReactClass({ onClick={this._openHistoryDialog} title={_t("Edited at %(date)s. Click to view edits.", {date: dateString})} tooltip={tooltip} - tooltipClassName="mx_Tooltip_timeline" > {`(${_t("edited")})`} diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index fde24524cd..71e9d9d6e1 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -93,7 +93,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent this.viewRoom(r, i)} aria-label={_t("Room %(name)s", {name: r.name})} title={r.name} - tooltipClassName={"mx_RoomBreadcrumbs2_Tooltip"} + tooltipClassName="mx_RoomBreadcrumbs2_Tooltip" > { return ; } From 0a08fb09a2fc57bd869b373e6b2d63feeed7830d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 16:22:56 +0100 Subject: [PATCH 076/179] Fix references to IAccessibleButtonProps Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/accessibility/context_menu/ContextMenuButton.tsx | 4 ++-- src/components/views/elements/AccessibleButton.tsx | 2 +- src/components/views/elements/AccessibleTooltipButton.tsx | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index c358155e10..e211a4c933 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -18,9 +18,9 @@ limitations under the License. import React from "react"; -import AccessibleButton, {IProps as IAccessibleButtonProps} from "../../components/views/elements/AccessibleButton"; +import AccessibleButton from "../../components/views/elements/AccessibleButton"; -interface IProps extends IAccessibleButtonProps { +interface IProps extends React.ComponentProps { label?: string; // whether or not the context menu is currently open isExpanded: boolean; diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 34481601f7..7d3d55507a 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -27,7 +27,7 @@ export type ButtonEvent = React.MouseEvent | React.KeyboardEvent { +interface IProps extends React.InputHTMLAttributes { inputRef?: React.Ref; element?: string; // The kind of button, similar to how Bootstrap works. diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 641151bbfd..6bb6f9e529 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -19,10 +19,9 @@ import React from 'react'; import classNames from 'classnames'; import AccessibleButton from "./AccessibleButton"; -import {IProps} from "./AccessibleButton"; import Tooltip from './Tooltip'; -interface ITooltipProps extends IProps { +interface ITooltipProps extends React.ComponentProps { title: string; tooltip?: React.ReactNode; tooltipClassName?: string; From c578f40ad8201a75efe6bfee22f0ffd7cb938640 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 17 Jul 2020 16:27:21 +0100 Subject: [PATCH 077/179] Sort recent emoji in descending order Fixes https://github.com/vector-im/riot-web/issues/14594 --- src/emojipicker/recent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emojipicker/recent.ts b/src/emojipicker/recent.ts index e3977faadd..1ba15d87b8 100644 --- a/src/emojipicker/recent.ts +++ b/src/emojipicker/recent.ts @@ -16,7 +16,7 @@ limitations under the License. */ import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; -import {sortBy} from "lodash"; +import {orderBy} from "lodash"; interface ILegacyFormat { [emoji: string]: [number, number]; // [count, date] @@ -68,6 +68,6 @@ export function get(limit = 24) { } // perform a stable sort on `count` to keep the recent (date) order as a secondary sort factor - const sorted = sortBy(recents, "1"); + const sorted = orderBy(recents, "1", "desc"); return sorted.slice(0, limit).map(([emoji]) => emoji); } From 4380ebcbb87ddf15e1d597b6314e49626ae4cc14 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 16:43:24 +0100 Subject: [PATCH 078/179] Add ContextMenuTooltipButton Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../context_menu/ContextMenuTooltipButton.tsx | 51 +++++++++++++++++++ src/components/structures/ContextMenu.tsx | 1 + 2 files changed, 52 insertions(+) create mode 100644 src/accessibility/context_menu/ContextMenuTooltipButton.tsx diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx new file mode 100644 index 0000000000..7f248d0a5a --- /dev/null +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -0,0 +1,51 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 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 React from "react"; + +import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; + +interface IProps extends React.ComponentProps { + label?: string; + // whether or not the context menu is currently open + isExpanded: boolean; +} + +// Semantic component for representing the AccessibleButton which launches a +export const ContextMenuTooltipButton: React.FC = ({ + label, + isExpanded, + children, + onClick, + onContextMenu, + ...props +}) => { + return ( + + { children } + + ); +}; diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 62964c5799..f1bd297730 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -461,6 +461,7 @@ export function createMenu(ElementClass, props) { // re-export the semantic helper components for simplicity export {ContextMenuButton} from "../../accessibility/context_menu/ContextMenuButton"; +export {ContextMenuTooltipButton} from "../../accessibility/context_menu/ContextMenuTooltipButton"; export {MenuGroup} from "../../accessibility/context_menu/MenuGroup"; export {MenuItem} from "../../accessibility/context_menu/MenuItem"; export {MenuItemCheckbox} from "../../accessibility/context_menu/MenuItemCheckbox"; From a8f5731c076ae64a30289e2e5c74d9c379c67a63 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 17:09:43 +0100 Subject: [PATCH 079/179] Fix tooltip towards right side of screen going wrong direction Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/Tooltip.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index 2aa3391af2..9b1e0c8350 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -18,7 +18,7 @@ limitations under the License. */ -import React, { Component } from 'react'; +import React, {Component, CSSProperties} from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; @@ -70,7 +70,7 @@ export default class Tooltip extends React.Component { window.removeEventListener('scroll', this.renderTooltip, true); } - private updatePosition(style: {[key: string]: any}) { + private updatePosition(style: CSSProperties) { const parentBox = this.parent.getBoundingClientRect(); let offset = 0; if (parentBox.height > MIN_TOOLTIP_HEIGHT) { @@ -80,8 +80,14 @@ export default class Tooltip extends React.Component { // we need so that we're still centered. offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT); } + style.top = (parentBox.top - 2) + window.pageYOffset + offset; - style.left = 6 + parentBox.right + window.pageXOffset; + if (parentBox.right > window.innerWidth / 2) { + style.right = window.innerWidth - parentBox.right - window.pageXOffset - 8; + } else { + style.left = parentBox.right + window.pageXOffset + 6; + } + return style; } From 3b5a02d804ef5b95d45c408f428139c886cbb0af Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 18:15:08 +0100 Subject: [PATCH 080/179] Fixup ContextMenuTooltipButton.tsx Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/accessibility/context_menu/ContextMenuTooltipButton.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx index 7f248d0a5a..abc5412100 100644 --- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -21,14 +21,12 @@ import React from "react"; import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; interface IProps extends React.ComponentProps { - label?: string; // whether or not the context menu is currently open isExpanded: boolean; } // Semantic component for representing the AccessibleButton which launches a export const ContextMenuTooltipButton: React.FC = ({ - label, isExpanded, children, onClick, @@ -40,8 +38,6 @@ export const ContextMenuTooltipButton: React.FC = ({ {...props} onClick={onClick} onContextMenu={onContextMenu || onClick} - title={label} - aria-label={label} aria-haspopup={true} aria-expanded={isExpanded} > From efd0bd3d009a56845046552e1d99375fa392f66d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 18:15:44 +0100 Subject: [PATCH 081/179] Fix AccessibleButton defaultProps to match its types Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/AccessibleButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 7d3d55507a..ae822204df 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -118,7 +118,7 @@ export default function AccessibleButton({ AccessibleButton.defaultProps = { element: 'div', role: 'button', - tabIndex: "0", + tabIndex: 0, }; AccessibleButton.displayName = "AccessibleButton"; From 23fa9529055114f46c6c747d375a3a6896bc3bc9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 18:16:00 +0100 Subject: [PATCH 082/179] Add tooltips to minimized roomlist2 tiles and sublists Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomSublist2.tsx | 6 +-- src/components/views/rooms/RoomTile2.tsx | 21 ++++++--- src/components/views/rooms/TemporaryTile.tsx | 49 +++++++++++--------- 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 145bd8d68e..8805530292 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -28,7 +28,7 @@ import { ListLayout } from "../../../stores/room-list/ListLayout"; import { ChevronFace, ContextMenu, - ContextMenuButton, + ContextMenuTooltipButton, StyledMenuItemCheckbox, StyledMenuItemRadio, } from "../../structures/ContextMenu"; @@ -499,10 +499,10 @@ export default class RoomSublist2 extends React.Component { return ( - {contextMenu} diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index fe6a19f2ed..b19bb23160 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -29,7 +29,7 @@ import { _t } from "../../../languageHandler"; import { ChevronFace, ContextMenu, - ContextMenuButton, + ContextMenuTooltipButton, MenuItemRadio, MenuItemCheckbox, MenuItem, @@ -54,6 +54,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import {ActionPayload} from "../../../dispatcher/payloads"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationState } from "../../../stores/notifications/NotificationState"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367 @@ -373,10 +374,10 @@ export default class RoomTile2 extends React.Component { return ( - @@ -441,10 +442,10 @@ export default class RoomTile2 extends React.Component { return ( - {contextMenu} @@ -537,11 +538,16 @@ export default class RoomTile2 extends React.Component { ariaDescribedBy = messagePreviewId(this.props.room.roomId); } + let Button: React.ComponentType> = AccessibleButton; + if (this.props.isMinimized) { + Button = AccessibleTooltipButton; + } + return ( {({onFocus, isActive, ref}) => - { aria-label={ariaLabel} aria-selected={this.state.selected} aria-describedby={ariaDescribedBy} + title={this.props.isMinimized ? name : undefined} > {roomAvatar} {nameContainer} {badge} {this.renderGeneralMenu()} {this.renderNotificationsMenu(isActive)} - + } diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx index 9baaba817d..990c619918 100644 --- a/src/components/views/rooms/TemporaryTile.tsx +++ b/src/components/views/rooms/TemporaryTile.tsx @@ -16,7 +16,11 @@ limitations under the License. import React from "react"; import classNames from "classnames"; -import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; +import { + RovingAccessibleButton, + RovingAccessibleTooltipButton, + RovingTabIndexWrapper +} from "../../../accessibility/RovingTabIndex"; import AccessibleButton from "../../views/elements/AccessibleButton"; import NotificationBadge from "./NotificationBadge"; import { NotificationState } from "../../../stores/notifications/NotificationState"; @@ -86,30 +90,29 @@ export default class TemporaryTile extends React.Component { ); if (this.props.isMinimized) nameContainer = null; + let Button = RovingAccessibleButton; + if (this.props.isMinimized) { + Button = RovingAccessibleTooltipButton; + } + return ( - - {({onFocus, isActive, ref}) => - -
- {this.props.avatar} -
- {nameContainer} -
- {badge} -
-
- } -
+
); } From a402f7e38f3bad992541c2f4cc38e8be02eb9552 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 18:16:21 +0100 Subject: [PATCH 083/179] Add tooltips to top right buttons Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/elements/ManageIntegsButton.js | 4 +- .../views/right_panel/HeaderButton.js | 7 +-- src/components/views/rooms/RoomHeader.js | 50 ++++++++----------- 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index b631ddee73..d82af5e136 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -21,6 +21,7 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import SettingsStore from "../../../settings/SettingsStore"; +import AccessibleTooltipButton from "./AccessibleTooltipButton"; export default class ManageIntegsButton extends React.Component { constructor(props) { @@ -45,9 +46,8 @@ export default class ManageIntegsButton extends React.Component { render() { let integrationsButton =
; if (IntegrationManagers.sharedInstance().hasManager()) { - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); integrationsButton = ( - - ; + onClick={this.onClick} + />; } } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 8311a98784..6c61524297 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -34,6 +34,7 @@ import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import E2EIcon from './E2EIcon'; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import {DefaultTagID} from "../../../stores/room-list/models"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; export default createReactClass({ displayName: 'RoomHeader', @@ -220,11 +221,10 @@ export default createReactClass({ if (this.props.onSettingsClick) { settingsButton = - - ; + title={_t("Settings")} />; } if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) { @@ -236,55 +236,45 @@ export default createReactClass({ } pinnedEventsButton = - + { pinsIndicator } - ; + ; } -// var leave_button; -// if (this.props.onLeaveClick) { -// leave_button = -//
-// -//
; -// } - let forgetButton; if (this.props.onForgetClick) { forgetButton = - - ; + title={_t("Forget room")} />; } let searchButton; if (this.props.onSearchClick && this.props.inRoom) { searchButton = - - ; + title={_t("Search")} />; } let shareRoomButton; if (this.props.inRoom) { shareRoomButton = - - ; + title={_t('Share room')} />; } let manageIntegsButton; if (this.props.room && this.props.room.roomId && this.props.inRoom) { - manageIntegsButton = ; + manageIntegsButton = ; } const rightRow = From 1c205c7704f20cd5e952fce65a1b86a1597cde23 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 18:16:31 +0100 Subject: [PATCH 084/179] Add buttons to composer actions Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposer.js | 36 ++++++++++--------- src/components/views/rooms/Stickerpicker.js | 5 +-- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 84a5a3a9a0..bf4700ed97 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -27,7 +27,8 @@ import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks'; import ContentMessages from '../../../ContentMessages'; import E2EIcon from './E2EIcon'; import SettingsStore from "../../../settings/SettingsStore"; -import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu"; +import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; function ComposerAvatar(props) { const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); @@ -41,7 +42,6 @@ ComposerAvatar.propTypes = { }; function CallButton(props) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const onVoiceCallClick = (ev) => { dis.dispatch({ action: 'place_call', @@ -50,10 +50,11 @@ function CallButton(props) { }); }; - return (); + return (); } CallButton.propTypes = { @@ -61,7 +62,6 @@ CallButton.propTypes = { }; function VideoCallButton(props) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const onCallClick = (ev) => { dis.dispatch({ action: 'place_call', @@ -70,7 +70,8 @@ function VideoCallButton(props) { }); }; - return ; @@ -117,14 +118,15 @@ const EmojiButton = ({addEmoji}) => { } return - - + { contextMenu } ; @@ -185,9 +187,9 @@ class UploadButton extends React.Component { render() { const uploadInputStyle = {display: 'none'}; - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return ( - @@ -198,7 +200,7 @@ class UploadButton extends React.Component { multiple onChange={this.onUploadFileInputChange} /> - + ); } } diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index fc6e80fc61..2e56e49be1 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -27,6 +27,7 @@ import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import SettingsStore from "../../../settings/SettingsStore"; import {ContextMenu} from "../../structures/ContextMenu"; import {WidgetType} from "../../../widgets/WidgetType"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; // This should be below the dialog level (4000), but above the rest of the UI (1000-2000). // We sit in a context menu, so this should be given to the context menu. @@ -409,14 +410,14 @@ export default class Stickerpicker extends React.Component { } else { // Show show-stickers button stickersButton = - - ; + ; } return { stickersButton } From fc66a1550441ca2025e3957c9c19bfc2f95ad842 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 18:36:32 +0100 Subject: [PATCH 085/179] Add tooltips to the Message Action Bar Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/messages/MessageActionBar.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 7959ad8a93..513f21b8b7 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -22,11 +22,11 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; -import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu'; +import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from '../../structures/ContextMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import RoomContext from "../../../contexts/RoomContext"; import Toolbar from "../../../accessibility/Toolbar"; -import {RovingAccessibleButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex"; +import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex"; const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -55,9 +55,9 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo } return - { } return - Date: Fri, 17 Jul 2020 18:43:42 +0100 Subject: [PATCH 086/179] Replace non-functional Interactive Tooltip Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_components.scss | 1 - .../views/elements/_InteractiveTooltip.scss | 91 ----- .../views/elements/InteractiveTooltip.js | 336 ------------------ .../views/rooms/MessageComposerFormatBar.js | 29 +- 4 files changed, 14 insertions(+), 443 deletions(-) delete mode 100644 res/css/views/elements/_InteractiveTooltip.scss delete mode 100644 src/components/views/elements/InteractiveTooltip.js diff --git a/res/css/_components.scss b/res/css/_components.scss index d0432b2f23..27eebf0ea9 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -108,7 +108,6 @@ @import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; -@import "./views/elements/_InteractiveTooltip.scss"; @import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_ProgressBar.scss"; diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss deleted file mode 100644 index db98d95709..0000000000 --- a/res/css/views/elements/_InteractiveTooltip.scss +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2019 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. -*/ - -.mx_InteractiveTooltip_wrapper { - position: fixed; - z-index: 5000; -} - -.mx_InteractiveTooltip { - border-radius: 3px; - background-color: $interactive-tooltip-bg-color; - color: $interactive-tooltip-fg-color; - position: absolute; - font-size: $font-10px; - font-weight: 600; - padding: 6px; - z-index: 5001; -} - -.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top { - top: 10px; // 8px chevron + 2px spacing -} - -.mx_InteractiveTooltip_chevron_top { - position: absolute; - left: calc(50% - 8px); - top: -8px; - width: 0; - height: 0; - border-left: 8px solid transparent; - border-bottom: 8px solid $interactive-tooltip-bg-color; - border-right: 8px solid transparent; -} - -// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path -// by Sebastiano Guerriero (@guerriero_se) -@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) { - .mx_InteractiveTooltip_chevron_top { - height: 16px; - width: 16px; - background-color: inherit; - border: none; - clip-path: polygon(0% 0%, 100% 100%, 0% 100%); - transform: rotate(135deg); - border-radius: 0 0 0 3px; - top: calc(-8px / 1.414); // sqrt(2) because of rotation - } -} - -.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom { - bottom: 10px; // 8px chevron + 2px spacing -} - -.mx_InteractiveTooltip_chevron_bottom { - position: absolute; - left: calc(50% - 8px); - bottom: -8px; - width: 0; - height: 0; - border-left: 8px solid transparent; - border-top: 8px solid $interactive-tooltip-bg-color; - border-right: 8px solid transparent; -} - -// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path -// by Sebastiano Guerriero (@guerriero_se) -@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) { - .mx_InteractiveTooltip_chevron_bottom { - height: 16px; - width: 16px; - background-color: inherit; - border: none; - clip-path: polygon(0% 0%, 100% 100%, 0% 100%); - transform: rotate(-45deg); - border-radius: 0 0 0 3px; - bottom: calc(-8px / 1.414); // sqrt(2) because of rotation - } -} diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js deleted file mode 100644 index 7f5f24a094..0000000000 --- a/src/components/views/elements/InteractiveTooltip.js +++ /dev/null @@ -1,336 +0,0 @@ -/* -Copyright 2019 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 React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container"; - -// If the distance from tooltip to window edge is below this value, the tooltip -// will flip around to the other side of the target. -const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20; - -function getOrCreateContainer() { - let container = document.getElementById(InteractiveTooltipContainerId); - - if (!container) { - container = document.createElement("div"); - container.id = InteractiveTooltipContainerId; - document.body.appendChild(container); - } - - return container; -} - -function isInRect(x, y, rect) { - const { top, right, bottom, left } = rect; - return x >= left && x <= right && y >= top && y <= bottom; -} - -/** - * Returns the positive slope of the diagonal of the rect. - * - * @param {DOMRect} rect - * @return {integer} - */ -function getDiagonalSlope(rect) { - const { top, right, bottom, left } = rect; - return (bottom - top) / (right - left); -} - -function isInUpperLeftHalf(x, y, rect) { - const { bottom, left } = rect; - // Negative slope because Y values grow downwards and for this case, the - // diagonal goes from larger to smaller Y values. - const diagonalSlope = getDiagonalSlope(rect) * -1; - return isInRect(x, y, rect) && (y <= bottom + diagonalSlope * (x - left)); -} - -function isInLowerRightHalf(x, y, rect) { - const { bottom, left } = rect; - // Negative slope because Y values grow downwards and for this case, the - // diagonal goes from larger to smaller Y values. - const diagonalSlope = getDiagonalSlope(rect) * -1; - return isInRect(x, y, rect) && (y >= bottom + diagonalSlope * (x - left)); -} - -function isInUpperRightHalf(x, y, rect) { - const { top, left } = rect; - // Positive slope because Y values grow downwards and for this case, the - // diagonal goes from smaller to larger Y values. - const diagonalSlope = getDiagonalSlope(rect) * 1; - return isInRect(x, y, rect) && (y <= top + diagonalSlope * (x - left)); -} - -function isInLowerLeftHalf(x, y, rect) { - const { top, left } = rect; - // Positive slope because Y values grow downwards and for this case, the - // diagonal goes from smaller to larger Y values. - const diagonalSlope = getDiagonalSlope(rect) * 1; - return isInRect(x, y, rect) && (y >= top + diagonalSlope * (x - left)); -} - -/* - * This style of tooltip takes a "target" element as its child and centers the - * tooltip along one edge of the target. - */ -export default class InteractiveTooltip extends React.Component { - static propTypes = { - // Content to show in the tooltip - content: PropTypes.node.isRequired, - // Function to call when visibility of the tooltip changes - onVisibilityChange: PropTypes.func, - // flag to forcefully hide this tooltip - forceHidden: PropTypes.bool, - }; - - constructor() { - super(); - - this.state = { - contentRect: null, - visible: false, - }; - } - - componentDidUpdate() { - // Whenever this passthrough component updates, also render the tooltip - // in a separate DOM tree. This allows the tooltip content to participate - // the normal React rendering cycle: when this component re-renders, the - // tooltip content re-renders. - // Once we upgrade to React 16, this could be done a bit more naturally - // using the portals feature instead. - this.renderTooltip(); - } - - componentWillUnmount() { - document.removeEventListener("mousemove", this.onMouseMove); - } - - collectContentRect = (element) => { - // We don't need to clean up when unmounting, so ignore - if (!element) return; - - this.setState({ - contentRect: element.getBoundingClientRect(), - }); - } - - collectTarget = (element) => { - this.target = element; - } - - canTooltipFitAboveTarget() { - const { contentRect } = this.state; - const targetRect = this.target.getBoundingClientRect(); - const targetTop = targetRect.top + window.pageYOffset; - return ( - !contentRect || - (targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE) - ); - } - - onMouseMove = (ev) => { - const { clientX: x, clientY: y } = ev; - const { contentRect } = this.state; - const targetRect = this.target.getBoundingClientRect(); - - // When moving the mouse from the target to the tooltip, we create a - // safe area that includes the tooltip, the target, and the trapezoid - // ABCD between them: - // ┌───────────┐ - // │ │ - // │ │ - // A └───E───F───┘ B - // V - // ┌─┐ - // │ │ - // C└─┘D - // - // As long as the mouse remains inside the safe area, the tooltip will - // stay open. - const buffer = 50; - if (isInRect(x, y, targetRect)) { - return; - } - if (this.canTooltipFitAboveTarget()) { - const contentRectWithBuffer = { - top: contentRect.top - buffer, - right: contentRect.right + buffer, - bottom: contentRect.bottom, - left: contentRect.left - buffer, - }; - const trapezoidLeft = { - top: contentRect.bottom, - right: targetRect.left, - bottom: targetRect.bottom, - left: contentRect.left - buffer, - }; - const trapezoidCenter = { - top: contentRect.bottom, - right: targetRect.right, - bottom: targetRect.bottom, - left: targetRect.left, - }; - const trapezoidRight = { - top: contentRect.bottom, - right: contentRect.right + buffer, - bottom: targetRect.bottom, - left: targetRect.right, - }; - - if ( - isInRect(x, y, contentRectWithBuffer) || - isInUpperRightHalf(x, y, trapezoidLeft) || - isInRect(x, y, trapezoidCenter) || - isInUpperLeftHalf(x, y, trapezoidRight) - ) { - return; - } - } else { - const contentRectWithBuffer = { - top: contentRect.top, - right: contentRect.right + buffer, - bottom: contentRect.bottom + buffer, - left: contentRect.left - buffer, - }; - const trapezoidLeft = { - top: targetRect.top, - right: targetRect.left, - bottom: contentRect.top, - left: contentRect.left - buffer, - }; - const trapezoidCenter = { - top: targetRect.top, - right: targetRect.right, - bottom: contentRect.top, - left: targetRect.left, - }; - const trapezoidRight = { - top: targetRect.top, - right: contentRect.right + buffer, - bottom: contentRect.top, - left: targetRect.right, - }; - - if ( - isInRect(x, y, contentRectWithBuffer) || - isInLowerRightHalf(x, y, trapezoidLeft) || - isInRect(x, y, trapezoidCenter) || - isInLowerLeftHalf(x, y, trapezoidRight) - ) { - return; - } - } - - this.hideTooltip(); - } - - onTargetMouseOver = (ev) => { - this.showTooltip(); - } - - showTooltip() { - // Don't enter visible state if we haven't collected the target yet - if (!this.target) { - return; - } - this.setState({ - visible: true, - }); - if (this.props.onVisibilityChange) { - this.props.onVisibilityChange(true); - } - document.addEventListener("mousemove", this.onMouseMove); - } - - hideTooltip() { - this.setState({ - visible: false, - }); - if (this.props.onVisibilityChange) { - this.props.onVisibilityChange(false); - } - document.removeEventListener("mousemove", this.onMouseMove); - } - - renderTooltip() { - const { contentRect, visible } = this.state; - if (this.props.forceHidden === true || !visible) { - ReactDOM.render(null, getOrCreateContainer()); - return null; - } - - const targetRect = this.target.getBoundingClientRect(); - - // The window X and Y offsets are to adjust position when zoomed in to page - const targetLeft = targetRect.left + window.pageXOffset; - const targetBottom = targetRect.bottom + window.pageYOffset; - const targetTop = targetRect.top + window.pageYOffset; - - // Place the tooltip above the target by default. If we find that the - // tooltip content would extend past the safe area towards the window - // edge, flip around to below the target. - const position = {}; - let chevronFace = null; - if (this.canTooltipFitAboveTarget()) { - position.bottom = window.innerHeight - targetTop; - chevronFace = "bottom"; - } else { - position.top = targetBottom; - chevronFace = "top"; - } - - // Center the tooltip horizontally with the target's center. - position.left = targetLeft + targetRect.width / 2; - - const chevron =
; - - const menuClasses = classNames({ - 'mx_InteractiveTooltip': true, - 'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top', - 'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom', - }); - - const menuStyle = {}; - if (contentRect) { - menuStyle.left = `-${contentRect.width / 2}px`; - } - - const tooltip =
-
- {chevron} - {this.props.content} -
-
; - - ReactDOM.render(tooltip, getOrCreateContainer()); - } - - render() { - // We use `cloneElement` here to append some props to the child content - // without using a wrapper element which could disrupt layout. - return React.cloneElement(this.props.children, { - ref: this.collectTarget, - onMouseOver: this.onTargetMouseOver, - }); - } -} diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.js index 42d54f5987..71aef1e833 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.js +++ b/src/components/views/rooms/MessageComposerFormatBar.js @@ -17,9 +17,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -import * as sdk from '../../../index'; import classNames from 'classnames'; -import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; export default class MessageComposerFormatBar extends React.PureComponent { static propTypes = { @@ -68,28 +67,28 @@ class FormatButton extends React.PureComponent { }; render() { - const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip'); const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`; let shortcut; if (this.props.shortcut) { shortcut =
{this.props.shortcut}
; } - const tooltipContent = ( -
-
{this.props.label}
+ const tooltip =
+
+ {this.props.label} +
+
{shortcut}
- ); +
; return ( - - - + ); } } From 8ea806b43e6b518e0ee7a5f95716389a2b26b249 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 18:54:09 +0100 Subject: [PATCH 087/179] Add tooltip to collapsed sublists Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomSublist2.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 8805530292..7b8967052c 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -561,6 +561,11 @@ export default class RoomSublist2 extends React.Component {
); + let Button = AccessibleButton; + if (this.props.isMinimized) { + Button = AccessibleTooltipButton; + } + // Note: the addRoomButton conditionally gets moved around // the DOM depending on whether or not the list is minimized. // If we're minimized, we want it below the header so it @@ -569,7 +574,7 @@ export default class RoomSublist2 extends React.Component { return (
- { aria-level={1} onClick={this.onHeaderClick} onContextMenu={this.onContextMenu} + title={this.props.isMinimized ? this.props.label : undefined} > {this.props.label} - + {this.renderMenu()} {this.props.isMinimized ? null : badgeContainer} {this.props.isMinimized ? null : addRoomButton} From b951516077802769fda2f109dd7dd59692e4789e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 18:57:38 +0100 Subject: [PATCH 088/179] Add tooltip to Explore rooms button Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanel2.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 7fe1c24062..717ec240ac 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -34,6 +34,7 @@ import SettingsStore from "../../settings/SettingsStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2"; import {Key} from "../../Keyboard"; import IndicatorScrollbar from "../structures/IndicatorScrollbar"; +import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367 @@ -347,7 +348,7 @@ export default class LeftPanel2 extends React.Component { onVerticalArrow={this.onKeyDown} onEnter={this.onEnter} /> - Date: Fri, 17 Jul 2020 19:00:02 +0100 Subject: [PATCH 089/179] fix types Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomSublist2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 7b8967052c..4883d4f2a3 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -561,7 +561,7 @@ export default class RoomSublist2 extends React.Component {
); - let Button = AccessibleButton; + let Button: React.ComponentType> = AccessibleButton; if (this.props.isMinimized) { Button = AccessibleTooltipButton; } From 7529bb8bc056d01fec0c94518b8b0199c4e0956e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 19:25:37 +0100 Subject: [PATCH 090/179] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/ManageIntegsButton.js | 1 - src/components/views/right_panel/HeaderButton.js | 1 - src/components/views/rooms/RoomHeader.js | 1 - 3 files changed, 3 deletions(-) diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index d82af5e136..ac8a98a94a 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import SettingsStore from "../../../settings/SettingsStore"; diff --git a/src/components/views/right_panel/HeaderButton.js b/src/components/views/right_panel/HeaderButton.js index ae32873437..2cfc060bba 100644 --- a/src/components/views/right_panel/HeaderButton.js +++ b/src/components/views/right_panel/HeaderButton.js @@ -22,7 +22,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Analytics from '../../../Analytics'; -import AccessibleButton from '../elements/AccessibleButton'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; export default class HeaderButton extends React.Component { diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 6c61524297..1dedd53d00 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -26,7 +26,6 @@ import Modal from "../../../Modal"; import RateLimitedFunc from '../../../ratelimitedfunc'; import { linkifyElement } from '../../../HtmlUtils'; -import AccessibleButton from '../elements/AccessibleButton'; import ManageIntegsButton from '../elements/ManageIntegsButton'; import {CancelButton} from './SimpleRoomHeader'; import SettingsStore from "../../../settings/SettingsStore"; From e75b33c66cdc0fe9f7f6bb5185d420edbf659916 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Jul 2020 19:41:45 +0100 Subject: [PATCH 091/179] fix e2e tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/end-to-end-tests/src/usecases/room-settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js index b705463965..fac3fa0855 100644 --- a/test/end-to-end-tests/src/usecases/room-settings.js +++ b/test/end-to-end-tests/src/usecases/room-settings.js @@ -45,7 +45,7 @@ async function findTabs(session) { /// XXX delay is needed here, possibly because the header is being rerendered /// click doesn't do anything otherwise await session.delay(1000); - const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[title=Settings]"); + const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[aria-label=Settings]"); await settingsButton.click(); //find tabs From 06336a88b3ca534a1a99f0ce79ebfadcaf832eef Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 14:02:51 -0600 Subject: [PATCH 092/179] Remove setting for old room list --- src/components/structures/LoggedInView.tsx | 16 ++------ src/components/views/voip/CallPreview2.tsx | 40 +++++++------------ src/i18n/strings/en_EN.json | 1 - src/settings/Settings.js | 8 ---- src/stores/RoomListStore.js | 10 +---- src/stores/room-list/RoomListStore2.ts | 7 +--- .../room-list/RoomListStoreTempProxy.ts | 2 +- 7 files changed, 21 insertions(+), 63 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index b65f176089..d4b0f7902a 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -607,7 +607,6 @@ class LoggedInView extends React.Component { }; render() { - const LeftPanel = sdk.getComponent('structures.LeftPanel'); const RoomView = sdk.getComponent('structures.RoomView'); const UserView = sdk.getComponent('structures.UserView'); const GroupView = sdk.getComponent('structures.GroupView'); @@ -661,21 +660,12 @@ class LoggedInView extends React.Component { bodyClasses += ' mx_MatrixChat_useCompactLayout'; } - let leftPanel = ( - ); - if (SettingsStore.getValue("feature_new_room_list")) { - leftPanel = ( - - ); - } return ( diff --git a/src/components/views/voip/CallPreview2.tsx b/src/components/views/voip/CallPreview2.tsx index 1f2caf5ef8..31b67a01ad 100644 --- a/src/components/views/voip/CallPreview2.tsx +++ b/src/components/views/voip/CallPreview2.tsx @@ -37,7 +37,6 @@ interface IProps { interface IState { roomId: string; activeCall: any; - newRoomListActive: boolean; } export default class CallPreview extends React.Component { @@ -51,12 +50,7 @@ export default class CallPreview extends React.Component { this.state = { roomId: RoomViewStore.getRoomId(), activeCall: CallHandler.getAnyActiveCall(), - newRoomListActive: SettingsStore.getValue("feature_new_room_list"), }; - - this.settingsWatcherRef = SettingsStore.watchSetting("feature_new_room_list", null, (name, roomId, level, valAtLevel, newVal) => this.setState({ - newRoomListActive: newVal, - })); } public componentDidMount() { @@ -102,28 +96,24 @@ export default class CallPreview extends React.Component { }; public render() { - if (this.state.newRoomListActive) { - const callForRoom = CallHandler.getCallForRoom(this.state.roomId); - const showCall = ( - this.state.activeCall && - this.state.activeCall.call_state === 'connected' && - !callForRoom + const callForRoom = CallHandler.getCallForRoom(this.state.roomId); + const showCall = ( + this.state.activeCall && + this.state.activeCall.call_state === 'connected' && + !callForRoom + ); + + if (showCall) { + return ( + ); - - if (showCall) { - return ( - - ); - } - - return ; } - return null; + return ; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index aae035b90b..360a29dc16 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -488,7 +488,6 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Use the improved room list (will refresh to apply changes)": "Use the improved room list (will refresh to apply changes)", "Support adding custom themes": "Support adding custom themes", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 81ca0382cf..a8b6310b90 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -140,14 +140,6 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, - "feature_new_room_list": { - // TODO: Remove setting: https://github.com/vector-im/riot-web/issues/14367 - // XXX: We shouldn't have non-features appear like features. - displayName: _td("Use the improved room list (will refresh to apply changes)"), - supportedLevels: LEVELS_FEATURE, - default: true, - controller: new ReloadOnChangeController(), - }, "feature_custom_themes": { isFeature: true, displayName: _td("Support adding custom themes"), diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 1861085a27..6c18aa83ad 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -92,19 +92,12 @@ class RoomListStore extends Store { constructor() { super(dis); - this._checkDisabled(); + this.disabled = true; this._init(); this._getManualComparator = this._getManualComparator.bind(this); this._recentsComparator = this._recentsComparator.bind(this); } - _checkDisabled() { - this.disabled = SettingsStore.getValue("feature_new_room_list"); - if (this.disabled) { - console.warn("👋 legacy room list store has been disabled"); - } - } - /** * Changes the sorting algorithm used by the RoomListStore. * @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants. @@ -196,7 +189,6 @@ class RoomListStore extends Store { break; } - this._checkDisabled(); if (this.disabled) return; // Always ensure that we set any state needed for settings here. It is possible that diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 6f0fbb5afa..9576ae8ed6 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -52,7 +52,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { public static TEST_MODE = false; private initialListsGenerated = false; - private enabled = false; + private enabled = true; private algorithm = new Algorithm(); private filterConditions: IFilterCondition[] = []; private tagWatcher = new TagWatcher(this); @@ -121,12 +121,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { this.updateFn.trigger(); } - // TODO: Remove enabled flag with the old RoomListStore: https://github.com/vector-im/riot-web/issues/14367 private checkEnabled() { - this.enabled = SettingsStore.getValue("feature_new_room_list"); - if (this.enabled) { - console.log("⚡ new room list store engaged"); - } if (SettingsStore.getValue("advancedRoomListLogging")) { console.warn("Advanced room list logging is enabled"); } diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 2a5348ab6e..55d710004e 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -28,7 +28,7 @@ import { ITagMap } from "./algorithms/models"; */ export class RoomListStoreTempProxy { public static isUsingNewStore(): boolean { - return SettingsStore.getValue("feature_new_room_list"); + return true; } public static addListener(handler: () => void): RoomListStoreTempToken { From 3c047cecfd7f269e83bf7903c1d5f48ed8cb7773 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 14:09:05 -0600 Subject: [PATCH 093/179] Remove core structures for the old room list --- res/css/_components.scss | 9 - res/css/structures/_LeftPanel.scss | 178 ---- res/css/structures/_RoomSubList.scss | 187 ---- res/css/structures/_TopLeftMenuButton.scss | 49 - res/css/views/rooms/_InviteOnlyIcon.scss | 58 -- res/css/views/rooms/_RoomBreadcrumbs.scss | 112 --- res/css/views/rooms/_RoomDropTarget.scss | 55 -- res/css/views/rooms/_RoomList.scss | 70 -- res/css/views/rooms/_RoomTile.scss | 228 ----- res/css/views/rooms/_UserOnlineDot.scss | 23 - src/components/structures/LeftPanel.js | 305 ------- src/components/structures/RoomSubList.js | 496 ----------- .../structures/TopLeftMenuButton.js | 158 ---- .../views/create_room/CreateRoomButton.js | 44 - .../views/elements/CreateRoomButton.js | 40 - src/components/views/rooms/InviteOnlyIcon.js | 53 -- src/components/views/rooms/RoomBreadcrumbs.js | 394 -------- src/components/views/rooms/RoomDropTarget.js | 35 - src/components/views/rooms/RoomList.js | 838 ------------------ src/components/views/rooms/RoomTile.js | 565 ------------ src/components/views/rooms/UserOnlineDot.js | 48 - src/i18n/strings/en_EN.json | 25 +- 22 files changed, 9 insertions(+), 3961 deletions(-) delete mode 100644 res/css/structures/_LeftPanel.scss delete mode 100644 res/css/structures/_RoomSubList.scss delete mode 100644 res/css/structures/_TopLeftMenuButton.scss delete mode 100644 res/css/views/rooms/_InviteOnlyIcon.scss delete mode 100644 res/css/views/rooms/_RoomBreadcrumbs.scss delete mode 100644 res/css/views/rooms/_RoomDropTarget.scss delete mode 100644 res/css/views/rooms/_RoomList.scss delete mode 100644 res/css/views/rooms/_RoomTile.scss delete mode 100644 res/css/views/rooms/_UserOnlineDot.scss delete mode 100644 src/components/structures/LeftPanel.js delete mode 100644 src/components/structures/RoomSubList.js delete mode 100644 src/components/structures/TopLeftMenuButton.js delete mode 100644 src/components/views/create_room/CreateRoomButton.js delete mode 100644 src/components/views/elements/CreateRoomButton.js delete mode 100644 src/components/views/rooms/InviteOnlyIcon.js delete mode 100644 src/components/views/rooms/RoomBreadcrumbs.js delete mode 100644 src/components/views/rooms/RoomDropTarget.js delete mode 100644 src/components/views/rooms/RoomList.js delete mode 100644 src/components/views/rooms/RoomTile.js delete mode 100644 src/components/views/rooms/UserOnlineDot.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 27eebf0ea9..77462ad4c1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -11,7 +11,6 @@ @import "./structures/_GroupView.scss"; @import "./structures/_HeaderButtons.scss"; @import "./structures/_HomePage.scss"; -@import "./structures/_LeftPanel.scss"; @import "./structures/_LeftPanel2.scss"; @import "./structures/_MainSplit.scss"; @import "./structures/_MatrixChat.scss"; @@ -21,14 +20,12 @@ @import "./structures/_RoomDirectory.scss"; @import "./structures/_RoomSearch.scss"; @import "./structures/_RoomStatusBar.scss"; -@import "./structures/_RoomSubList.scss"; @import "./structures/_RoomView.scss"; @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; @import "./structures/_TabbedView.scss"; @import "./structures/_TagPanel.scss"; @import "./structures/_ToastContainer.scss"; -@import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; @import "./structures/_UserMenu.scss"; @import "./structures/_ViewSource.scss"; @@ -167,7 +164,6 @@ @import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_IRCLayout.scss"; -@import "./views/rooms/_InviteOnlyIcon.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; @import "./views/rooms/_MemberInfo.scss"; @@ -179,16 +175,12 @@ @import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_ReplyPreview.scss"; -@import "./views/rooms/_RoomBreadcrumbs.scss"; @import "./views/rooms/_RoomBreadcrumbs2.scss"; -@import "./views/rooms/_RoomDropTarget.scss"; @import "./views/rooms/_RoomHeader.scss"; -@import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomList2.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; @import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomSublist2.scss"; -@import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomTile2.scss"; @import "./views/rooms/_RoomTileIcon.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss"; @@ -196,7 +188,6 @@ @import "./views/rooms/_SendMessageComposer.scss"; @import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss"; -@import "./views/rooms/_UserOnlineDot.scss"; @import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_AvatarSetting.scss"; @import "./views/settings/_CrossSigningPanel.scss"; diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss deleted file mode 100644 index 35d9f0e7da..0000000000 --- a/res/css/structures/_LeftPanel.scss +++ /dev/null @@ -1,178 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd - -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. -*/ - -.mx_LeftPanel_container { - display: flex; - /* LeftPanel 260px */ - min-width: 260px; - max-width: 50%; - flex: 0 0 auto; -} - -.mx_LeftPanel_container.collapsed { - min-width: unset; - /* Collapsed LeftPanel 50px */ - flex: 0 0 50px; -} - -.mx_LeftPanel_container.collapsed.mx_LeftPanel_container_hasTagPanel { - /* TagPanel 70px + Collapsed LeftPanel 50px */ - flex: 0 0 120px; -} - -.mx_LeftPanel_tagPanelContainer { - flex: 0 0 70px; - height: 100%; -} - -.mx_LeftPanel_hideButton { - position: absolute; - top: 10px; - right: 0px; - padding: 8px; - cursor: pointer; -} - -.mx_LeftPanel { - flex: 1; - overflow-x: hidden; - display: flex; - flex-direction: column; - min-height: 0; -} - -.mx_LeftPanel .mx_AppTile_mini { - height: 132px; -} - -.mx_LeftPanel .mx_RoomList_scrollbar { - order: 1; - - flex: 1 1 0; - - overflow-y: auto; - z-index: 6; -} - -.mx_LeftPanel .mx_BottomLeftMenu { - order: 3; - - border-top: 1px solid $panel-divider-color; - margin-left: 16px; /* gutter */ - margin-right: 16px; /* gutter */ - flex: 0 0 60px; - z-index: 1; -} - -.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu { - flex: 0 0 160px; - margin-bottom: 9px; -} - -.mx_LeftPanel .mx_BottomLeftMenu_options { - margin-top: 18px; -} - -.mx_BottomLeftMenu_options object { - pointer-events: none; -} - -.mx_BottomLeftMenu_options > div { - display: inline-block; -} - -.mx_BottomLeftMenu_options .mx_RoleButton { - margin-left: 0px; - margin-right: 10px; - height: 30px; -} - -.mx_BottomLeftMenu_options .mx_BottomLeftMenu_settings { - float: right; -} - -.mx_BottomLeftMenu_options .mx_BottomLeftMenu_settings .mx_RoleButton { - margin-right: 0px; -} - -.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu_settings { - float: none; -} - -.mx_MatrixChat_useCompactLayout { - .mx_LeftPanel .mx_BottomLeftMenu { - flex: 0 0 50px; - } - - .mx_LeftPanel_container.collapsed .mx_BottomLeftMenu { - flex: 0 0 160px; - } - - .mx_LeftPanel .mx_BottomLeftMenu_options { - margin-top: 12px; - } -} - -.mx_LeftPanel_exploreAndFilterRow { - display: flex; - - .mx_SearchBox { - flex: 1 1 0; - min-width: 0; - margin: 4px 9px 1px 9px; - } -} - -.mx_LeftPanel_explore { - flex: 0 0 50%; - overflow: hidden; - transition: flex-basis 0.2s; - box-sizing: border-box; - - &.mx_LeftPanel_explore_hidden { - flex-basis: 0; - } - - .mx_AccessibleButton { - font-size: $font-14px; - margin: 4px 0 1px 9px; - padding: 9px; - padding-left: 42px; - font-weight: 600; - color: $notice-secondary-color; - position: relative; - border-radius: 4px; - - &:hover { - background-color: $primary-bg-color; - } - - &::before { - cursor: pointer; - mask: url('$(res)/img/explore.svg'); - mask-repeat: no-repeat; - mask-position: center center; - content: ""; - left: 14px; - top: 10px; - width: 16px; - height: 16px; - background-color: $notice-secondary-color; - position: absolute; - } - } -} diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss deleted file mode 100644 index 2c53258b08..0000000000 --- a/res/css/structures/_RoomSubList.scss +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -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. -*/ - -/* a word of explanation about the flex-shrink values employed here: - there are 3 priotized categories of screen real-estate grabbing, - each with a flex-shrink difference of 4 order of magnitude, - so they ideally wouldn't affect each other. - lowest category: .mx_RoomSubList - flex-shrink: 10000000 - distribute size of items within the same category by their size - middle category: .mx_RoomSubList.resized-sized - flex-shrink: 1000 - applied when using the resizer, will have a max-height set to it, - to limit the size - highest category: .mx_RoomSubList.resized-all - flex-shrink: 1 - small flex-shrink value (1), is only added if you can drag the resizer so far - so in practice you can only assign this category if there is enough space. -*/ - -.mx_RoomSubList { - display: flex; - flex-direction: column; -} - - -.mx_RoomSubList_nonEmpty .mx_AutoHideScrollbar_offset { - padding-bottom: 4px; -} - -.mx_RoomSubList_labelContainer { - display: flex; - flex-direction: row; - align-items: center; - flex: 0 0 auto; - margin: 0 8px; - padding: 0 8px; - height: 36px; -} - -.mx_RoomSubList_labelContainer.focus-visible:focus-within { - background-color: $roomtile-focused-bg-color; -} - -.mx_RoomSubList_label { - flex: 1; - cursor: pointer; - display: flex; - align-items: center; - padding: 0 6px; -} - -.mx_RoomSubList_label > span { - flex: 1 1 auto; - text-transform: uppercase; - color: $roomsublist-label-fg-color; - font-weight: 700; - font-size: $font-12px; - margin-left: 8px; -} - -.mx_RoomSubList_badge > div { - flex: 0 0 auto; - border-radius: $font-16px; - font-weight: 600; - font-size: $font-12px; - padding: 0 5px; - color: $roomtile-badge-fg-color; - background-color: $roomtile-name-color; - cursor: pointer; -} - -.mx_RoomSubList_addRoom, .mx_RoomSubList_badge { - margin-left: 7px; -} - -.mx_RoomSubList_addRoom { - background-color: $roomheader-addroom-bg-color; - border-radius: 10px; // 16/2 + 2 padding - height: 16px; - flex: 0 0 16px; - position: relative; - - &::before { - background-color: $roomheader-addroom-fg-color; - mask: url('$(res)/img/icons-room-add.svg'); - mask-repeat: no-repeat; - mask-position: center; - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - } -} - -.mx_RoomSubList_badgeHighlight > div { - color: $accent-fg-color; - background-color: $warning-color; -} - -.mx_RoomSubList_chevron { - pointer-events: none; - mask: url('$(res)/img/feather-customised/dropdown-arrow.svg'); - mask-repeat: no-repeat; - transition: transform 0.2s ease-in; - width: 10px; - height: 6px; - margin-left: 2px; - background-color: $roomsublist-label-fg-color; -} - -.mx_RoomSubList_chevronDown { - transform: rotateZ(0deg); -} - -.mx_RoomSubList_chevronUp { - transform: rotateZ(180deg); -} - -.mx_RoomSubList_chevronRight { - transform: rotateZ(-90deg); -} - -.mx_RoomSubList_scroll { - /* let rooms list grab as much space as it needs (auto), - potentially overflowing and showing a scrollbar */ - flex: 0 1 auto; - padding: 0 8px; -} - -.collapsed { - .mx_RoomSubList_scroll { - padding: 0; - } - - .mx_RoomSubList_labelContainer { - margin-right: 8px; - margin-left: 2px; - padding: 0; - } - - .mx_RoomSubList_addRoom { - margin-left: 3px; - margin-right: 10px; - } - - .mx_RoomSubList_label > span { - display: none; - } -} - -// overflow indicators -.mx_RoomSubList:not(.resized-all) > .mx_RoomSubList_scroll { - &.mx_IndicatorScrollbar_topOverflow::before { - position: sticky; - content: ""; - top: 0; - left: 0; - right: 0; - height: 8px; - z-index: 100; - display: block; - pointer-events: none; - transition: background-image 0.1s ease-in; - background: linear-gradient(to top, $panel-gradient); - } - - - &.mx_IndicatorScrollbar_topOverflow { - margin-top: -8px; - } -} diff --git a/res/css/structures/_TopLeftMenuButton.scss b/res/css/structures/_TopLeftMenuButton.scss deleted file mode 100644 index 8d2e36bcd6..0000000000 --- a/res/css/structures/_TopLeftMenuButton.scss +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -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. -*/ - -.mx_TopLeftMenuButton { - flex: 0 0 52px; - border-bottom: 1px solid $panel-divider-color; - color: $topleftmenu-color; - background-color: $primary-bg-color; - display: flex; - align-items: center; - min-width: 0; - padding: 0 4px; - overflow: hidden; -} - -.mx_TopLeftMenuButton .mx_BaseAvatar { - margin: 0 7px; -} - -.mx_TopLeftMenuButton_name { - margin: 0 7px; - font-size: $font-18px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - font-weight: 600; -} - -.mx_TopLeftMenuButton_chevron { - margin: 0 7px; - mask: url('$(res)/img/feather-customised/dropdown-arrow.svg'); - mask-repeat: no-repeat; - width: $font-22px; - height: 6px; - background-color: $roomsublist-label-fg-color; -} diff --git a/res/css/views/rooms/_InviteOnlyIcon.scss b/res/css/views/rooms/_InviteOnlyIcon.scss deleted file mode 100644 index b71fd6348d..0000000000 --- a/res/css/views/rooms/_InviteOnlyIcon.scss +++ /dev/null @@ -1,58 +0,0 @@ -/* -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. -*/ - -@define-mixin mx_InviteOnlyIcon { - width: 12px; - height: 12px; - position: relative; - display: block !important; -} - -@define-mixin mx_InviteOnlyIcon_padlock { - background-color: $roomtile-name-color; - mask-image: url("$(res)/img/feather-customised/lock-solid.svg"); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - content: ""; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} - -.mx_InviteOnlyIcon_large { - @mixin mx_InviteOnlyIcon; - margin: 0 4px; - - &::before { - @mixin mx_InviteOnlyIcon_padlock; - width: 12px; - height: 12px; - } -} - -.mx_InviteOnlyIcon_small { - @mixin mx_InviteOnlyIcon; - left: -2px; - - &::before { - @mixin mx_InviteOnlyIcon_padlock; - width: 10px; - height: 10px; - } -} diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss deleted file mode 100644 index 3858d836e6..0000000000 --- a/res/css/views/rooms/_RoomBreadcrumbs.scss +++ /dev/null @@ -1,112 +0,0 @@ -/* -Copyright 2019 New Vector Ltd - -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. -*/ - -.mx_RoomBreadcrumbs { - position: relative; - height: 42px; - padding: 8px; - padding-bottom: 0; - display: flex; - flex-direction: row; - - // repeating circles as empty placeholders - background: - radial-gradient( - circle at center, - $breadcrumb-placeholder-bg-color, - $breadcrumb-placeholder-bg-color 15px, - transparent 16px - ); - background-size: 36px; - background-position: 6px -1px; - background-repeat: repeat-x; - - - // Autohide the scrollbar - overflow-x: hidden; - &:hover { - overflow-x: visible; - } - - .mx_AutoHideScrollbar { - display: flex; - flex-direction: row; - height: 100%; - } - - .mx_RoomBreadcrumbs_crumb { - margin-left: 4px; - height: 32px; - display: inline-block; - transition: transform 0.3s, width 0.3s; - position: relative; - - .mx_RoomTile_badge { - position: absolute; - top: -3px; - right: -4px; - } - - .mx_RoomBreadcrumbs_dmIndicator { - position: absolute; - bottom: 0; - right: -4px; - } - } - - .mx_RoomBreadcrumbs_animate { - margin-left: 0; - width: 32px; - transform: scale(1); - } - - .mx_RoomBreadcrumbs_preAnimate { - width: 0; - transform: scale(0); - } - - .mx_RoomBreadcrumbs_left { - opacity: 0.5; - } - - // Note: we have to manually control the gradient and stuff, but the IndicatorScrollbar - // will deal with left/right positioning for us. Normally we'd use position:sticky on - // a few key elements, however that doesn't work in horizontal scrolling scenarios. - - .mx_IndicatorScrollbar_leftOverflowIndicator, - .mx_IndicatorScrollbar_rightOverflowIndicator { - display: none; - } - - .mx_IndicatorScrollbar_leftOverflowIndicator { - background: linear-gradient(to left, $panel-gradient); - } - - .mx_IndicatorScrollbar_rightOverflowIndicator { - background: linear-gradient(to right, $panel-gradient); - } - - &.mx_IndicatorScrollbar_leftOverflow .mx_IndicatorScrollbar_leftOverflowIndicator, - &.mx_IndicatorScrollbar_rightOverflow .mx_IndicatorScrollbar_rightOverflowIndicator { - position: absolute; - top: 0; - bottom: 0; - width: 15px; - display: block; - pointer-events: none; - z-index: 100; - } -} diff --git a/res/css/views/rooms/_RoomDropTarget.scss b/res/css/views/rooms/_RoomDropTarget.scss deleted file mode 100644 index 2e8145c2c9..0000000000 --- a/res/css/views/rooms/_RoomDropTarget.scss +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -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. -*/ - -.mx_RoomDropTarget_container { - background-color: $secondary-accent-color; - padding-left: 18px; - padding-right: 18px; - padding-top: 8px; - padding-bottom: 7px; -} - -.collapsed .mx_RoomDropTarget_container { - padding-right: 10px; - padding-left: 10px; -} - -.mx_RoomDropTarget { - font-size: $font-13px; - padding-top: 5px; - padding-bottom: 5px; - border: 1px dashed $accent-color; - color: $primary-fg-color; - background-color: $droptarget-bg-color; - border-radius: 4px; -} - - -.mx_RoomDropTarget_label { - position: relative; - margin-top: 3px; - line-height: $font-21px; - z-index: 1; - text-align: center; -} - -.collapsed .mx_RoomDropTarget_avatar { - float: none; -} - -.collapsed .mx_RoomDropTarget_label { - display: none; -} diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss deleted file mode 100644 index c23c19699d..0000000000 --- a/res/css/views/rooms/_RoomList.scss +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd - -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. -*/ - -.mx_RoomList.mx_RoomList2 { - overflow-y: auto; -} - -.mx_RoomList { - /* take up remaining space below TopLeftMenu */ - flex: 1; - min-height: 0; - overflow-y: hidden; -} - -.mx_RoomList .mx_ResizeHandle { - // needed so the z-index takes effect - position: relative; -} - -/* hide resize handles next to collapsed / empty sublists */ -.mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle { - display: none; -} - -.mx_RoomList_expandButton { - margin-left: 8px; - cursor: pointer; - padding-left: 12px; - padding-right: 12px; -} - -.mx_RoomList_emptySubListTip_container { - padding-left: 18px; - padding-right: 18px; - padding-top: 8px; - padding-bottom: 7px; -} - -.mx_RoomList_emptySubListTip { - font-size: $font-13px; - padding: 5px; - border: 1px dashed $accent-color; - color: $primary-fg-color; - background-color: $droptarget-bg-color; - border-radius: 4px; - line-height: $font-16px; -} - -.mx_RoomList_emptySubListTip .mx_RoleButton { - vertical-align: -2px; -} - -.mx_RoomList_headerButtons { - position: absolute; - right: 60px; -} diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss deleted file mode 100644 index 7f93da0bbf..0000000000 --- a/res/css/views/rooms/_RoomTile.scss +++ /dev/null @@ -1,228 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 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. -*/ - -.mx_RoomTile { - display: flex; - flex-direction: row; - align-items: center; - cursor: pointer; - height: 34px; - margin: 0; - padding: 0 8px 0 10px; - position: relative; - - .mx_RoomTile_menuButton { - display: none; - flex: 0 0 16px; - height: 16px; - background-image: url('$(res)/img/icon_context.svg'); - background-repeat: no-repeat; - background-position: center; - } - - .mx_UserOnlineDot { - display: block; - margin-right: 5px; - } -} - -.mx_RoomTile:focus { - filter: none !important; - background-color: $roomtile-focused-bg-color; -} - -.mx_RoomTile_tooltip { - display: inline-block; - position: relative; - top: -54px; - left: -12px; -} - -.mx_RoomTile_nameContainer { - display: flex; - align-items: center; - flex: 1; - vertical-align: middle; - min-width: 0; -} - -.mx_RoomTile_labelContainer { - display: flex; - flex-direction: column; - flex: 1; - min-width: 0; -} - -.mx_RoomTile_subtext { - display: inline-block; - font-size: $font-11px; - padding: 0 0 0 7px; - margin: 0; - overflow: hidden; - white-space: nowrap; - text-overflow: clip; - position: relative; - bottom: 4px; -} - -.mx_RoomTile_avatar_container { - position: relative; - display: flex; -} - -.mx_RoomTile_avatar { - flex: 0; - padding: 4px; - width: 24px; - vertical-align: middle; -} - -.mx_RoomTile_hasSubtext .mx_RoomTile_avatar { - padding-top: 0; - vertical-align: super; -} - -.mx_RoomTile_dm { - display: block; - position: absolute; - bottom: 0; - right: -5px; - z-index: 2; -} - -// Note we match .mx_E2EIcon to make sure this matches more tightly than just -// .mx_E2EIcon on its own -.mx_RoomTile_e2eIcon.mx_E2EIcon { - height: 14px; - width: 14px; - display: block; - position: absolute; - bottom: -2px; - right: -5px; - z-index: 1; - margin: 0; -} - -.mx_RoomTile_name { - font-size: $font-14px; - padding: 0 4px; - color: $roomtile-name-color; - white-space: nowrap; - overflow-x: hidden; - text-overflow: ellipsis; -} - -.mx_RoomTile_badge { - flex: 0 1 content; - border-radius: 0.8em; - padding: 0 0.4em; - color: $roomtile-badge-fg-color; - font-weight: 600; - font-size: $font-12px; -} - -.collapsed { - .mx_RoomTile { - margin: 0 6px; - padding: 0 2px; - position: relative; - justify-content: center; - } - - .mx_RoomTile_name { - display: none; - } - - .mx_RoomTile_badge { - position: absolute; - right: 6px; - top: 0px; - border-radius: 16px; - z-index: 3; - border: 0.18em solid $secondary-accent-color; - } - - .mx_RoomTile_menuButton { - display: none; // no design for this for now - } - .mx_UserOnlineDot { - display: none; // no design for this for now - } -} - -// toggle menuButton and badge on menu displayed -.mx_RoomTile_menuDisplayed, -// or on keyboard focus of room tile -.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:focus-within, -// or on pointer hover -.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover { - .mx_RoomTile_menuButton { - display: block; - } - .mx_UserOnlineDot { - display: none; - } -} - -.mx_RoomTile_unreadNotify .mx_RoomTile_badge, -.mx_RoomTile_badge.mx_RoomTile_badgeUnread { - background-color: $roomtile-name-color; -} - -.mx_RoomTile_highlight .mx_RoomTile_badge, -.mx_RoomTile_badge.mx_RoomTile_badgeRed { - color: $accent-fg-color; - background-color: $warning-color; -} - -.mx_RoomTile_unread, .mx_RoomTile_highlight { - .mx_RoomTile_name { - font-weight: 600; - color: $roomtile-selected-color; - } -} - -.mx_RoomTile_selected { - border-radius: 4px; - background-color: $roomtile-selected-bg-color; -} - -.mx_DNDRoomTile { - transform: none; - transition: transform 0.2s; -} - -.mx_DNDRoomTile_dragging { - transform: scale(1.05, 1.05); -} - -.mx_RoomTile_arrow { - position: absolute; - right: 0px; -} - -.mx_RoomTile.mx_RoomTile_transparent { - background-color: transparent; -} - -.mx_RoomTile.mx_RoomTile_transparent:focus { - background-color: $roomtile-transparent-focused-color; -} - -.mx_GroupInviteTile .mx_RoomTile_name { - flex: 1; -} diff --git a/res/css/views/rooms/_UserOnlineDot.scss b/res/css/views/rooms/_UserOnlineDot.scss deleted file mode 100644 index f9da8648ed..0000000000 --- a/res/css/views/rooms/_UserOnlineDot.scss +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2019 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. -*/ - -.mx_UserOnlineDot { - border-radius: 50%; - background-color: $accent-color; - height: 6px; - width: 6px; - display: inline-block; -} diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js deleted file mode 100644 index bae69b5631..0000000000 --- a/src/components/structures/LeftPanel.js +++ /dev/null @@ -1,305 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 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 React from 'react'; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { Key } from '../../Keyboard'; -import * as sdk from '../../index'; -import dis from '../../dispatcher/dispatcher'; -import * as VectorConferenceHandler from '../../VectorConferenceHandler'; -import SettingsStore from '../../settings/SettingsStore'; -import {_t} from "../../languageHandler"; -import Analytics from "../../Analytics"; -import {Action} from "../../dispatcher/actions"; - - -const LeftPanel = createReactClass({ - displayName: 'LeftPanel', - - // NB. If you add props, don't forget to update - // shouldComponentUpdate! - propTypes: { - collapsed: PropTypes.bool.isRequired, - }, - - getInitialState: function() { - return { - searchFilter: '', - breadcrumbs: false, - }; - }, - - // TODO: [REACT-WARNING] Move this to constructor - UNSAFE_componentWillMount: function() { - this.focusedElement = null; - - this._breadcrumbsWatcherRef = SettingsStore.watchSetting( - "breadcrumbs", null, this._onBreadcrumbsChanged); - this._tagPanelWatcherRef = SettingsStore.watchSetting( - "TagPanel.enableTagPanel", null, () => this.forceUpdate()); - - const useBreadcrumbs = !!SettingsStore.getValue("breadcrumbs"); - Analytics.setBreadcrumbs(useBreadcrumbs); - this.setState({breadcrumbs: useBreadcrumbs}); - }, - - componentWillUnmount: function() { - SettingsStore.unwatchSetting(this._breadcrumbsWatcherRef); - SettingsStore.unwatchSetting(this._tagPanelWatcherRef); - }, - - shouldComponentUpdate: function(nextProps, nextState) { - // MatrixChat will update whenever the user switches - // rooms, but propagating this change all the way down - // the react tree is quite slow, so we cut this off - // here. The RoomTiles listen for the room change - // events themselves to know when to update. - // We just need to update if any of these things change. - if ( - this.props.collapsed !== nextProps.collapsed || - this.props.disabled !== nextProps.disabled - ) { - return true; - } - - if (this.state.searchFilter !== nextState.searchFilter) { - return true; - } - if (this.state.searchExpanded !== nextState.searchExpanded) { - return true; - } - - return false; - }, - - componentDidUpdate(prevProps, prevState) { - if (prevState.breadcrumbs !== this.state.breadcrumbs) { - Analytics.setBreadcrumbs(this.state.breadcrumbs); - } - }, - - _onBreadcrumbsChanged: function(settingName, roomId, level, valueAtLevel, value) { - // Features are only possible at a single level, so we can get away with using valueAtLevel. - // The SettingsStore runs on the same tick as the update, so `value` will be wrong. - this.setState({breadcrumbs: valueAtLevel}); - - // For some reason the setState doesn't trigger a render of the component, so force one. - // Probably has to do with the change happening outside of a change detector cycle. - this.forceUpdate(); - }, - - _onFocus: function(ev) { - this.focusedElement = ev.target; - }, - - _onBlur: function(ev) { - this.focusedElement = null; - }, - - _onFilterKeyDown: function(ev) { - if (!this.focusedElement) return; - - switch (ev.key) { - // On enter of rooms filter select and activate first room if such one exists - case Key.ENTER: { - const firstRoom = ev.target.closest(".mx_LeftPanel").querySelector(".mx_RoomTile"); - if (firstRoom) { - firstRoom.click(); - } - break; - } - } - }, - - _onKeyDown: function(ev) { - if (!this.focusedElement) return; - - switch (ev.key) { - case Key.ARROW_UP: - this._onMoveFocus(ev, true, true); - break; - case Key.ARROW_DOWN: - this._onMoveFocus(ev, false, true); - break; - } - }, - - _onMoveFocus: function(ev, up, trap) { - let element = this.focusedElement; - - // unclear why this isn't needed - // var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending; - // this.focusDirection = up; - - let descending = false; // are we currently descending or ascending through the DOM tree? - let classes; - - do { - const child = up ? element.lastElementChild : element.firstElementChild; - const sibling = up ? element.previousElementSibling : element.nextElementSibling; - - if (descending) { - if (child) { - element = child; - } else if (sibling) { - element = sibling; - } else { - descending = false; - element = element.parentElement; - } - } else { - if (sibling) { - element = sibling; - descending = true; - } else { - element = element.parentElement; - } - } - - if (element) { - classes = element.classList; - } - } while (element && !( - classes.contains("mx_RoomTile") || - classes.contains("mx_RoomSubList_label") || - classes.contains("mx_LeftPanel_filterRooms"))); - - if (element) { - ev.stopPropagation(); - ev.preventDefault(); - element.focus(); - this.focusedElement = element; - } else if (trap) { - // if navigation is via up/down arrow-keys, trap in the widget so it doesn't send to composer - ev.stopPropagation(); - ev.preventDefault(); - } - }, - - onSearch: function(term) { - this.setState({ searchFilter: term }); - }, - - onSearchCleared: function(source) { - if (source === "keyboard") { - dis.fire(Action.FocusComposer); - } - this.setState({searchExpanded: false}); - }, - - collectRoomList: function(ref) { - this._roomList = ref; - }, - - _onSearchFocus: function() { - this.setState({searchExpanded: true}); - }, - - _onSearchBlur: function(event) { - if (event.target.value.length === 0) { - this.setState({searchExpanded: false}); - } - }, - - render: function() { - const RoomList = sdk.getComponent('rooms.RoomList'); - const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs'); - const TagPanel = sdk.getComponent('structures.TagPanel'); - const CustomRoomTagPanel = sdk.getComponent('structures.CustomRoomTagPanel'); - const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton'); - const SearchBox = sdk.getComponent('structures.SearchBox'); - const CallPreview = sdk.getComponent('voip.CallPreview'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - - const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel"); - let tagPanelContainer; - - const isCustomTagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags"); - - if (tagPanelEnabled) { - tagPanelContainer = (
- - { isCustomTagsEnabled ? : undefined } -
); - } - - const containerClasses = classNames( - "mx_LeftPanel_container", "mx_fadable", - { - "collapsed": this.props.collapsed, - "mx_LeftPanel_container_hasTagPanel": tagPanelEnabled, - "mx_fadable_faded": this.props.disabled, - }, - ); - - let exploreButton; - if (!this.props.collapsed) { - exploreButton = ( -
- dis.fire(Action.ViewRoomDirectory)}>{_t("Explore")} -
- ); - } - - const searchBox = (); - - let breadcrumbs; - if (this.state.breadcrumbs) { - breadcrumbs = (); - } - - const roomList = ; - - return ( -
- { tagPanelContainer } - -
- ); - }, -}); - -export default LeftPanel; diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js deleted file mode 100644 index 090f3de22a..0000000000 --- a/src/components/structures/RoomSubList.js +++ /dev/null @@ -1,496 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018, 2019 New Vector Ltd -Copyright 2019 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 React, {createRef} from 'react'; -import classNames from 'classnames'; -import * as sdk from '../../index'; -import dis from '../../dispatcher/dispatcher'; -import * as Unread from '../../Unread'; -import * as RoomNotifs from '../../RoomNotifs'; -import * as FormattingUtils from '../../utils/FormattingUtils'; -import IndicatorScrollbar from './IndicatorScrollbar'; -import {Key} from '../../Keyboard'; -import { Group } from 'matrix-js-sdk'; -import PropTypes from 'prop-types'; -import RoomTile from "../views/rooms/RoomTile"; -import LazyRenderList from "../views/elements/LazyRenderList"; -import {_t} from "../../languageHandler"; -import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex"; -import {toPx} from "../../utils/units"; - -// turn this on for drop & drag console debugging galore -const debug = false; - -class RoomTileErrorBoundary extends React.PureComponent { - constructor(props) { - super(props); - - this.state = { - error: null, - }; - } - - static getDerivedStateFromError(error) { - // Side effects are not permitted here, so we only update the state so - // that the next render shows an error message. - return { error }; - } - - componentDidCatch(error, { componentStack }) { - // Browser consoles are better at formatting output when native errors are passed - // in their own `console.error` invocation. - console.error(error); - console.error( - "The above error occured while React was rendering the following components:", - componentStack, - ); - } - - render() { - if (this.state.error) { - return (
- {this.props.roomId} -
); - } else { - return this.props.children; - } - } -} - -export default class RoomSubList extends React.PureComponent { - static displayName = 'RoomSubList'; - static debug = debug; - - static propTypes = { - list: PropTypes.arrayOf(PropTypes.object).isRequired, - label: PropTypes.string.isRequired, - tagName: PropTypes.string, - addRoomLabel: PropTypes.string, - - // passed through to RoomTile and used to highlight room with `!` regardless of notifications count - isInvite: PropTypes.bool, - - startAsHidden: PropTypes.bool, - showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded - collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed? - onHeaderClick: PropTypes.func, - incomingCall: PropTypes.object, - extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles - forceExpand: PropTypes.bool, - }; - - static defaultProps = { - onHeaderClick: function() { - }, // NOP - extraTiles: [], - isInvite: false, - }; - - static getDerivedStateFromProps(props, state) { - return { - listLength: props.list.length, - scrollTop: props.list.length === state.listLength ? state.scrollTop : 0, - }; - } - - constructor(props) { - super(props); - - this.state = { - hidden: this.props.startAsHidden || false, - // some values to get LazyRenderList starting - scrollerHeight: 800, - scrollTop: 0, - // React 16's getDerivedStateFromProps(props, state) doesn't give the previous props so - // we have to store the length of the list here so we can see if it's changed or not... - listLength: null, - }; - - this._header = createRef(); - this._subList = createRef(); - this._scroller = createRef(); - this._headerButton = createRef(); - } - - componentDidMount() { - this.dispatcherRef = dis.register(this.onAction); - } - - componentWillUnmount() { - dis.unregister(this.dispatcherRef); - } - - // The header is collapsible if it is hidden or not stuck - // The dataset elements are added in the RoomList _initAndPositionStickyHeaders method - isCollapsibleOnClick() { - const stuck = this._header.current.dataset.stuck; - if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) { - return true; - } else { - return false; - } - } - - onAction = (payload) => { - switch (payload.action) { - case 'on_room_read': - // XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched, - // but this is no longer true, so we must do it here (and can apply the small - // optimisation of checking that we care about the room being read). - // - // Ultimately we need to transition to a state pushing flow where something - // explicitly notifies the components concerned that the notif count for a room - // has change (e.g. a Flux store). - if (this.props.list.some((r) => r.roomId === payload.roomId)) { - this.forceUpdate(); - } - break; - - case 'view_room': - if (this.state.hidden && !this.props.forceExpand && payload.show_room_tile && - this.props.list.some((r) => r.roomId === payload.room_id) - ) { - this.toggle(); - } - } - }; - - toggle = () => { - if (this.isCollapsibleOnClick()) { - // The header isCollapsible, so the click is to be interpreted as collapse and truncation logic - const isHidden = !this.state.hidden; - this.setState({hidden: isHidden}, () => { - this.props.onHeaderClick(isHidden); - }); - } else { - // The header is stuck, so the click is to be interpreted as a scroll to the header - this.props.onHeaderClick(this.state.hidden, this._header.current.dataset.originalPosition); - } - }; - - onClick = (ev) => { - this.toggle(); - }; - - onHeaderKeyDown = (ev) => { - switch (ev.key) { - case Key.ARROW_LEFT: - // On ARROW_LEFT collapse the room sublist - if (!this.state.hidden && !this.props.forceExpand) { - this.onClick(); - } - ev.stopPropagation(); - break; - case Key.ARROW_RIGHT: { - ev.stopPropagation(); - if (this.state.hidden && !this.props.forceExpand) { - // sublist is collapsed, expand it - this.onClick(); - } else if (!this.props.forceExpand) { - // sublist is expanded, go to first room - const element = this._subList.current && this._subList.current.querySelector(".mx_RoomTile"); - if (element) { - element.focus(); - } - } - break; - } - } - }; - - onKeyDown = (ev) => { - switch (ev.key) { - // On ARROW_LEFT go to the sublist header - case Key.ARROW_LEFT: - ev.stopPropagation(); - this._headerButton.current.focus(); - break; - // Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer - case Key.ARROW_RIGHT: - ev.stopPropagation(); - } - }; - - onRoomTileClick = (roomId, ev) => { - dis.dispatch({ - action: 'view_room', - show_room_tile: true, // to make sure the room gets scrolled into view - room_id: roomId, - clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)), - }); - }; - - _updateSubListCount = () => { - // Force an update by setting the state to the current state - // Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate() - // method is honoured - this.setState(this.state); - }; - - makeRoomTile = (room) => { - return 0} - notificationCount={RoomNotifs.getUnreadNotificationCount(room)} - isInvite={this.props.isInvite} - refreshSubList={this._updateSubListCount} - incomingCall={null} - onClick={this.onRoomTileClick} - />; - }; - - _onNotifBadgeClick = (e) => { - // prevent the roomsublist collapsing - e.preventDefault(); - e.stopPropagation(); - const room = this.props.list.find(room => RoomNotifs.getRoomHasBadge(room)); - if (room) { - dis.dispatch({ - action: 'view_room', - room_id: room.roomId, - }); - } - }; - - _onInviteBadgeClick = (e) => { - // prevent the roomsublist collapsing - e.preventDefault(); - e.stopPropagation(); - // switch to first room in sortedList as that'll be the top of the list for the user - if (this.props.list && this.props.list.length > 0) { - dis.dispatch({ - action: 'view_room', - room_id: this.props.list[0].roomId, - }); - } else if (this.props.extraTiles && this.props.extraTiles.length > 0) { - // Group Invites are different in that they are all extra tiles and not rooms - // XXX: this is a horrible special case because Group Invite sublist is a hack - if (this.props.extraTiles[0].props && this.props.extraTiles[0].props.group instanceof Group) { - dis.dispatch({ - action: 'view_group', - group_id: this.props.extraTiles[0].props.group.groupId, - }); - } - } - }; - - onAddRoom = (e) => { - e.stopPropagation(); - if (this.props.onAddRoom) this.props.onAddRoom(); - }; - - _getHeaderJsx(isCollapsed) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton'); - const subListNotifications = !this.props.isInvite ? - RoomNotifs.aggregateNotificationCount(this.props.list) : - {count: 0, highlight: true}; - const subListNotifCount = subListNotifications.count; - const subListNotifHighlight = subListNotifications.highlight; - - // When collapsed, allow a long hover on the header to show user - // the full tag name and room count - let title; - if (this.props.collapsed) { - title = this.props.label; - } - - let incomingCall; - if (this.props.incomingCall) { - // We can assume that if we have an incoming call then it is for this list - const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); - incomingCall = - ; - } - - const len = this.props.list.length + this.props.extraTiles.length; - let chevron; - if (len) { - const chevronClasses = classNames({ - 'mx_RoomSubList_chevron': true, - 'mx_RoomSubList_chevronRight': isCollapsed, - 'mx_RoomSubList_chevronDown': !isCollapsed, - }); - chevron = (
); - } - - return - {({onFocus, isActive, ref}) => { - const tabIndex = isActive ? 0 : -1; - - let badge; - if (!this.props.collapsed) { - const badgeClasses = classNames({ - 'mx_RoomSubList_badge': true, - 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, - }); - // Wrap the contents in a div and apply styles to the child div so that the browser default outline works - if (subListNotifCount > 0) { - badge = ( - -
- { FormattingUtils.formatCount(subListNotifCount) } -
-
- ); - } else if (this.props.isInvite && this.props.list.length) { - // no notifications but highlight anyway because this is an invite badge - badge = ( - -
- { this.props.list.length } -
-
- ); - } - } - - let addRoomButton; - if (this.props.onAddRoom) { - addRoomButton = ( - - ); - } - - return ( -
- - { chevron } - {this.props.label} - { incomingCall } - - { badge } - { addRoomButton } -
- ); - } } -
; - } - - checkOverflow = () => { - if (this._scroller.current) { - this._scroller.current.checkOverflow(); - } - }; - - setHeight = (height) => { - if (this._subList.current) { - this._subList.current.style.height = toPx(height); - } - this._updateLazyRenderHeight(height); - }; - - _updateLazyRenderHeight(height) { - this.setState({scrollerHeight: height}); - } - - _onScroll = () => { - this.setState({scrollTop: this._scroller.current.getScrollTop()}); - }; - - _canUseLazyListRendering() { - // for now disable lazy rendering as they are already rendered tiles - // not rooms like props.list we pass to LazyRenderList - return !this.props.extraTiles || !this.props.extraTiles.length; - } - - render() { - const len = this.props.list.length + this.props.extraTiles.length; - const isCollapsed = this.state.hidden && !this.props.forceExpand; - - const subListClasses = classNames({ - "mx_RoomSubList": true, - "mx_RoomSubList_hidden": len && isCollapsed, - "mx_RoomSubList_nonEmpty": len && !isCollapsed, - }); - - let content; - if (len) { - if (isCollapsed) { - // no body - } else if (this._canUseLazyListRendering()) { - content = ( - - - - ); - } else { - const roomTiles = this.props.list.map(r => this.makeRoomTile(r)); - const tiles = roomTiles.concat(this.props.extraTiles); - content = ( - - { tiles } - - ); - } - } else { - if (this.props.showSpinner && !isCollapsed) { - const Loader = sdk.getComponent("elements.Spinner"); - content = ; - } - } - - return ( -
- { this._getHeaderJsx(isCollapsed) } - { content } -
- ); - } -} diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js deleted file mode 100644 index 71e7e61406..0000000000 --- a/src/components/structures/TopLeftMenuButton.js +++ /dev/null @@ -1,158 +0,0 @@ -/* -Copyright 2018 New Vector Ltd -Copyright 2019 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 React from 'react'; -import PropTypes from 'prop-types'; -import TopLeftMenu from '../views/context_menus/TopLeftMenu'; -import BaseAvatar from '../views/avatars/BaseAvatar'; -import {MatrixClientPeg} from '../../MatrixClientPeg'; -import * as Avatar from '../../Avatar'; -import { _t } from '../../languageHandler'; -import dis from "../../dispatcher/dispatcher"; -import {ContextMenu, ContextMenuButton} from "./ContextMenu"; -import {Action} from "../../dispatcher/actions"; - -const AVATAR_SIZE = 28; - -export default class TopLeftMenuButton extends React.Component { - static propTypes = { - collapsed: PropTypes.bool.isRequired, - }; - - static displayName = 'TopLeftMenuButton'; - - constructor() { - super(); - this.state = { - menuDisplayed: false, - profileInfo: null, - }; - } - - async _getProfileInfo() { - const cli = MatrixClientPeg.get(); - const userId = cli.getUserId(); - const profileInfo = await cli.getProfileInfo(userId); - const avatarUrl = Avatar.avatarUrlForUser( - {avatarUrl: profileInfo.avatar_url}, - AVATAR_SIZE, AVATAR_SIZE, "crop"); - - return { - userId, - name: profileInfo.displayname, - avatarUrl, - }; - } - - async componentDidMount() { - this._dispatcherRef = dis.register(this.onAction); - - try { - const profileInfo = await this._getProfileInfo(); - this.setState({profileInfo}); - } catch (ex) { - console.log("could not fetch profile"); - console.error(ex); - } - } - - componentWillUnmount() { - dis.unregister(this._dispatcherRef); - } - - onAction = (payload) => { - // For accessibility - if (payload.action === Action.ToggleUserMenu) { - if (this._buttonRef) this._buttonRef.click(); - } - }; - - _getDisplayName() { - if (MatrixClientPeg.get().isGuest()) { - return _t("Guest"); - } else if (this.state.profileInfo) { - return this.state.profileInfo.name; - } else { - return MatrixClientPeg.get().getUserId(); - } - } - - openMenu = (e) => { - e.preventDefault(); - e.stopPropagation(); - this.setState({ menuDisplayed: true }); - }; - - closeMenu = () => { - this.setState({ - menuDisplayed: false, - }); - }; - - render() { - const cli = MatrixClientPeg.get().getUserId(); - - const name = this._getDisplayName(); - let nameElement; - let chevronElement; - if (!this.props.collapsed) { - nameElement =
- { name } -
; - chevronElement = ; - } - - let contextMenu; - if (this.state.menuDisplayed) { - const elementRect = this._buttonRef.getBoundingClientRect(); - - contextMenu = ( - - - - ); - } - - return - this._buttonRef = r} - label={_t("Your profile")} - isExpanded={this.state.menuDisplayed} - > - - { nameElement } - { chevronElement } - - - { contextMenu } - ; - } -} diff --git a/src/components/views/create_room/CreateRoomButton.js b/src/components/views/create_room/CreateRoomButton.js deleted file mode 100644 index adf3972eff..0000000000 --- a/src/components/views/create_room/CreateRoomButton.js +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 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 React from 'react'; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import { _t } from '../../../languageHandler'; - -export default createReactClass({ - displayName: 'CreateRoomButton', - propTypes: { - onCreateRoom: PropTypes.func, - }, - - getDefaultProps: function() { - return { - onCreateRoom: function() {}, - }; - }, - - onClick: function() { - this.props.onCreateRoom(); - }, - - render: function() { - return ( - - ); - }, -}); diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js deleted file mode 100644 index 1410bdabdb..0000000000 --- a/src/components/views/elements/CreateRoomButton.js +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -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 React from 'react'; -import * as sdk from '../../../index'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; - -const CreateRoomButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -CreateRoomButton.propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, -}; - -export default CreateRoomButton; diff --git a/src/components/views/rooms/InviteOnlyIcon.js b/src/components/views/rooms/InviteOnlyIcon.js deleted file mode 100644 index b02f9843d9..0000000000 --- a/src/components/views/rooms/InviteOnlyIcon.js +++ /dev/null @@ -1,53 +0,0 @@ -/* -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 React from 'react'; -import { _t } from '../../../languageHandler'; -import * as sdk from '../../../index'; - -export default class InviteOnlyIcon extends React.Component { - constructor() { - super(); - - this.state = { - hover: false, - }; - } - - onHoverStart = () => { - this.setState({hover: true}); - }; - - onHoverEnd = () => { - this.setState({hover: false}); - }; - - render() { - const classes = this.props.collapsedPanel ? "mx_InviteOnlyIcon_small": "mx_InviteOnlyIcon_large"; - - const Tooltip = sdk.getComponent("elements.Tooltip"); - let tooltip; - if (this.state.hover) { - tooltip = ; - } - return (
- { tooltip } -
); - } -} diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js deleted file mode 100644 index fe443d720f..0000000000 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ /dev/null @@ -1,394 +0,0 @@ -/* -Copyright 2019 New Vector Ltd - -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 React, {createRef} from "react"; -import dis from "../../../dispatcher/dispatcher"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; -import AccessibleButton from '../elements/AccessibleButton'; -import RoomAvatar from '../avatars/RoomAvatar'; -import classNames from 'classnames'; -import * as sdk from "../../../index"; -import Analytics from "../../../Analytics"; -import * as RoomNotifs from '../../../RoomNotifs'; -import * as FormattingUtils from "../../../utils/FormattingUtils"; -import DMRoomMap from "../../../utils/DMRoomMap"; -import {_t} from "../../../languageHandler"; - -const MAX_ROOMS = 20; -const MIN_ROOMS_BEFORE_ENABLED = 10; - -// The threshold time in milliseconds to wait for an autojoined room to show up. -const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90 seconds - -export default class RoomBreadcrumbs extends React.Component { - constructor(props) { - super(props); - this.state = {rooms: [], enabled: false}; - - this.onAction = this.onAction.bind(this); - this._dispatcherRef = null; - - // The room IDs we're waiting to come down the Room handler and when we - // started waiting for them. Used to track a room over an upgrade/autojoin. - this._waitingRoomQueue = [/* { roomId, addedTs } */]; - - this._scroller = createRef(); - } - - // TODO: [REACT-WARNING] Move this to constructor - UNSAFE_componentWillMount() { // eslint-disable-line camelcase - this._dispatcherRef = dis.register(this.onAction); - - const storedRooms = SettingsStore.getValue("breadcrumb_rooms"); - this._loadRoomIds(storedRooms || []); - - this._settingWatchRef = SettingsStore.watchSetting("breadcrumb_rooms", null, this.onBreadcrumbsChanged); - - this.setState({enabled: this._shouldEnable()}); - - MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); - MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); - MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted); - MatrixClientPeg.get().on("Room", this.onRoom); - } - - componentWillUnmount() { - dis.unregister(this._dispatcherRef); - - SettingsStore.unwatchSetting(this._settingWatchRef); - - const client = MatrixClientPeg.get(); - if (client) { - client.removeListener("Room.myMembership", this.onMyMembership); - client.removeListener("Room.receipt", this.onRoomReceipt); - client.removeListener("Room.timeline", this.onRoomTimeline); - client.removeListener("Event.decrypted", this.onEventDecrypted); - client.removeListener("Room", this.onRoom); - } - } - - componentDidUpdate() { - const rooms = this.state.rooms.slice(); - - if (rooms.length) { - const roomModel = rooms[0]; - if (!roomModel.animated) { - roomModel.animated = true; - setTimeout(() => this.setState({rooms}), 0); - } - } - } - - onAction(payload) { - switch (payload.action) { - case 'view_room': - if (payload.auto_join && !MatrixClientPeg.get().getRoom(payload.room_id)) { - // Queue the room instead of pushing it immediately - we're probably just waiting - // for a join to complete (ie: joining the upgraded room). - this._waitingRoomQueue.push({roomId: payload.room_id, addedTs: (new Date).getTime()}); - break; - } - this._appendRoomId(payload.room_id); - break; - - // XXX: slight hack in order to zero the notification count when a room - // is read. Copied from RoomTile - case 'on_room_read': { - const room = MatrixClientPeg.get().getRoom(payload.roomId); - this._calculateRoomBadges(room, /*zero=*/true); - break; - } - } - } - - onMyMembership = (room, membership) => { - if (membership === "leave" || membership === "ban") { - const rooms = this.state.rooms.slice(); - const roomState = rooms.find((r) => r.room.roomId === room.roomId); - if (roomState) { - roomState.left = true; - this.setState({rooms}); - } - } - this.onRoomMembershipChanged(); - }; - - onRoomReceipt = (event, room) => { - if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) { - this._calculateRoomBadges(room); - } - }; - - onRoomTimeline = (event, room) => { - if (!room) return; // Can be null for the notification timeline, etc. - if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) { - this._calculateRoomBadges(room); - } - }; - - onEventDecrypted = (event) => { - if (this.state.rooms.map(r => r.room.roomId).includes(event.getRoomId())) { - this._calculateRoomBadges(MatrixClientPeg.get().getRoom(event.getRoomId())); - } - }; - - onBreadcrumbsChanged = (settingName, roomId, level, valueAtLevel, value) => { - if (!value) return; - - const currentState = this.state.rooms.map((r) => r.room.roomId); - if (currentState.length === value.length) { - let changed = false; - for (let i = 0; i < currentState.length; i++) { - if (currentState[i] !== value[i]) { - changed = true; - break; - } - } - if (!changed) return; - } - - this._loadRoomIds(value); - }; - - onRoomMembershipChanged = () => { - if (!this.state.enabled && this._shouldEnable()) { - this.setState({enabled: true}); - } - }; - - onRoom = (room) => { - // Always check for membership changes when we see new rooms - this.onRoomMembershipChanged(); - - const waitingRoom = this._waitingRoomQueue.find(r => r.roomId === room.roomId); - if (!waitingRoom) return; - this._waitingRoomQueue.splice(this._waitingRoomQueue.indexOf(waitingRoom), 1); - - const now = (new Date()).getTime(); - if ((now - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago. - this._appendRoomId(room.roomId); // add the room we've been waiting for - }; - - _shouldEnable() { - const client = MatrixClientPeg.get(); - const joinedRoomCount = client.getRooms().reduce((count, r) => { - return count + (r.getMyMembership() === "join" ? 1 : 0); - }, 0); - return joinedRoomCount >= MIN_ROOMS_BEFORE_ENABLED; - } - - _loadRoomIds(roomIds) { - if (!roomIds || roomIds.length <= 0) return; // Skip updates with no rooms - - // If we're here, the list changed. - const rooms = roomIds.map((r) => MatrixClientPeg.get().getRoom(r)).filter((r) => r).map((r) => { - const badges = this._calculateBadgesForRoom(r) || {}; - return { - room: r, - animated: false, - ...badges, - }; - }); - this.setState({ - rooms: rooms, - }); - } - - _calculateBadgesForRoom(room, zero=false) { - if (!room) return null; - - // Reset the notification variables for simplicity - const roomModel = { - redBadge: false, - formattedCount: "0", - showCount: false, - }; - - if (zero) return roomModel; - - const notifState = RoomNotifs.getRoomNotifsState(room.roomId); - if (RoomNotifs.MENTION_BADGE_STATES.includes(notifState)) { - const highlightNotifs = RoomNotifs.getUnreadNotificationCount(room, 'highlight'); - const unreadNotifs = RoomNotifs.getUnreadNotificationCount(room); - - const redBadge = highlightNotifs > 0; - const greyBadge = redBadge || (unreadNotifs > 0 && RoomNotifs.BADGE_STATES.includes(notifState)); - - if (redBadge || greyBadge) { - const notifCount = redBadge ? highlightNotifs : unreadNotifs; - const limitedCount = FormattingUtils.formatCount(notifCount); - - roomModel.redBadge = redBadge; - roomModel.formattedCount = limitedCount; - roomModel.showCount = true; - } - } - - return roomModel; - } - - _calculateRoomBadges(room, zero=false) { - if (!room) return; - - const rooms = this.state.rooms.slice(); - const roomModel = rooms.find((r) => r.room.roomId === room.roomId); - if (!roomModel) return; // No applicable room, so don't do math on it - - const badges = this._calculateBadgesForRoom(room, zero); - if (!badges) return; // No badges for some reason - - Object.assign(roomModel, badges); - this.setState({rooms}); - } - - _appendRoomId(roomId) { - let room = MatrixClientPeg.get().getRoom(roomId); - if (!room) return; - - const rooms = this.state.rooms.slice(); - - // If the room is upgraded, use that room instead. We'll also splice out - // any children of the room. - const history = MatrixClientPeg.get().getRoomUpgradeHistory(roomId); - if (history.length > 1) { - room = history[history.length - 1]; // Last room is most recent - - // Take out any room that isn't the most recent room - for (let i = 0; i < history.length - 1; i++) { - const idx = rooms.findIndex((r) => r.room.roomId === history[i].roomId); - if (idx !== -1) rooms.splice(idx, 1); - } - } - - const existingIdx = rooms.findIndex((r) => r.room.roomId === room.roomId); - if (existingIdx !== -1) { - rooms.splice(existingIdx, 1); - } - - rooms.splice(0, 0, {room, animated: false}); - - if (rooms.length > MAX_ROOMS) { - rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS); - } - this.setState({rooms}); - - if (this._scroller.current) { - this._scroller.current.moveToOrigin(); - } - - // We don't track room aesthetics (badges, membership, etc) over the wire so we - // don't need to do this elsewhere in the file. Just where we alter the room IDs - // and their order. - const roomIds = rooms.map((r) => r.room.roomId); - if (roomIds.length > 0) { - SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds); - } - } - - _viewRoom(room, index) { - Analytics.trackEvent("Breadcrumbs", "click_node", index); - dis.dispatch({action: "view_room", room_id: room.roomId}); - } - - _onMouseEnter(room) { - this._onHover(room); - } - - _onMouseLeave(room) { - this._onHover(null); // clear hover states - } - - _onHover(room) { - const rooms = this.state.rooms.slice(); - for (const r of rooms) { - r.hover = room && r.room.roomId === room.roomId; - } - this.setState({rooms}); - } - - _isDmRoom(room) { - const dmRooms = DMRoomMap.shared().getUserIdForRoomId(room.roomId); - return Boolean(dmRooms); - } - - render() { - const Tooltip = sdk.getComponent('elements.Tooltip'); - const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar'); - - // check for collapsed here and not at parent so we keep rooms in our state - // when collapsing and expanding - if (this.props.collapsed || !this.state.enabled) { - return null; - } - - const rooms = this.state.rooms; - const avatars = rooms.map((r, i) => { - const isFirst = i === 0; - const classes = classNames({ - "mx_RoomBreadcrumbs_crumb": true, - "mx_RoomBreadcrumbs_preAnimate": isFirst && !r.animated, - "mx_RoomBreadcrumbs_animate": isFirst, - "mx_RoomBreadcrumbs_left": r.left, - }); - - let tooltip = null; - if (r.hover) { - tooltip = ; - } - - let badge; - if (r.showCount) { - const badgeClasses = classNames({ - 'mx_RoomTile_badge': true, - 'mx_RoomTile_badgeButton': true, - 'mx_RoomTile_badgeRed': r.redBadge, - 'mx_RoomTile_badgeUnread': !r.redBadge, - }); - - badge =
{r.formattedCount}
; - } - - return ( - this._viewRoom(r.room, i)} - onMouseEnter={() => this._onMouseEnter(r.room)} - onMouseLeave={() => this._onMouseLeave(r.room)} - aria-label={_t("Room %(name)s", {name: r.room.name})} - > - - {badge} - {tooltip} - - ); - }); - return ( -
- - { avatars } - -
- ); - } -} diff --git a/src/components/views/rooms/RoomDropTarget.js b/src/components/views/rooms/RoomDropTarget.js deleted file mode 100644 index 61b7ca6d59..0000000000 --- a/src/components/views/rooms/RoomDropTarget.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 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 React from 'react'; -import createReactClass from 'create-react-class'; - -export default createReactClass({ - displayName: 'RoomDropTarget', - - render: function() { - return ( -
-
-
- { this.props.label } -
-
-
- ); - }, -}); diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js deleted file mode 100644 index dee4015003..0000000000 --- a/src/components/views/rooms/RoomList.js +++ /dev/null @@ -1,838 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017, 2018 Vector Creations Ltd -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 from "../../../settings/SettingsStore"; -import Timer from "../../../utils/Timer"; -import React from "react"; -import ReactDOM from "react-dom"; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import * as utils from "matrix-js-sdk/src/utils"; -import { _t } from '../../../languageHandler'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import rate_limited_func from "../../../ratelimitedfunc"; -import * as Rooms from '../../../Rooms'; -import DMRoomMap from '../../../utils/DMRoomMap'; -import TagOrderStore from '../../../stores/TagOrderStore'; -import CustomRoomTagStore from '../../../stores/CustomRoomTagStore'; -import GroupStore from '../../../stores/GroupStore'; -import RoomSubList from '../../structures/RoomSubList'; -import ResizeHandle from '../elements/ResizeHandle'; -import CallHandler from "../../../CallHandler"; -import dis from "../../../dispatcher/dispatcher"; -import * as sdk from "../../../index"; -import * as Receipt from "../../../utils/Receipt"; -import {Resizer} from '../../../resizer'; -import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; -import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex"; -import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; -import {DefaultTagID} from "../../../stores/room-list/models"; -import * as Unread from "../../../Unread"; -import RoomViewStore from "../../../stores/RoomViewStore"; -import {TAG_DM} from "../../../stores/RoomListStore"; - -const HIDE_CONFERENCE_CHANS = true; -const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; -const HOVER_MOVE_TIMEOUT = 1000; - -function labelForTagName(tagName) { - if (tagName.startsWith('u.')) return tagName.slice(2); - return tagName; -} - -export default createReactClass({ - displayName: 'RoomList', - - propTypes: { - ConferenceHandler: PropTypes.any, - collapsed: PropTypes.bool.isRequired, - searchFilter: PropTypes.string, - }, - - getInitialState: function() { - - this._hoverClearTimer = null; - this._subListRefs = { - // key => RoomSubList ref - }; - - const sizesJson = window.localStorage.getItem("mx_roomlist_sizes"); - const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed"); - this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {}; - this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {}; - this._layoutSections = []; - - const unfilteredOptions = { - allowWhitespace: false, - handleHeight: 1, - }; - this._unfilteredlayout = new Layout((key, size) => { - const subList = this._subListRefs[key]; - if (subList) { - subList.setHeight(size); - } - // update overflow indicators - this._checkSubListsOverflow(); - // don't store height for collapsed sublists - if (!this.collapsedState[key]) { - this.subListSizes[key] = size; - window.localStorage.setItem("mx_roomlist_sizes", - JSON.stringify(this.subListSizes)); - } - }, this.subListSizes, this.collapsedState, unfilteredOptions); - - this._filteredLayout = new Layout((key, size) => { - const subList = this._subListRefs[key]; - if (subList) { - subList.setHeight(size); - } - }, null, null, { - allowWhitespace: false, - handleHeight: 0, - }); - - this._layout = this._unfilteredlayout; - - return { - isLoadingLeftRooms: false, - totalRoomCount: null, - lists: {}, - incomingCallTag: null, - incomingCall: null, - selectedTags: [], - hover: false, - customTags: CustomRoomTagStore.getTags(), - }; - }, - - // TODO: [REACT-WARNING] Replace component with real class, put this in the constructor. - UNSAFE_componentWillMount: function() { - this.mounted = false; - - const cli = MatrixClientPeg.get(); - - cli.on("Room", this.onRoom); - cli.on("deleteRoom", this.onDeleteRoom); - cli.on("Room.receipt", this.onRoomReceipt); - cli.on("RoomMember.name", this.onRoomMemberName); - cli.on("Event.decrypted", this.onEventDecrypted); - cli.on("accountData", this.onAccountData); - cli.on("Group.myMembership", this._onGroupMyMembership); - cli.on("RoomState.events", this.onRoomStateEvents); - - const dmRoomMap = DMRoomMap.shared(); - // A map between tags which are group IDs and the room IDs of rooms that should be kept - // in the room list when filtering by that tag. - this._visibleRoomsForGroup = { - // $groupId: [$roomId1, $roomId2, ...], - }; - // All rooms that should be kept in the room list when filtering. - // By default, show all rooms. - this._visibleRooms = MatrixClientPeg.get().getVisibleRooms(); - - // Listen to updates to group data. RoomList cares about members and rooms in order - // to filter the room list when group tags are selected. - this._groupStoreToken = GroupStore.registerListener(null, () => { - (TagOrderStore.getOrderedTags() || []).forEach((tag) => { - if (tag[0] !== '+') { - return; - } - // This group's rooms or members may have updated, update rooms for its tag - this.updateVisibleRoomsForTag(dmRoomMap, tag); - this.updateVisibleRooms(); - }); - }); - - this._tagStoreToken = TagOrderStore.addListener(() => { - // Filters themselves have changed - this.updateVisibleRooms(); - }); - - this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => { - this._delayedRefreshRoomList(); - }); - - - if (SettingsStore.isFeatureEnabled("feature_custom_tags")) { - this._customTagStoreToken = CustomRoomTagStore.addListener(() => { - this.setState({ - customTags: CustomRoomTagStore.getTags(), - }); - }); - } - - this.refreshRoomList(); - - // order of the sublists - //this.listOrder = []; - - // loop count to stop a stack overflow if the user keeps waggling the - // mouse for >30s in a row, or if running under mocha - this._delayedRefreshRoomListLoopCount = 0; - }, - - componentDidMount: function() { - this.dispatcherRef = dis.register(this.onAction); - const cfg = { - getLayout: () => this._layout, - }; - this.resizer = new Resizer(this.resizeContainer, Distributor, cfg); - this.resizer.setClassNames({ - handle: "mx_ResizeHandle", - vertical: "mx_ResizeHandle_vertical", - reverse: "mx_ResizeHandle_reverse", - }); - this._layout.update( - this._layoutSections, - this.resizeContainer && this.resizeContainer.offsetHeight, - ); - this._checkSubListsOverflow(); - - this.resizer.attach(); - if (this.props.resizeNotifier) { - this.props.resizeNotifier.on("leftPanelResized", this.onResize); - } - this.mounted = true; - }, - - componentDidUpdate: function(prevProps) { - let forceLayoutUpdate = false; - this._repositionIncomingCallBox(undefined, false); - if (!this.props.searchFilter && prevProps.searchFilter) { - this._layout = this._unfilteredlayout; - forceLayoutUpdate = true; - } else if (this.props.searchFilter && !prevProps.searchFilter) { - this._layout = this._filteredLayout; - forceLayoutUpdate = true; - } - this._layout.update( - this._layoutSections, - this.resizeContainer && this.resizeContainer.clientHeight, - forceLayoutUpdate, - ); - this._checkSubListsOverflow(); - }, - - onAction: function(payload) { - switch (payload.action) { - case 'call_state': - var call = CallHandler.getCall(payload.room_id); - if (call && call.call_state === 'ringing') { - this.setState({ - incomingCall: call, - incomingCallTag: this.getTagNameForRoomId(payload.room_id), - }); - this._repositionIncomingCallBox(undefined, true); - } else { - this.setState({ - incomingCall: null, - incomingCallTag: null, - }); - } - break; - case 'view_room_delta': { - const currentRoomId = RoomViewStore.getRoomId(); - const { - "im.vector.fake.invite": inviteRooms, - "m.favourite": favouriteRooms, - [TAG_DM]: dmRooms, - "im.vector.fake.recent": recentRooms, - "m.lowpriority": lowPriorityRooms, - "im.vector.fake.archived": historicalRooms, - "m.server_notice": serverNoticeRooms, - ...tags - } = this.state.lists; - - const shownCustomTagRooms = Object.keys(tags).filter(tagName => { - return (!this.state.customTags || this.state.customTags[tagName]) && - !tagName.match(STANDARD_TAGS_REGEX); - }).map(tagName => tags[tagName]); - - // this order matches the one when generating the room sublists below. - let rooms = this._applySearchFilter([ - ...inviteRooms, - ...favouriteRooms, - ...dmRooms, - ...recentRooms, - ...[].concat.apply([], shownCustomTagRooms), // eslint-disable-line prefer-spread - ...lowPriorityRooms, - ...historicalRooms, - ...serverNoticeRooms, - ], this.props.searchFilter); - - if (payload.unread) { - // filter to only notification rooms (and our current active room so we can index properly) - rooms = rooms.filter(room => { - return room.roomId === currentRoomId || Unread.doesRoomHaveUnreadMessages(room); - }); - } - - const currentIndex = rooms.findIndex(room => room.roomId === currentRoomId); - // use slice to account for looping around the start - const [room] = rooms.slice((currentIndex + payload.delta) % rooms.length); - if (room) { - dis.dispatch({ - action: 'view_room', - room_id: room.roomId, - show_room_tile: true, // to make sure the room gets scrolled into view - }); - } - break; - } - } - }, - - componentWillUnmount: function() { - this.mounted = false; - - dis.unregister(this.dispatcherRef); - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("Room", this.onRoom); - MatrixClientPeg.get().removeListener("deleteRoom", this.onDeleteRoom); - MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); - MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); - MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted); - MatrixClientPeg.get().removeListener("accountData", this.onAccountData); - MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership); - MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); - } - - if (this.props.resizeNotifier) { - this.props.resizeNotifier.removeListener("leftPanelResized", this.onResize); - } - - - if (this._tagStoreToken) { - this._tagStoreToken.remove(); - } - - if (this._roomListStoreToken) { - this._roomListStoreToken.remove(); - } - if (this._customTagStoreToken) { - this._customTagStoreToken.remove(); - } - - // NB: GroupStore is not a Flux.Store - if (this._groupStoreToken) { - this._groupStoreToken.unregister(); - } - - // cancel any pending calls to the rate_limited_funcs - this._delayedRefreshRoomList.cancelPendingCall(); - }, - - - onResize: function() { - if (this.mounted && this._layout && this.resizeContainer && - Array.isArray(this._layoutSections) - ) { - this._layout.update( - this._layoutSections, - this.resizeContainer.offsetHeight, - ); - } - }, - - onRoom: function(room) { - this.updateVisibleRooms(); - }, - - onRoomStateEvents: function(ev, state) { - if (ev.getType() === "m.room.create" || ev.getType() === "m.room.tombstone") { - this.updateVisibleRooms(); - } - }, - - onDeleteRoom: function(roomId) { - this.updateVisibleRooms(); - }, - - onArchivedHeaderClick: function(isHidden, scrollToPosition) { - if (!isHidden) { - const self = this; - this.setState({ isLoadingLeftRooms: true }); - // we don't care about the response since it comes down via "Room" - // events. - MatrixClientPeg.get().syncLeftRooms().catch(function(err) { - console.error("Failed to sync left rooms: %s", err); - console.error(err); - }).finally(function() { - self.setState({ isLoadingLeftRooms: false }); - }); - } - }, - - onRoomReceipt: function(receiptEvent, room) { - // because if we read a notification, it will affect notification count - // only bother updating if there's a receipt from us - if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) { - this._delayedRefreshRoomList(); - } - }, - - onRoomMemberName: function(ev, member) { - this._delayedRefreshRoomList(); - }, - - onEventDecrypted: function(ev) { - // An event being decrypted may mean we need to re-order the room list - this._delayedRefreshRoomList(); - }, - - onAccountData: function(ev) { - if (ev.getType() == 'm.direct') { - this._delayedRefreshRoomList(); - } - }, - - _onGroupMyMembership: function(group) { - this.forceUpdate(); - }, - - onMouseMove: async function(ev) { - if (!this._hoverClearTimer) { - this.setState({hover: true}); - this._hoverClearTimer = new Timer(HOVER_MOVE_TIMEOUT); - this._hoverClearTimer.start(); - let finished = true; - try { - await this._hoverClearTimer.finished(); - } catch (err) { - finished = false; - } - this._hoverClearTimer = null; - if (finished) { - this.setState({hover: false}); - this._delayedRefreshRoomList(); - } - } else { - this._hoverClearTimer.restart(); - } - }, - - onMouseLeave: function(ev) { - if (this._hoverClearTimer) { - this._hoverClearTimer.abort(); - this._hoverClearTimer = null; - } - this.setState({hover: false}); - - // Refresh the room list just in case the user missed something. - this._delayedRefreshRoomList(); - }, - - _delayedRefreshRoomList: rate_limited_func(function() { - this.refreshRoomList(); - }, 500), - - // Update which rooms and users should appear in RoomList for a given group tag - updateVisibleRoomsForTag: function(dmRoomMap, tag) { - if (!this.mounted) return; - // For now, only handle group tags - if (tag[0] !== '+') return; - - this._visibleRoomsForGroup[tag] = []; - GroupStore.getGroupRooms(tag).forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId)); - GroupStore.getGroupMembers(tag).forEach((member) => { - if (member.userId === MatrixClientPeg.get().credentials.userId) return; - dmRoomMap.getDMRoomsForUserId(member.userId).forEach( - (roomId) => this._visibleRoomsForGroup[tag].push(roomId), - ); - }); - // TODO: Check if room has been tagged to the group by the user - }, - - // Update which rooms and users should appear according to which tags are selected - updateVisibleRooms: function() { - const selectedTags = TagOrderStore.getSelectedTags(); - const visibleGroupRooms = []; - selectedTags.forEach((tag) => { - (this._visibleRoomsForGroup[tag] || []).forEach( - (roomId) => visibleGroupRooms.push(roomId), - ); - }); - - // If there are any tags selected, constrain the rooms listed to the - // visible rooms as determined by visibleGroupRooms. Here, we - // de-duplicate and filter out rooms that the client doesn't know - // about (hence the Set and the null-guard on `room`). - if (selectedTags.length > 0) { - const roomSet = new Set(); - visibleGroupRooms.forEach((roomId) => { - const room = MatrixClientPeg.get().getRoom(roomId); - if (room) { - roomSet.add(room); - } - }); - this._visibleRooms = Array.from(roomSet); - } else { - // Show all rooms - this._visibleRooms = MatrixClientPeg.get().getVisibleRooms(); - } - this._delayedRefreshRoomList(); - }, - - refreshRoomList: function() { - if (this.state.hover) { - // Don't re-sort the list if we're hovering over the list - return; - } - - // TODO: ideally we'd calculate this once at start, and then maintain - // any changes to it incrementally, updating the appropriate sublists - // as needed. - // Alternatively we'd do something magical with Immutable.js or similar. - const lists = this.getRoomLists(); - let totalRooms = 0; - for (const l of Object.values(lists)) { - totalRooms += l.length; - } - this.setState({ - lists, - totalRoomCount: totalRooms, - // Do this here so as to not render every time the selected tags - // themselves change. - selectedTags: TagOrderStore.getSelectedTags(), - }, () => { - // we don't need to restore any size here, do we? - // i guess we could have triggered a new group to appear - // that already an explicit size the last time it appeared ... - this._checkSubListsOverflow(); - }); - - // this._lastRefreshRoomListTs = Date.now(); - }, - - getTagNameForRoomId: function(roomId) { - const lists = RoomListStoreTempProxy.getRoomLists(); - for (const tagName of Object.keys(lists)) { - for (const room of lists[tagName]) { - // Should be impossible, but guard anyways. - if (!room) { - continue; - } - const myUserId = MatrixClientPeg.get().getUserId(); - if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, myUserId, this.props.ConferenceHandler)) { - continue; - } - - if (room.roomId === roomId) return tagName; - } - } - - return null; - }, - - getRoomLists: function() { - const lists = RoomListStoreTempProxy.getRoomLists(); - - const filteredLists = {}; - - const isRoomVisible = { - // $roomId: true, - }; - - this._visibleRooms.forEach((r) => { - isRoomVisible[r.roomId] = true; - }); - - Object.keys(lists).forEach((tagName) => { - const filteredRooms = lists[tagName].filter((taggedRoom) => { - // Somewhat impossible, but guard against it anyway - if (!taggedRoom) { - return; - } - const myUserId = MatrixClientPeg.get().getUserId(); - if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, myUserId, this.props.ConferenceHandler)) { - return; - } - - return Boolean(isRoomVisible[taggedRoom.roomId]); - }); - - if (filteredRooms.length > 0 || tagName.match(STANDARD_TAGS_REGEX)) { - filteredLists[tagName] = filteredRooms; - } - }); - - return filteredLists; - }, - - _getScrollNode: function() { - if (!this.mounted) return null; - const panel = ReactDOM.findDOMNode(this); - if (!panel) return null; - - if (panel.classList.contains('gm-prevented')) { - return panel; - } else { - return panel.children[2]; // XXX: Fragile! - } - }, - - _repositionIncomingCallBox: function(e, firstTime) { - const incomingCallBox = document.getElementById("incomingCallBox"); - if (incomingCallBox && incomingCallBox.parentElement) { - const scrollArea = this._getScrollNode(); - if (!scrollArea) return; - // Use the offset of the top of the scroll area from the window - // as this is used to calculate the CSS fixed top position for the stickies - const scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; - // Use the offset of the top of the component from the window - // as this is used to calculate the CSS fixed top position for the stickies - const scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; - - let top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset); - // Make sure we don't go too far up, if the headers aren't sticky - top = (top < scrollAreaOffset) ? scrollAreaOffset : top; - // make sure we don't go too far down, if the headers aren't sticky - const bottomMargin = scrollAreaOffset + (scrollAreaHeight - 45); - top = (top > bottomMargin) ? bottomMargin : top; - - incomingCallBox.style.top = top + "px"; - incomingCallBox.style.left = scrollArea.offsetLeft + scrollArea.offsetWidth + 12 + "px"; - } - }, - - _makeGroupInviteTiles(filter) { - const ret = []; - const lcFilter = filter && filter.toLowerCase(); - - const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile'); - for (const group of MatrixClientPeg.get().getGroups()) { - const {groupId, name, myMembership} = group; - // filter to only groups in invite state and group_id starts with filter or group name includes it - if (myMembership !== 'invite') continue; - if (lcFilter && !groupId.toLowerCase().startsWith(lcFilter) && - !(name && name.toLowerCase().includes(lcFilter))) continue; - ret.push(); - } - - return ret; - }, - - _applySearchFilter: function(list, filter) { - if (filter === "") return list; - const lcFilter = filter.toLowerCase(); - // apply toLowerCase before and after removeHiddenChars because different rules get applied - // e.g M -> M but m -> n, yet some unicode homoglyphs come out as uppercase, e.g 𝚮 -> H - const fuzzyFilter = utils.removeHiddenChars(lcFilter).toLowerCase(); - // case insensitive if room name includes filter, - // or if starts with `#` and one of room's aliases starts with filter - return list.filter((room) => { - if (filter[0] === "#") { - if (room.getCanonicalAlias() && room.getCanonicalAlias().toLowerCase().startsWith(lcFilter)) { - return true; - } - if (room.getAltAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))) { - return true; - } - } - return room.name && utils.removeHiddenChars(room.name.toLowerCase()).toLowerCase().includes(fuzzyFilter); - }); - }, - - _handleCollapsedState: function(key, collapsed) { - // persist collapsed state - this.collapsedState[key] = collapsed; - window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState)); - // load the persisted size configuration of the expanded sub list - if (collapsed) { - this._layout.collapseSection(key); - } else { - this._layout.expandSection(key, this.subListSizes[key]); - } - // check overflow, as sub lists sizes have changed - // important this happens after calling resize above - this._checkSubListsOverflow(); - }, - - // check overflow for scroll indicator gradient - _checkSubListsOverflow() { - Object.values(this._subListRefs).forEach(l => l.checkOverflow()); - }, - - _subListRef: function(key, ref) { - if (!ref) { - delete this._subListRefs[key]; - } else { - this._subListRefs[key] = ref; - } - }, - - _mapSubListProps: function(subListsProps) { - this._layoutSections = []; - const defaultProps = { - collapsed: this.props.collapsed, - isFiltered: !!this.props.searchFilter, - }; - - subListsProps.forEach((p) => { - p.list = this._applySearchFilter(p.list, this.props.searchFilter); - }); - - subListsProps = subListsProps.filter((props => { - const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0); - return len !== 0 || props.onAddRoom; - })); - - return subListsProps.reduce((components, props, i) => { - props = {...defaultProps, ...props}; - const isLast = i === subListsProps.length - 1; - const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0); - const {key, label, onHeaderClick, ...otherProps} = props; - const chosenKey = key || label; - const onSubListHeaderClick = (collapsed) => { - this._handleCollapsedState(chosenKey, collapsed); - if (onHeaderClick) { - onHeaderClick(collapsed); - } - }; - const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey]; - this._layoutSections.push({ - id: chosenKey, - count: len, - }); - const subList = (); - - if (!isLast) { - return components.concat( - subList, - - ); - } else { - return components.concat(subList); - } - }, []); - }, - - _collectResizeContainer: function(el) { - this.resizeContainer = el; - }, - - render: function() { - const incomingCallIfTaggedAs = (tagName) => { - if (!this.state.incomingCall) return null; - if (this.state.incomingCallTag !== tagName) return null; - return this.state.incomingCall; - }; - - let subLists = [ - { - list: [], - extraTiles: this._makeGroupInviteTiles(this.props.searchFilter), - label: _t('Community Invites'), - isInvite: true, - }, - { - list: this.state.lists['im.vector.fake.invite'], - label: _t('Invites'), - incomingCall: incomingCallIfTaggedAs('im.vector.fake.invite'), - isInvite: true, - }, - { - list: this.state.lists['m.favourite'], - label: _t('Favourites'), - tagName: "m.favourite", - incomingCall: incomingCallIfTaggedAs('m.favourite'), - }, - { - list: this.state.lists[DefaultTagID.DM], - label: _t('Direct Messages'), - tagName: DefaultTagID.DM, - incomingCall: incomingCallIfTaggedAs(DefaultTagID.DM), - onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});}, - addRoomLabel: _t("Start chat"), - }, - { - list: this.state.lists['im.vector.fake.recent'], - label: _t('Rooms'), - incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'), - onAddRoom: () => {dis.dispatch({action: 'view_create_room'});}, - addRoomLabel: _t("Create room"), - }, - ]; - const tagSubLists = Object.keys(this.state.lists) - .filter((tagName) => { - return (!this.state.customTags || this.state.customTags[tagName]) && - !tagName.match(STANDARD_TAGS_REGEX); - }).map((tagName) => { - return { - list: this.state.lists[tagName], - key: tagName, - label: labelForTagName(tagName), - tagName: tagName, - incomingCall: incomingCallIfTaggedAs(tagName), - }; - }); - subLists = subLists.concat(tagSubLists); - subLists = subLists.concat([ - { - list: this.state.lists['m.lowpriority'], - label: _t('Low priority'), - tagName: "m.lowpriority", - incomingCall: incomingCallIfTaggedAs('m.lowpriority'), - }, - { - list: this.state.lists['im.vector.fake.archived'], - label: _t('Historical'), - incomingCall: incomingCallIfTaggedAs('im.vector.fake.archived'), - startAsHidden: true, - showSpinner: this.state.isLoadingLeftRooms, - onHeaderClick: this.onArchivedHeaderClick, - }, - { - list: this.state.lists['m.server_notice'], - label: _t('System Alerts'), - tagName: "m.lowpriority", - incomingCall: incomingCallIfTaggedAs('m.server_notice'), - }, - ]); - - const subListComponents = this._mapSubListProps(subLists); - - const {resizeNotifier, collapsed, searchFilter, ConferenceHandler, onKeyDown, ...props} = this.props; // eslint-disable-line - return ( - - {({onKeyDownHandler}) =>
- { subListComponents } -
} -
- ); - }, -}); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js deleted file mode 100644 index 5917f2ae77..0000000000 --- a/src/components/views/rooms/RoomTile.js +++ /dev/null @@ -1,565 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd -Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 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 React, {createRef} from 'react'; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import classNames from 'classnames'; -import dis from '../../../dispatcher/dispatcher'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import DMRoomMap from '../../../utils/DMRoomMap'; -import * as sdk from '../../../index'; -import {ContextMenu, ContextMenuButton, toRightOf} from '../../structures/ContextMenu'; -import * as RoomNotifs from '../../../RoomNotifs'; -import * as FormattingUtils from '../../../utils/FormattingUtils'; -import ActiveRoomObserver from '../../../ActiveRoomObserver'; -import RoomViewStore from '../../../stores/RoomViewStore'; -import SettingsStore from "../../../settings/SettingsStore"; -import {_t} from "../../../languageHandler"; -import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex"; -import E2EIcon from './E2EIcon'; -import InviteOnlyIcon from './InviteOnlyIcon'; -// eslint-disable-next-line camelcase -import rate_limited_func from '../../../ratelimitedfunc'; -import { shieldStatusForRoom } from '../../../utils/ShieldUtils'; - -export default createReactClass({ - displayName: 'RoomTile', - - propTypes: { - onClick: PropTypes.func, - - room: PropTypes.object.isRequired, - collapsed: PropTypes.bool.isRequired, - unread: PropTypes.bool.isRequired, - highlight: PropTypes.bool.isRequired, - // If true, apply mx_RoomTile_transparent class - transparent: PropTypes.bool, - isInvite: PropTypes.bool.isRequired, - incomingCall: PropTypes.object, - }, - - getDefaultProps: function() { - return { - isDragging: false, - }; - }, - - getInitialState: function() { - const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", ""); - const joinRule = joinRules && joinRules.getContent().join_rule; - - return ({ - joinRule, - hover: false, - badgeHover: false, - contextMenuPosition: null, // DOM bounding box, null if non-shown - roomName: this.props.room.name, - notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), - notificationCount: this.props.room.getUnreadNotificationCount(), - selected: this.props.room.roomId === RoomViewStore.getRoomId(), - statusMessage: this._getStatusMessage(), - e2eStatus: null, - }); - }, - - _shouldShowStatusMessage() { - if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { - return false; - } - const isInvite = this.props.room.getMyMembership() === "invite"; - const isJoined = this.props.room.getMyMembership() === "join"; - const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2; - return !isInvite && isJoined && looksLikeDm; - }, - - _getStatusMessageUser() { - if (!MatrixClientPeg.get()) return null; // We've probably been logged out - - const selfId = MatrixClientPeg.get().getUserId(); - const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0]; - if (!otherMember) { - return null; - } - return otherMember.user; - }, - - _getStatusMessage() { - const statusUser = this._getStatusMessageUser(); - if (!statusUser) { - return ""; - } - return statusUser._unstable_statusMessage; - }, - - onRoomStateMember: function(ev, state, member) { - // we only care about leaving users - // because trust state will change if someone joins a megolm session anyway - if (member.membership !== "leave") { - return; - } - // ignore members in other rooms - if (member.roomId !== this.props.room.roomId) { - return; - } - - this._updateE2eStatus(); - }, - - onUserVerificationChanged: function(userId, _trustStatus) { - if (!this.props.room.getMember(userId)) { - // Not in this room - return; - } - this._updateE2eStatus(); - }, - - onCrossSigningKeysChanged: function() { - this._updateE2eStatus(); - }, - - onRoomTimeline: function(ev, room) { - if (!room) return; - if (room.roomId != this.props.room.roomId) return; - if (ev.getType() !== "m.room.encryption") return; - MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); - this.onFindingRoomToBeEncrypted(); - }, - - onFindingRoomToBeEncrypted: function() { - const cli = MatrixClientPeg.get(); - cli.on("RoomState.members", this.onRoomStateMember); - cli.on("userTrustStatusChanged", this.onUserVerificationChanged); - cli.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged); - this._updateE2eStatus(); - }, - - _updateE2eStatus: async function() { - const cli = MatrixClientPeg.get(); - if (!cli.isRoomEncrypted(this.props.room.roomId)) { - return; - } - - /* At this point, the user has encryption on and cross-signing on */ - this.setState({ - e2eStatus: await shieldStatusForRoom(cli, this.props.room), - }); - }, - - onRoomName: function(room) { - if (room !== this.props.room) return; - this.setState({ - roomName: this.props.room.name, - }); - }, - - onJoinRule: function(ev) { - if (ev.getType() !== "m.room.join_rules") return; - if (ev.getRoomId() !== this.props.room.roomId) return; - this.setState({ joinRule: ev.getContent().join_rule }); - }, - - onAccountData: function(accountDataEvent) { - if (accountDataEvent.getType() === 'm.push_rules') { - this.setState({ - notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), - }); - } - }, - - onAction: function(payload) { - switch (payload.action) { - // XXX: slight hack in order to zero the notification count when a room - // is read. Ideally this state would be given to this via props (as we - // do with `unread`). This is still better than forceUpdating the entire - // RoomList when a room is read. - case 'on_room_read': - if (payload.roomId !== this.props.room.roomId) break; - this.setState({ - notificationCount: this.props.room.getUnreadNotificationCount(), - }); - break; - // RoomTiles are one of the few components that may show custom status and - // also remain on screen while in Settings toggling the feature. This ensures - // you can clearly see the status hide and show when toggling the feature. - case 'feature_custom_status_changed': - this.forceUpdate(); - break; - - case 'view_room': - // when the room is selected make sure its tile is visible, for breadcrumbs/keyboard shortcut access - if (payload.room_id === this.props.room.roomId && payload.show_room_tile) { - this._scrollIntoView(); - } - break; - } - }, - - _scrollIntoView: function() { - if (!this._roomTile.current) return; - this._roomTile.current.scrollIntoView({ - block: "nearest", - behavior: "auto", - }); - }, - - _onActiveRoomChange: function() { - this.setState({ - selected: this.props.room.roomId === RoomViewStore.getRoomId(), - }); - }, - - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { - this._roomTile = createRef(); - }, - - componentDidMount: function() { - /* We bind here rather than in the definition because otherwise we wind up with the - method only being callable once every 500ms across all instances, which would be wrong */ - this._updateE2eStatus = rate_limited_func(this._updateE2eStatus, 500); - - const cli = MatrixClientPeg.get(); - cli.on("accountData", this.onAccountData); - cli.on("Room.name", this.onRoomName); - cli.on("RoomState.events", this.onJoinRule); - if (cli.isRoomEncrypted(this.props.room.roomId)) { - this.onFindingRoomToBeEncrypted(); - } else { - cli.on("Room.timeline", this.onRoomTimeline); - } - ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange); - this.dispatcherRef = dis.register(this.onAction); - - if (this._shouldShowStatusMessage()) { - const statusUser = this._getStatusMessageUser(); - if (statusUser) { - statusUser.on("User._unstable_statusMessage", this._onStatusMessageCommitted); - } - } - - // when we're first rendered (or our sublist is expanded) make sure we are visible if we're active - if (this.state.selected) { - this._scrollIntoView(); - } - }, - - componentWillUnmount: function() { - const cli = MatrixClientPeg.get(); - if (cli) { - MatrixClientPeg.get().removeListener("accountData", this.onAccountData); - MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); - cli.removeListener("RoomState.events", this.onJoinRule); - cli.removeListener("RoomState.members", this.onRoomStateMember); - cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); - cli.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged); - cli.removeListener("Room.timeline", this.onRoomTimeline); - } - ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange); - dis.unregister(this.dispatcherRef); - - if (this._shouldShowStatusMessage()) { - const statusUser = this._getStatusMessageUser(); - if (statusUser) { - statusUser.removeListener( - "User._unstable_statusMessage", - this._onStatusMessageCommitted, - ); - } - } - }, - - // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(props) { - // XXX: This could be a lot better - this makes the assumption that - // the notification count may have changed when the properties of - // the room tile change. - this.setState({ - notificationCount: this.props.room.getUnreadNotificationCount(), - }); - }, - - // Do a simple shallow comparison of props and state to avoid unnecessary - // renders. The assumption made here is that only state and props are used - // in rendering this component and children. - // - // RoomList is frequently made to forceUpdate, so this decreases number of - // RoomTile renderings. - shouldComponentUpdate: function(newProps, newState) { - if (Object.keys(newProps).some((k) => newProps[k] !== this.props[k])) { - return true; - } - if (Object.keys(newState).some((k) => newState[k] !== this.state[k])) { - return true; - } - return false; - }, - - _onStatusMessageCommitted() { - // The status message `User` object has observed a message change. - this.setState({ - statusMessage: this._getStatusMessage(), - }); - }, - - onClick: function(ev) { - if (this.props.onClick) { - this.props.onClick(this.props.room.roomId, ev); - } - }, - - onMouseEnter: function() { - this.setState( { hover: true }); - this.badgeOnMouseEnter(); - }, - - onMouseLeave: function() { - this.setState( { hover: false }); - this.badgeOnMouseLeave(); - }, - - badgeOnMouseEnter: function() { - // Only allow non-guests to access the context menu - // and only change it if it needs to change - if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) { - this.setState( { badgeHover: true } ); - } - }, - - badgeOnMouseLeave: function() { - this.setState( { badgeHover: false } ); - }, - - _showContextMenu: function(boundingClientRect) { - // Only allow non-guests to access the context menu - if (MatrixClientPeg.get().isGuest()) return; - - const state = { - contextMenuPosition: boundingClientRect, - }; - - // If the badge is clicked, then no longer show tooltip - if (this.props.collapsed) { - state.hover = false; - } - - this.setState(state); - }, - - onContextMenuButtonClick: function(e) { - // Prevent the RoomTile onClick event firing as well - e.stopPropagation(); - e.preventDefault(); - - this._showContextMenu(e.target.getBoundingClientRect()); - }, - - onContextMenu: function(e) { - // Prevent the native context menu - e.preventDefault(); - - this._showContextMenu({ - right: e.clientX, - top: e.clientY, - height: 0, - }); - }, - - closeMenu: function() { - this.setState({ - contextMenuPosition: null, - }); - this.props.refreshSubList(); - }, - - render: function() { - const isInvite = this.props.room.getMyMembership() === "invite"; - const notificationCount = this.props.notificationCount; - // var highlightCount = this.props.room.getUnreadNotificationCount("highlight"); - - const notifBadges = notificationCount > 0 && RoomNotifs.shouldShowNotifBadge(this.state.notifState); - const mentionBadges = this.props.highlight && RoomNotifs.shouldShowMentionBadge(this.state.notifState); - const badges = notifBadges || mentionBadges; - - let subtext = null; - if (this._shouldShowStatusMessage()) { - subtext = this.state.statusMessage; - } - - const isMenuDisplayed = Boolean(this.state.contextMenuPosition); - - const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId); - - const classes = classNames({ - 'mx_RoomTile': true, - 'mx_RoomTile_selected': this.state.selected, - 'mx_RoomTile_unread': this.props.unread, - 'mx_RoomTile_unreadNotify': notifBadges, - 'mx_RoomTile_highlight': mentionBadges, - 'mx_RoomTile_invited': isInvite, - 'mx_RoomTile_menuDisplayed': isMenuDisplayed, - 'mx_RoomTile_noBadges': !badges, - 'mx_RoomTile_transparent': this.props.transparent, - 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, - }); - - const avatarClasses = classNames({ - 'mx_RoomTile_avatar': true, - }); - - const badgeClasses = classNames({ - 'mx_RoomTile_badge': true, - 'mx_RoomTile_badgeButton': this.state.badgeHover || isMenuDisplayed, - }); - - let name = this.state.roomName; - if (typeof name !== 'string') name = ''; - name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon - - let badge; - if (badges) { - const limitedCount = FormattingUtils.formatCount(notificationCount); - const badgeContent = notificationCount ? limitedCount : '!'; - badge =
{ badgeContent }
; - } - - let label; - let subtextLabel; - let tooltip; - if (!this.props.collapsed) { - const nameClasses = classNames({ - 'mx_RoomTile_name': true, - 'mx_RoomTile_invite': this.props.isInvite, - 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || isMenuDisplayed, - }); - - subtextLabel = subtext ? { subtext } : null; - // XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex] - label =
{ name }
; - } else if (this.state.hover) { - const Tooltip = sdk.getComponent("elements.Tooltip"); - tooltip = ; - } - - //var incomingCallBox; - //if (this.props.incomingCall) { - // var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); - // incomingCallBox = ; - //} - - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - - let contextMenuButton; - if (!MatrixClientPeg.get().isGuest()) { - contextMenuButton = ( - - ); - } - - const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); - - let ariaLabel = name; - - let dmOnline; - const { room } = this.props; - const member = room.getMember(dmUserId); - if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) { - const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot'); - dmOnline = ; - } - - // The following labels are written in such a fashion to increase screen reader efficiency (speed). - if (notifBadges && mentionBadges && !isInvite) { - ariaLabel += " " + _t("%(count)s unread messages including mentions.", { - count: notificationCount, - }); - } else if (notifBadges) { - ariaLabel += " " + _t("%(count)s unread messages.", { count: notificationCount }); - } else if (mentionBadges && !isInvite) { - ariaLabel += " " + _t("Unread mentions."); - } else if (this.props.unread) { - ariaLabel += " " + _t("Unread messages."); - } - - let contextMenu; - if (isMenuDisplayed) { - const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); - contextMenu = ( - - - - ); - } - - let privateIcon = null; - if (this.state.joinRule === "invite" && !dmUserId) { - privateIcon = ; - } - - let e2eIcon = null; - if (this.state.e2eStatus) { - e2eIcon = ; - } - - return - - {({onFocus, isActive, ref}) => - -
-
- - { e2eIcon } -
-
- { privateIcon } -
-
- { label } - { subtextLabel } -
- { dmOnline } - { contextMenuButton } - { badge } -
- { /* { incomingCallBox } */ } - { tooltip } -
- } -
- - { contextMenu } -
; - }, -}); diff --git a/src/components/views/rooms/UserOnlineDot.js b/src/components/views/rooms/UserOnlineDot.js deleted file mode 100644 index 426dd1bf64..0000000000 --- a/src/components/views/rooms/UserOnlineDot.js +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2019 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 React, {useContext, useEffect, useMemo, useState, useCallback} from "react"; -import PropTypes from "prop-types"; - -import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; - -const UserOnlineDot = ({userId}) => { - const cli = useContext(MatrixClientContext); - const user = useMemo(() => cli.getUser(userId), [cli, userId]); - - const [isOnline, setIsOnline] = useState(false); - - // Recheck if the user or client changes - useEffect(() => { - setIsOnline(user && (user.currentlyActive || user.presence === "online")); - }, [cli, user]); - // Recheck also if we receive a User.currentlyActive event - const currentlyActiveHandler = useCallback((ev) => { - const content = ev.getContent(); - setIsOnline(content.currently_active || content.presence === "online"); - }, []); - useEventEmitter(user, "User.currentlyActive", currentlyActiveHandler); - useEventEmitter(user, "User.presence", currentlyActiveHandler); - - return isOnline ? : null; -}; - -UserOnlineDot.propTypes = { - userId: PropTypes.string.isRequired, -}; - -export default UserOnlineDot; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 360a29dc16..60ff5d8c05 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1141,7 +1141,6 @@ "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s", "Replying": "Replying", "Room %(name)s": "Room %(name)s", - "Recent rooms": "Recent rooms", "No recently visited rooms": "No recently visited rooms", "No rooms to show": "No rooms to show", "Unnamed room": "Unnamed room", @@ -1154,17 +1153,15 @@ "Forget room": "Forget room", "Search": "Search", "Share room": "Share room", - "Community Invites": "Community Invites", "Invites": "Invites", "Favourites": "Favourites", - "Direct Messages": "Direct Messages", + "People": "People", "Start chat": "Start chat", "Rooms": "Rooms", "Create room": "Create room", "Low priority": "Low priority", - "Historical": "Historical", "System Alerts": "System Alerts", - "People": "People", + "Historical": "Historical", "This room": "This room", "Joining room …": "Joining room …", "Loading …": "Loading …", @@ -1220,13 +1217,6 @@ "Add room": "Add room", "Show %(count)s more|other": "Show %(count)s more", "Show %(count)s more|one": "Show %(count)s more", - "Options": "Options", - "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", - "%(count)s unread messages including mentions.|one": "1 unread mention.", - "%(count)s unread messages.|other": "%(count)s unread messages.", - "%(count)s unread messages.|one": "1 unread message.", - "Unread mentions.": "Unread mentions.", - "Unread messages.": "Unread messages.", "Use default": "Use default", "All messages": "All messages", "Mentions & Keywords": "Mentions & Keywords", @@ -1236,6 +1226,11 @@ "Leave Room": "Leave Room", "Forget Room": "Forget Room", "Room options": "Room options", + "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", + "%(count)s unread messages including mentions.|one": "1 unread mention.", + "%(count)s unread messages.|other": "%(count)s unread messages.", + "%(count)s unread messages.|one": "1 unread message.", + "Unread messages.": "Unread messages.", "This room is public": "This room is public", "Away": "Away", "Add a topic": "Add a topic", @@ -1333,6 +1328,7 @@ "Invite": "Invite", "Share Link to User": "Share Link to User", "Direct message": "Direct message", + "Options": "Options", "Demote yourself?": "Demote yourself?", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", "Demote": "Demote", @@ -1724,6 +1720,7 @@ "Recent Conversations": "Recent Conversations", "Suggestions": "Suggestions", "Recently Direct Messaged": "Recently Direct Messaged", + "Direct Messages": "Direct Messages", "Start a conversation with someone using their name, username (like ) or email address.": "Start a conversation with someone using their name, username (like ) or email address.", "Go": "Go", "Invite someone using their name, username (like ), email address or share this room.": "Invite someone using their name, username (like ), email address or share this room.", @@ -2057,9 +2054,6 @@ "Send a Direct Message": "Send a Direct Message", "Explore Public Rooms": "Explore Public Rooms", "Create a Group Chat": "Create a Group Chat", - "Explore": "Explore", - "Filter": "Filter", - "Filter rooms…": "Filter rooms…", "Explore rooms": "Explore rooms", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", @@ -2135,7 +2129,6 @@ "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", - "Your profile": "Your profile", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", From 1810711380a87d420394dc7b3457f9752936520d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 14:25:09 -0600 Subject: [PATCH 094/179] Dismantle usage of the proxy store class --- src/actions/RoomListActions.ts | 9 +++++---- src/components/structures/LoggedInView.tsx | 12 ++++++------ src/components/views/dialogs/InviteDialog.js | 5 ++--- .../tabs/user/PreferencesUserSettingsTab.js | 18 +----------------- src/stores/BreadcrumbsStore.ts | 10 ---------- src/stores/CustomRoomTagStore.js | 19 +++++++++---------- src/stores/room-list/MessagePreviewStore.ts | 4 ---- 7 files changed, 23 insertions(+), 54 deletions(-) diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index e15e1b0c65..d8c6723d7b 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -24,7 +24,8 @@ import * as sdk from '../index'; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { AsyncActionPayload } from "../dispatcher/payloads"; -import { RoomListStoreTempProxy } from "../stores/room-list/RoomListStoreTempProxy"; +import RoomListStore from "../stores/room-list/RoomListStore2"; +import { SortAlgorithm } from "../stores/room-list/algorithms/models"; export default class RoomListActions { /** @@ -51,9 +52,9 @@ export default class RoomListActions { let metaData = null; // Is the tag ordered manually? - if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - const lists = RoomListStoreTempProxy.getRoomLists(); - const newList = [...lists[newTag]]; + const store = RoomListStore.instance; + if (newTag && store.getTagSorting(newTag) === SortAlgorithm.Manual) { + const newList = [...store.orderedLists[newTag]]; newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index d4b0f7902a..9b7a87c1dc 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -40,7 +40,6 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; import HomePage from "./HomePage"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PlatformPeg from "../../PlatformPeg"; -import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy"; import { DefaultTagID } from "../../stores/room-list/models"; import { showToast as showSetPasswordToast, @@ -54,6 +53,7 @@ import { Action } from "../../dispatcher/actions"; import LeftPanel2 from "./LeftPanel2"; import CallContainer from '../views/voip/CallContainer'; import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; +import RoomListStore from "../../stores/room-list/RoomListStore2"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -308,8 +308,8 @@ class LoggedInView extends React.Component { }; onRoomStateEvents = (ev, state) => { - const roomLists = RoomListStoreTempProxy.getRoomLists(); - if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) { + const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; + if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) { this._updateServerNoticeEvents(); } }; @@ -328,11 +328,11 @@ class LoggedInView extends React.Component { } _updateServerNoticeEvents = async () => { - const roomLists = RoomListStoreTempProxy.getRoomLists(); - if (!roomLists[DefaultTagID.ServerNotice]) return []; + const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; + if (!serverNoticeList) return []; const events = []; - for (const room of roomLists[DefaultTagID.ServerNotice]) { + for (const room of serverNoticeList) { const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 7ac9e21518..0c1e0c5387 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -35,8 +35,8 @@ import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../ import {inviteMultipleToRoom} from "../../../RoomInvite"; import {Key} from "../../../Keyboard"; import {Action} from "../../../dispatcher/actions"; -import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; import {DefaultTagID} from "../../../stores/room-list/models"; +import RoomListStore from "../../../stores/room-list/RoomListStore2"; export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; @@ -346,8 +346,7 @@ export default class InviteDialog extends React.PureComponent { // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the // room list doesn't tag the room for the DMRoomMap, but does for the room list. - const taggedRooms = RoomListStoreTempProxy.getRoomLists(); - const dmTaggedRooms = taggedRooms[DefaultTagID.DM]; + const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM]; const myUserId = MatrixClientPeg.get().getUserId(); for (const dmRoom of dmTaggedRooms) { const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index abe6b48712..fe60a4a179 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -23,28 +23,12 @@ import SettingsStore from "../../../../../settings/SettingsStore"; import Field from "../../../elements/Field"; import * as sdk from "../../../../.."; import PlatformPeg from "../../../../../PlatformPeg"; -import {RoomListStoreTempProxy} from "../../../../../stores/room-list/RoomListStoreTempProxy"; export default class PreferencesUserSettingsTab extends React.Component { static ROOM_LIST_SETTINGS = [ - 'RoomList.orderAlphabetically', - 'RoomList.orderByImportance', 'breadcrumbs', ]; - // TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14367 - static ROOM_LIST_2_SETTINGS = [ - 'breadcrumbs', - ]; - - // TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14367 - static eligibleRoomListSettings = () => { - if (RoomListStoreTempProxy.isUsingNewStore()) { - return PreferencesUserSettingsTab.ROOM_LIST_2_SETTINGS; - } - return PreferencesUserSettingsTab.ROOM_LIST_SETTINGS; - }; - static COMPOSER_SETTINGS = [ 'MessageComposerInput.autoReplaceEmoji', 'MessageComposerInput.suggestEmoji', @@ -189,7 +173,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
{_t("Room list")} - {this._renderGroup(PreferencesUserSettingsTab.eligibleRoomListSettings())} + {this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 5639a9104c..2c6fd320a6 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -20,7 +20,6 @@ import { ActionPayload } from "../dispatcher/payloads"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; -import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; const MAX_ROOMS = 20; // arbitrary @@ -62,9 +61,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { protected async onAction(payload: ActionPayload) { if (!this.matrixClient) return; - // TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367 - if (!RoomListStoreTempProxy.isUsingNewStore()) return; - if (payload.action === 'setting_updated') { if (payload.settingName === 'breadcrumb_rooms') { await this.updateRooms(); @@ -85,9 +81,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } protected async onReady() { - // TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367 - if (!RoomListStoreTempProxy.isUsingNewStore()) return; - await this.updateRooms(); await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)}); @@ -96,9 +89,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } protected async onNotReady() { - // TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367 - if (!RoomListStoreTempProxy.isUsingNewStore()) return; - this.matrixClient.removeListener("Room.myMembership", this.onMyMembership); this.matrixClient.removeListener("Room", this.onRoom); } diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index 48c80294b4..ed96e40dfd 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -18,8 +18,9 @@ import * as RoomNotifs from '../RoomNotifs'; import EventEmitter from 'events'; import { throttle } from "lodash"; import SettingsStore from "../settings/SettingsStore"; -import {RoomListStoreTempProxy} from "./room-list/RoomListStoreTempProxy"; +import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore2"; +// TODO: All of this needs updating for new custom tags: https://github.com/vector-im/riot-web/issues/14091 const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; function commonPrefix(a, b) { @@ -60,9 +61,7 @@ class CustomRoomTagStore extends EventEmitter { trailing: true, }, ); - this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => { - this._setState({tags: this._getUpdatedTags()}); - }); + RoomListStore.instance.on(LISTS_UPDATE_EVENT,this._onListsUpdated); dis.register(payload => this._onDispatch(payload)); } @@ -85,7 +84,7 @@ class CustomRoomTagStore extends EventEmitter { } getSortedTags() { - const roomLists = RoomListStoreTempProxy.getRoomLists(); + const roomLists = RoomListStore.instance.orderedLists; const tagNames = Object.keys(this._state.tags).sort(); const prefixes = tagNames.map((name, i) => { @@ -109,6 +108,9 @@ class CustomRoomTagStore extends EventEmitter { }); } + _onListsUpdated = () => { + this._setState({tags: this._getUpdatedTags()}); + }; _onDispatch(payload) { switch (payload.action) { @@ -126,10 +128,7 @@ class CustomRoomTagStore extends EventEmitter { case 'on_logged_out': { // we assume to always have a tags object in the state this._state = {tags: {}}; - if (this._roomListStoreToken) { - this._roomListStoreToken.remove(); - this._roomListStoreToken = null; - } + RoomListStore.instance.off(LISTS_UPDATE_EVENT,this._onListsUpdated); } break; } @@ -140,7 +139,7 @@ class CustomRoomTagStore extends EventEmitter { return; } - const newTagNames = Object.keys(RoomListStoreTempProxy.getRoomLists()) + const newTagNames = Object.keys(RoomListStore.instance.orderedLists) .filter((tagName) => { return !tagName.match(STANDARD_TAGS_REGEX); }).sort(); diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index ea7fa830cd..e5ef735927 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -18,7 +18,6 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { ActionPayload } from "../../dispatcher/payloads"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import defaultDispatcher from "../../dispatcher/dispatcher"; -import { RoomListStoreTempProxy } from "./RoomListStoreTempProxy"; import { MessageEventPreview } from "./previews/MessageEventPreview"; import { NameEventPreview } from "./previews/NameEventPreview"; import { TagID } from "./models"; @@ -192,9 +191,6 @@ export class MessagePreviewStore extends AsyncStoreWithClient { protected async onAction(payload: ActionPayload) { if (!this.matrixClient) return; - // TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367 - if (!RoomListStoreTempProxy.isUsingNewStore()) return; - if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') { const event = payload.event; // TODO: Type out the dispatcher if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important From 62b58e18e9f977dbda83c209aa1641ff4bc81175 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 14:25:56 -0600 Subject: [PATCH 095/179] Remove the temporary room list store proxy --- .../room-list/RoomListStoreTempProxy.ts | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 src/stores/room-list/RoomListStoreTempProxy.ts diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts deleted file mode 100644 index 55d710004e..0000000000 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* -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 from "../../settings/SettingsStore"; -import RoomListStore from "./RoomListStore2"; -import OldRoomListStore from "../RoomListStore"; -import { UPDATE_EVENT } from "../AsyncStore"; -import { ITagMap } from "./algorithms/models"; - -/** - * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when - * it is available to everyone. - * - * TODO: Delete this: https://github.com/vector-im/riot-web/issues/14367 - */ -export class RoomListStoreTempProxy { - public static isUsingNewStore(): boolean { - return true; - } - - public static addListener(handler: () => void): RoomListStoreTempToken { - if (RoomListStoreTempProxy.isUsingNewStore()) { - const offFn = () => RoomListStore.instance.off(UPDATE_EVENT, handler); - RoomListStore.instance.on(UPDATE_EVENT, handler); - return new RoomListStoreTempToken(offFn); - } else { - const token = OldRoomListStore.addListener(handler); - return new RoomListStoreTempToken(() => token.remove()); - } - } - - public static getRoomLists(): ITagMap { - if (RoomListStoreTempProxy.isUsingNewStore()) { - return RoomListStore.instance.orderedLists; - } else { - return OldRoomListStore.getRoomLists(); - } - } -} - -export class RoomListStoreTempToken { - constructor(private offFn: () => void) { - } - - public remove(): void { - this.offFn(); - } -} From 1f9c07861eb65970b535a6e65eb166798bec7ad0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 14:26:56 -0600 Subject: [PATCH 096/179] Remove the old room list store --- src/actions/RoomListActions.ts | 12 +- src/stores/RoomListStore.js | 805 --------------------------------- 2 files changed, 6 insertions(+), 811 deletions(-) delete mode 100644 src/stores/RoomListStore.js diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index d8c6723d7b..f12d4d3084 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -16,7 +16,6 @@ limitations under the License. */ import { asyncAction } from './actionCreators'; -import { TAG_DM } from '../stores/RoomListStore'; import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; @@ -26,6 +25,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { AsyncActionPayload } from "../dispatcher/payloads"; import RoomListStore from "../stores/room-list/RoomListStore2"; import { SortAlgorithm } from "../stores/room-list/algorithms/models"; +import { DefaultTagID } from "../stores/room-list/models"; export default class RoomListActions { /** @@ -82,11 +82,11 @@ export default class RoomListActions { const roomId = room.roomId; // Evil hack to get DMs behaving - if ((oldTag === undefined && newTag === TAG_DM) || - (oldTag === TAG_DM && newTag === undefined) + if ((oldTag === undefined && newTag === DefaultTagID.DM) || + (oldTag === DefaultTagID.DM && newTag === undefined) ) { return Rooms.guessAndSetDMRoom( - room, newTag === TAG_DM, + room, newTag === DefaultTagID.DM, ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set direct chat tag " + err); @@ -103,7 +103,7 @@ export default class RoomListActions { // but we avoid ever doing a request with TAG_DM. // // if we moved lists, remove the old tag - if (oldTag && oldTag !== TAG_DM && + if (oldTag && oldTag !== DefaultTagID.DM && hasChangedSubLists ) { const promiseToDelete = matrixClient.deleteRoomTag( @@ -121,7 +121,7 @@ export default class RoomListActions { } // if we moved lists or the ordering changed, add the new tag - if (newTag && newTag !== TAG_DM && + if (newTag && newTag !== DefaultTagID.DM && (hasChangedSubLists || metaData) ) { // metaData is the body of the PUT to set the tag, so it must diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js deleted file mode 100644 index 6c18aa83ad..0000000000 --- a/src/stores/RoomListStore.js +++ /dev/null @@ -1,805 +0,0 @@ -/* -Copyright 2018, 2019 New Vector Ltd - -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 {Store} from 'flux/utils'; -import dis from '../dispatcher/dispatcher'; -import DMRoomMap from '../utils/DMRoomMap'; -import * as Unread from '../Unread'; -import SettingsStore from "../settings/SettingsStore"; - -/* -Room sorting algorithm: -* Always prefer to have red > grey > bold > idle -* The room being viewed should be sticky (not jump down to the idle list) -* When switching to a new room, sort the last sticky room to the top of the idle list. - -The approach taken by the store is to generate an initial representation of all the -tagged lists (accepting that it'll take a little bit longer to calculate) and make -small changes to that over time. This results in quick changes to the room list while -also having update operations feel more like popping/pushing to a stack. - */ - -const CATEGORY_RED = "red"; // Mentions in the room -const CATEGORY_GREY = "grey"; // Unread notified messages (not mentions) -const CATEGORY_BOLD = "bold"; // Unread messages (not notified, 'Mentions Only' rooms) -const CATEGORY_IDLE = "idle"; // Nothing of interest - -export const TAG_DM = "im.vector.fake.direct"; - -/** - * Identifier for manual sorting behaviour: sort by the user defined order. - * @type {string} - */ -export const ALGO_MANUAL = "manual"; - -/** - * Identifier for alphabetic sorting behaviour: sort by the room name alphabetically first. - * @type {string} - */ -export const ALGO_ALPHABETIC = "alphabetic"; - -/** - * Identifier for classic sorting behaviour: sort by the most recent message first. - * @type {string} - */ -export const ALGO_RECENT = "recent"; - -const CATEGORY_ORDER = [CATEGORY_RED, CATEGORY_GREY, CATEGORY_BOLD, CATEGORY_IDLE]; - -const getListAlgorithm = (listKey, settingAlgorithm) => { - // apply manual sorting only to m.favourite, otherwise respect the global setting - // all the known tags are listed explicitly here to simplify future changes - switch (listKey) { - case "im.vector.fake.invite": - case "im.vector.fake.recent": - case "im.vector.fake.archived": - case "m.lowpriority": - case TAG_DM: - return settingAlgorithm; - - case "m.favourite": - default: // custom-tags - return ALGO_MANUAL; - } -}; - -const knownLists = new Set([ - "m.favourite", - "im.vector.fake.invite", - "im.vector.fake.recent", - "im.vector.fake.archived", - "m.lowpriority", - TAG_DM, -]); - -/** - * A class for storing application state for categorising rooms in - * the RoomList. - */ -class RoomListStore extends Store { - constructor() { - super(dis); - - this.disabled = true; - this._init(); - this._getManualComparator = this._getManualComparator.bind(this); - this._recentsComparator = this._recentsComparator.bind(this); - } - - /** - * Changes the sorting algorithm used by the RoomListStore. - * @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants. - * @param {boolean} orderImportantFirst Whether to sort by categories of importance - */ - updateSortingAlgorithm(algorithm, orderImportantFirst) { - // Dev note: We only have two algorithms at the moment, but it isn't impossible that we want - // multiple in the future. Also constants make things slightly clearer. - console.log("Updating room sorting algorithm: ", {algorithm, orderImportantFirst}); - this._setState({algorithm, orderImportantFirst}); - - // Trigger a resort of the entire list to reflect the change in algorithm - this._generateInitialRoomLists(); - } - - _init() { - if (this.disabled) return; - - // Initialise state - const defaultLists = { - "m.server_notice": [/* { room: js-sdk room, category: string } */], - "im.vector.fake.invite": [], - "m.favourite": [], - "im.vector.fake.recent": [], - [TAG_DM]: [], - "m.lowpriority": [], - "im.vector.fake.archived": [], - }; - this._state = { - // The rooms in these arrays are ordered according to either the - // 'recents' behaviour or 'manual' behaviour. - lists: defaultLists, - presentationLists: defaultLists, // like `lists`, but with arrays of rooms instead - ready: false, - stickyRoomId: null, - algorithm: ALGO_RECENT, - orderImportantFirst: false, - }; - - SettingsStore.monitorSetting('RoomList.orderAlphabetically', null); - SettingsStore.monitorSetting('RoomList.orderByImportance', null); - SettingsStore.monitorSetting('feature_custom_tags', null); - } - - _setState(newState) { - if (this.disabled) return; - - // If we're changing the lists, transparently change the presentation lists (which - // is given to requesting components). This dramatically simplifies our code elsewhere - // while also ensuring we don't need to update all the calling components to support - // categories. - if (newState['lists']) { - const presentationLists = {}; - for (const key of Object.keys(newState['lists'])) { - presentationLists[key] = newState['lists'][key].map((e) => e.room); - } - newState['presentationLists'] = presentationLists; - } - this._state = Object.assign(this._state, newState); - this.__emitChange(); - } - - __onDispatch(payload) { - if (this.disabled) return; - - const logicallyReady = this._matrixClient && this._state.ready; - switch (payload.action) { - case 'setting_updated': { - if (!logicallyReady) break; - - switch (payload.settingName) { - case "RoomList.orderAlphabetically": - this.updateSortingAlgorithm(payload.newValue ? ALGO_ALPHABETIC : ALGO_RECENT, - this._state.orderImportantFirst); - break; - case "RoomList.orderByImportance": - this.updateSortingAlgorithm(this._state.algorithm, payload.newValue); - break; - case "feature_custom_tags": - this._setState({tagsEnabled: payload.newValue}); - this._generateInitialRoomLists(); // Tags means we have to start from scratch - break; - } - } - break; - // Initialise state after initial sync - case 'MatrixActions.sync': { - if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) { - break; - } - - if (this.disabled) return; - - // Always ensure that we set any state needed for settings here. It is possible that - // setting updates trigger on startup before we are ready to sync, so we want to make - // sure that the right state is in place before we actually react to those changes. - - this._setState({tagsEnabled: SettingsStore.isFeatureEnabled("feature_custom_tags")}); - - this._matrixClient = payload.matrixClient; - - const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance"); - const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically"); - this.updateSortingAlgorithm(orderAlphabetically ? ALGO_ALPHABETIC : ALGO_RECENT, orderByImportance); - } - break; - case 'MatrixActions.Room.receipt': { - if (!logicallyReady) break; - - // First see if the receipt event is for our own user. If it was, trigger - // a room update (we probably read the room on a different device). - const myUserId = this._matrixClient.getUserId(); - for (const eventId of Object.keys(payload.event.getContent())) { - const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {}); - if (receiptUsers.includes(myUserId)) { - this._roomUpdateTriggered(payload.room.roomId); - return; - } - } - } - break; - case 'MatrixActions.Room.tags': { - if (!logicallyReady) break; - // TODO: Figure out which rooms changed in the tag and only change those. - // This is very blunt and wipes out the sticky room stuff - this._generateInitialRoomLists(); - } - break; - case 'MatrixActions.Room.timeline': { - if (!logicallyReady || - !payload.isLiveEvent || - !payload.isLiveUnfilteredRoomTimelineEvent || - !this._eventTriggersRecentReorder(payload.event) || - this._state.algorithm !== ALGO_RECENT - ) { - break; - } - - this._roomUpdateTriggered(payload.event.getRoomId()); - } - break; - // When an event is decrypted, it could mean we need to reorder the room - // list because we now know the type of the event. - case 'MatrixActions.Event.decrypted': { - if (!logicallyReady) break; - - const roomId = payload.event.getRoomId(); - - // We may have decrypted an event without a roomId (e.g to_device) - if (!roomId) break; - - const room = this._matrixClient.getRoom(roomId); - - // We somehow decrypted an event for a room our client is unaware of - if (!room) break; - - const liveTimeline = room.getLiveTimeline(); - const eventTimeline = room.getTimelineForEvent(payload.event.getId()); - - // Either this event was not added to the live timeline (e.g. pagination) - // or it doesn't affect the ordering of the room list. - if (liveTimeline !== eventTimeline || !this._eventTriggersRecentReorder(payload.event)) { - break; - } - - this._roomUpdateTriggered(roomId); - } - break; - case 'MatrixActions.accountData': { - if (!logicallyReady) break; - if (payload.event_type !== 'm.direct') break; - // TODO: Figure out which rooms changed in the direct chat and only change those. - // This is very blunt and wipes out the sticky room stuff - this._generateInitialRoomLists(); - } - break; - case 'MatrixActions.Room.myMembership': { - if (!logicallyReady) break; - this._roomUpdateTriggered(payload.room.roomId, true); - } - break; - // This could be a new room that we've been invited to, joined or created - // we won't get a RoomMember.membership for these cases if we're not already - // a member. - case 'MatrixActions.Room': { - if (!logicallyReady) break; - this._roomUpdateTriggered(payload.room.roomId, true); - } - break; - // TODO: Re-enable optimistic updates when we support dragging again - // case 'RoomListActions.tagRoom.pending': { - // if (!logicallyReady) break; - // // XXX: we only show one optimistic update at any one time. - // // Ideally we should be making a list of in-flight requests - // // that are backed by transaction IDs. Until the js-sdk - // // supports this, we're stuck with only being able to use - // // the most recent optimistic update. - // console.log("!! Optimistic tag: ", payload); - // } - // break; - // case 'RoomListActions.tagRoom.failure': { - // if (!logicallyReady) break; - // // Reset state according to js-sdk - // console.log("!! Optimistic tag failure: ", payload); - // } - // break; - case 'on_client_not_viable': - case 'on_logged_out': { - // Reset state without pushing an update to the view, which generally assumes that - // the matrix client isn't `null` and so causing a re-render will cause NPEs. - this._init(); - this._matrixClient = null; - } - break; - case 'view_room': { - if (!logicallyReady) break; - - // Note: it is important that we set a new stickyRoomId before setting the old room - // to IDLE. If we don't, the wrong room gets counted as sticky. - const currentStickyId = this._state.stickyRoomId; - this._setState({stickyRoomId: payload.room_id}); - if (currentStickyId) { - this._setRoomCategory(this._matrixClient.getRoom(currentStickyId), CATEGORY_IDLE); - } - } - break; - } - } - - _roomUpdateTriggered(roomId, ignoreSticky) { - // We don't calculate categories for sticky rooms because we have a moderate - // interest in trying to maintain the category that they were last in before - // being artificially flagged as IDLE. Also, this reduces the amount of time - // we spend in _setRoomCategory ever so slightly. - if (this._state.stickyRoomId !== roomId || ignoreSticky) { - // Micro optimization: Only look up the room if we're confident we'll need it. - const room = this._matrixClient.getRoom(roomId); - if (!room) return; - - const category = this._calculateCategory(room); - this._setRoomCategory(room, category); - } - } - - _filterTags(tags) { - tags = tags ? Object.keys(tags) : []; - if (this._state.tagsEnabled) return tags; - return tags.filter((t) => knownLists.has(t)); - } - - _getRecommendedTagsForRoom(room) { - const tags = []; - - const myMembership = room.getMyMembership(); - if (myMembership === 'join' || myMembership === 'invite') { - // Stack the user's tags on top - tags.push(...this._filterTags(room.tags)); - - // Order matters here: The DMRoomMap updates before invites - // are accepted, so we check to see if the room is an invite - // first, then if it is a direct chat, and finally default - // to the "recents" list. - const dmRoomMap = DMRoomMap.shared(); - if (myMembership === 'invite') { - tags.push("im.vector.fake.invite"); - } else if (dmRoomMap.getUserIdForRoomId(room.roomId) && tags.length === 0) { - // We intentionally don't duplicate rooms in other tags into the people list - // as a feature. - tags.push(TAG_DM); - } else if (tags.length === 0) { - tags.push("im.vector.fake.recent"); - } - } else if (myMembership) { // null-guard as null means it was peeked - tags.push("im.vector.fake.archived"); - } - - - return tags; - } - - _slotRoomIntoList(room, category, tag, existingEntries, newList, lastTimestampFn) { - const targetCategoryIndex = CATEGORY_ORDER.indexOf(category); - - let categoryComparator = (a, b) => lastTimestampFn(a.room) >= lastTimestampFn(b.room); - const sortAlgorithm = getListAlgorithm(tag, this._state.algorithm); - if (sortAlgorithm === ALGO_RECENT) { - categoryComparator = (a, b) => this._recentsComparator(a, b, lastTimestampFn); - } else if (sortAlgorithm === ALGO_ALPHABETIC) { - categoryComparator = (a, b) => this._lexicographicalComparator(a, b); - } - - // The slotting algorithm works by trying to position the room in the most relevant - // category of the list (red > grey > etc). To accomplish this, we need to consider - // a couple cases: the category existing in the list but having other rooms in it and - // the case of the category simply not existing and needing to be started. In order to - // do this efficiently, we only want to iterate over the list once and solve our sorting - // problem as we go. - // - // Firstly, we'll remove any existing entry that references the room we're trying to - // insert. We don't really want to consider the old entry and want to recreate it. We - // also exclude the sticky (currently active) room from the categorization logic and - // let it pass through wherever it resides in the list: it shouldn't be moving around - // the list too much, so we want to keep it where it is. - // - // The case of the category we want existing is easy to handle: once we hit the category, - // find the room that has a most recent event later than our own and insert just before - // that (making us the more recent room). If we end up hitting the next category before - // we can slot the room in, insert the room at the top of the category as a fallback. We - // do this to ensure that the room doesn't go too far down the list given it was previously - // considered important (in the case of going down in category) or is now more important - // (suddenly becoming red, for instance). The boundary tracking is how we end up achieving - // this, as described in the next paragraphs. - // - // The other case of the category not already existing is a bit more complicated. We track - // the boundaries of each category relative to the list we're currently building so that - // when we miss the category we can insert the room at the right spot. Most importantly, we - // can't assume that the end of the list being built is the right spot because of the last - // paragraph's requirement: the room should be put to the top of a category if the category - // runs out of places to put it. - // - // All told, our tracking looks something like this: - // - // ------ A <- Category boundary (start of red) - // RED - // RED - // RED - // ------ B <- In this example, we have a grey room we want to insert. - // BOLD - // BOLD - // ------ C - // IDLE - // IDLE - // ------ D <- End of list - // - // Given that example, and our desire to insert a GREY room into the list, this iterates - // over the room list until it realizes that BOLD comes after GREY and we're no longer - // in the RED section. Because there's no rooms there, we simply insert there which is - // also a "category boundary". If we change the example to wanting to insert a BOLD room - // which can't be ordered by timestamp with the existing couple rooms, we would still make - // use of the boundary flag to insert at B before changing the boundary indicator to C. - - let desiredCategoryBoundaryIndex = 0; - let foundBoundary = false; - let pushedEntry = false; - - for (const entry of existingEntries) { - // We insert our own record as needed, so don't let the old one through. - if (entry.room.roomId === room.roomId) { - continue; - } - - // if the list is a recent list, and the room appears in this list, and we're - // not looking at a sticky room (sticky rooms have unreliable categories), try - // to slot the new room in - if (entry.room.roomId !== this._state.stickyRoomId && !pushedEntry) { - const entryCategoryIndex = CATEGORY_ORDER.indexOf(entry.category); - - // As per above, check if we're meeting that boundary we wanted to locate. - if (entryCategoryIndex >= targetCategoryIndex && !foundBoundary) { - desiredCategoryBoundaryIndex = newList.length - 1; - foundBoundary = true; - } - - // If we've hit the top of a boundary beyond our target category, insert at the top of - // the grouping to ensure the room isn't slotted incorrectly. Otherwise, try to insert - // based on most recent timestamp. - const changedBoundary = entryCategoryIndex > targetCategoryIndex; - const currentCategory = entryCategoryIndex === targetCategoryIndex; - if (changedBoundary || (currentCategory && categoryComparator({room}, entry) <= 0)) { - if (changedBoundary) { - // If we changed a boundary, then we've gone too far - go to the top of the last - // section instead. - newList.splice(desiredCategoryBoundaryIndex, 0, {room, category}); - } else { - // If we're ordering by timestamp, just insert normally - newList.push({room, category}); - } - pushedEntry = true; - } - } - - // Fall through and clone the list. - newList.push(entry); - } - - if (!pushedEntry && desiredCategoryBoundaryIndex >= 0) { - console.warn(`!! Room ${room.roomId} nearly lost: Ran off the end of ${tag}`); - console.warn(`!! Inserting at position ${desiredCategoryBoundaryIndex} with category ${category}`); - newList.splice(desiredCategoryBoundaryIndex, 0, {room, category}); - pushedEntry = true; - } - - return pushedEntry; - } - - _setRoomCategory(room, category) { - if (!room) return; // This should only happen in tests - - const listsClone = {}; - - // Micro optimization: Support lazily loading the last timestamp in a room - const timestampCache = {}; // {roomId => ts} - const lastTimestamp = (room) => { - if (!timestampCache[room.roomId]) { - timestampCache[room.roomId] = this._tsOfNewestEvent(room); - } - return timestampCache[room.roomId]; - }; - const targetTags = this._getRecommendedTagsForRoom(room); - const insertedIntoTags = []; - - // We need to make sure all the tags (lists) are updated with the room's new position. We - // generally only get called here when there's a new room to insert or a room has potentially - // changed positions within the list. - // - // We do all our checks by iterating over the rooms in the existing lists, trying to insert - // our room where we can. As a guiding principle, we should be removing the room from all - // tags, and insert the room into targetTags. We should perform the deletion before the addition - // where possible to keep a consistent state. By the end of this, targetTags should be the - // same as insertedIntoTags. - - for (const key of Object.keys(this._state.lists)) { - const shouldHaveRoom = targetTags.includes(key); - - // Speed optimization: Don't do complicated math if we don't have to. - if (!shouldHaveRoom) { - listsClone[key] = this._state.lists[key].filter((e) => e.room.roomId !== room.roomId); - } else if (getListAlgorithm(key, this._state.algorithm) === ALGO_MANUAL) { - // Manually ordered tags are sorted later, so for now we'll just clone the tag - // and add our room if needed - listsClone[key] = this._state.lists[key].filter((e) => e.room.roomId !== room.roomId); - listsClone[key].push({room, category}); - insertedIntoTags.push(key); - } else { - listsClone[key] = []; - - const pushedEntry = this._slotRoomIntoList( - room, category, key, this._state.lists[key], listsClone[key], lastTimestamp); - - if (!pushedEntry) { - // This should rarely happen: _slotRoomIntoList has several checks which attempt - // to make sure that a room is not lost in the list. If we do lose the room though, - // we shouldn't throw it on the floor and forget about it. Instead, we should insert - // it somewhere. We'll insert it at the top for a couple reasons: 1) it is probably - // an important room for the user and 2) if this does happen, we'd want a bug report. - console.warn(`!! Room ${room.roomId} nearly lost: Failed to find a position`); - console.warn(`!! Inserting at position 0 in the list and flagging as inserted`); - console.warn("!! Additional info: ", { - category, - key, - upToIndex: listsClone[key].length, - expectedCount: this._state.lists[key].length, - }); - listsClone[key].splice(0, 0, {room, category}); - } - insertedIntoTags.push(key); - } - } - - // Double check that we inserted the room in the right places. - // There should never be a discrepancy. - for (const targetTag of targetTags) { - let count = 0; - for (const insertedTag of insertedIntoTags) { - if (insertedTag === targetTag) count++; - } - - if (count !== 1) { - console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`); - } - - // This is a workaround for https://github.com/vector-im/riot-web/issues/11303 - // The logging is to try and identify what happened exactly. - if (count === 0) { - // Something went very badly wrong - try to recover the room. - // We don't bother checking how the target list is ordered - we're expecting - // to just insert it. - console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`); - if (!listsClone[targetTag]) { - console.warn(`!! List for tag ${targetTag} does not exist - creating`); - listsClone[targetTag] = []; - } - listsClone[targetTag].splice(0, 0, {room, category}); - } - } - - // Sort the favourites before we set the clone - for (const tag of Object.keys(listsClone)) { - if (getListAlgorithm(tag, this._state.algorithm) !== ALGO_MANUAL) continue; // skip recents (pre-sorted) - listsClone[tag].sort(this._getManualComparator(tag)); - } - - this._setState({lists: listsClone}); - } - - _generateInitialRoomLists() { - // Log something to show that we're throwing away the old results. This is for the inevitable - // question of "why is 100% of my CPU going towards Riot?" - a quick look at the logs would reveal - // that something is wrong with the RoomListStore. - console.log("Generating initial room lists"); - - const lists = { - "m.server_notice": [], - "im.vector.fake.invite": [], - "m.favourite": [], - "im.vector.fake.recent": [], - [TAG_DM]: [], - "m.lowpriority": [], - "im.vector.fake.archived": [], - }; - - const dmRoomMap = DMRoomMap.shared(); - - this._matrixClient.getRooms().forEach((room) => { - const myUserId = this._matrixClient.getUserId(); - const membership = room.getMyMembership(); - const me = room.getMember(myUserId); - - if (membership === "invite") { - lists["im.vector.fake.invite"].push({room, category: CATEGORY_RED}); - } else if (membership === "join" || membership === "ban" || (me && me.isKicked())) { - // Used to split rooms via tags - let tagNames = Object.keys(room.tags); - - // ignore any m. tag names we don't know about - tagNames = tagNames.filter((t) => { - // Speed optimization: Avoid hitting the SettingsStore at all costs by making it the - // last condition possible. - return lists[t] !== undefined || (!t.startsWith('m.') && this._state.tagsEnabled); - }); - - if (tagNames.length) { - for (let i = 0; i < tagNames.length; i++) { - const tagName = tagNames[i]; - lists[tagName] = lists[tagName] || []; - - // Default to an arbitrary category for tags which aren't ordered by recents - let category = CATEGORY_IDLE; - if (getListAlgorithm(tagName, this._state.algorithm) !== ALGO_MANUAL) { - category = this._calculateCategory(room); - } - lists[tagName].push({room, category}); - } - } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { - // "Direct Message" rooms (that we're still in and that aren't otherwise tagged) - lists[TAG_DM].push({room, category: this._calculateCategory(room)}); - } else { - lists["im.vector.fake.recent"].push({room, category: this._calculateCategory(room)}); - } - } else if (membership === "leave") { - // The category of these rooms is not super important, so deprioritize it to the lowest - // possible value. - lists["im.vector.fake.archived"].push({room, category: CATEGORY_IDLE}); - } - }); - - // We use this cache in the recents comparator because _tsOfNewestEvent can take a while. This - // cache only needs to survive the sort operation below and should not be implemented outside - // of this function, otherwise the room lists will almost certainly be out of date and wrong. - const latestEventTsCache = {}; // roomId => timestamp - const tsOfNewestEventFn = (room) => { - if (!room) return Number.MAX_SAFE_INTEGER; // Should only happen in tests - - if (latestEventTsCache[room.roomId]) { - return latestEventTsCache[room.roomId]; - } - - const ts = this._tsOfNewestEvent(room); - latestEventTsCache[room.roomId] = ts; - return ts; - }; - - Object.keys(lists).forEach((listKey) => { - let comparator; - switch (getListAlgorithm(listKey, this._state.algorithm)) { - case ALGO_RECENT: - comparator = (entryA, entryB) => this._recentsComparator(entryA, entryB, tsOfNewestEventFn); - break; - case ALGO_ALPHABETIC: - comparator = this._lexicographicalComparator; - break; - case ALGO_MANUAL: - default: - comparator = this._getManualComparator(listKey); - break; - } - - if (this._state.orderImportantFirst) { - lists[listKey].sort((entryA, entryB) => { - if (entryA.category !== entryB.category) { - const idxA = CATEGORY_ORDER.indexOf(entryA.category); - const idxB = CATEGORY_ORDER.indexOf(entryB.category); - if (idxA > idxB) return 1; - if (idxA < idxB) return -1; - return 0; // Technically not possible - } - return comparator(entryA, entryB); - }); - } else { - // skip the category comparison even though it should no-op when orderImportantFirst disabled - lists[listKey].sort(comparator); - } - }); - - this._setState({ - lists, - ready: true, // Ready to receive updates to ordering - }); - } - - _eventTriggersRecentReorder(ev) { - return ev.getTs() && ( - Unread.eventTriggersUnreadCount(ev) || - ev.getSender() === this._matrixClient.credentials.userId - ); - } - - _tsOfNewestEvent(room) { - // Apparently we can have rooms without timelines, at least under testing - // environments. Just return MAX_INT when this happens. - if (!room || !room.timeline) return Number.MAX_SAFE_INTEGER; - - for (let i = room.timeline.length - 1; i >= 0; --i) { - const ev = room.timeline[i]; - if (this._eventTriggersRecentReorder(ev)) { - return ev.getTs(); - } - } - - // we might only have events that don't trigger the unread indicator, - // in which case use the oldest event even if normally it wouldn't count. - // This is better than just assuming the last event was forever ago. - if (room.timeline.length && room.timeline[0].getTs()) { - return room.timeline[0].getTs(); - } else { - return Number.MAX_SAFE_INTEGER; - } - } - - _calculateCategory(room) { - if (!this._state.orderImportantFirst) { - // Effectively disable the categorization of rooms if we're supposed to - // be sorting by more recent messages first. This triggers the timestamp - // comparison bit of _setRoomCategory and _recentsComparator instead of - // the category ordering. - return CATEGORY_IDLE; - } - - const mentions = room.getUnreadNotificationCount("highlight") > 0; - if (mentions) return CATEGORY_RED; - - let unread = room.getUnreadNotificationCount() > 0; - if (unread) return CATEGORY_GREY; - - unread = Unread.doesRoomHaveUnreadMessages(room); - if (unread) return CATEGORY_BOLD; - - return CATEGORY_IDLE; - } - - _recentsComparator(entryA, entryB, tsOfNewestEventFn) { - const timestampA = tsOfNewestEventFn(entryA.room); - const timestampB = tsOfNewestEventFn(entryB.room); - return timestampB - timestampA; - } - - _lexicographicalComparator(entryA, entryB) { - return entryA.room.name.localeCompare(entryB.room.name); - } - - _getManualComparator(tagName, optimisticRequest) { - return (entryA, entryB) => { - const roomA = entryA.room; - const roomB = entryB.room; - - let metaA = roomA.tags[tagName]; - let metaB = roomB.tags[tagName]; - - if (optimisticRequest && roomA === optimisticRequest.room) metaA = optimisticRequest.metaData; - if (optimisticRequest && roomB === optimisticRequest.room) metaB = optimisticRequest.metaData; - - // Make sure the room tag has an order element, if not set it to be the bottom - const a = metaA ? Number(metaA.order) : undefined; - const b = metaB ? Number(metaB.order) : undefined; - - // Order undefined room tag orders to the bottom - if (a === undefined && b !== undefined) { - return 1; - } else if (a !== undefined && b === undefined) { - return -1; - } - - return a === b ? this._lexicographicalComparator(entryA, entryB) : (a > b ? 1 : -1); - }; - } - - getRoomLists() { - return this._state.presentationLists; - } -} - -if (global.singletonRoomListStore === undefined) { - global.singletonRoomListStore = new RoomListStore(); -} -export default global.singletonRoomListStore; From 209a5d222003c0e864c526d46dff05c04ceb4f89 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 15:10:30 -0600 Subject: [PATCH 097/179] Rename RoomListStore2 class name We use `RoomListStore` as a singleton, and don't want the ugly `2` at the end of the actual store instance, so here we rename it to something half-decent. --- src/@types/global.d.ts | 4 ++-- src/stores/room-list/RoomListStore2.ts | 12 ++++++------ src/stores/room-list/TagWatcher.ts | 4 ++-- test/components/views/rooms/RoomList-test.js | 7 +++++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 9424cdcd17..2d446e444a 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -20,7 +20,7 @@ import { IMatrixClientPeg } from "../MatrixClientPeg"; import ToastStore from "../stores/ToastStore"; import DeviceListener from "../DeviceListener"; import RebrandListener from "../RebrandListener"; -import { RoomListStore2 } from "../stores/room-list/RoomListStore2"; +import { RoomListStoreClass } from "../stores/room-list/RoomListStore2"; import { PlatformPeg } from "../PlatformPeg"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; @@ -37,7 +37,7 @@ declare global { mx_ToastStore: ToastStore; mx_DeviceListener: DeviceListener; mx_RebrandListener: RebrandListener; - mx_RoomListStore2: RoomListStore2; + mx_RoomListStore: RoomListStoreClass; mx_RoomListLayoutStore: RoomListLayoutStore; mxPlatformPeg: PlatformPeg; mxIntegrationManagers: typeof IntegrationManagers; diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 9576ae8ed6..62e515c5b3 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -44,7 +44,7 @@ interface IState { */ export const LISTS_UPDATE_EVENT = "lists_update"; -export class RoomListStore2 extends AsyncStoreWithClient { +export class RoomListStoreClass extends AsyncStoreWithClient { /** * Set to true if you're running tests on the store. Should not be touched in * any other environment. @@ -175,7 +175,7 @@ export class RoomListStore2 extends AsyncStoreWithClient { protected async onAction(payload: ActionPayload) { // When we're running tests we can't reliably use setImmediate out of timing concerns. // As such, we use a more synchronous model. - if (RoomListStore2.TEST_MODE) { + if (RoomListStoreClass.TEST_MODE) { await this.onDispatchAsync(payload); return; } @@ -608,15 +608,15 @@ export class RoomListStore2 extends AsyncStoreWithClient { } export default class RoomListStore { - private static internalInstance: RoomListStore2; + private static internalInstance: RoomListStoreClass; - public static get instance(): RoomListStore2 { + public static get instance(): RoomListStoreClass { if (!RoomListStore.internalInstance) { - RoomListStore.internalInstance = new RoomListStore2(); + RoomListStore.internalInstance = new RoomListStoreClass(); } return RoomListStore.internalInstance; } } -window.mx_RoomListStore2 = RoomListStore.instance; +window.mx_RoomListStore = RoomListStore.instance; diff --git a/src/stores/room-list/TagWatcher.ts b/src/stores/room-list/TagWatcher.ts index 56b6437524..6f011271d5 100644 --- a/src/stores/room-list/TagWatcher.ts +++ b/src/stores/room-list/TagWatcher.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { RoomListStore2 } from "./RoomListStore2"; +import { RoomListStoreClass } from "./RoomListStore2"; import TagOrderStore from "../TagOrderStore"; import { CommunityFilterCondition } from "./filters/CommunityFilterCondition"; import { arrayDiff, arrayHasDiff } from "../../utils/arrays"; @@ -26,7 +26,7 @@ export class TagWatcher { // TODO: Support custom tags, somehow: https://github.com/vector-im/riot-web/issues/14091 private filters = new Map(); - constructor(private store: RoomListStore2) { + constructor(private store: RoomListStoreClass) { TagOrderStore.addListener(this.onTagsUpdated); } diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index e84f943708..43aa3dc2f8 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -14,7 +14,10 @@ import GroupStore from '../../../../src/stores/GroupStore.js'; import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; import {DefaultTagID} from "../../../../src/stores/room-list/models"; -import RoomListStore, {LISTS_UPDATE_EVENT, RoomListStore2} from "../../../../src/stores/room-list/RoomListStore2"; +import RoomListStore, { + LISTS_UPDATE_EVENT, + RoomListStoreClass +} from "../../../../src/stores/room-list/RoomListStore2"; import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore"; function generateRoomId() { @@ -49,7 +52,7 @@ describe('RoomList', () => { let myOtherMember; beforeEach(async function(done) { - RoomListStore2.TEST_MODE = true; + RoomListStoreClass.TEST_MODE = true; TestUtils.stubClient(); client = MatrixClientPeg.get(); From 2b15ba21ddc99a7545b92d516f982c2dbce7efe5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 15:11:34 -0600 Subject: [PATCH 098/179] Rename RoomListStore file --- src/@types/global.d.ts | 2 +- src/actions/RoomListActions.ts | 2 +- src/components/structures/LeftPanel2.tsx | 2 +- src/components/structures/LoggedInView.tsx | 2 +- src/components/views/dialogs/InviteDialog.js | 2 +- src/components/views/rooms/RoomBreadcrumbs2.tsx | 2 +- src/components/views/rooms/RoomList2.tsx | 2 +- src/components/views/rooms/RoomSublist2.tsx | 2 +- src/components/views/rooms/RoomTile2.tsx | 2 +- .../views/settings/tabs/room/AdvancedRoomSettingsTab.js | 2 +- src/stores/CustomRoomTagStore.js | 2 +- src/stores/room-list/{RoomListStore2.ts => RoomListStore.ts} | 0 src/stores/room-list/TagWatcher.ts | 2 +- test/components/views/rooms/RoomList-test.js | 2 +- 14 files changed, 13 insertions(+), 13 deletions(-) rename src/stores/room-list/{RoomListStore2.ts => RoomListStore.ts} (100%) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 2d446e444a..f556ff8b5c 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -20,7 +20,7 @@ import { IMatrixClientPeg } from "../MatrixClientPeg"; import ToastStore from "../stores/ToastStore"; import DeviceListener from "../DeviceListener"; import RebrandListener from "../RebrandListener"; -import { RoomListStoreClass } from "../stores/room-list/RoomListStore2"; +import { RoomListStoreClass } from "../stores/room-list/RoomListStore"; import { PlatformPeg } from "../PlatformPeg"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index f12d4d3084..da07bcb169 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -23,7 +23,7 @@ import * as sdk from '../index'; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { AsyncActionPayload } from "../dispatcher/payloads"; -import RoomListStore from "../stores/room-list/RoomListStore2"; +import RoomListStore from "../stores/room-list/RoomListStore"; import { SortAlgorithm } from "../stores/room-list/algorithms/models"; import { DefaultTagID } from "../stores/room-list/models"; diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 717ec240ac..c8ab37e014 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -31,7 +31,7 @@ import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import ResizeNotifier from "../../utils/ResizeNotifier"; import SettingsStore from "../../settings/SettingsStore"; -import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2"; +import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore"; import {Key} from "../../Keyboard"; import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 9b7a87c1dc..7e47620b05 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -53,7 +53,7 @@ import { Action } from "../../dispatcher/actions"; import LeftPanel2 from "./LeftPanel2"; import CallContainer from '../views/voip/CallContainer'; import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; -import RoomListStore from "../../stores/room-list/RoomListStore2"; +import RoomListStore from "../../stores/room-list/RoomListStore"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 0c1e0c5387..68c71300fb 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -36,7 +36,7 @@ import {inviteMultipleToRoom} from "../../../RoomInvite"; import {Key} from "../../../Keyboard"; import {Action} from "../../../dispatcher/actions"; import {DefaultTagID} from "../../../stores/room-list/models"; -import RoomListStore from "../../../stores/room-list/RoomListStore2"; +import RoomListStore from "../../../stores/room-list/RoomListStore"; export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index 71e9d9d6e1..c857cd9968 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -23,7 +23,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import Analytics from "../../../Analytics"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { CSSTransition } from "react-transition-group"; -import RoomListStore from "../../../stores/room-list/RoomListStore2"; +import RoomListStore from "../../../stores/room-list/RoomListStore"; import { DefaultTagID } from "../../../stores/room-list/models"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import Toolbar from "../../../accessibility/Toolbar"; diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 86becc2fca..1ef36c8a1f 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -23,7 +23,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { _t, _td } from "../../../languageHandler"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; -import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2"; +import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import RoomViewStore from "../../../stores/RoomViewStore"; import { ITagMap } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 4883d4f2a3..f2dffeeb1e 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -32,7 +32,7 @@ import { StyledMenuItemCheckbox, StyledMenuItemRadio, } from "../../structures/ContextMenu"; -import RoomListStore from "../../../stores/room-list/RoomListStore2"; +import RoomListStore from "../../../stores/room-list/RoomListStore"; import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import dis from "../../../dispatcher/dispatcher"; diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index b19bb23160..a66a415c58 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -48,7 +48,7 @@ import { import { MatrixClientPeg } from "../../../MatrixClientPeg"; import NotificationBadge from "./NotificationBadge"; import { Volume } from "../../../RoomNotifsTypes"; -import RoomListStore from "../../../stores/room-list/RoomListStore2"; +import RoomListStore from "../../../stores/room-list/RoomListStore"; import RoomListActions from "../../../actions/RoomListActions"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {ActionPayload} from "../../../dispatcher/payloads"; diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js index 2edf3021dc..391f4f7845 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js @@ -22,7 +22,7 @@ import * as sdk from "../../../../.."; import AccessibleButton from "../../../elements/AccessibleButton"; import Modal from "../../../../../Modal"; import dis from "../../../../../dispatcher/dispatcher"; -import RoomListStore from "../../../../../stores/room-list/RoomListStore2"; +import RoomListStore from "../../../../../stores/room-list/RoomListStore"; import RoomListActions from "../../../../../actions/RoomListActions"; import { DefaultTagID } from '../../../../../stores/room-list/models'; import LabelledToggleSwitch from '../../../elements/LabelledToggleSwitch'; diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index ed96e40dfd..b1f9ad6d36 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -18,7 +18,7 @@ import * as RoomNotifs from '../RoomNotifs'; import EventEmitter from 'events'; import { throttle } from "lodash"; import SettingsStore from "../settings/SettingsStore"; -import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore2"; +import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore"; // TODO: All of this needs updating for new custom tags: https://github.com/vector-im/riot-web/issues/14091 const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore.ts similarity index 100% rename from src/stores/room-list/RoomListStore2.ts rename to src/stores/room-list/RoomListStore.ts diff --git a/src/stores/room-list/TagWatcher.ts b/src/stores/room-list/TagWatcher.ts index 6f011271d5..1c16571e5b 100644 --- a/src/stores/room-list/TagWatcher.ts +++ b/src/stores/room-list/TagWatcher.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { RoomListStoreClass } from "./RoomListStore2"; +import { RoomListStoreClass } from "./RoomListStore"; import TagOrderStore from "../TagOrderStore"; import { CommunityFilterCondition } from "./filters/CommunityFilterCondition"; import { arrayDiff, arrayHasDiff } from "../../utils/arrays"; diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index 43aa3dc2f8..56a62472fe 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -17,7 +17,7 @@ import {DefaultTagID} from "../../../../src/stores/room-list/models"; import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStoreClass -} from "../../../../src/stores/room-list/RoomListStore2"; +} from "../../../../src/stores/room-list/RoomListStore"; import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore"; function generateRoomId() { From 1cce6e2e3282031bb1a444a34662fe44d43a042d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 15:14:58 -0600 Subject: [PATCH 099/179] Enable new room list store forever --- src/stores/AsyncStoreWithClient.ts | 1 - src/stores/room-list/RoomListStore.ts | 21 ++++----------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/stores/AsyncStoreWithClient.ts b/src/stores/AsyncStoreWithClient.ts index ce7fd45eec..5b9f95f991 100644 --- a/src/stores/AsyncStoreWithClient.ts +++ b/src/stores/AsyncStoreWithClient.ts @@ -18,7 +18,6 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import { AsyncStore } from "./AsyncStore"; import { ActionPayload } from "../dispatcher/payloads"; - export abstract class AsyncStoreWithClient extends AsyncStore { protected matrixClient: MatrixClient; diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 62e515c5b3..2bf238a84a 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -44,7 +44,7 @@ interface IState { */ export const LISTS_UPDATE_EVENT = "lists_update"; -export class RoomListStoreClass extends AsyncStoreWithClient { +export class RoomListStoreClass extends AsyncStoreWithClient { /** * Set to true if you're running tests on the store. Should not be touched in * any other environment. @@ -52,7 +52,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient { public static TEST_MODE = false; private initialListsGenerated = false; - private enabled = true; private algorithm = new Algorithm(); private filterConditions: IFilterCondition[] = []; private tagWatcher = new TagWatcher(this); @@ -66,7 +65,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { constructor() { super(defaultDispatcher); - this.checkEnabled(); + this.checkLoggingEnabled(); for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null); RoomViewStore.addListener(() => this.handleRVSUpdate({})); this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); @@ -106,9 +105,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { super.matrixClient = forcedClient; } - // TODO: Remove with https://github.com/vector-im/riot-web/issues/14367 - this.checkEnabled(); - if (!this.enabled) return; + this.checkLoggingEnabled(); // Update any settings here, as some may have happened before we were logically ready. // Update any settings here, as some may have happened before we were logically ready. @@ -121,7 +118,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.updateFn.trigger(); } - private checkEnabled() { + private checkLoggingEnabled() { if (SettingsStore.getValue("advancedRoomListLogging")) { console.warn("Advanced room list logging is enabled"); } @@ -141,7 +138,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient { * be used if the calling code will manually trigger the update. */ private async handleRVSUpdate({trigger = true}) { - if (!this.enabled) return; // TODO: Remove with https://github.com/vector-im/riot-web/issues/14367 if (!this.matrixClient) return; // We assume there won't be RVS updates without a client const activeRoomId = RoomViewStore.getRoomId(); @@ -186,9 +182,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } protected async onDispatchAsync(payload: ActionPayload) { - // TODO: Remove this once the RoomListStore becomes default - if (!this.enabled) return; - // Everything here requires a MatrixClient or some sort of logical readiness. const logicallyReady = this.matrixClient && this.initialListsGenerated; if (!logicallyReady) return; @@ -509,12 +502,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } } - protected async updateState(newState: IState) { - if (!this.enabled) return; - - await super.updateState(newState); - } - private onAlgorithmListUpdated = () => { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 From 52219a8341559ecf402132d739a122fab5e4f8bf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 15:18:42 -0600 Subject: [PATCH 100/179] Remove legacy resizing code --- src/resizer/distributors/roomsublist.js | 132 --------- src/resizer/distributors/roomsublist2.js | 332 ----------------------- src/resizer/index.js | 1 - 3 files changed, 465 deletions(-) delete mode 100644 src/resizer/distributors/roomsublist.js delete mode 100644 src/resizer/distributors/roomsublist2.js diff --git a/src/resizer/distributors/roomsublist.js b/src/resizer/distributors/roomsublist.js deleted file mode 100644 index cc7875bfb0..0000000000 --- a/src/resizer/distributors/roomsublist.js +++ /dev/null @@ -1,132 +0,0 @@ -/* -Copyright 2019 New Vector Ltd - -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 Sizer from "../sizer"; -import ResizeItem from "../item"; - -class RoomSizer extends Sizer { - setItemSize(item, size) { - item.style.maxHeight = `${Math.round(size)}px`; - item.classList.add("resized-sized"); - } - - clearItemSize(item) { - item.style.maxHeight = null; - item.classList.remove("resized-sized"); - } -} - -class RoomSubListItem extends ResizeItem { - isCollapsed() { - return this.domNode.classList.contains("mx_RoomSubList_hidden"); - } - - maxSize() { - const header = this.domNode.querySelector(".mx_RoomSubList_labelContainer"); - const scrollItem = this.domNode.querySelector(".mx_RoomSubList_scroll"); - const headerHeight = this.sizer.getItemSize(header); - return headerHeight + (scrollItem ? scrollItem.scrollHeight : 0); - } - - minSize() { - const isNotEmpty = this.domNode.classList.contains("mx_RoomSubList_nonEmpty"); - return isNotEmpty ? 74 : 31; //size of header + 1? room tile (see room sub list css) - } - - isSized() { - return this.domNode.classList.contains("resized-sized"); - } -} - -export default class RoomSubListDistributor { - static createItem(resizeHandle, resizer, sizer) { - return new RoomSubListItem(resizeHandle, resizer, sizer); - } - - static createSizer(containerElement, vertical, reverse) { - return new RoomSizer(containerElement, vertical, reverse); - } - - constructor(item) { - this.item = item; - } - - _handleSize() { - return 1; - } - - resize(size) { - //console.log("*** starting resize session with size", size); - let item = this.item; - while (item) { - const minSize = item.minSize(); - if (item.isCollapsed()) { - item = item.previous(); - } else if (size <= minSize) { - //console.log(" - resizing", item.id, "to min size", minSize); - item.setSize(minSize); - const remainder = minSize - size; - item = item.previous(); - if (item) { - size = item.size() - remainder - this._handleSize(); - } - } else { - const maxSize = item.maxSize(); - if (size > maxSize) { - // console.log(" - resizing", item.id, "to maxSize", maxSize); - item.setSize(maxSize); - const remainder = size - maxSize; - item = item.previous(); - if (item) { - size = item.size() + remainder; // todo: handle size here? - } - } else { - //console.log(" - resizing", item.id, "to size", size); - item.setSize(size); - item = null; - size = 0; - } - } - } - //console.log("*** ending resize session"); - } - - resizeFromContainerOffset(containerOffset) { - this.resize(containerOffset - this.item.offset()); - } - - start() { - // set all max-height props to the actual height. - let item = this.item.first(); - const sizes = []; - while (item) { - if (!item.isCollapsed()) { - sizes.push(item.size()); - } else { - sizes.push(100); - } - item = item.next(); - } - item = this.item.first(); - sizes.forEach((size) => { - item.setSize(size); - item = item.next(); - }); - } - - finish() { - } -} diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js deleted file mode 100644 index a715087630..0000000000 --- a/src/resizer/distributors/roomsublist2.js +++ /dev/null @@ -1,332 +0,0 @@ -/* -Copyright 2019 New Vector Ltd - -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 FixedDistributor from "./fixed"; - -function clamp(height, min, max) { - if (height > max) return max; - if (height < min) return min; - return height; -} - -export class Layout { - constructor(applyHeight, initialSizes, collapsedState, options) { - // callback to set height of section - this._applyHeight = applyHeight; - // list of {id, count} objects, - // determines sections and order of them - this._sections = []; - // stores collapsed by id - this._collapsedState = Object.assign({}, collapsedState); - // total available height to the layout - // (including resize handles, ...) - this._availableHeight = 0; - // heights stored by section section id - this._sectionHeights = Object.assign({}, initialSizes); - // in-progress heights, while dragging. Committed on mouse-up. - this._heights = []; - // use while manually resizing to cancel - // the resize for a given mouse position - // when the previous resize made the layout - // constrained - this._clampedOffset = 0; - // used while manually resizing, to clear - // _clampedOffset when the direction of resizing changes - this._lastOffset = 0; - - this._allowWhitespace = options && options.allowWhitespace; - this._handleHeight = (options && options.handleHeight) || 0; - } - - setAvailableHeight(newSize) { - this._availableHeight = newSize; - // needs more work - this._applyNewSize(); - } - - expandSection(id, height) { - this._collapsedState[id] = false; - this._applyNewSize(); - this.openHandle(id).setHeight(height).finish(); - } - - collapseSection(id) { - this._collapsedState[id] = true; - this._applyNewSize(); - } - - update(sections, availableHeight, force = false) { - let heightChanged = false; - - if (Number.isFinite(availableHeight) && availableHeight !== this._availableHeight) { - heightChanged = true; - this._availableHeight = availableHeight; - } - - const sectionsChanged = - sections.length !== this._sections.length || - sections.some((a, i) => { - const b = this._sections[i]; - return a.id !== b.id || a.count !== b.count; - }); - - if (!heightChanged && !sectionsChanged && !force) { - return; - } - - this._sections = sections; - const totalHeight = this._getAvailableHeight(); - const defaultHeight = Math.floor(totalHeight / this._sections.length); - this._sections.forEach((section, i) => { - if (!this._sectionHeights[section.id]) { - this._sectionHeights[section.id] = clamp( - defaultHeight, - this._getMinHeight(i), - this._getMaxHeight(i), - ); - } - }); - this._applyNewSize(); - } - - openHandle(id) { - const index = this._getSectionIndex(id); - return new Handle(this, index, this._sectionHeights[id]); - } - - _getAvailableHeight() { - const nonCollapsedSectionCount = this._sections.reduce((count, section) => { - const collapsed = this._collapsedState[section.id]; - return count + (collapsed ? 0 : 1); - }, 0); - return this._availableHeight - ((nonCollapsedSectionCount - 1) * this._handleHeight); - } - - _applyNewSize() { - const newHeight = this._getAvailableHeight(); - const currHeight = this._sections.reduce((sum, section) => { - return sum + this._sectionHeights[section.id]; - }, 0); - const offset = newHeight - currHeight; - this._heights = this._sections.map((section) => this._sectionHeights[section.id]); - const sections = this._sections.map((_, i) => i); - this._applyOverflow(-offset, sections, true); - this._applyHeights(); - this._commitHeights(); - } - - _getSectionIndex(id) { - return this._sections.findIndex((s) => s.id === id); - } - - _getMaxHeight(i) { - const section = this._sections[i]; - const collapsed = this._collapsedState[section.id]; - - if (collapsed) { - return this._sectionHeight(0); - } else if (!this._allowWhitespace) { - return this._sectionHeight(section.count); - } else { - return 100000; - } - } - - _sectionHeight(count) { - return 36 + (count === 0 ? 0 : 4 + (count * 34)); - } - - _getMinHeight(i) { - const section = this._sections[i]; - const collapsed = this._collapsedState[section.id]; - const maxItems = collapsed ? 0 : 1; - return this._sectionHeight(Math.min(section.count, maxItems)); - } - - _applyOverflow(overflow, sections, blend) { - // take the given overflow amount, and applies it to the given sections. - // calls itself recursively until it has distributed all the overflow - // or run out of unclamped sections. - - const unclampedSections = []; - - let overflowPerSection = blend ? (overflow / sections.length) : overflow; - for (const i of sections) { - const newHeight = clamp( - this._heights[i] - overflowPerSection, - this._getMinHeight(i), - this._getMaxHeight(i), - ); - if (newHeight == this._heights[i] - overflowPerSection) { - unclampedSections.push(i); - } - // when section is growing, overflow increases? - // 100 -= 200 - 300 - // 100 -= -100 - // 200 - overflow -= this._heights[i] - newHeight; - this._heights[i] = newHeight; - if (!blend) { - overflowPerSection = overflow; - if (Math.abs(overflow) < 1.0) break; - } - } - - if (Math.abs(overflow) > 1.0 && unclampedSections.length > 0) { - // we weren't able to distribute all the overflow so recurse and try again - overflow = this._applyOverflow(overflow, unclampedSections, blend); - } - - return overflow; - } - - _rebalanceAbove(sectionIndex, overflowAbove) { - if (Math.abs(overflowAbove) > 1.0) { - const sections = []; - for (let i = sectionIndex - 1; i >= 0; i--) { - sections.push(i); - } - overflowAbove = this._applyOverflow(overflowAbove, sections); - } - return overflowAbove; - } - - _rebalanceBelow(sectionIndex, overflowBelow) { - if (Math.abs(overflowBelow) > 1.0) { - const sections = []; - for (let i = sectionIndex + 1; i < this._sections.length; i++) { - sections.push(i); - } - overflowBelow = this._applyOverflow(overflowBelow, sections); - } - return overflowBelow; - } - - // @param offset the amount the sectionIndex is moved from what is stored in _sectionHeights, positive if downwards - // if we're constrained, return the offset we should be constrained at. - _relayout(sectionIndex = 0, offset = 0, constrained = false) { - this._heights = this._sections.map((section) => this._sectionHeights[section.id]); - // are these the amounts the items above/below shrank/grew and need to be relayouted? - let overflowAbove; - let overflowBelow; - const maxHeight = this._getMaxHeight(sectionIndex); - const minHeight = this._getMinHeight(sectionIndex); - // new height > max ? - if (this._heights[sectionIndex] + offset > maxHeight) { - // we're pulling downwards and constrained - // overflowAbove = minus how much are we above max height - overflowAbove = (maxHeight - this._heights[sectionIndex]) - offset; - overflowBelow = offset; - } else if (this._heights[sectionIndex] + offset < minHeight) { // new height < min? - // we're pulling upwards and constrained - overflowAbove = (minHeight - this._heights[sectionIndex]) - offset; - overflowBelow = offset; - } else { - overflowAbove = 0; - overflowBelow = offset; - } - this._heights[sectionIndex] = clamp(this._heights[sectionIndex] + offset, minHeight, maxHeight); - - // these are reassigned the amount of overflow that could not be rebalanced - // meaning we dragged the handle too far and it can't follow the cursor anymore - overflowAbove = this._rebalanceAbove(sectionIndex, overflowAbove); - overflowBelow = this._rebalanceBelow(sectionIndex, overflowBelow); - - if (!constrained) { // to avoid risk of infinite recursion - // clamp to avoid overflowing or underflowing the page - if (Math.abs(overflowAbove) > 1.0) { - // here we do the layout again with offset - the amount of space we took too much - this._relayout(sectionIndex, offset + overflowAbove, true); - return offset + overflowAbove; - } - - if (Math.abs(overflowBelow) > 1.0) { - // here we do the layout again with offset - the amount of space we took too much - this._relayout(sectionIndex, offset - overflowBelow, true); - return offset - overflowBelow; - } - } - - this._applyHeights(); - return undefined; - } - - _applyHeights() { - // apply the heights - for (let i = 0; i < this._sections.length; i++) { - const section = this._sections[i]; - this._applyHeight(section.id, this._heights[i]); - } - } - - _commitHeights() { - this._sections.forEach((section, i) => { - this._sectionHeights[section.id] = this._heights[i]; - }); - } - - _setUncommittedSectionHeight(sectionIndex, offset) { - if (Math.sign(offset) != Math.sign(this._lastOffset)) { - this._clampedOffset = undefined; - } - if (this._clampedOffset !== undefined) { - if (offset < 0 && offset < this._clampedOffset) { - return; - } - if (offset > 0 && offset > this._clampedOffset) { - return; - } - } - this._clampedOffset = this._relayout(sectionIndex, offset); - this._lastOffset = offset; - } -} - -class Handle { - constructor(layout, sectionIndex, height) { - this._layout = layout; - this._sectionIndex = sectionIndex; - this._initialHeight = height; - } - - setHeight(height) { - this._layout._setUncommittedSectionHeight( - this._sectionIndex, - height - this._initialHeight, - ); - return this; - } - - finish() { - this._layout._commitHeights(); - return this; - } -} - -export class Distributor extends FixedDistributor { - constructor(item, cfg) { - super(item); - this._handle = cfg.getLayout().openHandle(item.id); - } - - finish() { - this._handle.finish(); - } - - resize(height) { - this._handle.setHeight(height); - } -} diff --git a/src/resizer/index.js b/src/resizer/index.js index 7c4b2bd493..1fd8f4da46 100644 --- a/src/resizer/index.js +++ b/src/resizer/index.js @@ -17,5 +17,4 @@ limitations under the License. export FixedDistributor from "./distributors/fixed"; export CollapseDistributor from "./distributors/collapse"; -export RoomSubListDistributor from "./distributors/roomsublist"; export Resizer from "./resizer"; From 2441cbc9ac301770c99e9d0ce8850535e8dd56ca Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 15:22:18 -0600 Subject: [PATCH 101/179] LeftPanel2 -> LeftPanel --- res/css/_components.scss | 2 +- .../{_LeftPanel2.scss => _LeftPanel.scss} | 42 +++++++++---------- res/css/structures/_MatrixChat.scss | 2 +- res/css/views/rooms/_RoomSublist2.scss | 2 +- res/themes/light/css/_mods.scss | 4 +- .../{LeftPanel2.tsx => LeftPanel.tsx} | 36 ++++++++-------- src/components/structures/LoggedInView.tsx | 4 +- src/components/views/rooms/RoomSublist2.tsx | 2 +- .../src/usecases/create-room.js | 2 +- 9 files changed, 46 insertions(+), 50 deletions(-) rename res/css/structures/{_LeftPanel2.scss => _LeftPanel.scss} (85%) rename src/components/structures/{LeftPanel2.tsx => LeftPanel.tsx} (92%) diff --git a/res/css/_components.scss b/res/css/_components.scss index 77462ad4c1..eabdcd6843 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -11,7 +11,7 @@ @import "./structures/_GroupView.scss"; @import "./structures/_HeaderButtons.scss"; @import "./structures/_HomePage.scss"; -@import "./structures/_LeftPanel2.scss"; +@import "./structures/_LeftPanel.scss"; @import "./structures/_MainSplit.scss"; @import "./structures/_MatrixChat.scss"; @import "./structures/_MyGroups.scss"; diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel.scss similarity index 85% rename from res/css/structures/_LeftPanel2.scss rename to res/css/structures/_LeftPanel.scss index 9603731dd5..b142d6ee3d 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel.scss @@ -14,11 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367 - $tagPanelWidth: 56px; // only applies in this file, used for calculations -.mx_LeftPanel2 { +.mx_LeftPanel { background-color: $roomlist2-bg-color; min-width: 260px; max-width: 50%; @@ -26,7 +24,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations // Create a row-based flexbox for the TagPanel and the room list display: flex; - .mx_LeftPanel2_tagPanelContainer { + .mx_LeftPanel_tagPanelContainer { flex-grow: 0; flex-shrink: 0; flex-basis: $tagPanelWidth; @@ -38,15 +36,15 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations // TagPanel handles its own CSS } - &:not(.mx_LeftPanel2_hasTagPanel) { - .mx_LeftPanel2_roomListContainer { + &:not(.mx_LeftPanel_hasTagPanel) { + .mx_LeftPanel_roomListContainer { width: 100%; } } // Note: The 'room list' in this context is actually everything that isn't the tag // panel, such as the menu options, breadcrumbs, filtering, etc - .mx_LeftPanel2_roomListContainer { + .mx_LeftPanel_roomListContainer { width: calc(100% - $tagPanelWidth); background-color: $roomlist2-bg-color; @@ -54,7 +52,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations display: flex; flex-direction: column; - .mx_LeftPanel2_userHeader { + .mx_LeftPanel_userHeader { /* 12px top, 12px sides, 20px bottom (using 13px bottom to account * for internal whitespace in the breadcrumbs) */ @@ -66,7 +64,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations flex-direction: column; } - .mx_LeftPanel2_breadcrumbsContainer { + .mx_LeftPanel_breadcrumbsContainer { overflow-y: hidden; overflow-x: scroll; margin: 12px 12px 0 12px; @@ -89,7 +87,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations } } - .mx_LeftPanel2_filterContainer { + .mx_LeftPanel_filterContainer { margin-left: 12px; margin-right: 12px; @@ -99,7 +97,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations display: flex; align-items: center; - .mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton { + .mx_RoomSearch_expanded + .mx_LeftPanel_exploreButton { // Cheaty way to return the occupied space to the filter input flex-basis: 0; margin: 0; @@ -112,7 +110,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations } } - .mx_LeftPanel2_exploreButton { + .mx_LeftPanel_exploreButton { width: 28px; height: 28px; border-radius: 20px; @@ -136,7 +134,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations } } - .mx_LeftPanel2_roomListWrapper { + .mx_LeftPanel_roomListWrapper { // Create a flexbox to ensure the containing items cause appropriate overflow. display: flex; @@ -145,16 +143,16 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations min-height: 0; margin-top: 10px; // so we're not up against the search/filter - &.mx_LeftPanel2_roomListWrapper_stickyBottom { + &.mx_LeftPanel_roomListWrapper_stickyBottom { padding-bottom: 32px; } - &.mx_LeftPanel2_roomListWrapper_stickyTop { + &.mx_LeftPanel_roomListWrapper_stickyTop { padding-top: 32px; } } - .mx_LeftPanel2_actualRoomListContainer { + .mx_LeftPanel_actualRoomListContainer { flex-grow: 1; // fill the available space overflow-y: auto; width: 100%; @@ -167,26 +165,26 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations } // These styles override the defaults for the minimized (66px) layout - &.mx_LeftPanel2_minimized { + &.mx_LeftPanel_minimized { min-width: unset; // We have to forcefully set the width to override the resizer's style attribute. - &.mx_LeftPanel2_hasTagPanel { + &.mx_LeftPanel_hasTagPanel { width: calc(68px + $tagPanelWidth) !important; } - &:not(.mx_LeftPanel2_hasTagPanel) { + &:not(.mx_LeftPanel_hasTagPanel) { width: 68px !important; } - .mx_LeftPanel2_roomListContainer { + .mx_LeftPanel_roomListContainer { width: 68px; - .mx_LeftPanel2_filterContainer { + .mx_LeftPanel_filterContainer { // Organize the flexbox into a centered column layout flex-direction: column; justify-content: center; - .mx_LeftPanel2_exploreButton { + .mx_LeftPanel_exploreButton { margin-left: 0; margin-top: 8px; background-color: transparent; diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index 08ed9e5559..88b29a96e8 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -66,7 +66,7 @@ limitations under the License. } /* not the left panel, and not the resize handle, so the roomview/groupview/... */ -.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_LeftPanel2):not(.mx_ResizeHandle) { +.mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_ResizeHandle) { background-color: $primary-bg-color; flex: 1 1 0; diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 77a762b4d8..eac2aa838d 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -43,7 +43,7 @@ limitations under the License. // all works by ensuring the header text has a fixed height when sticky so the // fixed height of the container can maintain the scroll position. - // The combined height must be set in the LeftPanel2 component for sticky headers + // The combined height must be set in the LeftPanel component for sticky headers // to work correctly. padding-bottom: 8px; height: 24px; diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss index 810e0375ba..54ba7795ee 100644 --- a/res/themes/light/css/_mods.scss +++ b/res/themes/light/css/_mods.scss @@ -5,7 +5,7 @@ // it can be blurred by the tag panel and room list @supports (backdrop-filter: none) { - .mx_LeftPanel2 { + .mx_LeftPanel { background-image: var(--avatar-url); background-repeat: no-repeat; background-size: cover; @@ -16,7 +16,7 @@ backdrop-filter: blur($tagpanel-background-blur-amount); } - .mx_LeftPanel2 .mx_LeftPanel2_roomListContainer { + .mx_LeftPanel .mx_LeftPanel_roomListContainer { backdrop-filter: blur($roomlist-background-blur-amount); } } diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel.tsx similarity index 92% rename from src/components/structures/LeftPanel2.tsx rename to src/components/structures/LeftPanel.tsx index c8ab37e014..a8e763c6ab 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -36,8 +36,6 @@ import {Key} from "../../Keyboard"; import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; -// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367 - interface IProps { isMinimized: boolean; resizeNotifier: ResizeNotifier; @@ -58,7 +56,7 @@ const cssClasses = [ "mx_RoomSublist2_showNButton", ]; -export default class LeftPanel2 extends React.Component { +export default class LeftPanel extends React.Component { private listContainerRef: React.RefObject = createRef(); private tagPanelWatcherRef: string; private focusedElement = null; @@ -222,16 +220,16 @@ export default class LeftPanel2 extends React.Component { // add appropriate sticky classes to wrapper so it has // the necessary top/bottom padding to put the sticky header in - const listWrapper = list.parentElement; // .mx_LeftPanel2_roomListWrapper + const listWrapper = list.parentElement; // .mx_LeftPanel_roomListWrapper if (lastTopHeader) { - listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyTop"); + listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyTop"); } else { - listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyTop"); + listWrapper.classList.remove("mx_LeftPanel_roomListWrapper_stickyTop"); } if (firstBottomHeader) { - listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyBottom"); + listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyBottom"); } else { - listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyBottom"); + listWrapper.classList.remove("mx_LeftPanel_roomListWrapper_stickyBottom"); } } @@ -315,7 +313,7 @@ export default class LeftPanel2 extends React.Component { private renderHeader(): React.ReactNode { return ( -
+
); @@ -325,7 +323,7 @@ export default class LeftPanel2 extends React.Component { if (this.state.showBreadcrumbs && !this.props.isMinimized) { return ( @@ -337,7 +335,7 @@ export default class LeftPanel2 extends React.Component { private renderSearchExplore(): React.ReactNode { return (
{ onEnter={this.onEnter} /> @@ -359,7 +357,7 @@ export default class LeftPanel2 extends React.Component { public render(): React.ReactNode { const tagPanel = !this.state.showTagPanel ? null : ( -
+
); @@ -376,24 +374,24 @@ export default class LeftPanel2 extends React.Component { />; const containerClasses = classNames({ - "mx_LeftPanel2": true, - "mx_LeftPanel2_hasTagPanel": !!tagPanel, - "mx_LeftPanel2_minimized": this.props.isMinimized, + "mx_LeftPanel": true, + "mx_LeftPanel_hasTagPanel": !!tagPanel, + "mx_LeftPanel_minimized": this.props.isMinimized, }); const roomListClasses = classNames( - "mx_LeftPanel2_actualRoomListContainer", + "mx_LeftPanel_actualRoomListContainer", "mx_AutoHideScrollbar", ); return (
{tagPanel} -