From 7e4bbfebc62e9e3acf27235df354494bff04a9bb Mon Sep 17 00:00:00 2001 From: Marco Zehe Date: Fri, 14 Feb 2020 12:34:22 +0100 Subject: [PATCH 001/308] Don't speak the outgoing message if it is in the Sending state. Signed-off-by: Marco Zehe --- src/components/views/rooms/EventTile.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 916ddc3c5b..ba27c7c276 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -671,6 +671,9 @@ export default createReactClass({ mx_EventTile_redacted: isRedacted, }); + // If the tile is in the Sending state, don't speak the message. + const suppressSpeech = (isSending) ? "off" : undefined; + let permalink = "#"; if (this.props.permalinkCreator) { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); @@ -789,7 +792,7 @@ export default createReactClass({ case 'notif': { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); return ( -
+
{ room ? room.name : '' } @@ -815,7 +818,7 @@ export default createReactClass({ } case 'file_grid': { return ( -
+
+
{ avatar } { sender }
@@ -879,7 +882,7 @@ export default createReactClass({ ); // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( -
+
{ readAvatars }
From 7699aafcaf27c51950306451a82648d4d376be71 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 23 Jun 2020 16:41:36 +0100 Subject: [PATCH 002/308] 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 003/308] 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 004/308] 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 005/308] 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 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 006/308] 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 007/308] 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 008/308] 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 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 009/308] 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 010/308] 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 011/308] 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 012/308] 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 013/308] 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 636b0236b666fbe70442f41c7c32c8696548ed2c Mon Sep 17 00:00:00 2001 From: Marco Zehe Date: Wed, 15 Jul 2020 19:02:00 +0200 Subject: [PATCH 014/308] Update to use more strict rules for suppressing Signed-off-by: Marco Zehe --- src/components/views/rooms/EventTile.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index d8cd126964..43ab7c6b22 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -686,8 +686,7 @@ export default createReactClass({ }); // If the tile is in the Sending state, don't speak the message. - const ariaLive = (isSending) ? "off" : undefined; - const ariaBusy = (isSending) ? "true" : undefined; + const ariaLive = (this.props.eventSendStatus !== null) ? 'off' : undefined; let permalink = "#"; if (this.props.permalinkCreator) { @@ -823,7 +822,7 @@ export default createReactClass({ case 'notif': { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); return ( -
+
{ room ? room.name : '' } @@ -849,7 +848,7 @@ export default createReactClass({ } case 'file_grid': { return ( -
+
+
{ ircTimestamp } { avatar } { sender } @@ -915,7 +914,7 @@ export default createReactClass({ // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( -
+
{ ircTimestamp }
{ readAvatars } From e873ba96084e4a527157c11538e73c950043a3db Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Jul 2020 02:52:16 +0100 Subject: [PATCH 015/308] 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 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 016/308] 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 f649edd6aa6c2eec2c8800706972f1125ee9b70d Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 15 Jul 2020 12:48:10 +0000 Subject: [PATCH 017/308] Translated using Weblate (Albanian) Currently translated at 98.7% (2345 of 2377 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 1eda92480b..b36a8c00fd 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2345,5 +2345,9 @@ "Set up Secure backup": "Ujdisni kopjeruajtje të Sigurt", "Set a Security Phrase": "Caktoni një Frazë Sigurie", "Confirm Security Phrase": "Ripohoni Frazë Sigurie", - "Save your Security Key": "Ruani Kyçin tuaj të Sigurisë" + "Save your Security Key": "Ruani Kyçin tuaj të Sigurisë", + "Use your account to sign in to the latest version": "Përdorni llogarinë tuaj për të bërë hyrjen te versioni më i ri", + "We’re excited to announce Riot is now Element": "Jemi të ngazëllyer t’ju njoftojmë se Riot tanimë është Element", + "Riot is now Element!": "Riot-i tani është Element!", + "Learn More": "Mësoni Më Tepër" } From ed2fdfa2436eee259dcbfc462db74b17a37a3bed Mon Sep 17 00:00:00 2001 From: Tirifto Date: Wed, 15 Jul 2020 15:27:23 +0000 Subject: [PATCH 018/308] Translated using Weblate (Esperanto) Currently translated at 99.9% (2374 of 2377 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 157 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index c49e72a1f2..94ecda776a 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -131,7 +131,7 @@ "Not a valid %(brand)s keyfile": "Nevalida ŝlosila dosiero de %(brand)s", "Authentication check failed: incorrect password?": "Aŭtentikiga kontrolo malsukcesis: ĉu pro malĝusta pasvorto?", "Failed to join room": "Malsukcesis aliĝi al ĉambro", - "Message Pinning": "Fikso de mesaĝoj", + "Message Pinning": "Fiksado de mesaĝoj", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Montri tempindikojn en 12-hora formo (ekz. 2:30 post.)", "Always show message timestamps": "Ĉiam montri mesaĝajn tempindikojn", "Autoplay GIFs and videos": "Memfare ludi GIF-bildojn kaj filmojn", @@ -800,7 +800,7 @@ "There was an error joining the room": "Aliĝo al la ĉambro eraris", "Sorry, your homeserver is too old to participate in this room.": "Pardonon, via hejmservilo estas tro malnova por partoprenado en la ĉambro.", "Please contact your homeserver administrator.": "Bonvolu kontakti la administranton de via hejmservilo.", - "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Montri memorigilon por ŝalti Sekuran Ricevon de Mesaĝoj en ĉifrataj ĉambroj", + "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Montri memorigilon por ŝalti Sekuran ricevon de mesaĝoj en ĉifrataj ĉambroj", "Show developer tools": "Montri verkistajn ilojn", "Messages containing @room": "Mesaĝoj enhavantaj @room", "Encrypted messages in one-to-one chats": "Ĉifritaj mesaĝoj en duopaj babiloj", @@ -1357,9 +1357,9 @@ "If you don't want to set this up now, you can later in Settings.": "Se vi ne volas agordi tion nun, vi povas fari ĝin poste per agordoj.", "Don't ask again": "Ne demandi ree", "New Recovery Method": "Nova rehava metodo", - "A new recovery passphrase and key for Secure Messages have been detected.": "Novaj rehava pasfrazo kaj ŝlosilo por sekuraj mesaĝoj troviĝis.", + "A new recovery passphrase and key for Secure Messages have been detected.": "Novaj rehava pasfrazo kaj ŝlosilo por Sekuraj mesaĝoj troviĝis.", "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Se vi ne agordis la novan rehavan metodon, eble atakanto provas aliri vian konton. Vi tuj ŝanĝu la pasvorton de via konto, kaj agordu novan rehavan metodon en la agordoj.", - "Set up Secure Messages": "Agordi sekurajn mesaĝojn", + "Set up Secure Messages": "Agordi Sekurajn mesaĝojn", "Recovery Method Removed": "Rehava metodo foriĝis", "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Se vi ne forigis la rehavan metodon, eble atakanto provas aliri vian konton. Vi tuj ŝanĝu la pasvorton de via konto, kaj agordu novan rehavan metodon en la agordoj.", "Use a longer keyboard pattern with more turns": "Uzu pli longan tekston kun plia varieco", @@ -1386,7 +1386,7 @@ "Save it on a USB key or backup drive": "Konservu ĝin en poŝmemorilo aŭ savkopia disko", "Copy it to your personal cloud storage": "Kopiu ĝin al via persona enreta konservejo", "Your keys are being backed up (the first backup could take a few minutes).": "Viaj ŝlosiloj estas savkopiataj (la unua savkopio povas daŭri kelkajn minutojn).", - "Set up Secure Message Recovery": "Agordi sekuran rehavon de mesaĝoj", + "Set up Secure Message Recovery": "Agordi Sekuran rehavon de mesaĝoj", "Starting backup...": "Komencante savkopion…", "Unable to create key backup": "Ne povas krei savkopion de ŝlosiloj", "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Sen agordo de Sekura rehavo de mesaĝoj, vi perdos vian sekuran historion de mesaĝoj per adiaŭo.", @@ -2230,5 +2230,150 @@ "Upgrade your %(brand)s": "Gradaltigi vian %(brand)son", "A new version of %(brand)s is available!": "Nova versio de %(brand)s estas disponebla!", "New version available. Update now.": "Nova versio estas disponebla. Ĝisdatigu nun.", - "Emoji picker": "Elektilo de bildsignoj" + "Emoji picker": "Elektilo de bildsignoj", + "Use your account to sign in to the latest version": "Uzu vian konton por saluti la plej freŝan version", + "We’re excited to announce Riot is now Element": "Ni ekscite anoncas, ke Riot nun estas Elemento", + "Riot is now Element!": "Riot nun estas Elemento!", + "Learn More": "Eksciu plion", + "Light": "Hela", + "Dark": "Malhela", + "You joined the call": "Vi aliĝis al la voko", + "%(senderName)s joined the call": "%(senderName)s aliĝis al la voko", + "Call in progress": "Voko okazas", + "You left the call": "Vi foriris de la voko", + "%(senderName)s left the call": "%(senderName)s foriris de la voko", + "Call ended": "Voko finiĝis", + "You started a call": "Vi komencis vokon", + "%(senderName)s started a call": "%(senderName)s komencis vokon", + "Waiting for answer": "Atendante respondon", + "%(senderName)s is calling": "%(senderName)s vokas", + "You created the room": "Vi kreis la ĉambron", + "%(senderName)s created the room": "%(senderName)s kreis la ĉambron", + "You made the chat encrypted": "Vi ekĉifris la babilon", + "%(senderName)s made the chat encrypted": "%(senderName)s ekĉifris la babilon", + "You made history visible to new members": "Vi videbligis la historion al novaj anoj", + "%(senderName)s made history visible to new members": "%(senderName)s videbligis la historion al novaj anoj", + "You made history visible to anyone": "Vi videbligis la historion al ĉiu ajn", + "%(senderName)s made history visible to anyone": "%(senderName)s videbligis la historion al ĉiu ajn", + "You made history visible to future members": "Vi videbligis la historion al osaj anoj.", + "%(senderName)s made history visible to future members": "%(senderName)s videbligis la historion al osaj anoj.", + "You were invited": "Vi estis invitita", + "%(targetName)s was invited": "%(senderName)s estis invitita", + "You left": "Vi foriris", + "%(targetName)s left": "%(senderName)s foriris", + "You were kicked (%(reason)s)": "Vi estis forpelita (%(reason)s)", + "%(targetName)s was kicked (%(reason)s)": "%(targetName)s estis forpelita (%(reason)s)", + "You were kicked": "Vi estis forpelita", + "%(targetName)s was kicked": "%(targetName)s estis forpelita", + "You rejected the invite": "Vi rifuzis la inviton", + "%(targetName)s rejected the invite": "%(targetName)s rifuzis la inviton", + "You were uninvited": "Vi estis malinvitita", + "%(targetName)s was uninvited": "%(targetName)s estis malinvitita", + "You were banned (%(reason)s)": "Vi estis forbarita (%(reason)s)", + "%(targetName)s was banned (%(reason)s)": "%(targetName)s estis forbarita (%(reason)s)", + "You were banned": "Vi estis forbarita", + "%(targetName)s was banned": "%(targetName)s estis forbarita", + "You joined": "Vi aliĝis", + "%(targetName)s joined": "%(targetName)s aliĝis", + "You changed your name": "Vi ŝanĝis vian nomon", + "%(targetName)s changed their name": "%(targetName)s ŝanĝis sian nomon", + "You changed your avatar": "Vi ŝanĝis vian profilbildon", + "%(targetName)s changed their avatar": "%(targetName)s ŝanĝis sian profilbildon", + "%(senderName)s %(emote)s": "%(senderName)s %(emote)s", + "%(senderName)s: %(message)s": "%(senderName)s: %(message)s", + "You changed the room name": "Vi ŝanĝis la nomon de la ĉambro", + "%(senderName)s changed the room name": "%(senderName)s ŝanĝis la nomon de la ĉambro", + "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", + "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", + "You uninvited %(targetName)s": "Vi malinvitis uzanton %(targetName)s", + "%(senderName)s uninvited %(targetName)s": "%(senderName)s malinvitis uzanton %(targetName)s", + "You invited %(targetName)s": "Vi invitis uzanton %(targetName)s", + "%(senderName)s invited %(targetName)s": "%(senderName)s invitis uzanton %(targetName)s", + "You changed the room topic": "Vi ŝanĝis la temon de la ĉambro", + "%(senderName)s changed the room topic": "%(senderName)s ŝanĝis la temon de la ĉambro", + "Use the improved room list (will refresh to apply changes)": "Uzi la plibonigitan ĉambrobreton (aktualigos la paĝon por apliki la ŝanĝojn)", + "Use custom size": "Uzi propran grandon", + "Use a more compact ‘Modern’ layout": "Uzi pli densan »Modernan« aranĝon", + "Use a system font": "Uzi sisteman tiparon", + "System font name": "Nomo de sistema tiparo", + "Enable experimental, compact IRC style layout": "Ŝalti eksperimentan, densan IRC-ecan aranĝon", + "Unknown caller": "Nekonata vokanto", + "Incoming voice call": "Envena voĉvoko", + "Incoming video call": "Envena vidvoko", + "Incoming call": "Envena voko", + "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s ne povas sekure kaŝkopii ĉifritajn mesaĝojn loke, funkciante per foliumilo. Uzu %(brand)s Desktop por aperigi ĉifritajn mesaĝojn en serĉrezultoj.", + "There are advanced notifications which are not shown here.": "Ekzistas specialaj sciigoj, kiuj ne estas montrataj ĉi tie.", + "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Vi eble agordis ilin en kliento alia ol %(brand)s. Vi ne povas agordi ilin en %(brand)s, sed ili tamen estas aplikataj.", + "Hey you. You're the best!": "He, vi. Vi bonegas!", + "Message layout": "Aranĝo de mesaĝoj", + "Compact": "Densa", + "Modern": "Moderna", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Agordu la nomon de tiparo instalita en via sistemo kaj %(brand)s provos ĝin uzi.", + "Customise your appearance": "Adaptu vian aspekton", + "Appearance Settings only affect this %(brand)s session.": "Agordoj de aspekto nur efikos sur ĉi tiu salutaĵo de %(brand)s", + "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Aldonu uzantojn kaj servilojn, kiujn vi volas malatenti, ĉi tien. Uzu steletojn por ke %(brand)s atendu iujn ajn signojn. Ekzemple, @bot:* malatentigus ĉiujn uzantojn, kiuj havas la nomon «bot» sur ĉiu ajn servilo.", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "La administranto de via servilo malŝaltis implicitan tutvojan ĉifradon en privataj kaj rektaj ĉambroj.", + "Make this room low priority": "Doni al la ĉambro malaltan prioritaton", + "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Ĉambroj kun malalta prioritato montriĝas en aparta sekcio, en la suba parto de via ĉambrobreto,", + "The authenticity of this encrypted message can't be guaranteed on this device.": "La aŭtentikeco de ĉi tiu ĉifrita mesaĝo ne povas esti garantiita sur ĉi tiu aparato.", + "No recently visited rooms": "Neniuj freŝdate vizititaj ĉambroj", + "People": "Homoj", + "Unread rooms": "Nelegitaj ĉambroj", + "Always show first": "Ĉiam montri unuaj", + "Show": "Montri", + "Message preview": "Antaŭrigardo al mesaĝo", + "Sort by": "Ordigi laŭ", + "Activity": "Aktiveco", + "A-Z": "A–Z", + "List options": "Elektebloj pri listo", + "Show %(count)s more|other": "Montri %(count)s pliajn", + "Show %(count)s more|one": "Montri %(count)s plian", + "Use default": "Uzi implicitan", + "Mentions & Keywords": "Mencioj kaj ŝlosilvortoj", + "Notification options": "Elektebloj pri sciigoj", + "Favourited": "Elstarigita", + "Leave Room": "Foriri de ĉambro", + "Forget Room": "Forgesi ĉambron", + "Room options": "Elektebloj pri ĉambro", + "Message deleted on %(date)s": "Mesaĝo forigita je %(date)s", + "Use your account to sign in to the latest version of the app at ": "Uzu vian konton por saluti la plej freŝan version de la aplikaĵo je ", + "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at element.io/get-started.": "Vi jam estas salutinta kaj preta ĉi tie, sed vi povas ankaŭ ekhavi la plej freŝajn versiojn de la aplikaĵoj sur ĉiuj platformoj je element.io/get-started.", + "Go to Element": "Iri al Elemento", + "We’re excited to announce Riot is now Element!": "Ni estas ekscititaj anonci, ke Riot nun estas Elemento!", + "Learn more at element.io/previously-riot": "Eksciu plion je element.io/previously-riot", + "Wrong file type": "Neĝusta dosiertipo", + "Looks good!": "Ŝajnas bona!", + "Wrong Recovery Key": "Neĝusta rehava ŝlosilo", + "Invalid Recovery Key": "Nevalida rehava ŝlosilo", + "Security Phrase": "Sekureca frazo", + "Enter your Security Phrase or to continue.": "Enigu vian sekurecan frazon aŭ por daŭrigi.", + "Security Key": "Sekureca ŝlosilo", + "Use your Security Key to continue.": "Uzu vian sekurecan ŝlosilon por daŭrigi.", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "Vi povas uzi proprajn elekteblojn pri servilo por saluti aliajn servilojn de Matrix, per specifo de alia URL de hejmservilo. Tio ebligas al vi uzi la programon %(brand)s kun jama konto de Matrix je alia hejmservilo.", + "Search rooms": "Serĉi ĉambrojn", + "Switch to light mode": "Ŝalti helan reĝimon", + "Switch to dark mode": "Ŝalti malhelan reĝimon", + "Switch theme": "Ŝalti haŭton", + "Security & privacy": "Sekureco kaj privateco", + "All settings": "Ĉiuj agordoj", + "User menu": "Menuo de uzanto", + "Use Recovery Key or Passphrase": "Uzi rehavan ŝlosilon aŭ pasfrazon", + "Use Recovery Key": "Uzi rehavan ŝlosilon", + "%(brand)s Web": "%(brand)s por Reto", + "%(brand)s Desktop": "%(brand)s por Labortablo", + "%(brand)s iOS": "%(brand)s por iOS", + "%(brand)s X for Android": "%(brand)s X por Android", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Malhelpu perdon de aliro al ĉifritaj mesaĝoj kaj datumoj per savkopiado de ĉifraj ŝlosiloj al via servilo.", + "Generate a Security Key": "Generi sekurecan ŝlosilon", + "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Ni estigos sekurecan ŝlosilon, kiun vi devus konservi en sekura loko, ekzemple administrilo de pasvortoj, aŭ sekurŝranko.", + "Enter a Security Phrase": "Enigiu sekurecan frazon", + "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Uzu sekretan frazon kiun konas nur vi, kaj laŭplaĉe konservu sekurecan ŝlosilon, uzotan por savkopiado.", + "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Enigu sekurecan pasfrazon kiun konas nur vi, ĉar ĝi protektos viajn datumojn. Por esti certa, vi ne reuzu la pasvorton de via konto.", + "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Deponu vian sekurecan ŝlosilon en sekura loko, ekzemple administrilo de pasvortoj aŭ sekurŝranko, ĉar ĝi protektos viajn ĉifritajn datumojn.", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Se vi nuligos nun, vi eble perdos ĉifritajn mesaĝojn kaj datumojn se vi perdos aliron al viaj salutoj.", + "You can also set up Secure Backup & manage your keys in Settings.": "Vi ankaŭ povas agordi Sekuran savkopiadon kaj administri viajn ŝlosilojn per Agordoj.", + "Set up Secure backup": "Agordi Sekuran savkopiadon", + "Set a Security Phrase": "Agordi Sekurecan frazon", + "Confirm Security Phrase": "Konfirmi Sekurecan frazon", + "Save your Security Key": "Konservi vian Sekurecan ŝlosilon" } From 48aa2e7bb347b194cb98e54109ffc4cd0cbb4ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 15 Jul 2020 18:10:46 +0000 Subject: [PATCH 019/308] Translated using Weblate (Estonian) Currently translated at 84.0% (1996 of 2377 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 249 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 248 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 0a6837fb48..710178b32f 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -1750,5 +1750,252 @@ "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s lahkusid ja liitusid uuesti %(count)s korda", "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s lahkusid ja liitusid uuesti", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s lahkus ja liitus uuesti %(count)s korda", - "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s lahkus ja liitus uuesti" + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s lahkus ja liitus uuesti", + "To use it, just wait for autocomplete results to load and tab through them.": "Selle kasutamiseks oota, kuni automaatne sõnalõpetus laeb kõik valikud ja sa saad nad läbi lapata.", + "Bans user with given id": "Keela ligipääs antud tunnusega kasutajale", + "Unbans user with given ID": "Taasta ligipääs antud tunnusega kasutajale", + "Ignores a user, hiding their messages from you": "Eirab kasutajat peites kõik tema sõnumid sinu eest", + "Ignored user": "Eiratud kasutaja", + "You are now ignoring %(userId)s": "Sa praegu eirad kasutajat %(userId)s", + "%(senderName)s banned %(targetName)s.": "%(senderName)s keelas ligipääsu kasutajale %(targetName)s.", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s taastas ligipääsu kasutajale %(targetName)s.", + "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s võttis tagasi %(targetName)s kutse.", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s müksas kasutajat %(targetName)s.", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s muutis selle jututoa klammerdatud sõnumeid.", + "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s eemaldas kasutajate ligipääsukeelu reegli, mis vastas tingimusele %(glob)s", + "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s eemaldas jututubade ligipääsukeelu reegli, mis vastas tingimusele %(glob)s", + "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s eemaldas serverite ligipääsukeelu reegli, mis vastas tingimusele %(glob)s", + "%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s eemaldas ligipääsukeelu reegli, mis vastas tingimusele %(glob)s", + "%(senderName)s updated an invalid ban rule": "%(senderName)s uuendas vigast ligipääsukeelu reeglit", + "%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s uuendas %(reason)s põhjusel kasutajate ligipääsukeelu reeglit, mis vastas tingimusele %(glob)s", + "%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s uuendas %(reason)s põhjusel jututubade ligipääsukeelu reeglit, mis vastas tingimusele %(glob)s", + "%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s uuendas %(reason)s põhjusel serverite ligipääsukeelu reeglit, mis vastas tingimusele %(glob)s", + "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s uuendas %(reason)s põhjusel ligipääsukeelu reeglit, mis vastas tingimusele %(glob)s", + "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s määras %(reason)s tõttu kasutajate ligipääsukeelu reegli, mis vastas tingimusele %(glob)s", + "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s määras %(reason)s tõttu jututubade ligipääsukeelu reegli, mis vastas tingimusele %(glob)s", + "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s määras %(reason)s tõttu serverite ligipääsukeelu reegli, mis vastas tingimusele %(glob)s", + "%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s määras %(reason)s tõttu ligipääsukeelu reegli, mis vastas tingimusele %(glob)s", + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s muutis %(reason)s tõttu kasutajate ligipääsukeelu reegli algset tingimust %(oldGlob)s uueks tingimuseks %(newGlob)s", + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s muutis %(reason)s tõttu jututubade ligipääsukeelu reegli algset tingimust %(oldGlob)s uueks tingimuseks %(newGlob)s", + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s muutis %(reason)s tõttu serverite ligipääsukeelu reegli algset tingimust %(oldGlob)s uueks tingimuseks %(newGlob)s", + "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s muutis %(reason)s tõttu ligipääsukeelu reegli algset tingimust %(oldGlob)s uueks tingimuseks %(newGlob)s", + "The user must be unbanned before they can be invited.": "Enne kutse saatmist peab kasutajalt olema eemaldatud ligipääsukeeld.", + "Your homeserver has exceeded its user limit.": "Sinu koduserver on ületanud kasutajate arvu ülempiiri.", + "Your homeserver has exceeded one of its resource limits.": "Sinu koduserver on ületanud ühe oma ressursipiirangutest.", + "Contact your server admin.": "Võta ühendust oma serveri haldajaga.", + "Warning": "Hoiatus", + "Ok": "Sobib", + "Set password": "Määra salasõna", + "To return to your account in future you need to set a password": "Selleks, et sa saaksid tulevikus oma konto juurde tagasi pöörduda, peaksid määrama salasõna", + "You were banned (%(reason)s)": "Sinule on seatud ligipääsukeeld %(reason)s", + "%(targetName)s was banned (%(reason)s)": "%(targetName)s seati ligipääsukeeld %(reason)s", + "You were banned": "Sinule seati ligipääsukeeld", + "%(targetName)s was banned": "%(targetName)s'le seati ligipääsukeeld", + "New spinner design": "Uus vurri-moodi paigutus", + "Message Pinning": "Sõnumite klammerdamine", + "IRC display name width": "IRC kuvatava nime laius", + "Enable experimental, compact IRC style layout": "Võta kasutusele katseline, IRC-stiilis kompaktne sõnumite paigutus", + "My Ban List": "Minu poolt seatud ligipääsukeeldude loend", + "Active call (%(roomName)s)": "Käsilolev kõne (%(roomName)s)", + "Unknown caller": "Tundmatu helistaja", + "Incoming voice call": "Saabuv häälkõne", + "Incoming video call": "Saabuv videokõne", + "Incoming call": "Saabuv kõne", + "Waiting for your other session to verify…": "Ootan, et sinu teine sessioon alustaks verifitseerimist…", + "This bridge was provisioned by .": "Selle võrgusilla võttis kasutusele .", + "This bridge is managed by .": "Seda võrgusilda haldab .", + "Workspace: %(networkName)s": "Tööruum: %(networkName)s", + "Channel: %(channelName)s": "Kanal: %(channelName)s", + "Upload new:": "Lae üles uus:", + "Export E2E room keys": "Ekspordi jututubade läbiva krüptimise võtmed", + "Your homeserver does not support cross-signing.": "Sinu koduserver ei toeta risttunnustamist.", + "Cross-signing and secret storage are enabled.": "Risttunnustamine ja turvahoidla on kasutusel.", + "Connecting to integration manager...": "Ühendan lõiminguhalduriga...", + "Cannot connect to integration manager": "Ei saa ühendust lõiminguhalduriga", + "The integration manager is offline or it cannot reach your homeserver.": "Lõiminguhaldur kas ei tööta või ei õnnestu tal teha päringuid sinu koduserveri suunas.", + "Delete Backup": "Kustuta varundatud koopia", + "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Kas sa oled kindel? Kui sul muud varundust pole, siis kaotad ligipääsu oma krüptitud sõnumitele.", + "Your keys are not being backed up from this session.": "Sinu selle sessiooni krüptovõtmeid ei varundata.", + "Back up your keys before signing out to avoid losing them.": "Vältimaks nende kaotamist, varunda krüptovõtmed enne väljalogimist.", + "Advanced notification settings": "Teavituste lisaseadistused", + "Enable desktop notifications for this session": "Võta selleks sessiooniks kasutusele töölauakeskkonnale omased teavitused", + "Show message in desktop notification": "Näita sõnumit töölauakeskkonnale omases teavituses", + "Enable audible notifications for this session": "Võta selleks sessiooniks kasutusele kuuldavad teavitused", + "Off": "Välja lülitatud", + "On": "Kasutusel", + "Noisy": "Jutukas", + "Upgrade to your own domain": "Võta kasutusele oma domeen", + "Error encountered (%(errorDetail)s).": "Tekkis viga (%(errorDetail)s).", + "Checking for an update...": "Kontrollin uuenduste olemasolu...", + "No update available.": "Uuendusi pole saadaval.", + "New version available. Update now.": "Saadaval on uus versioon. Uuenda nüüd.", + "Check for update": "Kontrolli uuendusi", + "Hey you. You're the best!": "Hei sina. Sa oled parim!", + "Size must be a number": "Suurus peab olema number", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Kohandatud fondisuurus peab olema vahemikus %(min)s pt ja %(max)s pt", + "Use between %(min)s pt and %(max)s pt": "Kasuta suurust vahemikus %(min)s pt ja %(max)s pt", + "Message layout": "Sõnumite paigutus", + "Compact": "Kompaktne", + "Modern": "Moodne", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Vali sinu seadmes leiduv fondi nimi ning %(brand)s proovib seda kasutada.", + "Customise your appearance": "Kohenda välimust", + "Appearance Settings only affect this %(brand)s session.": "Välimuse kohendused kehtivad vaid selles %(brand)s'i sessioonis.", + "Legal": "Juriidiline teave", + "Credits": "Tänuavaldused", + "Bug reporting": "Vigadest teatamine", + "Clear cache and reload": "Tühjenda puhver ja lae uuesti", + "FAQ": "Korduma kippuvad küsimused", + "Versions": "Versioonid", + "%(brand)s version:": "%(brand)s'i versioon:", + "olm version:": "olm'i versioon:", + "Homeserver is": "Koduserver on", + "Identity Server is": "Isikutuvastusserver on", + "Access Token:": "Pääsuluba:", + "click to reveal": "kuvamiseks klõpsi siin", + "Labs": "Katsed", + "Customise your experience with experimental labs features. Learn more.": "Sa võid täiendada oma kasutuskogemust katseliste funktsionaalsusetega. Vaata lisateavet.", + "Ignored/Blocked": "Eiratud või ligipääs blokeeritud", + "Error adding ignored user/server": "Viga eiratud kasutaja või serveri lisamisel", + "Something went wrong. Please try again or view your console for hints.": "Midagi läks valesti. Proovi uuesti või otsi lisavihjeid konsoolilt.", + "Error subscribing to list": "Viga loendiga liitumisel", + "Please verify the room ID or address and try again.": "Palun kontrolli, kas jututoa tunnus või aadress on õiged ja proovi uuesti.", + "Error removing ignored user/server": "Viga eiratud kasutaja või serveri eemaldamisel", + "Error unsubscribing from list": "Viga loendist lahkumisel", + "Please try again or view your console for hints.": "Palun proovi uuesti või otsi lisavihjeid konsoolilt.", + "None": "Ei midagi", + "Ban list rules - %(roomName)s": "Ligipääsukeelu reeglid - %(roomName)s", + "Server rules": "Serveri kasutustingimused", + "User rules": "Kasutajaga seotud tingimused", + "You have not ignored anyone.": "Sa ei ole veel kedagi eiranud.", + "You are currently ignoring:": "Hetkel eiratavate kasutajate loend:", + "You are not subscribed to any lists": "Sa ei ole liitunud ühegi loendiga", + "Unsubscribe": "Lõpeta liitumine", + "View rules": "Näita reegleid", + "You are currently subscribed to:": "Sa oled hetkel liitunud:", + "Ignored users": "Eiratud kasutajad", + "⚠ These settings are meant for advanced users.": "⚠ Need seadistused on mõeldud kogenud kasutajatele.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Kasutajate eiramine toimub ligipääsukeelu reeglite loendite alusel ning seal on kirjas blokeeritavad kasutajad, jututoad või serverid. Sellise loendi kasutusele võtmine tähendab et blokeeritud kasutajad või serverid ei ole sulle nähtavad.", + "Personal ban list": "Minu isiklik ligipääsukeelu reeglite loend", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Sinu isiklikus ligipääsukeelu reeglite loendis on kasutajad ja serverid, kellelt sa ei soovi sõnumeid saada. Peale esimese kasutaja või serveri blokeerimist tekib sinu jututubade loendisse uus jututuba \"Minu isiklik ligipääsukeelu reeglite loend\" ning selle jõustamiseks ära logi nimetatud jututoast välja.", + "Subscribing to a ban list will cause you to join it!": "Ligipääsukeelu reeglite loendi tellimine tähendab sellega liitumist!", + "Room ID or address of ban list": "Ligipääsukeelu reeglite loendi jututoa tunnus või aadress", + "Show tray icon and minimize window to it on close": "Näita süsteemisalve ikooni ja Element'i akna sulgemisel minimeeri ta salve", + "Read Marker off-screen lifetime (ms)": "Lugemise markeri iga, kui Element pole fookuses (ms)", + "Make this room low priority": "Muuda see jututuba vähetähtsaks", + "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Vähetähtsad jututoad kuvatakse jututubade loendi lõpus omaette grupina", + "Failed to unban": "Ligipääsu taastamine ei õnnestunud", + "Unban": "Taasta ligipääs", + "Banned by %(displayName)s": "Ligipääs on keelatud %(displayName)s poolt", + "Error changing power level requirement": "Viga õiguste taseme nõuete muutmisel", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Jututoa õiguste taseme nõuete muutmisel tekkis viga. Kontrolli, et sul on selleks piisavalt õigusi ja proovi uuesti.", + "Error changing power level": "Viga õiguste muutmisel", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Kasutaja õiguste muutmisel tekkis viga. Kontrolli, et sul on selleks piisavalt õigusi ja proovi uuesti.", + "To link to this room, please add an address.": "Sellele jututoale viitamiseks palun lisa talle aadress.", + "Discovery options will appear once you have added an email above.": "Otsinguvõimaluste loend kuvatakse, kui oled ülale sisestanud e-posti aadressi.", + "Discovery options will appear once you have added a phone number above.": "Otsinguvõimaluste loend kuvatakse, kui oled ülale sisestanud telefoninumbri.", + "Mod": "Moderaator", + "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Võtmete jagamise päringud saadetakse sinu teistele sessioonidele automaatselt. Kui sa oled mõnes muus sessioonis võtmete jagamise päringud tagasi lükanud või tühistanud, siis klõpsi siia võtmete uuesti pärimiseks selle sessiooni jaoks.", + "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Kui sinu muudel sesioonidel pole selle sõnumi jaoks võtmeid, siis nad ei suuda ka sõnumit dekrüptida.", + "Key request sent.": "Võtmete jagamise päring on saadetud.", + "Re-request encryption keys from your other sessions.": "Küsi oma muudest sessioonidest krüptimisvõtmed uuesti.", + "The authenticity of this encrypted message can't be guaranteed on this device.": "Selle krüptitud sõnumi autentsus pole selles seadmes tagatud.", + "and %(count)s others...|other": "ja %(count)s muud...", + "and %(count)s others...|one": "ja üks muu...", + "Invited": "Kutsutud", + "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (õigused %(powerLevelNumber)s)", + "Emoji picker": "Emoji'de valija", + "No pinned messages.": "Klammerdatud sõnumeid ei ole.", + "Loading...": "Laen...", + "Pinned Messages": "Klammerdatud sõnumid", + "Unpin Message": "Eemalda sõnumi klammerdus", + "No recently visited rooms": "Hiljuti külastatud jututubasid ei leidu", + "Create room": "Loo jututuba", + "People": "Inimesed", + "Unread rooms": "Lugemata jututoad", + "Always show first": "Näita alati esimisena", + "Error creating address": "Viga aadressi loomisel", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Aadressi loomisel tekkis viga. See kas on serveri poolt keelatud või tekkis ajutine tõrge.", + "You don't have permission to delete the address.": "Sinul pole õigusi selle aadressi kustutamiseks.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Selle aadressi kustutamisel tekkis viga. See kas juba on kustutatud või tekkis ajutine tõrge.", + "Error removing address": "Viga aadresi kustutamisel", + "This room has no local addresses": "Sellel jututoal puudub kohalik aadress", + "Local address": "Kohalik aadress", + "Published Addresses": "Avaldatud aadressid", + "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Avaldatud aadresse saab mis iganes serveri kasutaja pruukida sinu jututoaga liitumiseks. Aadressi avaldamiseks peab ta alustuseks olema määratud kohaliku aadressina.", + "Other published addresses:": "Muud avaldatud aadressid:", + "No other published addresses yet, add one below": "Ühtegi muud aadressi pole veel avaldatud, lisa üks alljärgnevalt", + "Local Addresses": "Kohalikud aadressid", + "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Et muud kasutajad saaks seda jututuba leida sinu koduserveri (%(localDomain)s) kaudu, lisa sellele jututoale aadresse", + "Invalid community ID": "Vigane kogukonna tunnus", + "'%(groupId)s' is not a valid community ID": "'%(groupId)s' ei ole korrektne kogukonna tunnus", + "Direct message": "Otsevestlus", + "Demote yourself?": "Kas vähendad enda õigusi?", + "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.": "Kuna sa vähendad enda õigusi, siis sul ei pruugi enam olla võimalik seda muutust tagasi pöörata. Kui sa juhtumisi oled viimane haldusõigustega kasutaja jututoas, siis hiljem on võimatu samu õigusi tagasi saada.", + "Demote": "Vähenda enda õigusi", + "Ban": "Keela ligipääs", + "Unban this user?": "Kas taastame selle kasutaja ligipääsu?", + "Ban this user?": "Kas keelame selle kasutaja ligipääsu?", + "Failed to ban user": "Kasutaja ligipääsu keelamine ei õnnestunud", + "Almost there! Is your other session showing the same shield?": "Peaaegu valmis! Kas sinu teises sessioonis kuvatakse sama kilpi?", + "Almost there! Is %(displayName)s showing the same shield?": "Peaaegu valmis! Kas %(displayName)s kuvab sama kilpi?", + "Yes": "Jah", + "Verify all users in a room to ensure it's secure.": "Tagamaks, et jututuba on turvaline, verifitseeri kõik selle kasutajad.", + "You've successfully verified your device!": "Sinu seadme verifitseerimine oli edukas!", + "You've successfully verified %(deviceName)s (%(deviceId)s)!": "Sa oled edukalt verifitseerinud seadme %(deviceName)s (%(deviceId)s)!", + "You've successfully verified %(displayName)s!": "Sa oled edukalt verifitseerinud kasutaja %(displayName)s!", + "Verified": "Verifitseeritud", + "Got it": "Selge lugu", + "Start verification again from the notification.": "Alusta verifitseerimist uuesti teavitusest.", + "Message deleted": "Sõnum on kustutatud", + "Message deleted by %(name)s": "%(name)s kustutas sõnumi", + "Message deleted on %(date)s": "Sõnum on kustutatud %(date)s", + "Add an Integration": "Lisa lõiming", + "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?": "Sind juhatatakse kolmanda osapoole veebisaiti, kus sa saad autentida oma kontoga %(integrationsUrl)s kasutamiseks. Kas sa soovid jätkata?", + "Categories": "Kategooriad", + "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s lükkasid tagasi oma kutse %(count)s korda", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s lükkasid tagasi oma kutse", + "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s lükkas tagasi oma kutse %(count)s korda", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s lükkas taagasi oma kutse", + "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)s kutse võeti tagasi %(count)s korda", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "Kasutajate %(severalUsers)s kutse võeti tagasi", + "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)s kutse võeti tagasi %(count)s korda", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "Kasutaja %(oneUser)s kutse võeti tagasi", + "were banned %(count)s times|other": "said ligipääsukeelu %(count)s korda", + "were banned %(count)s times|one": "said ligipääsukeelu", + "was banned %(count)s times|other": "sai ligipääsukeelu %(count)s korda", + "was banned %(count)s times|one": "sai ligipääsukeelu", + "were unbanned %(count)s times|other": "taastati ligipääs %(count)s korda", + "were unbanned %(count)s times|one": "taastati ligipääs", + "was unbanned %(count)s times|other": "taastati ligipääs %(count)s korda", + "was unbanned %(count)s times|one": "taastati ligipääs", + "were kicked %(count)s times|other": "müksati välja %(count)s korda", + "were kicked %(count)s times|one": "müksati välja", + "was kicked %(count)s times|other": "müksati välja %(count)s korda", + "was kicked %(count)s times|one": "müksati välja", + "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s ei teinud muudatusi %(count)s korda", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s ei teinud muudatusi", + "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s ei teinud muutusi %(count)s korda", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s ei teinud muudatusi", + "Power level": "Õiguste tase", + "Custom level": "Kohandatud õigused", + "QR Code": "QR kood", + "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Ei ole võimalik laadida seda sündmust, millele vastus on tehtud - teda kas pole olemas või sul pole õigusi seda näha.", + "Room address": "Jututoa aadress", + "Some characters not allowed": "Mõned tähemärgid ei ole siin lubatud", + "Please provide a room address": "Palun kirjuta jututoa aadress", + "This address is available to use": "See aadress on kasutatav", + "This address is already in use": "See aadress on juba kasutusel", + "Room directory": "Jututubade loend", + "Sign in with single sign-on": "Logi sisse ühekordse sisselogimise abil", + "And %(count)s more...|other": "Ja %(count)s muud...", + "ex. @bob:example.com": "näiteks @kati:mingidomeen.com", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Kui sul leidub lisateavet, mis võis selle vea analüüsimisel abiks olla, siis palun lisa need ka siia - näiteks mida sa vea tekkimise hetkel tegid, jututoa tunnus, kasutajate tunnused, jne.", + "Send logs": "Saada logikirjed", + "Unable to load commit detail: %(msg)s": "Ei õnnestu laadida sõnumi lisateavet: %(msg)s", + "Unavailable": "Ei ole saadaval", + "Changelog": "Versioonimuudatuste loend", + "You cannot delete this message. (%(code)s)": "Sa ei saa seda sõnumit kustutada. (%(code)s)", + "Navigate recent messages to edit": "Muutmiseks liigu viimaste sõnumite juurde", + "Move autocomplete selection up/down": "Liiguta automaatse sõnalõpetuse valikut üles või alla", + "Cancel autocomplete": "Lülita automaatne sõnalõpetus välja" } From b168ed90776dbdc79d43e9dcc8f4350fed01ec14 Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 15 Jul 2020 15:10:46 +0000 Subject: [PATCH 020/308] Translated using Weblate (Galician) Currently translated at 100.0% (2377 of 2377 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 58 +++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 386b78a61f..ac2ae790e8 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -273,7 +273,7 @@ "Leave room": "Deixar a sala", "Favourite": "Favorita", "Guests cannot join this room even if explicitly invited.": "As convidadas non se poden unir a esta sala ainda que fosen convidadas explicitamente.", - "Click here to fix": "Pulse aquí para solución", + "Click here to fix": "Preme aquí para solucionar", "Who can access this room?": "Quen pode acceder a esta sala?", "Only people who have been invited": "Só persoas que foron convidadas", "Anyone who knows the room's link, apart from guests": "Calquera que coñeza o enderezo da sala, aparte das convidadas", @@ -494,7 +494,7 @@ "%(inviter)s has invited you to join this community": "%(inviter)s convidoute a entrar nesta comunidade", "You are an administrator of this community": "Administras esta comunidade", "You are a member of this community": "É membro desta comunidade", - "Your community hasn't got a Long Description, a HTML page to show to community members.
Click here to open settings and give it one!": "A súa comunidade non ten unha descrición longa, ou unha páxina HTML que lle mostrar aos seus participantes.
Pulse aquí para abrir os axustes e publicar unha!", + "Your community hasn't got a Long Description, a HTML page to show to community members.
Click here to open settings and give it one!": "A túa comunidade non ten unha descrición longa, ou unha páxina HTML que lle mostrar ás participantes.
Preme aquí para abrir os axustes e publicar unha!", "Long Description (HTML)": "Descrición longa (HTML)", "Description": "Descrición", "Community %(groupId)s not found": "Non se atopou a comunidade %(groupId)s", @@ -530,11 +530,11 @@ "Room": "Sala", "Failed to reject invite": "Fallo ao rexeitar o convite", "Fill screen": "Encher pantalla", - "Click to unmute video": "Pulse para escoitar vídeo", - "Click to mute video": "Pulse para acalar video", - "Click to unmute audio": "Pulse para escoitar audio", - "Click to mute audio": "Pulse para acalar audio", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Intentouse cargar un punto concreto do historial desta sala, pero non ten permiso para ver a mensaxe en cuestión.", + "Click to unmute video": "Preme para escoitar vídeo", + "Click to mute video": "Preme para acalar video", + "Click to unmute audio": "Preme para escoitar audio", + "Click to mute audio": "Preme para acalar audio", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Intentouse cargar un punto concreto do historial desta sala, pero non tes permiso para ver a mensaxe en cuestión.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Intentouse cargar un punto específico do historial desta sala, pero non se puido atopar.", "Failed to load timeline position": "Fallo ao cargar posición da liña temporal", "Uploading %(filename)s and %(count)s others|other": "Subindo %(filename)s e %(count)s máis", @@ -567,7 +567,7 @@ "Profile": "Perfil", "Account": "Conta", "Access Token:": "Testemuño de acceso:", - "click to reveal": "pulse para revelar", + "click to reveal": "Preme para mostrar", "Homeserver is": "O servidor de inicio é", "Identity Server is": "O servidor de identidade é", "%(brand)s version:": "versión %(brand)s:", @@ -1254,7 +1254,7 @@ "Butterfly": "Bolboreta", "Flower": "Flor", "Tree": "Árbore", - "Cactus": "Cactus", + "Cactus": "Cacto", "Mushroom": "Cogomelo", "Globe": "Globo", "Moon": "Lúa", @@ -1507,7 +1507,7 @@ "Room version": "Versión da sala", "Room version:": "Versión da sala:", "Developer options": "Opcións desenvolvemento", - "Open Devtools": "Open Devtools", + "Open Devtools": "Abrir Devtools", "This room is bridging messages to the following platforms. Learn more.": "Esta sala está enviando mensaxes ás seguintes plataformas. Coñece máis.", "This room isn’t bridging messages to any platforms. Learn more.": "Esta sala non está enviando mensaxes a outras plataformas. Saber máis.", "Bridges": "Pontes", @@ -2232,7 +2232,7 @@ "Autocomplete": "Autocompletado", "Alt": "Alt", "Alt Gr": "Alt Gr", - "Shift": "Maiús.", + "Shift": "Maiús", "Super": "Super", "Ctrl": "Ctrl", "Toggle Bold": "Activa Resaltar", @@ -2344,5 +2344,39 @@ "Set up Secure backup": "Configurar Copia de apoio Segura", "Set a Security Phrase": "Establece a Frase de Seguridade", "Confirm Security Phrase": "Confirma a Frase de Seguridade", - "Save your Security Key": "Garda a Chave de Seguridade" + "Save your Security Key": "Garda a Chave de Seguridade", + "Use your account to sign in to the latest version": "Usa a túa conta para conectarte á última versión", + "We’re excited to announce Riot is now Element": "Emociónanos anunciar que Riot agora chámase Element", + "Riot is now Element!": "Riot agora é Element!", + "Learn More": "Saber máis", + "Enable experimental, compact IRC style layout": "Activar o estilo experimental IRC compacto", + "Unknown caller": "Descoñecido", + "Incoming voice call": "Chamada de voz entrante", + "Incoming video call": "Chamada de vídeo entrante", + "Incoming call": "Chamada entrante", + "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s non pode por na caché local de xeito as mensaxes cifradas cando usa un navegador web. Usa %(brand)s Desktop para que as mensaxes cifradas aparezan nos resultados.", + "There are advanced notifications which are not shown here.": "Existen notificacións avanzadas que non aparecen aquí.", + "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Deberialas ter configurado nun cliente diferente de %(brand)s. Non podes modificalas en %(brand)s pero aplícanse igualmente.", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Escolle unha das tipografías instaladas no teu sistema e %(brand)s intentará utilizalas.", + "Make this room low priority": "Marcar a sala como de baixa prioridade", + "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "As salas de baixa prioridade aparecen abaixo na lista de salas, nunha sección dedicada no final da lista", + "Use default": "Usar por omisión", + "Mentions & Keywords": "Mencións e Palabras chave", + "Notification options": "Opcións de notificación", + "Favourited": "Con marca de Favorita", + "Forget Room": "Esquecer sala", + "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Se a outra versión de %(brand)s aínda está aberta noutra lapela, péchaa xa que usar %(brand)s no mesmo servidor con carga preguiceira activada e desactivada ao mesmo tempo causará problemas.", + "Use your account to sign in to the latest version of the app at ": "Usa a túa conta para conectarte á última versión da app en ", + "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at element.io/get-started.": "Xa estás conectada e podes ir alá, pero tamén podes ir á última versión da app en tódalas plataformas en element.io/get-started.", + "Go to Element": "Ir a Element", + "We’re excited to announce Riot is now Element!": "Emociónanos comunicar que Riot agora é Element!", + "Learn more at element.io/previously-riot": "Coñece máis en element.io/previously-riot", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "Podes usar as opcións de servidor personalizado para conectar con outros servidores Matrix indicando un URL diferente para o servidor. Poderás usar %(brand)s cunha conta Matrix existente noutro servidor de inicio.", + "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of element.io.": "Escribe a localización do teu servidor Element Matrix Services. Podería ser o teu propio dominio ou un subdominio de element.io.", + "Search rooms": "Buscar salas", + "User menu": "Menú de usuaria", + "%(brand)s Web": "Web %(brand)s", + "%(brand)s Desktop": "%(brand)s Desktop", + "%(brand)s iOS": "%(brand)s iOS", + "%(brand)s X for Android": "%(brand)s X para Android" } From 8ec3b7583aa1dfde2887087a1a412452c26644c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= Date: Wed, 15 Jul 2020 17:46:10 +0000 Subject: [PATCH 021/308] Translated using Weblate (Hungarian) Currently translated at 99.7% (2369 of 2377 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index b983926d3d..05abf22049 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -145,7 +145,7 @@ "Invites": "Meghívók", "Invites user with given id to current room": "A megadott azonosítójú felhasználó meghívása a jelenlegi szobába", "Sign in with": "Belépés ezzel:", - "Join as voice or video.": "Csatlakozás hanggal vagy videóval.", + "Join as voice or video.": "Csatlakozás hanggal vagy videóval.", "Join Room": "Belépés a szobába", "%(targetName)s joined the room.": "%(targetName)s belépett a szobába.", "Jump to first unread message.": "Ugrás az első olvasatlan üzenetre.", @@ -266,7 +266,7 @@ "Users": "Felhasználók", "Verification Pending": "Ellenőrzés függőben", "Verified key": "Ellenőrzött kulcs", - "Video call": "Videó hívás", + "Video call": "Videóhívás", "Voice call": "Hang hívás", "VoIP conference finished.": "VoIP konferencia befejeződött.", "VoIP conference started.": "VoIP konferencia elkezdődött.", @@ -1021,7 +1021,7 @@ "Deactivating your account is a permanent action - be careful!": "A fiók felfüggesztése végleges - légy óvatos!", "For help with using %(brand)s, click here.": "A %(brand)s használatában való segítséghez kattints ide.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "A %(brand)s használatában való segítségér kattints ide vagy kezdj beszélgetni a botunkkal az alábbi gombra kattintva.", - "Help & About": "Segítség & Névjegy", + "Help & About": "Súgó és névjegy", "Bug reporting": "Hiba bejelentése", "FAQ": "GYIK", "Versions": "Verziók", @@ -1032,7 +1032,7 @@ "Autocomplete delay (ms)": "Automatikus kiegészítés késleltetése (ms)", "Roles & Permissions": "Szerepek & Jogosultságok", "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "A üzenetek olvashatóságának változtatása csak az új üzenetekre lesz érvényes. A régi üzenetek láthatósága nem fog változni.", - "Security & Privacy": "Biztonság & Adatvédelem", + "Security & Privacy": "Biztonság és adatvédelem", "Encryption": "Titkosítás", "Once enabled, encryption cannot be disabled.": "Ha egyszer bekapcsolod, már nem lehet kikapcsolni.", "Encrypted": "Titkosítva", @@ -1041,7 +1041,7 @@ "Key backup": "Kulcsok biztonsági mentése", "Missing media permissions, click the button below to request.": "Hiányzó média jogosultságok, kattints a gombra alul a jogok megadásához.", "Request media permissions": "Média jogosultságok megkérése", - "Voice & Video": "Hang & Videó", + "Voice & Video": "Hang és videó", "Main address": "Fő cím", "Room avatar": "Szoba képe", "Room Name": "Szoba neve", @@ -1445,7 +1445,7 @@ "Remove %(email)s?": "%(email)s törlése?", "Remove %(phone)s?": "%(phone)s törlése?", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A szöveges üzenetet elküldtük a +%(msisdn)s számra. Kérlek add meg az ellenőrző kódot amit tartalmazott.", - "Command Help": "Parancs Segítség", + "Command Help": "Parancsok súgója", "No identity server is configured: add one in server settings to reset your password.": "Nincs azonosítási szerver beállítva: adj meg egyet a szerver beállításoknál, hogy a jelszavadat vissza tudd állítani.", "This account has been deactivated.": "Ez a fiók zárolva van.", "You do not have the required permissions to use this command.": "A parancs használatához nincs meg a megfelelő jogosultságod.", @@ -1732,7 +1732,7 @@ "Language Dropdown": "Nyelvválasztó lenyíló menü", "Country Dropdown": "Ország lenyíló menü", "The message you are trying to send is too large.": "Túl nagy képet próbálsz elküldeni.", - "Help": "Segítség", + "Help": "Súgó", "Show more": "Mutass többet", "Recent Conversations": "Legújabb Beszélgetések", "Direct Messages": "Közvetlen Beszélgetések", @@ -2045,15 +2045,15 @@ "Shift": "Shift", "Super": "Super", "Ctrl": "Ctrl", - "Toggle Bold": "Félkövér váltása", - "Toggle Italics": "Dőlt váltása", - "Toggle Quote": "Idézet váltása", + "Toggle Bold": "Félkövér be/ki", + "Toggle Italics": "Dőlt be/ki", + "Toggle Quote": "Idézet be/ki", "New line": "Új sor", "Navigate recent messages to edit": "Friss üzenetekben navigálás a szerkesztéshez", "Jump to start/end of the composer": "Az üzenet elejére/végére ugrás a szerkesztőben", "Navigate composer history": "A szerkesztő korábbi üzeneteiben navigálás", - "Toggle microphone mute": "Mikrofon némítás váltása", - "Toggle video on/off": "Videó ki-/bekapcsolás váltása", + "Toggle microphone mute": "Mikrofon némítás be/ki", + "Toggle video on/off": "Videó be/ki", "Jump to room search": "A szoba keresésre ugrás", "Navigate up/down in the room list": "A szoba listában fel/le navigál", "Select room from the room list": "Szoba kiválasztása a szoba listából", @@ -2063,11 +2063,11 @@ "Scroll up/down in the timeline": "Az idővonalon görgetés fel/le", "Previous/next unread room or DM": "Előző/következő olvasatlan szoba vagy közvetlen üzenet", "Previous/next room or DM": "Előző/következő szoba vagy közvetlen üzenet", - "Toggle the top left menu": "Bal felső menü ki-/bekapcsolása", + "Toggle the top left menu": "Bal felső menü be/ki", "Close dialog or context menu": "Párbeszédablak vagy menü bezárása", "Activate selected button": "Kiválasztott gomb aktiválása", - "Toggle right panel": "Jobb oldali panel váltása", - "Toggle this dialog": "Ennek a párbeszédablaknak a váltása", + "Toggle right panel": "Jobb oldali panel be/ki", + "Toggle this dialog": "E párbeszédablak be/ki", "Move autocomplete selection up/down": "Automatikus kiegészítés kijelölésének mozgatása fel/le", "Cancel autocomplete": "Automatikus kiegészítés megszakítása", "Page Up": "Page Up", From 2aa11e152f675f7eac84971ee07ed7f3cd8251df Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 15 Jul 2020 19:19:21 +0000 Subject: [PATCH 022/308] Translated using Weblate (Hungarian) Currently translated at 99.7% (2369 of 2377 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 05abf22049..76e0f9f3bf 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2343,5 +2343,32 @@ "Set up Secure backup": "Biztonsági mentés beállítása", "Set a Security Phrase": "Biztonsági Jelmondat beállítása", "Confirm Security Phrase": "Biztonsági Jelmondat megerősítése", - "Save your Security Key": "Ments el a Biztonsági Kulcsodat" + "Save your Security Key": "Ments el a Biztonsági Kulcsodat", + "Use your account to sign in to the latest version": "A legutolsó verzióba való bejelentkezéshez használd a fiókodat", + "We’re excited to announce Riot is now Element": "Izgatottan jelentjük, hogy a Riot mostantól Element", + "Riot is now Element!": "Riot mostantól Element!", + "Learn More": "Tudj meg többet", + "Enable experimental, compact IRC style layout": "Egyszerű (kísérleti) IRC stílusú kinézet engedélyezése", + "Unknown caller": "Ismeretlen hívó", + "Incoming voice call": "Bejövő hanghívás", + "Incoming video call": "Bejövő videó hívás", + "Incoming call": "Bejövő hívás", + "There are advanced notifications which are not shown here.": "Vannak haladó értesítési beállítások amik itt nem jelennek meg.", + "Appearance Settings only affect this %(brand)s session.": "A megjelenítési beállítások csak erre az %(brand)s munkamenetre lesznek érvényesek.", + "Make this room low priority": "Ez a szoba legyen alacsony prioritású", + "Use default": "Alapértelmezett használata", + "Mentions & Keywords": "Megemlítések és kulcsszavak", + "Notification options": "Értesítési beállítások", + "Favourited": "Kedvencek", + "Forget Room": "Szoba elfelejtése", + "Use your account to sign in to the latest version of the app at ": "Az alkalmazás legutolsó verzióba való bejelentkezéshez itt használd a fiókodat", + "Go to Element": "Irány az Element", + "We’re excited to announce Riot is now Element!": "Izgatottan jelentjük, hogy a Riot mostantól Element!", + "Learn more at element.io/previously-riot": "Tudj meg többet: element.io/previously-riot", + "Search rooms": "Szobák keresése", + "User menu": "Felhasználói menü", + "%(brand)s Web": "%(brand)s Web", + "%(brand)s Desktop": "Asztali %(brand)s", + "%(brand)s iOS": "%(brand)s iOS", + "%(brand)s X for Android": "%(brand)s X Android" } From 2766ab114cd547504cb9aaa1ebb7f7dea8d22e3b Mon Sep 17 00:00:00 2001 From: random Date: Wed, 15 Jul 2020 14:38:12 +0000 Subject: [PATCH 023/308] Translated using Weblate (Italian) Currently translated at 100.0% (2377 of 2377 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 71 +++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 4d6fff078d..d2ceaa98c6 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -193,8 +193,8 @@ "Room Colour": "Colore della stanza", "Active call (%(roomName)s)": "Chiamata attiva (%(roomName)s)", "unknown caller": "Chiamante sconosciuto", - "Incoming voice call from %(name)s": "Chiamata vocale in arrivo da %(name)s", - "Incoming video call from %(name)s": "Chiamata video in arrivo da %(name)s", + "Incoming voice call from %(name)s": "Telefonata in arrivo da %(name)s", + "Incoming video call from %(name)s": "Videochiamata in arrivo da %(name)s", "Incoming call from %(name)s": "Chiamata in arrivo da %(name)s", "Decline": "Rifiuta", "Accept": "Accetta", @@ -254,8 +254,8 @@ "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (poteri %(powerLevelNumber)s)", "Attachment": "Allegato", "Hangup": "Riaggancia", - "Voice call": "Chiamata vocale", - "Video call": "Chiamata video", + "Voice call": "Telefonata", + "Video call": "Videochiamata", "Upload file": "Invia file", "Send an encrypted reply…": "Invia una risposta criptata…", "Send an encrypted message…": "Invia un messaggio criptato…", @@ -1665,10 +1665,10 @@ "Ignored/Blocked": "Ignorati/Bloccati", "Verification Request": "Richiesta verifica", "Match system theme": "Usa il tema di sistema", - "%(senderName)s placed a voice call.": "%(senderName)s ha effettuato una chiamata vocale.", - "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s ha effettuato una chiamata vocale. (non supportata da questo browser)", - "%(senderName)s placed a video call.": "%(senderName)s ha effettuato una videochiamata.", - "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s ha effettuato una videochiamata. (non supportata da questo browser)", + "%(senderName)s placed a voice call.": "%(senderName)s ha iniziato una telefonata.", + "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s ha iniziato una telefonata. (non supportata da questo browser)", + "%(senderName)s placed a video call.": "%(senderName)s ha iniziato una videochiamata.", + "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s ha iniziato una videochiamata. (non supportata da questo browser)", "Clear notifications": "Cancella le notifiche", "Customise your experience with experimental labs features. Learn more.": "Personalizza la tua esperienza con funzionalità sperimentali. Maggiori informazioni.", "Error upgrading room": "Errore di aggiornamento stanza", @@ -2328,5 +2328,58 @@ "New spinner design": "Nuovo design dello spinner", "Use a more compact ‘Modern’ layout": "Usa un layout più compatto e moderno", "Always show first": "Mostra sempre per prime", - "Message deleted on %(date)s": "Messaggio eliminato il %(date)s" + "Message deleted on %(date)s": "Messaggio eliminato il %(date)s", + "Use your account to sign in to the latest version": "Usa il tuo account per accedere alla versione più recente", + "We’re excited to announce Riot is now Element": "Siamo entusiasti di annunciare che Riot ora si chiama Element", + "Riot is now Element!": "Riot ora si chiama Element!", + "Learn More": "Maggiori info", + "Enable experimental, compact IRC style layout": "Attiva il layout in stile IRC, sperimentale e compatto", + "Unknown caller": "Chiamante sconosciuto", + "Incoming voice call": "Telefonata in arrivo", + "Incoming video call": "Videochiamata in arrivo", + "Incoming call": "Chiamata in arrivo", + "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s non può tenere in cache i messaggi cifrati quando usato in un browser web. Usa %(brand)s Desktop affinché i messaggi cifrati appaiano nei risultati di ricerca.", + "There are advanced notifications which are not shown here.": "Ci sono notifiche avanzate che non vengono mostrate qui.", + "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Potresti averle configurate in un client diverso da %(brand)s. Non puoi regolarle in %(brand)s ma sono comunque applicate.", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Imposta il nome di un font installato nel tuo sistema e %(brand)s proverà ad usarlo.", + "Make this room low priority": "Rendi questa stanza a bassa priorità", + "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Le stanze a bassa priorità vengono mostrate in fondo all'elenco stanze in una sezione dedicata", + "Use default": "Usa predefinito", + "Mentions & Keywords": "Citazioni e parole chiave", + "Notification options": "Opzioni di notifica", + "Favourited": "Preferito", + "Forget Room": "Dimentica stanza", + "Use your account to sign in to the latest version of the app at ": "Usa il tuo account per accedere alla versione più recente dell'app in ", + "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at element.io/get-started.": "Hai già fatto l'accesso e sei pronto ad iniziare, ma puoi anche ottenere le versioni più recenti dell'app su tutte le piattaforme in element.io/get-started.", + "Go to Element": "Vai su Element", + "We’re excited to announce Riot is now Element!": "Siamo entusiasti di annunciare che Riot ora si chiama Element!", + "Learn more at element.io/previously-riot": "Maggiori informazioni su element.io/previously-riot", + "Wrong file type": "Tipo di file errato", + "Wrong Recovery Key": "Chiave di ripristino errata", + "Invalid Recovery Key": "Chiave di ripristino non valida", + "Security Phrase": "Frase di sicurezza", + "Enter your Security Phrase or to continue.": "Inserisci una frase di sicurezza o per continuare.", + "Security Key": "Chiave di sicurezza", + "Use your Security Key to continue.": "Usa la tua chiave di sicurezza per continuare.", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "Puoi usare le opzioni di server personalizzato per accedere ad altri server Matrix, specificando un URL diverso di homeserver. Ciò ti consente di usare %(brand)s con un account Matrix esistente su un homeserver diverso.", + "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of element.io.": "Inserisci la posizione del tuo homeserver di Element Matrix Services. Potrebbe usare il tuo nome di dominio o essere un sottodominio di element.io.", + "Search rooms": "Cerca stanze", + "User menu": "Menu utente", + "%(brand)s Web": "%(brand)s Web", + "%(brand)s Desktop": "%(brand)s Desktop", + "%(brand)s iOS": "%(brand)s iOS", + "%(brand)s X for Android": "%(brand)s X per Android", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Proteggiti contro la perdita dell'accesso ai messaggi e dati cifrati facendo un backup delle chiavi crittografiche sul tuo server.", + "Generate a Security Key": "Genera una chiave di sicurezza", + "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Genereremo per te una chiave di sicurezza da conservare in un posto sicuro, come un gestore di password o una cassaforte.", + "Enter a Security Phrase": "Inserisci una frase di sicurezza", + "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Usa una frase segreta che conosci solo tu e salva facoltativamente una chiave di sicurezza da usare come backup.", + "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Inserisci una frase di sicurezza che conosci solo tu, dato che è usata per proteggere i tuoi dati. Per sicurezza, non dovresti riutilizzare la password dell'account.", + "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Conserva la chiave di sicurezza in un posto sicuro, come un gestore di password o una cassaforte, dato che è usata per proteggere i tuoi dati cifrati.", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Se annulli ora, potresti perdere i messaggi e dati cifrati in caso tu perda l'accesso ai tuoi login.", + "You can also set up Secure Backup & manage your keys in Settings.": "Puoi anche impostare il Backup Sicuro e gestire le tue chiavi nelle impostazioni.", + "Set up Secure backup": "Imposta il Backup Sicuro", + "Set a Security Phrase": "Imposta una frase di sicurezza", + "Confirm Security Phrase": "Conferma frase di sicurezza", + "Save your Security Key": "Salva la tua chiave di sicurezza" } From b3e4afb7f6cb799f699cc9d9cffd25d069e445d0 Mon Sep 17 00:00:00 2001 From: Imre Kristoffer Eilertsen Date: Wed, 15 Jul 2020 16:23:09 +0000 Subject: [PATCH 024/308] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 59.2% (1408 of 2377 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/nb_NO/ --- src/i18n/strings/nb_NO.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index 97a5e75b04..f0c9827c06 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -16,7 +16,7 @@ "Failed to set direct chat tag": "Kunne ikke angi direkte chat-tagg", "Today": "I dag", "Files": "Filer", - "You are not receiving desktop notifications": "Du mottar ikke skrivebords varsler", + "You are not receiving desktop notifications": "Du mottar ikke skrivebordsvarsler", "Friday": "Fredag", "Notifications": "Varsler", "Unable to fetch notification target list": "Kunne ikke hente varsel-mål liste", @@ -1401,5 +1401,11 @@ "Toggle Bold": "Veksle fet", "Toggle Italics": "Veksle kursiv", "Toggle Quote": "Veksle siteringsformat", - "Upload a file": "Last opp en fil" + "Upload a file": "Last opp en fil", + "Confirm the emoji below are displayed on both sessions, in the same order:": "Bekreft at emotene nedenfor vises på begge økter, i samme rekkefølge:", + "Not sure of your password? Set a new one": "Er du usikker på passordet ditt? Velg et nytt et", + "Sign in to your Matrix account on %(serverName)s": "Logg inn på Matrix-kontoen din på %(serverName)s", + "Sign in to your Matrix account on ": "Logg inn på Matrix-kontoen din på ", + "If you've joined lots of rooms, this might take a while": "Hvis du har blitt med i mange rom, kan dette ta en stund", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Den nye økten din er nå verifisert. Den har tilgang til dine krypterte meldinger, og andre brukere vil se at den blir stolt på." } From c713df4e2644da7667c34dc208087da9e98120b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 16 Jul 2020 09:36:46 +0000 Subject: [PATCH 025/308] Translated using Weblate (Estonian) Currently translated at 87.0% (2069 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 75 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 710178b32f..1924ebc829 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -1997,5 +1997,78 @@ "You cannot delete this message. (%(code)s)": "Sa ei saa seda sõnumit kustutada. (%(code)s)", "Navigate recent messages to edit": "Muutmiseks liigu viimaste sõnumite juurde", "Move autocomplete selection up/down": "Liiguta automaatse sõnalõpetuse valikut üles või alla", - "Cancel autocomplete": "Lülita automaatne sõnalõpetus välja" + "Cancel autocomplete": "Lülita automaatne sõnalõpetus välja", + "Removing…": "Eemaldan…", + "Destroy cross-signing keys?": "Kas hävitame risttunnustamise võtmed?", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Risttunnustamise võtmete kustutamine on tegevus, mida ei saa tagasi pöörata. Kõik sinu verifitseeritud vestluskaaslased näevad seejärel turvateateid. Kui sa just pole kaotanud ligipääsu kõikidele oma seadmetele, kust sa risttunnustamist oled teinud, siis sa ilmselgelt ei peaks kustutamist ette võtma.", + "Clear cross-signing keys": "Eemalda risttunnustamise võtmed", + "Confirm Removal": "Kinnita eemaldamine", + "Clear all data in this session?": "Kas eemaldame kõik selle sessiooni andmed?", + "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Sessiooni kõikide andmete kustutamine on tegevus, mida ei saa tagasi pöörata. Kui sa pole varundanud krüptovõtmeid, siis sa kaotad ligipääsu krüptitud sõnumitele.", + "Clear all data": "Eemalda kõik andmed", + "Community IDs cannot be empty.": "Kogukonna tunnus ei või olla puudu.", + "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Kogukonna tunnuses võivad olla vaid järgnevad tähemärgid: a-z, 0-9, or '=_-./'", + "Something went wrong whilst creating your community": "Kogukonna loomisel läks midagi viltu", + "Create Community": "Loo kogukond", + "Community Name": "Kogukonna nimi", + "Example": "Näiteks", + "Community ID": "Kogukonna tunnus", + "example": "näiteks", + "Create": "Loo", + "Please enter a name for the room": "Palun sisesta jututoa nimi", + "Set a room address to easily share your room with other people.": "Jagamaks jututuba teiste kasutajatega, sisesta jututoa aadress.", + "You can’t disable this later. Bridges & most bots won’t work yet.": "Seda funktsionaalsust sa ei saa hiljem kinni keerata. Sõnumisillad ja enamus roboteid veel ei oska seda kasutada.", + "Enable end-to-end encryption": "Võta läbiv krüptimine kasutusele", + "Confirm your account deactivation by using Single Sign On to prove your identity.": "Kinnitamaks seda, et soovid oma konto kasutusest eemaldada, kasuta oma isiku tuvastamiseks ühekordset sisselogimist.", + "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "See muudab sinu konto jäädavalt mittekasutatavaks. Sina ei saa enam sisse logida ja keegi teine ei saa seda kasutajatunnust uuesti pruukida. Samuti logitakse sind välja kõikidest jututubadest, kus sa osaled ning eemaldatakse kõik sinu andmed sinu isikutuvastusserverist. Seda tegevust ei saa tagasi pöörata.", + "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "Sinu konto kustutamine vaikimisi ei tähenda, et unustatakse ka sinu saadetud sõnumid. Kui sa siiski soovid seda, siis palun tee märge alljärgnevasse kasti.", + "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Matrix'i sõnumite nähtavus on sarnane e-posti kirjadega. Sõnumite unustamine tegelikult tähendab seda, et sinu varemsaadetud sõnumeid ei jagata uute või veel registreerumata kasutajatega, kuid registeerunud kasutajad, kes juba on need sõnumid saanud, võivad neid ka jätkuvalt lugeda.", + "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Minu konto kustutamisel palun unusta minu saadetud sõnumid (Hoiatus: seetõttu näevad tulevased kasutajad poolikuid vestlusi)", + "To continue, use Single Sign On to prove your identity.": "Jätkamaks tuvasta oma isik kasutades ühekordset sisselogimist.", + "Confirm to continue": "Soovin jätkata", + "Click the button below to confirm your identity.": "Oma isiku tuvastamiseks klõpsi alljärgnevat nuppu.", + "Incompatible local cache": "Kohalikud andmepuhvrid ei ühildu", + "Clear cache and resync": "Tühjenda puhver ja sünkroniseeri andmed uuesti", + "Confirm by comparing the following with the User Settings in your other session:": "Kinnita seda võrreldes järgnevaid andmeid oma teise sessiooni kasutajaseadetes:", + "Confirm this user's session by comparing the following with their User Settings:": "Kinnita selle kasutaja sessioon võrreldes järgnevaid andmeid tema kasutajaseadetes:", + "Session name": "Sessiooni nimi", + "Session ID": "Sessiooni tunnus", + "Session key": "Sessiooni võti", + "If they don't match, the security of your communication may be compromised.": "Kui nad omavahel ei klapi, siis teie suhtluse turvalisus võib olla ohus.", + "Your homeserver doesn't seem to support this feature.": "Tundub, et sinu koduserver ei toeta sellist funktsionaalsust.", + "Message edits": "Sõnumite muutmised", + "Your account is not secure": "Sinu kasutajakonto ei ole turvaline", + "This session, or the other session": "See või teine sessioon", + "The internet connection either session is using": "Internetiühendus, mida emb-kumb sessioon kasutab", + "New session": "Uus sessioon", + "Use this session to verify your new one, granting it access to encrypted messages:": "Kasuta seda sessiooni uute sessioonide verifitseerimiseks, andes sellega ligipääsu krüptitud sõnumitele:", + "If you didn’t sign in to this session, your account may be compromised.": "Kui sa pole sellesse sessiooni sisse loginud, siis sinu kasutajakonto andmed võivad olla sattunud valedesse kätesse.", + "This wasn't me": "See ei olnud mina", + "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Kui sa leiad vigu või soovid jagada muud tagasisidet, siis teata sellest GitHub'i vahendusel.", + "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "Topelt veateadete vältimiseks palun vaata esmalt olemasolevaid (ning lisa a + 1), aga kui sa samal teemal viga ei leia, siis koosta uus veateade.", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Sellest sõnumist teatamine saadab tema unikaalse sõnumi tunnuse sinu koduserveri haldurile. Kui selle jututoa sõnumid on krüptitud, siis sinu koduserveri haldur ei saa lugeda selle sõnumi teksti ega vaadata seal leiduvaid faile ja pilte.", + "Failed to upgrade room": "Jututoa versiooni uuendamine ei õnnestunud", + "The room upgrade could not be completed": "Jututoa uuendust ei õnnestunud teha", + "Please check your email and click on the link it contains. Once this is done, click continue.": "Palun vaata oma e-kirju ning klõpsi meie saadetud kirjas leiduvat linki. Kui see on tehtud, siis vajuta Jätka-nuppu.", + "Email address": "E-posti aadress", + "This will allow you to reset your password and receive notifications.": "See võimaldab sul luua uue salasõna ning saada teavitusi.", + "Wrong file type": "Vale failitüüp", + "Wrong Recovery Key": "Vale taastevõti", + "Invalid Recovery Key": "Vigane taastevõti", + "Security Phrase": "Turvafraas", + "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Ei õnnestu saada ligipääsu turvahoidlale. Palun kontrolli, et sa oleksid sisestanud õige taastamiseks mõeldud paroolifraasi.", + "Enter your Security Phrase or to continue.": "Jätkamiseks sisesta oma turvafraas või kasuta .", + "Security Key": "Turvavõti", + "Use your Security Key to continue.": "Jätkamiseks kasuta turvavõtit.", + "Restoring keys from backup": "Taastan võtmed varundusest", + "Fetching keys from server...": "Laen võtmed serverist...", + "%(completed)s of %(total)s keys restored": "%(completed)s / %(total)s võtit taastatud", + "Unable to load backup status": "Varunduse oleku laadimine ei õnnestunud", + "Recovery key mismatch": "Taastevõtmed ei klapi", + "Backup could not be decrypted with this recovery key: please verify that you entered the correct recovery key.": "Selle taastevõtmega ei õnnestunud varundust dekrüptida: palun kontrolli, kas sa kasutad õiget taastevõtit.", + "Incorrect recovery passphrase": "Vigane taastamiseks mõeldud paroolifraas", + "Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.": "Selle paroolifraasiga ei õnnestunud varundust dekrüptida: palun kontrolli, kas sa kasutad õiget taastamiseks mõeldud paroolifraasi.", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Sisestades taastamiseks mõeldud paroolifraasi, saad ligipääsu oma turvatud sõnumitele ning sätid üles krüptitud sõnumivahetuse.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Kui sa oled unudtanud taastamiseks mõeldud paroolifraasi, siis sa saad kasutada oma taastevõtit või seadistada uued taastamise võimalused", + "Warning: You should only set up key backup from a trusted computer.": "Hoiatus: Sa peaksid võtmete varundust seadistama vaid arvutist, mida sa usaldad." } From 1e0c6faa4d6c7632f2e4f1e588321e835c596815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 16 Jul 2020 09:47:23 +0000 Subject: [PATCH 026/308] Translated using Weblate (Estonian) Currently translated at 90.4% (2151 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 84 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 1924ebc829..2195bf441b 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -2070,5 +2070,87 @@ "Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.": "Selle paroolifraasiga ei õnnestunud varundust dekrüptida: palun kontrolli, kas sa kasutad õiget taastamiseks mõeldud paroolifraasi.", "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Sisestades taastamiseks mõeldud paroolifraasi, saad ligipääsu oma turvatud sõnumitele ning sätid üles krüptitud sõnumivahetuse.", "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Kui sa oled unudtanud taastamiseks mõeldud paroolifraasi, siis sa saad kasutada oma taastevõtit või seadistada uued taastamise võimalused", - "Warning: You should only set up key backup from a trusted computer.": "Hoiatus: Sa peaksid võtmete varundust seadistama vaid arvutist, mida sa usaldad." + "Warning: You should only set up key backup from a trusted computer.": "Hoiatus: Sa peaksid võtmete varundust seadistama vaid arvutist, mida sa usaldad.", + "We’re excited to announce Riot is now Element": "Meil on hea meel teatada, et Riot'i uus nimi on nüüd Element", + "Riot is now Element!": "Riot'i uus nimi on Element!", + "Secret storage public key:": "Turvahoidla avalik võti:", + "Verify User": "Verifitseeri kasutaja", + "For extra security, verify this user by checking a one-time code on both of your devices.": "Lisaturvalisus mõttes verifitseeri see kasutaja võrreldes selleks üheks korraks loodud koodi mõlemas seadmes.", + "Your messages are not secure": "Sinu sõnumid ei ole turvatud", + "Use your account to sign in to the latest version of the app at ": "Kasuta oma kontot logimaks sisse rakenduse viimasesse versiooni serveris", + "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at element.io/get-started.": "Sa oled juba sisse loginud ja võid rahumeeli jätkata, kuid alati on hea mõte, kui kasutad viimast rakenduse versiooni, mille erinevate süsteemide jaoks leiad element.io/get-started lehelt.", + "Go to Element": "Võta Element kasutusele", + "We’re excited to announce Riot is now Element!": "Meil on hea meel teatada, et Riot'i uus nimi on nüüd Element!", + "Learn more at element.io/previously-riot": "Lisateavet leiad element.io/previously-riot lehelt", + "Access your secure message history and set up secure messaging by entering your recovery key.": "Pääse ligi oma turvatud sõnumitele ning säti üles krüptitud sõnumivahetus.", + "If you've forgotten your recovery key you can ": "Kui sa oled unustanud oma taastevõtme, siis sa võid ", + "Custom": "Kohandatud", + "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of element.io.": "Sisesta Element Matrix Services kodiserveri aadress. See võib kasutada nii sinu oma domeeni, kui olla element.io alamdomeen.", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Tõhusamaks kasutuseks seadista filter sikutades kogukonna tunnuspilti filtriribale ekraani vasakus ääres. Sa saad sobival ajal klõpsata filtriribal asuvat tunnuspilti ning näha vaid kasutajaid ja jututubasid, kes on seotud selle kogukonnaga.", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "Kas kustutame jututoa aadressi %(alias)s ja eemaldame %(name)s kataloogist?", + "delete the address.": "kustuta aadress.", + "The server may be unavailable or overloaded": "Server kas pole kättesaadav või on ülekoormatud", + "Unable to join network": "Võrguga liitumine ei õnnestu", + "Search rooms": "Otsi jututube", + "User menu": "Kasutajamenüü", + "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Sa oled kõikidest sessioonidest välja logitud ning enam ei saa tõuketeavitusi. Nende taaskuvamiseks logi sisse kõikides oma seadmetes.", + "Return to login screen": "Mine tagasi sisselogimisvaatele", + "Set a new password": "Seadista uus salasõna", + "Invalid homeserver discovery response": "Vigane vastus koduserveri tuvastamise päringule", + "Failed to get autodiscovery configuration from server": "Serveri automaattuvastuse seadistuste laadimine ei õnnestunud", + "Invalid base_url for m.homeserver": "m.homeserver'i kehtetu base_url", + "Homeserver URL does not appear to be a valid Matrix homeserver": "Koduserveri URL ei tundu viitama korrektsele Matrix'i koduserverile", + "Invalid identity server discovery response": "Vigane vastus isikutuvastusserveri tuvastamise päringule", + "Invalid base_url for m.identity_server": "m.identity_server'i kehtetu base_url", + "Passphrases must match": "Paroolifraasid ei klapi omavahel", + "Passphrase must not be empty": "Paroolifraas ei tohi olla tühi", + "Export room keys": "Ekspordi jututoa võtmed", + "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.": "Selle toiminguga on sul võimalik saabunud krüptitud sõnumite võtmed eksportida sinu kontrollitavasse kohalikku faili. Seetõttu on sul tulevikus võimalik importida need võtmed mõnda teise Matrix'i klienti ning seeläbi muuta saabunud krüptitud sõnumid ka seal loetavaks.", + "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.": "Kes iganes saab kätte selle ekspordifaili, saab ka lugeda sinu krüptitud sõnumeid, seega ole hoolikas selle faili talletamisel. Andmaks lisakihi turvalisust, peaksid sa alljärgnevalt sisestama paroolifraasi, millega krüptitakse eksporditavad andmed. Faili hilisem importimine õnnestub vaid sama paroolifraasi sisestamisel.", + "Confirm passphrase": "Sisesta paroolifraas veel üks kord", + "Export": "Ekspordi", + "Import room keys": "Impordi jututoa võtmed", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Selle toiminguga saad importida krüptimisvõtmed, mis sa viimati olid teisest Matrix'i kliendist eksportinud. Seejärel on võimalik dekrüptida ka siin kõik need samad sõnumid, mida see teine klient suutis dekrüptida.", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Ekspordifail on turvatud paroolifraasiga ning alljärgnevalt peaksid dekrüptimiseks sisestama selle paroolifraasi.", + "File to import": "Imporditav fail", + "Import": "Impordi", + "Confirm encryption setup": "Krüptimise seadistuse kinnitamine", + "Click the button below to confirm setting up encryption.": "Kinnitamaks, et soovid krüptimist seadistada, klõpsi järgnevat nuppu.", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Tagamaks, et sa ei kaota ligipääsu krüptitud sõnumitele ja andmetele, varunda krüptimisvõtmed oma serveris.", + "Generate a Security Key": "Loo turvavõti", + "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Me loome turvavõtme, mida sa peaksid hoidma turvalises kohas, nagu näiteks arvutis salasõnade halduris või vana kooli seifis.", + "Enter a Security Phrase": "Sisesta turvafraas", + "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Sisesta turvafraas, mida vaid sina tead ning lisaks võid salvestada varunduse turvavõtme.", + "Enter your account password to confirm the upgrade:": "Kinnitamaks seda muudatust, sisesta oma konto salasõna:", + "Restore your key backup to upgrade your encryption": "Krüptimine uuendamiseks taasta oma varundatud võtmed", + "Restore": "Taasta", + "You'll need to authenticate with the server to confirm the upgrade.": "Uuenduse kinnitamiseks pead end autentima serveris.", + "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Teiste sessioonide verifitseerimiseks pead uuendama seda sessiooni. Muud verifitseeritud sessioonid saavad sellega ligipääsu krüptitud sõnumitele ning nad märgitakse usaldusväärseteks ka teiste kasutajate jaoks.", + "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Andmete kaitsmiseks sisesta turvafraas, mida vaid sina tead. On mõistlik ja palun ära kasuta selleks oma tavalist konto salasõna.", + "Enter a recovery passphrase": "Sisesta taastamiseks mõeldud paroolifraas", + "Great! This recovery passphrase looks strong enough.": "Suurepärane! Taastamiseks mõeldud paroolifraas on piisavalt kange.", + "That matches!": "Klapib!", + "Use a different passphrase?": "Kas kasutame muud paroolifraasi?", + "That doesn't match.": "Ei klapi mitte.", + "Go back to set it again.": "Mine tagasi ja sisesta nad uuesti.", + "Enter your recovery passphrase a second time to confirm it.": "Kinnitamaks sisesta taastamiseks mõeldud paroolifraas teist korda.", + "Confirm your recovery passphrase": "Kinnita oma taastamiseks mõeldud paroolifraas", + "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Kuna seda kasutatakse sinu krüptitud andmete kaitsmiseks, siis hoia oma turvavõtit kaitstud ja turvalises kohas, nagu näiteks arvutis salasõnade halduris või vana kooli seifis.", + "Unable to query secret storage status": "Ei õnnestu tuvastada turvahoidla olekut", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Kui sa tühistad nüüd, siis sa võid peale viimasest seadmest välja logimist kaotada ligipääsu oma krüptitud sõnumitele ja andmetele.", + "You can also set up Secure Backup & manage your keys in Settings.": "Samuti võid sa seadetes võtta kasutusse turvalise varunduse ning hallata oma krüptovõtmeid.", + "Set up Secure backup": "Võta kasutusele turvaline varundus", + "Set a Security Phrase": "Sisesta turvafraas", + "Confirm Security Phrase": "Kinnita turvafraas", + "Save your Security Key": "Salvesta turvavõti", + "Unable to set up secret storage": "Turvahoidla kasutuselevõtmine ei õnnestu", + "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "Me salvestame krüptitud koopia sinu krüptovõtmetest oma serveris. Seades taastamiseks mõeldud paroolifraasi, saad turvata oma varundust.", + "For maximum security, this should be different from your account password.": "Parima turvalisuse jaoks peaks paroolifraas olema erinev sinu konto salasõnast.", + "Set up with a recovery key": "Võta kasutusele taastevõti", + "Please enter your recovery passphrase a second time to confirm.": "Kinnitamiseks palun sisesta taastamiseks mõeldud paroolifraas teist korda.", + "Repeat your recovery passphrase...": "Korda oma taastamiseks mõeldud paroolifraasi...", + "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Sinu taastevõti toimib turvavõrguna - juhul, kui sa unustad taastamiseks mõeldud paroolifraasi, siis sa saad seda kasutada taastamaks ligipääsu krüptitud sõnumitele.", + "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Hoia seda turvalises kohas, nagu näiteks salasõnahalduris või vana kooli seifis.", + "Your keys are being backed up (the first backup could take a few minutes).": "Sinu krüptovõtmeid varundatakse (esimese varukoopia tegemine võib võtta paar minutit).", + "Autocomplete": "Automaatne sõnalõpetus" } From b99805872000398ae6ee5df0e229ba7fd97f6261 Mon Sep 17 00:00:00 2001 From: jadiof Date: Thu, 16 Jul 2020 10:35:55 +0000 Subject: [PATCH 027/308] Translated using Weblate (German) Currently translated at 97.9% (2329 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 0fb38fcb20..28cf71caa4 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2326,5 +2326,12 @@ "Message deleted on %(date)s": "Nachricht am %(date)s gelöscht", "Wrong file type": "Falscher Dateityp", "Wrong Recovery Key": "Falscher Wiederherstellungsschlüssel", - "Invalid Recovery Key": "Ungültiger Wiederherstellungsschlüssel" + "Invalid Recovery Key": "Ungültiger Wiederherstellungsschlüssel", + "Riot is now Element!": "Riot ist jetzt Element!", + "Learn More": "Mehr erfahren", + "Unknown caller": "Unbekannter Anrufer", + "Incoming voice call": "Eingehender Sprachanruf", + "Incoming video call": "Eingehender Videoanruf", + "Incoming call": "Eingehender Anruf", + "There are advanced notifications which are not shown here.": "Es sind erweiterte Benachrichtigungen vorhanden, die hier nicht angezeigt werden." } From 5c1644d3a438e85473f5961efa854b136b0b9586 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Thu, 16 Jul 2020 10:39:29 +0000 Subject: [PATCH 028/308] Translated using Weblate (Hungarian) Currently translated at 99.7% (2371 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 76e0f9f3bf..ca60b925c1 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2370,5 +2370,7 @@ "%(brand)s Web": "%(brand)s Web", "%(brand)s Desktop": "Asztali %(brand)s", "%(brand)s iOS": "%(brand)s iOS", - "%(brand)s X for Android": "%(brand)s X Android" + "%(brand)s X for Android": "%(brand)s X Android", + "This room is public": "Ez egy nyilvános szoba", + "Away": "Távol" } From eab61eaf593e3095a0f514ae5d54b5bff968bb10 Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Thu, 16 Jul 2020 11:14:09 +0000 Subject: [PATCH 029/308] Translated using Weblate (Slovak) Currently translated at 66.3% (1577 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 41 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 6cdc74bf05..bbce7326e5 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1594,5 +1594,44 @@ "To return to your account in future you need to set a password": "Aby ste sa k účtu mohli vrátiť aj neskôr, je potrebné nastaviť heslo", "Restart": "Reštartovať", "Upgrade your %(brand)s": "Upgradujte svoj %(brand)s", - "A new version of %(brand)s is available!": "Nová verzia %(brand)su je dostupná!" + "A new version of %(brand)s is available!": "Nová verzia %(brand)su je dostupná!", + "Which officially provided instance you are using, if any": "Ktorú oficiálne poskytovanú inštanciu používate, pokiaľ nejakú", + "Use your account to sign in to the latest version": "Použite svoj účet na prihlásenie sa do najnovšej verzie", + "We’re excited to announce Riot is now Element": "Sme nadšený oznámiť, že Riot je odteraz Element", + "Riot is now Element!": "Riot je odteraz Element!", + "Learn More": "Dozvedieť sa viac", + "Light": "Svetlý", + "Dark": "Tmavý", + "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.": "Požiadajte správcu vášho %(brand)su, aby skontroloval vašu konfiguráciu. Pravdepodobne obsahuje chyby alebo duplikáty.", + "You joined the call": "Pridali ste sa do hovoru", + "%(senderName)s joined the call": "%(senderName)s sa pridal/a do hovoru", + "Call in progress": "Práve prebieha hovor", + "You left the call": "Opustili ste hovor", + "%(senderName)s left the call": "%(senderName)s opustil/a hovor", + "Call ended": "Hovor skončil", + "You started a call": "Začali ste hovor", + "%(senderName)s started a call": "%(senderName)s začal/a hovor", + "Waiting for answer": "Čakám na odpoveď", + "%(senderName)s is calling": "%(senderName)s volá", + "You created the room": "Vytvorili ste miestnosť", + "%(senderName)s created the room": "%(senderName)s vytvoril/a miestnosť", + "You made the chat encrypted": "Zašifrovali ste čet", + "%(senderName)s made the chat encrypted": "%(senderName)s zašifroval/a čet", + "You made history visible to new members": "Zviditeľnili ste históriu pre nových členov", + "%(senderName)s made history visible to new members": "%(senderName)s zviditeľnil/a históriu pre nových členov", + "You made history visible to anyone": "Zviditeľnili ste históriu pre všetkých", + "%(senderName)s made history visible to anyone": "%(senderName)s zviditeľnil/a históriu pre všetkých", + "You made history visible to future members": "Zviditeľnili ste históriu pre budúcich členov", + "%(senderName)s made history visible to future members": "%(senderName)s zviditeľnil/a históriu pre budúcich členov", + "You were invited": "Boli ste pozvaný/á", + "%(targetName)s was invited": "%(targetName)s vás pozval/a", + "You left": "Odišli ste", + "%(targetName)s left": "%(targetName)s odišiel/odišla", + "You were kicked (%(reason)s)": "Boli ste vykopnutý/á (%(reason)s)", + "%(targetName)s was kicked (%(reason)s)": "%(targetName)s bol vykopnutý/á (%(reason)s)", + "You were kicked": "Boli ste vykopnutý/á", + "%(targetName)s was kicked": "%(targetName)s bol vykopnutý/á", + "You rejected the invite": "Odmietli ste pozvánku", + "%(targetName)s rejected the invite": "%(targetName)s odmietol/odmietla pozvánku", + "You were uninvited": "Boli ste odpozvaný" } From e9ade66572a4f45438522d2c357510f961c9005c Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Thu, 16 Jul 2020 11:34:57 +0000 Subject: [PATCH 030/308] Translated using Weblate (Slovak) Currently translated at 67.0% (1593 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index bbce7326e5..33649577c0 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1633,5 +1633,24 @@ "%(targetName)s was kicked": "%(targetName)s bol vykopnutý/á", "You rejected the invite": "Odmietli ste pozvánku", "%(targetName)s rejected the invite": "%(targetName)s odmietol/odmietla pozvánku", - "You were uninvited": "Boli ste odpozvaný" + "You were uninvited": "Boli ste odpozvaný/á", + "%(targetName)s was uninvited": "%(targetName)s bol odpozvaný/á", + "You were banned (%(reason)s)": "Boli ste vyhostený/á (%(reason)s)", + "%(targetName)s was banned (%(reason)s)": "%(targetName)s bol vyhostený/á (%(reason)s)", + "You were banned": "Boli ste vyhostený/á", + "%(targetName)s was banned": "%(targetName)s bol vyhostený/á", + "You joined": "Pridali ste sa", + "%(targetName)s joined": "%(targetName)s sa pridal/a", + "You changed your name": "Zmenili ste vaše meno", + "%(targetName)s changed their name": "%(targetName)s zmenil/a svoje meno", + "You changed your avatar": "Zmenili ste svojho avatara", + "%(targetName)s changed their avatar": "%(targetName)s zmenil/a svojho avatara", + "%(senderName)s %(emote)s": "%(senderName)s %(emote)s", + "%(senderName)s: %(message)s": "%(senderName)s: %(message)s", + "You changed the room name": "Zmenili ste meno miestnosti", + "%(senderName)s changed the room name": "%(senderName)s zmenil/a meno miestnosti", + "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", + "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", + "You uninvited %(targetName)s": "Odpozvali ste používateľa %(targetName)s", + "%(senderName)s uninvited %(targetName)s": "%(senderName)s odpozval používateľa %(targetName)s" } From 1d7314731a0e6118075bd6b3589b1e137dcdc421 Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Thu, 16 Jul 2020 11:41:14 +0000 Subject: [PATCH 031/308] Translated using Weblate (Slovak) Currently translated at 67.0% (1593 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 33649577c0..f201ced192 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1652,5 +1652,5 @@ "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "You uninvited %(targetName)s": "Odpozvali ste používateľa %(targetName)s", - "%(senderName)s uninvited %(targetName)s": "%(senderName)s odpozval používateľa %(targetName)s" + "%(senderName)s uninvited %(targetName)s": "%(senderName)s odpozval/a používateľa %(targetName)s" } From e10f8083c578938bbdf62bb4154fddeb1d0d930a Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Thu, 16 Jul 2020 11:41:32 +0000 Subject: [PATCH 032/308] Translated using Weblate (Slovak) Currently translated at 67.0% (1594 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index f201ced192..cb4b8ff1ad 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1652,5 +1652,6 @@ "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "You uninvited %(targetName)s": "Odpozvali ste používateľa %(targetName)s", - "%(senderName)s uninvited %(targetName)s": "%(senderName)s odpozval/a používateľa %(targetName)s" + "%(senderName)s uninvited %(targetName)s": "%(senderName)s odpozval/a používateľa %(targetName)s", + "You invited %(targetName)s": "Pozvali ste používateľa %(targetName)s" } From df96e4f07a859af647907bda0bb5c7fb0e2319ee Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Thu, 16 Jul 2020 11:41:50 +0000 Subject: [PATCH 033/308] Translated using Weblate (Slovak) Currently translated at 67.0% (1595 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index cb4b8ff1ad..575431940c 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1653,5 +1653,6 @@ "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "You uninvited %(targetName)s": "Odpozvali ste používateľa %(targetName)s", "%(senderName)s uninvited %(targetName)s": "%(senderName)s odpozval/a používateľa %(targetName)s", - "You invited %(targetName)s": "Pozvali ste používateľa %(targetName)s" + "You invited %(targetName)s": "Pozvali ste používateľa %(targetName)s", + "%(senderName)s invited %(targetName)s": "%(senderName)s pozval/a používateľa %(targetName)s" } From d5d0cfe42aafb87ae3f9b70615be7b59d6a11d91 Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Thu, 16 Jul 2020 11:42:10 +0000 Subject: [PATCH 034/308] Translated using Weblate (Slovak) Currently translated at 67.1% (1596 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 575431940c..156ce84406 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1654,5 +1654,6 @@ "You uninvited %(targetName)s": "Odpozvali ste používateľa %(targetName)s", "%(senderName)s uninvited %(targetName)s": "%(senderName)s odpozval/a používateľa %(targetName)s", "You invited %(targetName)s": "Pozvali ste používateľa %(targetName)s", - "%(senderName)s invited %(targetName)s": "%(senderName)s pozval/a používateľa %(targetName)s" + "%(senderName)s invited %(targetName)s": "%(senderName)s pozval/a používateľa %(targetName)s", + "You changed the room topic": "Zmenili ste tému miestnosti" } From 7cfb7da61b2be6a268fb60bf7b3d8b5c189fd96d Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Thu, 16 Jul 2020 11:42:27 +0000 Subject: [PATCH 035/308] Translated using Weblate (Slovak) Currently translated at 67.4% (1603 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 156ce84406..1ea73fd957 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1266,7 +1266,7 @@ "Show hidden events in timeline": "Zobrazovať skryté udalosti v histórii obsahu miestností", "Low bandwidth mode": "Režim šetrenia údajov", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Ak váš domovský server neposkytuje pomocný server pri uskutočňovaní hovorov, povoliť použitie záložného servera turn.matrix.org (týmto počas hovoru zdieľate svoju adresu IP)", - "When rooms are upgraded": "Keď sú miestnosti aktualizované", + "When rooms are upgraded": "Keď sú miestnosti upgradované", "Accept to continue:": "Ak chcete pokračovať, musíte prijať :", "ID": "ID", "Public Name": "Verejný názov", @@ -1655,5 +1655,17 @@ "%(senderName)s uninvited %(targetName)s": "%(senderName)s odpozval/a používateľa %(targetName)s", "You invited %(targetName)s": "Pozvali ste používateľa %(targetName)s", "%(senderName)s invited %(targetName)s": "%(senderName)s pozval/a používateľa %(targetName)s", - "You changed the room topic": "Zmenili ste tému miestnosti" + "You changed the room topic": "Zmenili ste tému miestnosti", + "%(senderName)s changed the room topic": "%(senderName)s zmenil/a tému miestnosti", + "New spinner design": "Nový točivý štýl", + "Use the improved room list (will refresh to apply changes)": "Použiť vylepšený list miestností (obnový stránku, aby sa aplikovali zmeny)", + "Use custom size": "Použiť vlastnú veľkosť", + "Use a more compact ‘Modern’ layout": "Použiť kompaktnejšie 'moderné' rozloženie", + "Use a system font": "Použiť systémové písmo", + "System font name": "Meno systémového písma", + "Enable experimental, compact IRC style layout": "Povoliť experimentálne, kompaktné rozloženie v štýle IRC", + "Unknown caller": "Neznámy volajúci", + "Incoming voice call": "Prichádzajúci hovor", + "Incoming video call": "Prichádzajúci video hovor", + "Incoming call": "Prichádzajúci hovor" } From ff619de43a2943e5257b39835d2747d2a7a225cf Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Thu, 16 Jul 2020 11:59:16 +0000 Subject: [PATCH 036/308] Translated using Weblate (Slovak) Currently translated at 67.4% (1603 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 1ea73fd957..40276dda3d 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1667,5 +1667,6 @@ "Unknown caller": "Neznámy volajúci", "Incoming voice call": "Prichádzajúci hovor", "Incoming video call": "Prichádzajúci video hovor", - "Incoming call": "Prichádzajúci hovor" + "Incoming call": "Prichádzajúci hovor", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Vytvorte si filter, potiahnite avatara komunity do panelu filtrov na ľavý okraj obrazovky. Môžete kliknúť na avatara v paneli filtorv, aby ste videli len miestnosti a ľudí patriacich do danej komunity." } From dc8bbc387b71d71d712475d71321c86a43c212d5 Mon Sep 17 00:00:00 2001 From: "[Redacted]" Date: Thu, 16 Jul 2020 12:16:42 +0000 Subject: [PATCH 037/308] Translated using Weblate (Slovak) Currently translated at 67.4% (1604 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 40276dda3d..41739ceca0 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1668,5 +1668,6 @@ "Incoming voice call": "Prichádzajúci hovor", "Incoming video call": "Prichádzajúci video hovor", "Incoming call": "Prichádzajúci hovor", - "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Vytvorte si filter, potiahnite avatara komunity do panelu filtrov na ľavý okraj obrazovky. Môžete kliknúť na avatara v paneli filtorv, aby ste videli len miestnosti a ľudí patriacich do danej komunity." + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Vytvorte si filter, potiahnite avatara komunity do panelu filtrov na ľavý okraj obrazovky. Môžete kliknúť na avatara v paneli filtorv, aby ste videli len miestnosti a ľudí patriacich do danej komunity.", + "%(num)s minutes ago": "Pred %(num)s minútami " } From 0665a844165cedca0d43ae061e25b880d4c4d22f Mon Sep 17 00:00:00 2001 From: random Date: Thu, 16 Jul 2020 12:58:44 +0000 Subject: [PATCH 038/308] Translated using Weblate (Italian) Currently translated at 100.0% (2379 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index d2ceaa98c6..ab5c91dde2 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2381,5 +2381,7 @@ "Set up Secure backup": "Imposta il Backup Sicuro", "Set a Security Phrase": "Imposta una frase di sicurezza", "Confirm Security Phrase": "Conferma frase di sicurezza", - "Save your Security Key": "Salva la tua chiave di sicurezza" + "Save your Security Key": "Salva la tua chiave di sicurezza", + "This room is public": "Questa stanza è pubblica", + "Away": "Assente" } From 2b4061ec5232f4398f9d02caa4c3cdc72ebbabd8 Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Thu, 16 Jul 2020 12:17:30 +0000 Subject: [PATCH 039/308] Translated using Weblate (Slovak) Currently translated at 69.0% (1642 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 57 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 41739ceca0..91808ad75a 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1669,5 +1669,60 @@ "Incoming video call": "Prichádzajúci video hovor", "Incoming call": "Prichádzajúci hovor", "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Vytvorte si filter, potiahnite avatara komunity do panelu filtrov na ľavý okraj obrazovky. Môžete kliknúť na avatara v paneli filtorv, aby ste videli len miestnosti a ľudí patriacich do danej komunity.", - "%(num)s minutes ago": "Pred %(num)s minútami " + "%(num)s minutes ago": "pred %(num)s minútami", + "%(num)s hours ago": "pred %(num)s hodinami", + "%(num)s days ago": "pred %(num)s dňami", + "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s nemôže bezpečne cachovať šiforvané správy lokálne pomocou prehlliadača. Použite %(brand)s Desktop na zobrazenie výsledkov vyhľadávania šiforavných správ.", + "There are advanced notifications which are not shown here.": "Sú tam pokročilé notifikácie, ktoré tu nie sú zobrazené.", + "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Možno ste ich nakonfigurovali v inom kliente než v %(brand)se. Nemôžete ich napasovať do %(brand)su, ale stále platia.", + "New version available. Update now.": "Je dostupná nová verzia. Aktualizovať.", + "Hey you. You're the best!": "Hej, ty. Si borec/borka!", + "Use between %(min)s pt and %(max)s pt": "Použite veľkosť mezi %(min)s pt a %(max)s pt", + "Invalid theme schema.": "Neplatná schéma vzhľadu.", + "Error downloading theme information.": "Chyba pri stiahnutí informácií o vzhľade.", + "Theme added!": "Vzhľad pridaný!", + "Custom theme URL": "URL adresa vlastného vzhľadu", + "Add theme": "Pridať vzhľad", + "Message layout": "Rozloženie správy", + "Compact": "Kompaktný", + "Modern": "Moderný", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Nastavte meno písma, ktoré máte nainštalované na vašom systéme & %(brand)s sa ho pokúsi použiť.", + "Customise your appearance": "Upravte si svoj výzor", + "Appearance Settings only affect this %(brand)s session.": "Nastavenia Výzoru ovplyvnia len túto reláciu %(brand)su.", + "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Vaše heslo bolo úspešne zmenené. Nebudete dostávať žiadne notifikácie z iných relácií, dokiaľ sa na nich znovu neprihlásite", + "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "Pre nahlásenie bezpečnostnej chyby súvisiacu s Matrixom si prosím prečítajte Bezpečnostnú Politiku Matrix.org-u (anglicky).", + "Keyboard Shortcuts": "Klávesové skratky", + "Please verify the room ID or address and try again.": "Prosím, overte ID miestnosti alebo adresu a skúste to znovu.", + "Error unsubscribing from list": "Chyba pri zrušení odberu z listu", + "Please try again or view your console for hints.": "Skúste to prosím znovu alebo nahliadnite do konzoly po nápovedy.", + "None": "Žiadne", + "Ban list rules - %(roomName)s": "Pravidlá vyhosťovania - %(roomName)s", + "Server rules": "Pravidlá serveru", + "User rules": "Pravidlá používateľa", + "You have not ignored anyone.": "Nikoho neignorujete.", + "You are currently ignoring:": "Ignorujete:", + "You are not subscribed to any lists": "Neodbreráte žiadne listy", + "Unsubscribe": "Prestať odberať", + "View rules": "Zobraziť pravidlá", + "You are currently subscribed to:": "Odberáte:", + "⚠ These settings are meant for advanced users.": "⚠ Tieto nastavenia sú určené pre pokročilých používateľov.", + "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Pridajte používateľov a servery, ktorých chcete ignorovať. Použite hviezdičku '*', aby %(brand)s ju priradil každému symbolu. Napríklad @bot:* by odignoroval všetkých používateľov s menom 'bot' na akomkoľvek serveri.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ľudia a servery sú ignorovaný pomocou vyhosťovacích listov obsahujúce pravidlá, koho vyhostiť. Odberom vyhosťovacieho listu skryjete používateľov a servery, ktorý sú v ňom blokovaný.", + "Personal ban list": "Osobný zoznam vyhostených", + "Server or user ID to ignore": "Server alebo ID používateľa na odignorovanie", + "eg: @bot:* or example.org": "napr.: @bot:* alebo example.org", + "Subscribed lists": "Odberané listy", + "Subscribing to a ban list will cause you to join it!": "Odbraním listu vyhostených sa pridáte do jeho miestnosti!", + "If this isn't what you want, please use a different tool to ignore users.": "Pokiaľ to nechcete, tak prosím použite iný nástroj na ignorovanie používateľov.", + "Room ID or address of ban list": "ID miestnosti alebo adresa listu vyhostených", + "Subscribe": "Odberať", + "Always show the window menu bar": "Vždy zobraziť hornú lištu okna", + "Show tray icon and minimize window to it on close": "Zobraziť systémovú ikonu a minimalizovať pri zavretí", + "Read Marker lifetime (ms)": "Platnosť značky Prečítané (ms)", + "Read Marker off-screen lifetime (ms)": "Platnosť značky Prečítané mimo obrazovku (ms)", + "Session ID:": "ID relácie:", + "Session key:": "Kľúč relácie:", + "Message search": "Vyhľadávanie v správach", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Váš administrátor serveru zakázal predvolené E2E šifrovanie súkromných miestností & Priamych konverzácií.", + "Where you’re logged in": "Kde ste prihlásený" } From 0dff41492f21290fe5ad89d1aaa26beb53319191 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 16 Jul 2020 13:43:01 +0000 Subject: [PATCH 040/308] Translated using Weblate (Galician) Currently translated at 100.0% (2379 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index ac2ae790e8..8f259d3c15 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -2378,5 +2378,7 @@ "%(brand)s Web": "Web %(brand)s", "%(brand)s Desktop": "%(brand)s Desktop", "%(brand)s iOS": "%(brand)s iOS", - "%(brand)s X for Android": "%(brand)s X para Android" + "%(brand)s X for Android": "%(brand)s X para Android", + "This room is public": "Esta é unha sala pública", + "Away": "Fóra" } From 19c53e9cee3784f628a2fb8801d3d40b60fd636f Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Thu, 16 Jul 2020 13:35:39 +0000 Subject: [PATCH 041/308] Translated using Weblate (Slovak) Currently translated at 70.7% (1682 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 47 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 91808ad75a..063a3ff9ba 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1724,5 +1724,50 @@ "Session key:": "Kľúč relácie:", "Message search": "Vyhľadávanie v správach", "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Váš administrátor serveru zakázal predvolené E2E šifrovanie súkromných miestností & Priamych konverzácií.", - "Where you’re logged in": "Kde ste prihlásený" + "Where you’re logged in": "Kde ste prihlásený", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Spravujte mená a odhláste sa z vašich relácií nižšie alebo ich overte vo vašom profile používateľa.", + "A session's public name is visible to people you communicate with": "Verejné meno relácie je viditeľné ľudom, s ktorými komunikujete", + "Upgrade this room to the recommended room version": "Upgradujte túto miestnosť na odporúčanú verziu", + "this room": "táto miestnosť", + "View older messages in %(roomName)s.": "Zobraziť staršie správy v miestnosti %(roomName)s.", + "Make this room low priority": "Priradiť tejto miestnosti nízku prioritu", + "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Miestnosti s nízkou prioritou sa zobrazia vo vyhradenej sekcií na konci vášho zoznamu miestností", + "This room is bridging messages to the following platforms. Learn more.": "Táto miestnosť premosťuje správy s nasledujúcimi platformami. Viac informácií", + "This room isn’t bridging messages to any platforms. Learn more.": "Táto miestnosť nepremosťuje správy so žiadnymi ďalšími platformami. Viac informácií", + "Bridges": "Mosty", + "Uploaded sound": "Nahratý zvuk", + "Sounds": "Zvuky", + "Notification sound": "Zvuk oznámenia", + "Reset": "Obnoviť predvolené", + "Set a new custom sound": "Nastaviť vlastný zvuk", + "Browse": "Prechádzať", + "Upgrade the room": "Upgradovať miestnosť", + "Enable room encryption": "Povoliť v miestnosti šifrovanie", + "Error changing power level requirement": "Chyba pri zmene požiadavky na úroveň moci", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Došlo k chybe pri zmene požiadavok na úroveň moci miestnosti. Ubezpečte sa, že na to máte dostatočné povolenia a skúste to znovu.", + "Error changing power level": "Chyba pri zmene úrovne moci", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Došlo k chybe pri zmene úrovne moci používateľa. Ubezpečte sa, že na to máte dostatočné povolenia a skúste to znova.", + "To link to this room, please add an address.": "Na prepojenie s touto miestnosťou pridajte prosím adresu.", + "Unable to revoke sharing for email address": "Nepodarilo sa zrušiť zdieľanie emailovej adresy", + "Unable to share email address": "Nepodarilo sa zdieľať emailovú adresu", + "Your email address hasn't been verified yet": "Vaša emailová adresa nebola zatiaľ overená", + "Click the link in the email you received to verify and then click continue again.": "Pre overenie kliknite na odkaz v emaile, ktorý ste dostali, a potom znova kliknite pokračovať.", + "Verify the link in your inbox": "Overte si odkaz v emailovej schránke", + "Complete": "Dokončiť", + "Revoke": "Odvolať", + "Share": "Zdieľať", + "Unable to revoke sharing for phone number": "Nepodarilo sa zrušiť zdieľanie telefónneho čísla", + "Unable to share phone number": "Nepodarilo sa zdieľanie telefónneho čísla", + "Please enter verification code sent via text.": "Zadajte prosím overovací SMS kód.", + "Remove %(email)s?": "Odstrániť adresu %(email)s?", + "Remove %(phone)s?": "Odstrániť číslo %(phone)s?", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "SMSka vám bola zaslaná na +%(msisdn)s. Zadajte prosím overovací kód, ktorý obsahuje.", + "This user has not verified all of their sessions.": "Tento používateľ si neoveril všetky svoje relácie.", + "You have not verified this user.": "Tohto používateľa ste neoverili.", + "You have verified this user. This user has verified all of their sessions.": "Tohto používateľa ste overili. Tento používateľ si overil všetky svoje relácie.", + "Someone is using an unknown session": "Niekto používa neznámu reláciu", + "This room is end-to-end encrypted": "Táto miestnosť je E2E šifrovaná", + "Everyone in this room is verified": "Všetci v tejto miestnosti sú overení", + "Edit message": "Upraviť správu", + "Mod": "Moderátor" } From d794e17d4d238978c1863ed4951265a2a32e20e7 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Jul 2020 02:08:24 +0100 Subject: [PATCH 042/308] Fix handlebar interaction --- res/css/structures/_MainSplit.scss | 26 +++---- res/css/structures/_MatrixChat.scss | 3 +- res/css/structures/_RightPanel.scss | 5 +- src/components/structures/MainSplit.js | 97 ++++++++------------------ 4 files changed, 43 insertions(+), 88 deletions(-) diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index 387879ea7b..dd817a04e4 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -21,26 +21,18 @@ limitations under the License. height: 100%; } -// move hit area 5px to the right so it doesn't overlap with the timeline scrollbar -.mx_MainSplit > .mx_ResizeHandle.mx_ResizeHandle_horizontal { - margin: 0 -10px 0 0; - padding: 0 10px 0 0; -} +.mx_MainSplit > .mx_RightPanel_ResizeWrapper { + padding: 5px; -.mx_MainSplit > .mx_ResizeHandle_horizontal:hover { - position: relative; - - &::before { - position: absolute; - left: 4px; - top: 50%; + &:hover .mx_RightPanel_ResizeHandle { + // Need to use important to override element style attributes + // set by re-resizable + top: 50% !important; transform: translate(0, -50%); - height: 30%; - width: 4px; - border-radius: 4px; - - content: ' '; + height: 30% !important; + width: 4px !important; + border-radius: 4px !important; background-color: $primary-fg-color; opacity: 0.8; diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index 926d10ee04..ff2fe9a162 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -79,12 +79,13 @@ limitations under the License. height: 100%; } +.mx_MatrixChat > .mx_LeftPanel2:hover + .mx_ResizeHandle_horizontal, .mx_MatrixChat > .mx_ResizeHandle_horizontal:hover { position: relative; &::before { position: absolute; - left: -2px; + left: 5px; top: 50%; transform: translate(0, -50%); diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 77114954eb..326720ef0f 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -19,13 +19,12 @@ limitations under the License. overflow-x: hidden; flex: 0 0 auto; position: relative; - min-width: 264px; - max-width: 50%; display: flex; flex-direction: column; border-radius: 8px; - margin: 5px; padding: 4px 0; + box-sizing: border-box; + height: 100%; .mx_RoomView_MessageList { padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 7c66f21a04..800ed76bb9 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -16,77 +16,24 @@ limitations under the License. */ import React from 'react'; -import ResizeHandle from '../views/elements/ResizeHandle'; -import {Resizer, FixedDistributor} from '../../resizer'; +import { Resizable } from 're-resizable'; export default class MainSplit extends React.Component { - constructor(props) { - super(props); - this._setResizeContainerRef = this._setResizeContainerRef.bind(this); - this._onResized = this._onResized.bind(this); + _onResized = (event, direction, refToElement, delta) => { + window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width); } - _onResized(size) { - window.localStorage.setItem("mx_rhs_size", size); - if (this.props.resizeNotifier) { - this.props.resizeNotifier.notifyRightHandleResized(); - } - } + _loadSidePanelSize() { + let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10); - _createResizer() { - const classNames = { - handle: "mx_ResizeHandle", - vertical: "mx_ResizeHandle_vertical", - reverse: "mx_ResizeHandle_reverse", - }; - const resizer = new Resizer( - this.resizeContainer, - FixedDistributor, - {onResized: this._onResized}, - ); - resizer.setClassNames(classNames); - let rhsSize = window.localStorage.getItem("mx_rhs_size"); - if (rhsSize !== null) { - rhsSize = parseInt(rhsSize, 10); - } else { + if (isNaN(rhsSize)) { rhsSize = 350; } - resizer.forHandleAt(0).resize(rhsSize); - resizer.attach(); - this.resizer = resizer; - } - - _setResizeContainerRef(div) { - this.resizeContainer = div; - } - - componentDidMount() { - if (this.props.panel) { - this._createResizer(); - } - } - - componentWillUnmount() { - if (this.resizer) { - this.resizer.detach(); - this.resizer = null; - } - } - - componentDidUpdate(prevProps) { - const wasPanelSet = this.props.panel && !prevProps.panel; - const wasPanelCleared = !this.props.panel && prevProps.panel; - - if (this.resizeContainer && wasPanelSet) { - // The resizer can only be created when **both** expanded and the panel is - // set. Once both are true, the container ref will mount, which is required - // for the resizer to work. - this._createResizer(); - } else if (this.resizer && wasPanelCleared) { - this.resizer.detach(); - this.resizer = null; - } + return { + height: "100%", + width: rhsSize, + }; } render() { @@ -97,13 +44,29 @@ export default class MainSplit extends React.Component { let children; if (hasResizer) { - children = - + children = { panelView } - ; + ; } - return
+ return
{ bodyView } { children }
; From 5e6f8b20bc88263a00a957dc08aabaee6b76312f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jul 2020 14:54:48 +0100 Subject: [PATCH 043/308] Visual WIP Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/_MainSplit.scss | 2 +- res/css/structures/_MatrixChat.scss | 4 ++-- res/css/views/elements/_ResizeHandle.scss | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index dd817a04e4..aee7b5a154 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -30,7 +30,7 @@ limitations under the License. top: 50% !important; transform: translate(0, -50%); - height: 30% !important; + height: 64px !important; // to match width of the ones on roomlist width: 4px !important; border-radius: 4px !important; diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index ff2fe9a162..2bb4a9c437 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -85,11 +85,11 @@ limitations under the License. &::before { position: absolute; - left: 5px; + left: 2px; top: 50%; transform: translate(0, -50%); - height: 30%; + height: 64px; // to match width of the ones on roomlist width: 4px; border-radius: 4px; diff --git a/res/css/views/elements/_ResizeHandle.scss b/res/css/views/elements/_ResizeHandle.scss index 5544799a34..5189f80b30 100644 --- a/res/css/views/elements/_ResizeHandle.scss +++ b/res/css/views/elements/_ResizeHandle.scss @@ -34,7 +34,7 @@ limitations under the License. .mx_MatrixChat > .mx_ResizeHandle.mx_ResizeHandle_horizontal { margin: 0 -10px 0 0; - padding: 0 10px 0 0; + padding: 0 8px 0 0; } .mx_ResizeHandle > div { From d4a0ca5df06131d3f9c09a1e27b4996ff800f6a9 Mon Sep 17 00:00:00 2001 From: Tirifto Date: Thu, 16 Jul 2020 16:27:56 +0000 Subject: [PATCH 044/308] Translated using Weblate (Esperanto) Currently translated at 99.6% (2370 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 94ecda776a..892d8f2423 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -1282,7 +1282,7 @@ "Encryption": "Ĉifrado", "Once enabled, encryption cannot be disabled.": "Post ŝalto, ne plu eblas malŝalti ĉifradon.", "Encrypted": "Ĉifrata", - "The conversation continues here.": "La interparolo pluas ĉi tie.", + "The conversation continues here.": "La interparolo daŭras ĉi tie.", "This room has been replaced and is no longer active.": "Ĉi tiu ĉambro estas anstataŭita, kaj ne plu aktivas.", "Loading room preview": "Preparas antaŭrigardon al la ĉambro", "Only room administrators will see this warning": "Nur administrantoj de ĉambro vidos ĉi tiun averton", From f67ac287d46c76415a29568aa545256302dfe6ca Mon Sep 17 00:00:00 2001 From: Salamandar Date: Thu, 16 Jul 2020 17:56:54 +0000 Subject: [PATCH 045/308] Translated using Weblate (French) Currently translated at 98.9% (2354 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index eb09eacd14..2cae55d09c 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2347,5 +2347,20 @@ "Set up Secure backup": "Configurer la sauvegarde sécurisée", "Set a Security Phrase": "Définir une phrase de sécurité", "Confirm Security Phrase": "Confirmer la phrase de sécurité", - "Save your Security Key": "Sauvegarder votre clé de sécurité" + "Save your Security Key": "Sauvegarder votre clé de sécurité", + "Use your account to sign in to the latest version": "Connectez-vous à la nouvelle version", + "We’re excited to announce Riot is now Element": "Nous sommes heureux de vous annoncer que Riot est désormais Element", + "Riot is now Element!": "Riot est maintenant Element !", + "Learn More": "Plus d'infos", + "Enable experimental, compact IRC style layout": "Disposition expérimentale compacte style IRC", + "Incoming voice call": "Appel vocal entrant", + "Incoming video call": "Appel vidéo entrant", + "Incoming call": "Appel entrant", + "Make this room low priority": "Définir ce salon en priorité basse", + "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Les salons de priorité basse s'affichent en bas de votre liste de salons, dans une section dédiée", + "Show rooms with unread messages first": "Afficher les salons non lus en premier", + "Show previews of messages": "Afficher un aperçu des messages", + "Use default": "Utiliser la valeur par défaut", + "Mentions & Keywords": "Mentions et mots-clés", + "Notification options": "Paramètres de notifications" } From c29da883db07aa67fbecccdead249037c69e07b7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 16 Jul 2020 14:43:43 -0600 Subject: [PATCH 046/308] Convert room list log setting to a real setting To debug https://github.com/vector-im/riot-web/issues/14554 and https://github.com/vector-im/riot-web/issues/14508 --- src/@types/global.d.ts | 3 -- src/components/views/rooms/RoomList2.tsx | 3 +- .../settings/tabs/user/LabsUserSettingsTab.js | 1 + src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 6 +++ src/stores/room-list/RoomListStore2.ts | 48 ++++++++++++------- src/stores/room-list/algorithms/Algorithm.ts | 35 +++++++------- 7 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 3e4eb13766..2d895e12eb 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -39,9 +39,6 @@ declare global { mx_RoomListStore2: RoomListStore2; mx_RoomListLayoutStore: RoomListLayoutStore; mxPlatformPeg: PlatformPeg; - - // TODO: Remove flag before launch: https://github.com/vector-im/riot-web/issues/14231 - mx_LoudRoomListLogging: boolean; } // workaround for https://github.com/microsoft/TypeScript/issues/30933 diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 757e57fcc7..86becc2fca 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -40,6 +40,7 @@ import { NotificationColor } from "../../../stores/notifications/NotificationCol import { Action } from "../../../dispatcher/actions"; import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; +import SettingsStore from "../../../settings/SettingsStore"; // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367 @@ -210,7 +211,7 @@ export default class RoomList2 extends React.Component { private updateLists = () => { const newLists = RoomListStore.instance.orderedLists; - if (window.mx_LoudRoomListLogging) { + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log("new lists", newLists); } diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index 9724b9934f..2b8d7c5d3f 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -66,6 +66,7 @@ export default class LabsUserSettingsTab extends React.Component { +
); 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 ccacb365746ce0b7e74fcfee1ceeabfb5876218f Mon Sep 17 00:00:00 2001 From: Tirifto Date: Thu, 16 Jul 2020 20:14:07 +0000 Subject: [PATCH 047/308] Translated using Weblate (Esperanto) Currently translated at 99.9% (2376 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 892d8f2423..fdba2ddf05 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -56,7 +56,7 @@ "Unable to enable Notifications": "Ne povas ŝalti sciigojn", "This email address was not found": "Tiu ĉi retpoŝtadreso ne troviĝis", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Via retpoŝtareso ŝajne ne ligiĝas al Matrix-identigilo sur tiu ĉi hejmservilo.", - "Default": "Norma", + "Default": "Ordinara", "Restricted": "Limigita", "Moderator": "Ĉambrestro", "Admin": "Administranto", @@ -915,7 +915,7 @@ "Change permissions": "Ŝanĝi permesojn", "Change topic": "Ŝanĝi temon", "Modify widgets": "Aliigi fenestraĵojn", - "Default role": "Norma rolo", + "Default role": "Ordinara rolo", "Send messages": "Sendi mesaĝojn", "Invite users": "Inviti uzantojn", "Change settings": "Ŝanĝi agordojn", @@ -2255,8 +2255,8 @@ "%(senderName)s made history visible to new members": "%(senderName)s videbligis la historion al novaj anoj", "You made history visible to anyone": "Vi videbligis la historion al ĉiu ajn", "%(senderName)s made history visible to anyone": "%(senderName)s videbligis la historion al ĉiu ajn", - "You made history visible to future members": "Vi videbligis la historion al osaj anoj.", - "%(senderName)s made history visible to future members": "%(senderName)s videbligis la historion al osaj anoj.", + "You made history visible to future members": "Vi videbligis la historion al osaj anoj", + "%(senderName)s made history visible to future members": "%(senderName)s videbligis la historion al osaj anoj", "You were invited": "Vi estis invitita", "%(targetName)s was invited": "%(senderName)s estis invitita", "You left": "Vi foriris", @@ -2310,7 +2310,7 @@ "Modern": "Moderna", "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Agordu la nomon de tiparo instalita en via sistemo kaj %(brand)s provos ĝin uzi.", "Customise your appearance": "Adaptu vian aspekton", - "Appearance Settings only affect this %(brand)s session.": "Agordoj de aspekto nur efikos sur ĉi tiu salutaĵo de %(brand)s", + "Appearance Settings only affect this %(brand)s session.": "Agordoj de aspekto nur efikos sur ĉi tiun salutaĵon de %(brand)s.", "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Aldonu uzantojn kaj servilojn, kiujn vi volas malatenti, ĉi tien. Uzu steletojn por ke %(brand)s atendu iujn ajn signojn. Ekzemple, @bot:* malatentigus ĉiujn uzantojn, kiuj havas la nomon «bot» sur ĉiu ajn servilo.", "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "La administranto de via servilo malŝaltis implicitan tutvojan ĉifradon en privataj kaj rektaj ĉambroj.", "Make this room low priority": "Doni al la ĉambro malaltan prioritaton", @@ -2375,5 +2375,12 @@ "Set up Secure backup": "Agordi Sekuran savkopiadon", "Set a Security Phrase": "Agordi Sekurecan frazon", "Confirm Security Phrase": "Konfirmi Sekurecan frazon", - "Save your Security Key": "Konservi vian Sekurecan ŝlosilon" + "Save your Security Key": "Konservi vian Sekurecan ŝlosilon", + "New spinner design": "", + "Show rooms with unread messages first": "Montri ĉambrojn kun nelegitaj mesaĝoj kiel unuajn", + "Show previews of messages": "Montri antaŭrigardojn al mesaĝoj", + "This room is public": "Ĉi tiu ĉambro estas publika", + "Away": "For", + "Edited at %(date)s": "Redaktita je %(date)s", + "Click to view edits": "Klaku por vidi redaktojn" } From ef14459c1606b9a0bb17b4f53db0e38456ac6bd4 Mon Sep 17 00:00:00 2001 From: Salamandar Date: Thu, 16 Jul 2020 19:01:56 +0000 Subject: [PATCH 048/308] Translated using Weblate (French) Currently translated at 99.6% (2369 of 2379 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 2cae55d09c..42f9d74502 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -754,7 +754,7 @@ "Show message in desktop notification": "Afficher le message dans les notifications de bureau", "Unhide Preview": "Dévoiler l'aperçu", "Unable to join network": "Impossible de rejoindre le réseau", - "Sorry, your browser is not able to run %(brand)s.": "Désolé, %(brand)s n'est pas supporté par votre navigateur.", + "Sorry, your browser is not able to run %(brand)s.": "Désolé, %(brand)s n'est pas supporté par votre navigateur.", "Uploaded on %(date)s by %(user)s": "Téléchargé le %(date)s par %(user)s", "Messages in group chats": "Messages dans les discussions de groupe", "Yesterday": "Hier", @@ -2362,5 +2362,20 @@ "Show previews of messages": "Afficher un aperçu des messages", "Use default": "Utiliser la valeur par défaut", "Mentions & Keywords": "Mentions et mots-clés", - "Notification options": "Paramètres de notifications" + "Notification options": "Paramètres de notifications", + "Unknown caller": "Appelant inconnu", + "Favourited": "Favori", + "Forget Room": "Oublier le salon", + "This room is public": "Ce salon est public", + "Edited at %(date)s": "Édité le %(date)s", + "Click to view edits": "Cliquez pour éditer", + "Go to Element": "Aller à Element", + "We’re excited to announce Riot is now Element!": "Nous sommes heureux d'annoncer que Riot est désormais Element !", + "Learn more at element.io/previously-riot": "Plus d'infos sur element.io/previously-riot", + "Search rooms": "Chercher des salons", + "User menu": "Menu d'utilisateur", + "%(brand)s Web": "%(brand)s Web", + "%(brand)s Desktop": "%(brand)s Desktop", + "%(brand)s iOS": "%(brand)s iOS", + "%(brand)s X for Android": "%(brand)s X pour Android" } From a85af47b0a5ed701dfc5fa4d660a94e22ef91e5a Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 16 Jul 2020 17:46:49 -0400 Subject: [PATCH 049/308] 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 050/308] 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 1271d5dde633004a07e5547d91f0b4301f1a6629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 16 Jul 2020 21:34:54 +0000 Subject: [PATCH 051/308] Translated using Weblate (Estonian) Currently translated at 90.3% (2150 of 2380 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 2195bf441b..5d92d288e4 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -2152,5 +2152,8 @@ "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Sinu taastevõti toimib turvavõrguna - juhul, kui sa unustad taastamiseks mõeldud paroolifraasi, siis sa saad seda kasutada taastamaks ligipääsu krüptitud sõnumitele.", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Hoia seda turvalises kohas, nagu näiteks salasõnahalduris või vana kooli seifis.", "Your keys are being backed up (the first backup could take a few minutes).": "Sinu krüptovõtmeid varundatakse (esimese varukoopia tegemine võib võtta paar minutit).", - "Autocomplete": "Automaatne sõnalõpetus" + "Autocomplete": "Automaatne sõnalõpetus", + "Favourited": "Märgitud lemmikuks", + "Leave Room": "Lahku jututoast", + "Forget Room": "Unusta jututuba ära" } From 6e5efd083948bfd905568adef4e794e3f1f746b2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 17 Jul 2020 00:35:41 +0100 Subject: [PATCH 052/308] 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 053/308] 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 054/308] 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 055/308] 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 056/308] 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 10bd804c28d918c715de2b30f060c28232d15fac Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Fri, 17 Jul 2020 09:54:37 +0000 Subject: [PATCH 057/308] Translated using Weblate (Bulgarian) Currently translated at 96.6% (2300 of 2380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index f914e928b5..32652669af 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2267,5 +2267,43 @@ "Leave Room": "Напусни стаята", "Room options": "Настройки на стаята", "Use Recovery Key or Passphrase": "Използвай ключ за възстановяване или парола", - "Use Recovery Key": "Използвай ключ за възстановяване" + "Use Recovery Key": "Използвай ключ за възстановяване", + "Use your account to sign in to the latest version": "Използвайте профила си за да влезете в последната версия", + "We’re excited to announce Riot is now Element": "Развълнувани сме да обявим, че Riot вече е Element", + "Riot is now Element!": "Riot вече е Element!", + "Learn More": "Научи повече", + "You joined the call": "Присъединихте се към разговор", + "%(senderName)s joined the call": "%(senderName)s се присъедини към разговор", + "Call in progress": "Тече разговор", + "You left the call": "Напуснахте разговора", + "%(senderName)s left the call": "%(senderName)s напусна разговора", + "Call ended": "Разговора приключи", + "You started a call": "Започнахте разговор", + "%(senderName)s started a call": "%(senderName)s започна разговор", + "Waiting for answer": "Изчакване на отговор", + "%(senderName)s is calling": "%(senderName)s се обажда", + "You created the room": "Създадохте стаята", + "%(senderName)s created the room": "%(senderName)s създаде стаята", + "You made the chat encrypted": "Направихте чата шифрован", + "%(senderName)s made the chat encrypted": "%(senderName)s направи чата шифрован", + "You made history visible to new members": "Направихте историята видима за нови членове", + "%(senderName)s made history visible to new members": "%(senderName)s направи историята видима за нови членове", + "You made history visible to anyone": "Направихте историята видима за всички", + "%(senderName)s made history visible to anyone": "%(senderName)s направи историята видима за всички", + "You made history visible to future members": "Направихте историята видима за бъдещи членове", + "%(senderName)s made history visible to future members": "%(senderName)s направи историята видима за бъдещи членове", + "You were invited": "Бяхте поканени", + "%(targetName)s was invited": "%(targetName)s беше поканен", + "You left": "Напуснахте", + "%(targetName)s left": "%(targetName)s напусна", + "You were kicked (%(reason)s)": "Бяхте изгонени (%(reason)s)", + "%(targetName)s was kicked (%(reason)s)": "%(targetName)s беше изгонен(а) (%(reason)s)", + "You were kicked": "Бяхте изгонени", + "%(targetName)s was kicked": "%(targetName)s беше изгонен(а)", + "You rejected the invite": "Отхвърлихте поканата", + "%(targetName)s rejected the invite": "%(targetName)s отхвърли поканата", + "You were uninvited": "Поканата към вас беше премахната", + "%(targetName)s was uninvited": "Поканата към %(targetName)s беше премахната", + "You were banned (%(reason)s)": "Бяхте блокирани (%(reason)s)", + "%(targetName)s was banned (%(reason)s)": "%(targetName)s беше блокиран(а) (%(reason)s)" } From dc136480e22db04d565323024ced58e85b3e009c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 17 Jul 2020 05:56:15 +0000 Subject: [PATCH 058/308] Translated using Weblate (Estonian) Currently translated at 95.1% (2263 of 2380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 119 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 5d92d288e4..4a9f403317 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -1178,7 +1178,7 @@ "Invite someone using their name, username (like ), email address or share this room.": "Kutsu kedagi tema nime, kasutajanime (nagu ), e-posti aadressi alusel või jaga seda jututuba.", "Upload completed": "Üleslaadimine valmis", "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s kasutab varasemaga võrreldes 3-5 korda vähem mälu, sest laeb teavet kasutajate kohta vaid siis, kui vaja. Palun oota hetke, kuni sünkroniseerime andmeid serveriga!", - "Updating %(brand)s": "Uuenda %(brand)s'it", + "Updating %(brand)s": "Uuendan rakendust %(brand)s", "I don't want my encrypted messages": "Ma ei soovi oma krüptitud sõnumeid", "Manually export keys": "Ekspordi võtmed käsitsi", "You'll lose access to your encrypted messages": "Sa kaotad ligipääsu oma krüptitud sõnumitele", @@ -1862,7 +1862,7 @@ "Error removing ignored user/server": "Viga eiratud kasutaja või serveri eemaldamisel", "Error unsubscribing from list": "Viga loendist lahkumisel", "Please try again or view your console for hints.": "Palun proovi uuesti või otsi lisavihjeid konsoolilt.", - "None": "Ei midagi", + "None": "Ei ühelgi juhul", "Ban list rules - %(roomName)s": "Ligipääsukeelu reeglid - %(roomName)s", "Server rules": "Serveri kasutustingimused", "User rules": "Kasutajaga seotud tingimused", @@ -2155,5 +2155,118 @@ "Autocomplete": "Automaatne sõnalõpetus", "Favourited": "Märgitud lemmikuks", "Leave Room": "Lahku jututoast", - "Forget Room": "Unusta jututuba ära" + "Forget Room": "Unusta jututuba ära", + "Which officially provided instance you are using, if any": "Mis iganes ametlikku klienti sa kasutad, kui üldse kasutad", + "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Kui kasutad %(brand)s seadmes, kus puuteekraan on põhiline sisestusviis", + "Use your account to sign in to the latest version": "Kasuta oma kontot selleks, et sisse logida rakenduse viimasesse versiooni", + "Learn More": "Lisateave", + "Upgrades a room to a new version": "Uuendab jututoa uue versioonini", + "You do not have the required permissions to use this command.": "Sul ei ole piisavalt õigusi selle käsu käivitamiseks.", + "Error upgrading room": "Viga jututoa uuendamisel", + "Double check that your server supports the room version chosen and try again.": "Kontrolli veel kord, kas sinu koduserver toetab seda jututoa versiooni ning proovi uuesti.", + "Changes your display nickname": "Muudab sinu kuvatavat nime", + "Changes your display nickname in the current room only": "Muudab sinu kuvatavat nime vaid selles jututoas", + "Changes the avatar of the current room": "Muudab selle jututoa tunnuspilti", + "Changes your avatar in this current room only": "Muudab sinu tunnuspilti vaid selles jututoas", + "Failed to set topic": "Teema määramine ei õnnestunud", + "This room has no topic.": "Sellel jututoal puudub teema.", + "Invites user with given id to current room": "Kutsub nimetatud kasutajatunnusega kasutaja sellesse jututuppa", + "Use an identity server": "Kasuta isikutuvastusserverit", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s võttis vastu kutse %(displayName)s nimel.", + "%(targetName)s accepted an invitation.": "%(targetName)s võttis kutse vastu.", + "%(senderName)s requested a VoIP conference.": "%(senderName)s soovib alustada VoIP rühmakõnet.", + "%(senderName)s made no change.": "%(senderName)s ei teinud muutusi.", + "VoIP conference started.": "VoIP rühmakõne algas.", + "VoIP conference finished.": "VoIP rühmakõne lõppes.", + "%(targetName)s rejected the invitation.": "%(targetName)s lükkas kutse tagasi.", + "%(targetName)s left the room.": "%(targetName)s lahkus jututoast.", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s õigused muutusid: %(fromPowerLevel)s -> %(toPowerLevel)s", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s muutis %(powerLevelDiffText)s õigusi.", + "Render simple counters in room header": "Näita jututoa päises lihtsaid loendure", + "Multiple integration managers": "Mitmed lõiminguhaldurid", + "Enable advanced debugging for the room list": "Lülita jututubade loendi jaoks sisse detailne silumine", + "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Näita krüptitud jututubades meeldetuletust krüptitud sõnumite taastamisvõimaluste kohta", + "Use a system font": "Kasuta süsteemset fonti", + "System font name": "Süsteemse fondi nimi", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Kui sinu koduserveris on seadistamata kõnehõlbustusserver, siis luba alternatiivina kasutada avalikku serverit turn.matrix.org (kõne ajal jagatakse nii sinu avalikku, kui privaatvõrgu IP-aadressi)", + "Send read receipts for messages (requires compatible homeserver to disable)": "Saada sõnumite lugemiskinnitusi (selle võimaluse keelamine eeldab, et koduserver sellist funktsionaalsust toetab)", + "This is your list of users/servers you have blocked - don't leave the room!": "See on sinu serverite ja kasutajate ligipääsukeeldude loend. Palun ära lahku sellest jututoast!", + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Kui sa just enne ekspordi selle jututoa võtmed ja impordi need hiljem, siis salasõna muutmine tühistab kõik läbiva krüptimise võtmed sinu kõikides sessioonides ning seega muutub kogu sinu krüptitud vestluste ajalugu loetamatuks. Tulevaste arenduste käigus tehakse see funktsionaalsus paremaks.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Sinu kontol on turvahoidlas olemas risttunnustamise identiteet, kuid seda veel ei loeta antud sessioonis usaldusväärseks.", + "Cross-signing and secret storage are not yet set up.": "Risttunnustamine ja turvahoidla pole veel seadistatud.", + "Reset cross-signing and secret storage": "Tühista risttunnustamise tulemused ja turvahoidla sisu", + "Bootstrap cross-signing and secret storage": "Võta risttunnustamine ja turvahoidla kasutusele", + "well formed": "korrektses vormingus", + "unexpected type": "tundmatut tüüpi", + "in secret storage": "turvahoidlas", + "Self signing private key:": "Sinu privaatvõtmed:", + "cached locally": "on puhverdatud kohalikus seadmes", + "not found locally": "ei leidu kohalikus seadmes", + "User signing private key:": "Kasutaja privaatvõti:", + "Session backup key:": "Sessiooni varundusvõti:", + "in account data": "kasutajakonto andmete hulgas", + "Homeserver feature support:": "Koduserver on tugi sellele funktsionaalusele:", + "exists": "olemas", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Kinnitamaks seda, et soovid neid sessioone kustutada, kasuta oma isiku tuvastamiseks ühekordset sisselogimist.", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Kinnitamaks seda, et soovid seda sessiooni kustutada, kasuta oma isiku tuvastamiseks ühekordset sisselogimist.", + "Confirm deleting these sessions": "Kinnita nende sessioonide kustutamine", + "Click the button below to confirm deleting these sessions.|other": "Nende sessioonide kustutamiseks klõpsi järgnevat nuppu.", + "Click the button below to confirm deleting these sessions.|one": "Selle sessiooni kustutamiseks klõpsi järgnevat nuppu.", + "Delete sessions|other": "Kustuta sessioonid", + "Delete sessions|one": "Kustuta sessioon", + "Authentication": "Autentimine", + "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s ei võimalda veebibrauseris töötades krüptitud sõnumeid turvaliselt puhverdada. Selleks, et krüptitud sõnumeid saaks otsida, kasuta %(brand)s Desktop rakendust Matrix'i kliendina.", + "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Krüptitud sõnumid kasutavad läbivat krüptimist. Ainult sinul ja saaja(te)l on võtmed selliste sõnumite lugemiseks.", + "Unable to load key backup status": "Võtmete varunduse oleku laadimine ei õnnestunud", + "Restore from Backup": "Taasta varundusest", + "This session is backing up your keys. ": "See sessioon varundab sinu krüptovõtmeid. ", + "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "See sessioon ei varunda sinu krüptovõtmeid, aga sul on olemas varundus, millest saad taastada ning millele saad võtmeid lisada.", + "Success": "Õnnestus", + "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Sinu salasõna sai edukalt muudetud. Sa ei saa oma teistes sessioonides tõuketeateid seni, kuni sa pole neist välja ja tagasi loginud", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Selleks, et sind võiks leida e-posti aadressi või telefoninumbri alusel, nõustu isikutuvastusserveri (%(serverName)s) kautustingimustega.", + "Account management": "Kontohaldus", + "Deactivating your account is a permanent action - be careful!": "Kuna kasutajakonto dektiveerimist ei saa tagasi pöörata, siis palun ole ettevaatlik!", + "Deactivate Account": "Deaktiveeri konto", + "Discovery": "Leia kasutajaid", + "Deactivate account": "Deaktiveeri kasutajakonto", + "For help with using %(brand)s, click here.": "Kui otsid lisateavet %(brand)s kasutamise kohta, palun vaata siia.", + "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Lisa siia kasutajad ja serverid, mida sa soovid eirata. Kui soovid, et %(brand)s kasutaks üldist asendamist, siis kasuta tärni. Näiteks @bot:* eirab kõikide serverite kasutajat 'bot'.", + "Use default": "Kasuta vaikimisi väärtusi", + "Mentions & Keywords": "Mainimised ja võtmesõnad", + "Notification options": "Teavituste eelistused", + "Room options": "Jututoa eelistused", + "This room is public": "See jututuba on avalik", + "Away": "Eemal", + "Room avatar": "Jututoa tunnuspilt ehk avatar", + "Publish this room to the public in %(domain)s's room directory?": "Kas avaldame selle jututoa %(domain)s jututubade loendis?", + "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Kui keegi lisab oma sõnumisse URL'i, siis võidakse näidata selle URL'i eelvaadet, mis annab lisateavet tema kohta, nagu näiteks pealkiri, kirjeldus ja kuidas ta välja näeb.", + "Waiting for you to accept on your other session…": "Ootan, et sa nõustuksid teises sessioonis…", + "Waiting for %(displayName)s to accept…": "Ootan, et %(displayName)s nõustuks…", + "Accepting…": "Nõustun …", + "Start Verification": "Alusta verifitseerimist", + "Messages in this room are end-to-end encrypted.": "See jututuba on läbivalt krüptitud.", + "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Sinu sõnumid on turvatud ning ainult sinul ja saaja(te)l on unikaalsed võtmed selliste sõnumite lugemiseks.", + "Messages in this room are not end-to-end encrypted.": "See jututuba ei ole läbivalt krüptitud.", + "One of the following may be compromised:": "Üks järgnevatest võib olla sattunud valedesse kätesse:", + "Your homeserver": "Sinu koduserver", + "The homeserver the user you’re verifying is connected to": "Sinu poolt verifitseeritava kasutaja koduserver", + "Yours, or the other users’ internet connection": "Sinu või teise kasutaja internetiühendus", + "Yours, or the other users’ session": "Sinu või teise kasutaja sessioon", + "Trusted": "Usaldusväärne", + "Not trusted": "Ei ole usaldusväärne", + "%(count)s verified sessions|other": "%(count)s verifitseeritud sessiooni", + "%(count)s verified sessions|one": "1 verifitseeritud sessioon", + "Hide verified sessions": "Peida verifitseeritud sessioonid", + "%(count)s sessions|other": "%(count)s sessiooni", + "%(count)s sessions|one": "%(count)s sessioon", + "Hide sessions": "Peida sessioonid", + "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "See sessioon, mida sa tahad verifitseerida ei toeta QR koodi ega emoji-põhist verifitseerimist, aga just neid %(brand)s oskab kasutada. Proovi mõne muu Matrix'i kliendiga.", + "Verify by scanning": "Verifitseeri skaneerides", + "Ask %(displayName)s to scan your code:": "Palu, et %(displayName)s skaneeriks sinu koodi:", + "If you can't scan the code above, verify by comparing unique emoji.": "Kui sa ei saa skaneerida eespool kuvatud koodi, siis verifitseeri unikaalsete emoji'de võrdlemise teel.", + "Verify by comparing unique emoji.": "Verifitseeri unikaalsete emoji'de võrdlemise teel.", + "Verify by emoji": "Verifitseeri emoji'de abil", + "Edited at %(date)s": "Muutmise kuupäev %(date)s", + "Click to view edits": "Muudatuste nägemiseks klõpsi", + "In reply to ": "Vastuseks kasutajale " } From da16257718cd92bd77311cbad76250c2ad222a5f Mon Sep 17 00:00:00 2001 From: XoseM Date: Fri, 17 Jul 2020 06:41:28 +0000 Subject: [PATCH 059/308] Translated using Weblate (Galician) Currently translated at 100.0% (2380 of 2380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 8f259d3c15..4f18f0bd0c 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -2380,5 +2380,10 @@ "%(brand)s iOS": "%(brand)s iOS", "%(brand)s X for Android": "%(brand)s X para Android", "This room is public": "Esta é unha sala pública", - "Away": "Fóra" + "Away": "Fóra", + "Enable advanced debugging for the room list": "Activar depuración avanzada para a lista da sala", + "Show rooms with unread messages first": "Mostrar primeiro as salas con mensaxes sen ler", + "Show previews of messages": "Mostrar vista previa das mensaxes", + "Edited at %(date)s": "Editado o %(date)s", + "Click to view edits": "Preme para ver as edicións" } From 8e194b19903b1fdfd3b241b0f50a98c9b32fdb88 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 17 Jul 2020 12:34:15 +0000 Subject: [PATCH 060/308] Translated using Weblate (Italian) Currently translated at 100.0% (2380 of 2380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index ab5c91dde2..068d545368 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2383,5 +2383,10 @@ "Confirm Security Phrase": "Conferma frase di sicurezza", "Save your Security Key": "Salva la tua chiave di sicurezza", "This room is public": "Questa stanza è pubblica", - "Away": "Assente" + "Away": "Assente", + "Enable advanced debugging for the room list": "Attiva il debug avanzato per l'elenco di stanze", + "Show rooms with unread messages first": "Mostra prima le stanze con messaggi non letti", + "Show previews of messages": "Mostra anteprime dei messaggi", + "Edited at %(date)s": "Modificato il %(date)s", + "Click to view edits": "Clicca per vedere le modifiche" } From bc885e2156c56c0adcb2a7fb84e046cd538714f6 Mon Sep 17 00:00:00 2001 From: Nikita Epifanov Date: Fri, 17 Jul 2020 10:40:45 +0000 Subject: [PATCH 061/308] Translated using Weblate (Russian) Currently translated at 84.3% (2006 of 2380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 01700c0e8e..4487ca7237 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -1993,5 +1993,21 @@ "Signature upload success": "Отпечаток успешно отправлен", "Signature upload failed": "Сбой отправки отпечатка", "Room name or address": "Имя или адрес комнаты", - "Unrecognised room address:": "Не удалось найти адрес комнаты:" + "Unrecognised room address:": "Не удалось найти адрес комнаты:", + "Use your account to sign in to the latest version": "Используйте свой аккаунт для входа в последнюю версию", + "We’re excited to announce Riot is now Element": "Мы рады объявить, что Riot теперь Element", + "Riot is now Element!": "Riot теперь Element!", + "Learn More": "Узнать больше", + "Joins room with given address": "Присоединиться к комнате с указанным адресом", + "Opens chat with the given user": "Открыть чат с данным пользователем", + "Sends a message to the given user": "Отправить сообщение данному пользователю", + "You signed in to a new session without verifying it:": "Вы вошли в новый сеанс, не подтвердив его:", + "Verify your other session using one of the options below.": "Проверьте другой сеанс, используя один из вариантов ниже.", + "Help us improve %(brand)s": "Помогите нам улучшить %(brand)s", + "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Отправьте анонимные данные об использовании, которые помогут нам улучшить %(brand)s. Для этого будеть использоваться cookie.", + "I want to help": "Я хочу помочь", + "Review where you’re logged in": "Проверьте, где вы вошли", + "Verify all your sessions to ensure your account & messages are safe": "Проверьте все свои сеансы, чтобы убедиться, что ваша учетная запись и сообщения в безопасности", + "Your homeserver has exceeded its user limit.": "Ваш домашний сервер превысил свой лимит пользователей.", + "Your homeserver has exceeded one of its resource limits.": "Ваш домашний сервер превысил один из своих лимитов ресурсов." } 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 062/308] 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 063/308] 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 064/308] 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 065/308] 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 066/308] 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 067/308] 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 068/308] 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 19d63e6dff962d024e98070eb6ce9f82add8d009 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 17 Jul 2020 15:29:55 +0000 Subject: [PATCH 069/308] Translated using Weblate (Italian) Currently translated at 100.0% (2380 of 2380 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 58 ++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 068d545368..4ff29870a3 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -532,7 +532,7 @@ "Failed to leave room": "Uscita dalla stanza fallita", "Signed Out": "Disconnesso", "For security, this session has been signed out. Please sign in again.": "Per sicurezza questa sessione è stata disconnessa. Accedi di nuovo.", - "Old cryptography data detected": "Rilevati dati di cifratura obsoleti", + "Old cryptography data detected": "Rilevati dati di crittografia obsoleti", "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Sono stati rilevati dati da una vecchia versione di %(brand)s. Ciò avrà causato malfunzionamenti della crittografia end-to-end nella vecchia versione. I messaggi cifrati end-to-end scambiati di recente usando la vecchia versione potrebbero essere indecifrabili in questa versione. Anche i messaggi scambiati con questa versione possono fallire. Se riscontri problemi, disconnettiti e riaccedi. Per conservare la cronologia, esporta e re-importa le tue chiavi.", "Logout": "Disconnetti", "Your Communities": "Le tue comunità", @@ -639,7 +639,7 @@ "Confirm passphrase": "Conferma password", "Export": "Esporta", "Import room keys": "Importa chiavi della stanza", - "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Questa procedura ti permette di importare le chiavi di cifratura precedentemente esportate da un altro client Matrix. Potrai poi decifrare tutti i messaggi che quel client poteva decifrare.", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Questa procedura ti permette di importare le chiavi di crittografia precedentemente esportate da un altro client Matrix. Potrai poi decifrare tutti i messaggi che quel client poteva decifrare.", "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Il file esportato sarà protetto da una password. Dovresti inserire la password qui, per decifrarlo.", "File to import": "File da importare", "Import": "Importa", @@ -783,7 +783,7 @@ "e.g. ": "es. ", "Your device resolution": "La risoluzione del dispositivo", "Missing roomId.": "ID stanza mancante.", - "Always show encryption icons": "Mostra sempre icone di cifratura", + "Always show encryption icons": "Mostra sempre icone di crittografia", "Enable widget screenshots on supported widgets": "Attiva le schermate dei widget sui widget supportati", "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Impossibile caricare l'evento a cui si è risposto, o non esiste o non hai il permesso di visualizzarlo.", "Refresh": "Aggiorna", @@ -914,7 +914,7 @@ "Please review and accept all of the homeserver's policies": "Si prega di rivedere e accettare tutte le politiche dell'homeserver", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Per evitare di perdere la cronologia della chat, devi esportare le tue chiavi della stanza prima di uscire. Dovrai tornare alla versione più recente di %(brand)s per farlo", "Incompatible Database": "Database non compatibile", - "Continue With Encryption Disabled": "Continua con la cifratura disattivata", + "Continue With Encryption Disabled": "Continua con la crittografia disattivata", "Unable to load backup status": "Impossibile caricare lo stato del backup", "Unable to restore backup": "Impossibile ripristinare il backup", "No backup found!": "Nessun backup trovato!", @@ -1082,7 +1082,7 @@ "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Ti abbiamo inviato un'email per verificare il tuo indirizzo. Segui le istruzioni contenute e poi clicca il pulsante sotto.", "Email Address": "Indirizzo email", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Sei sicuro? Perderai i tuoi messaggi cifrati se non hai salvato adeguatamente le tue chiavi.", - "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "I messaggi criptati sono resi sicuri con una cifratura end-to-end. Solo tu e il/i destinatario/i avete le chiavi per leggere questi messaggi.", + "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "I messaggi cifrati sono resi sicuri con una crittografia end-to-end. Solo tu e il/i destinatario/i avete le chiavi per leggere questi messaggi.", "Restore from Backup": "Ripristina da un backup", "Back up your keys before signing out to avoid losing them.": "Fai una copia delle tue chiavi prima di disconnetterti per evitare di perderle.", "Backing up %(sessionsRemaining)s keys...": "Copia di %(sessionsRemaining)s chiavi...", @@ -1148,11 +1148,11 @@ "Send %(eventType)s events": "Invia eventi %(eventType)s", "Roles & Permissions": "Ruoli e permessi", "Select the roles required to change various parts of the room": "Seleziona i ruoli necessari per cambiare varie parti della stanza", - "Enable encryption?": "Attivare la cifratura?", - "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Una volta attivata, la cifratura di una stanza non può essere disattivata. I messaggi inviati in una stanza cifrata non possono essere letti dal server, solo dai partecipanti della stanza. L'attivazione della cifratura può impedire il corretto funzionamento di bot e bridge. Maggiori informazioni sulla cifratura.", + "Enable encryption?": "Attivare la crittografia?", + "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Una volta attivata, la crittografia di una stanza non può essere disattivata. I messaggi inviati in una stanza cifrata non possono essere letti dal server, solo dai partecipanti della stanza. L'attivazione della crittografia può impedire il corretto funzionamento di bot e bridge. Maggiori informazioni sulla crittografia.", "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Le modifiche a chi può leggere la cronologia si applicheranno solo ai messaggi futuri in questa stanza. La visibilità della cronologia esistente rimarrà invariata.", - "Encryption": "Cifratura", - "Once enabled, encryption cannot be disabled.": "Una volta attivata, la cifratura non può essere disattivata.", + "Encryption": "Crittografia", + "Once enabled, encryption cannot be disabled.": "Una volta attivata, la crittografia non può essere disattivata.", "Encrypted": "Cifrato", "Never lose encrypted messages": "Non perdere mai i messaggi cifrati", "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "I messaggi in questa stanza sono protetti con crittografia end-to-end. Solo tu e i destinatari avete le chiavi per leggere questi messaggi.", @@ -1306,7 +1306,7 @@ "Notes": "Note", "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Se ci sono ulteriori dettagli che possono aiutare ad analizzare il problema, ad esempio cosa stavi facendo in quel momento, ID stanze, ID utenti, ecc., puoi includerli qui.", "View Servers in Room": "Vedi i server nella stanza", - "Sign out and remove encryption keys?": "Disconnettere e rimuovere le chiavi di cifratura?", + "Sign out and remove encryption keys?": "Disconnettere e rimuovere le chiavi di crittografia?", "To help us prevent this in future, please send us logs.": "Per aiutarci a prevenire questa cosa in futuro, inviaci i log.", "Missing session data": "Dati di sessione mancanti", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Alcuni dati di sessione, incluse le chiavi dei messaggi cifrati, sono mancanti. Esci e riaccedi per risolvere, ripristinando le chiavi da un backup.", @@ -1457,7 +1457,7 @@ "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Usa un server di identità per invitare via email. Clicca \"Continua\" per usare quello predefinito (%(defaultIdentityServerName)s) o gestiscilo nelle impostazioni.", "Use an identity server to invite by email. Manage in Settings.": "Usa un server di identità per invitare via email. Gestisci nelle impostazioni.", "Upgrade the room": "Aggiorna la stanza", - "Enable room encryption": "Attiva la cifratura della stanza", + "Enable room encryption": "Attiva la crittografia della stanza", "Deactivate user?": "Disattivare l'utente?", "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Disattivare questo utente lo disconnetterà e ne impedirà nuovi accessi. In aggiunta, abbandonerà tutte le stanze in cui è presente. Questa azione non può essere annullata. Sei sicuro di volere disattivare questo utente?", "Deactivate user": "Disattiva utente", @@ -1554,7 +1554,7 @@ "contact the administrators of identity server ": "contattare l'amministratore del server di identità ", "wait and try again later": "attendere e riprovare più tardi", "Failed to deactivate user": "Disattivazione utente fallita", - "This client does not support end-to-end encryption.": "Questo client non supporta la cifratura end-to-end.", + "This client does not support end-to-end encryption.": "Questo client non supporta la crittografia end-to-end.", "Messages in this room are not end-to-end encrypted.": "I messaggi in questa stanza non sono cifrati end-to-end.", "React": "Reagisci", "Frequently Used": "Usati di frequente", @@ -1649,7 +1649,7 @@ "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni per gestire bot, widget e pacchetti di adesivi.", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "I gestori di integrazione ricevono dati di configurazione e possono modificare widget, inviare inviti alla stanza, assegnare permessi a tuo nome.", "Failed to connect to integration manager": "Connessione al gestore di integrazioni fallita", - "Widgets do not use message encryption.": "I widget non usano la cifratura dei messaggi.", + "Widgets do not use message encryption.": "I widget non usano la crittografia dei messaggi.", "More options": "Altre opzioni", "Integrations are disabled": "Le integrazioni sono disattivate", "Enable 'Manage Integrations' in Settings to do this.": "Attiva 'Gestisci integrazioni' nelle impostazioni per continuare.", @@ -1787,10 +1787,10 @@ "Send as message": "Invia come messaggio", "Enter your account password to confirm the upgrade:": "Inserisci la password del tuo account per confermare l'aggiornamento:", "You'll need to authenticate with the server to confirm the upgrade.": "Dovrai autenticarti con il server per confermare l'aggiornamento.", - "Upgrade your encryption": "Aggiorna la tua cifratura", - "Set up encryption": "Imposta la cifratura", + "Upgrade your encryption": "Aggiorna la tua crittografia", + "Set up encryption": "Imposta la crittografia", "Verify this session": "Verifica questa sessione", - "Encryption upgrade available": "Aggiornamento cifratura disponibile", + "Encryption upgrade available": "Aggiornamento crittografia disponibile", "Enable message search in encrypted rooms": "Attiva la ricerca messaggi nelle stanze cifrate", "Waiting for %(displayName)s to verify…": "In attesa della verifica da %(displayName)s …", "They match": "Corrispondono", @@ -1816,7 +1816,7 @@ "Never send encrypted messages to unverified sessions from this session": "Non inviare mai messaggi cifrati a sessioni non verificate da questa sessione", "Never send encrypted messages to unverified sessions in this room from this session": "Non inviare mai messaggi cifrati a sessioni non verificate in questa stanza da questa sessione", "To be secure, do this in person or use a trusted way to communicate.": "Per sicurezza, fatelo di persona o usate un metodo fidato per comunicare.", - "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Il cambio della password reimposterà qualsiasi chiave di cifratura end-to-end in tutte le sessioni, rendendo illeggibile la cronologia di chat, a meno che prima non esporti le chiavi della stanza e le reimporti dopo. In futuro questa cosa verrà migliorata.", + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Il cambio della password reimposterà qualsiasi chiave di crittografia end-to-end in tutte le sessioni, rendendo illeggibile la cronologia di chat, a meno che prima non esporti le chiavi della stanza e le reimporti dopo. In futuro questa cosa verrà migliorata.", "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Il tuo account ha un'identità a firma incrociata nell'archivio segreto, ma non è ancora fidata da questa sessione.", "in memory": "in memoria", "Your homeserver does not support session management.": "Il tuo homeserver non supporta la gestione della sessione.", @@ -1859,7 +1859,7 @@ "Your key share request has been sent - please check your other sessions for key share requests.": "La tua richiesta di condivisione chiavi è stata inviata - controlla le tue altre sessioni per le richieste.", "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Le richieste di condivisione chiavi vengono inviate alle tue altre sessioni automaticamente. Se sulle altre sessioni l'hai rifiutata o annullata, clicca qui per chiedere di nuovo le chiavi per questa sessione.", "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Se le altre sessioni non hanno la chiave per questo messaggio non sarai in grado di decifrarlo.", - "Re-request encryption keys from your other sessions.": "Chiedi di nuovo le chiavi di cifratura dalle altre sessioni.", + "Re-request encryption keys from your other sessions.": "Chiedi di nuovo le chiavi di crittografia dalle altre sessioni.", "Encrypted by an unverified session": "Cifrato da una sessione non verificata", "Encrypted by a deleted session": "Cifrato da una sessione eliminata", "Waiting for %(displayName)s to accept…": "In attesa che %(displayName)s accetti…", @@ -1879,10 +1879,10 @@ "If you can't scan the code above, verify by comparing unique emoji.": "Se non riesci a scansionare il codice sopra, verifica confrontando emoji specifiche.", "You've successfully verified %(displayName)s!": "Hai verificato correttamente %(displayName)s!", "Got it": "Capito", - "Encryption enabled": "Cifratura attivata", + "Encryption enabled": "Crittografia attivata", "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "I messaggi in questa stanza sono cifrati end-to-end. Maggiori info e verifica di questo utente nel suo profilo.", - "Encryption not enabled": "Cifratura non attivata", - "The encryption used by this room isn't supported.": "La cifratura usata da questa stanza non è supportata.", + "Encryption not enabled": "Crittografia non attivata", + "The encryption used by this room isn't supported.": "La crittografia usata da questa stanza non è supportata.", "Clear all data in this session?": "Svuotare tutti i dati in questa sessione?", "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Lo svuotamento dei dati di questa sessione è permanente. I messaggi cifrati andranno persi a meno non si abbia un backup delle loro chiavi.", "Verify session": "Verifica sessione", @@ -1903,11 +1903,11 @@ "Confirm your identity by entering your account password below.": "Conferma la tua identità inserendo la password dell'account sotto.", "Your new session is now verified. Other users will see it as trusted.": "La tua nuova sessione è ora verificata. Gli altri utenti la vedranno come fidata.", "Without completing security on this session, it won’t have access to encrypted messages.": "Senza completare la sicurezza di questa sessione, essa non avrà accesso ai messaggi cifrati.", - "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "La modifica della password reimposterà qualsiasi chiave di cifratura end-to-end su tutte le sessioni, rendendo illeggibile la cronologia delle chat cifrate. Configura il Backup Chiavi o esporta le tue chiavi della stanza da un'altra sessione prima di reimpostare la password.", + "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "La modifica della password reimposterà qualsiasi chiave di crittografia end-to-end su tutte le sessioni, rendendo illeggibile la cronologia delle chat cifrate. Configura il Backup Chiavi o esporta le tue chiavi della stanza da un'altra sessione prima di reimpostare la password.", "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Sei stato disconnesso da tutte le sessioni e non riceverai più notifiche push. Per riattivare le notifiche, riaccedi su ogni dispositivo.", - "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Riprendi l'accesso al tuo account e recupera le chiavi di cifratura memorizzate in questa sessione. Senza di esse, non sarai in grado di leggere tutti i tuoi messaggi sicuri in qualsiasi sessione.", - "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Attenzione: i tuoi dati personali (incluse le chiavi di cifratura) sono ancora memorizzati in questa sessione. Cancellali se hai finito di usare questa sessione o se vuoi accedere ad un altro account.", - "Restore your key backup to upgrade your encryption": "Ripristina il tuo backup chiavi per aggiornare la cifratura", + "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Riprendi l'accesso al tuo account e recupera le chiavi di crittografia memorizzate in questa sessione. Senza di esse, non sarai in grado di leggere tutti i tuoi messaggi sicuri in qualsiasi sessione.", + "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Attenzione: i tuoi dati personali (incluse le chiavi di crittografia) sono ancora memorizzati in questa sessione. Cancellali se hai finito di usare questa sessione o se vuoi accedere ad un altro account.", + "Restore your key backup to upgrade your encryption": "Ripristina il tuo backup chiavi per aggiornare la crittografia", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Aggiorna questa sessione per consentirle di verificare altre sessioni, garantendo loro l'accesso ai messaggi cifrati e contrassegnandole come fidate per gli altri utenti.", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Conservane una copia in un luogo sicuro, come un gestore di password o una cassaforte.", "Your recovery key": "La tua chiave di recupero", @@ -2117,7 +2117,7 @@ "Click the button below to confirm deleting these sessions.|one": "Clicca il pulsante sottostante per confermare l'eliminazione di questa sessione.", "Delete sessions|other": "Elimina sessioni", "Delete sessions|one": "Elimina sessione", - "Enable end-to-end encryption": "Attiva cifratura end-to-end", + "Enable end-to-end encryption": "Attiva crittografia end-to-end", "You can’t disable this later. Bridges & most bots won’t work yet.": "Non potrai più disattivarla. I bridge e molti bot non funzioneranno.", "Confirm your account deactivation by using Single Sign On to prove your identity.": "Conferma la disattivazione del tuo account usando Single Sign On per dare prova della tua identità.", "Are you sure you want to deactivate your account? This is irreversible.": "Sei sicuro di volere disattivare il tuo account? È irreversibile.", @@ -2185,8 +2185,8 @@ "To continue, use Single Sign On to prove your identity.": "Per continuare, usa Single Sign On per provare la tua identità.", "Confirm to continue": "Conferma per continuare", "Click the button below to confirm your identity.": "Clicca il pulsante sotto per confermare la tua identità.", - "Confirm encryption setup": "Conferma impostazione cifratura", - "Click the button below to confirm setting up encryption.": "Clicca il pulsante sotto per confermare l'impostazione della cifratura.", + "Confirm encryption setup": "Conferma impostazione crittografia", + "Click the button below to confirm setting up encryption.": "Clicca il pulsante sotto per confermare l'impostazione della crittografia.", "QR Code": "Codice QR", "Dismiss read marker and jump to bottom": "Scarta il segno di lettura e salta alla fine", "Jump to oldest unread message": "Salta al messaggio non letto più vecchio", @@ -2216,7 +2216,7 @@ "This address is available to use": "Questo indirizzo è disponibile per l'uso", "This address is already in use": "Questo indirizzo è già in uso", "Set a room address to easily share your room with other people.": "Imposta un indirizzo della stanza per condividerla facilmente con le altre persone.", - "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Hai precedentemente usato una versione più recente di %(brand)s con questa sessione. Per usare ancora questa versione con la cifratura end to end, dovrai disconnetterti e riaccedere.", + "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Hai precedentemente usato una versione più recente di %(brand)s con questa sessione. Per usare ancora questa versione con la crittografia end to end, dovrai disconnetterti e riaccedere.", "Address (optional)": "Indirizzo (facoltativo)", "Delete the room address %(alias)s and remove %(name)s from the directory?": "Eliminare l'indirizzo della stanza %(alias)s e rimuovere %(name)s dalla cartella?", "delete the address.": "elimina l'indirizzo.", 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 070/308] 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 071/308] 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 072/308] 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 073/308] 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 074/308] 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 075/308] 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 076/308] 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 077/308] 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 078/308] 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 079/308] 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 080/308] 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 081/308] 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 082/308] 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 51b668c5eb22b4e54ea12891b9e2549537fe38e2 Mon Sep 17 00:00:00 2001 From: aalzehla Date: Fri, 17 Jul 2020 18:51:03 +0000 Subject: [PATCH 083/308] Translated using Weblate (Arabic) Currently translated at 3.6% (86 of 2378 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ar/ --- src/i18n/strings/ar.json | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ar.json b/src/i18n/strings/ar.json index 75ab15a778..d9e8091e0a 100644 --- a/src/i18n/strings/ar.json +++ b/src/i18n/strings/ar.json @@ -60,5 +60,29 @@ "powered by Matrix": "مشغل بواسطة Matrix", "The platform you're on": "المنصة الحالية", "Your language of choice": "اللغة المختارة", - "e.g. %(exampleValue)s": "مثال %(exampleValue)s" + "e.g. %(exampleValue)s": "مثال %(exampleValue)s", + "Use Single Sign On to continue": "استخدم تسجيل الدخول الموحد للاستمرار", + "Confirm adding this email address by using Single Sign On to prove your identity.": "اكد اضافة بريدك الالكتروني عن طريق الدخول الموحد (SSO) لتثبت هويتك.", + "Single Sign On": "تسجيل الدخول الموحد", + "Confirm adding email": "تأكيد اضافة بريدك الالكتروني", + "Click the button below to confirm adding this email address.": "انقر على الزر ادناه لتأكد اضافة هذا البريد الالكتروني.", + "Confirm": "تأكيد", + "Add Email Address": "اضافة بريد الكتروني", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "قم بتأكيد اضافة رقم الهاتف هذا باستخدام تقنية الدخول الموحد لتثبت هويتك.", + "Confirm adding phone number": "قم بتأكيد اضافة رقم الهاتف", + "Click the button below to confirm adding this phone number.": "انقر الزر ادناه لتأكيد اضافة رقم الهاتف.", + "Add Phone Number": "اضافة رقم هاتف", + "Which officially provided instance you are using, if any": "التي تقدم البيئة التي تستخدمها بشكل رسمي، اذا كان هناك", + "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "عندما تستخدم %(brand)s على جهاز تكون شاشة اللمس هي طريقة الادخال الرئيسية", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "اذا كنت تستخدم او لا تستخدم ميزة 'breadcrumbs' (الافاتار فوق قائمة الغرف)", + "Whether you're using %(brand)s as an installed Progressive Web App": "اذا كنت تستخدم %(brand)s كتطبيق ويب", + "Your user agent": "وكيل المستخدم الخاص بك", + "Unable to load! Check your network connectivity and try again.": "غير قادر على التحميل! قم فحص اتصالك الشبكي وحاول مرة اخرى.", + "Call Timeout": "مهلة الاتصال", + "Call failed due to misconfigured server": "فشل الاتصال بسبب إعداد السيرفر بشكل خاطئ", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "يرجى مطالبة مسئول سيرفرك (%(homeserverDomain)s) بإعداد سيرفر TURN لكي تعمل المكالمات بشكل صحيح.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "بدلاً من ذلك، يمكنك محاولة استخدام السيرفر العام على turn.matrix.org، ولكن هذا لن يكون موثوقًا به، وسيشارك عنوان IP الخاص بك مع هذا السيرفر. يمكنك أيضًا تعديل ذلك في الإعدادات.", + "Try using turn.matrix.org": "جرب استخدام turn.matrix.org", + "OK": "حسنا", + "Unable to capture screen": "غير قادر على التقاط الشاشة" } From 06336a88b3ca534a1a99f0ce79ebfadcaf832eef Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 14:02:51 -0600 Subject: [PATCH 084/308] 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 085/308] 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 086/308] 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 087/308] 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 088/308] 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 ebb63a622ce9532d75c097bb0b20ecdc84bc6979 Mon Sep 17 00:00:00 2001 From: aalzehla Date: Fri, 17 Jul 2020 19:08:41 +0000 Subject: [PATCH 089/308] Translated using Weblate (Arabic) Currently translated at 13.8% (328 of 2378 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ar/ --- src/i18n/strings/ar.json | 237 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 236 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ar.json b/src/i18n/strings/ar.json index d9e8091e0a..5195f8db13 100644 --- a/src/i18n/strings/ar.json +++ b/src/i18n/strings/ar.json @@ -84,5 +84,240 @@ "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "بدلاً من ذلك، يمكنك محاولة استخدام السيرفر العام على turn.matrix.org، ولكن هذا لن يكون موثوقًا به، وسيشارك عنوان IP الخاص بك مع هذا السيرفر. يمكنك أيضًا تعديل ذلك في الإعدادات.", "Try using turn.matrix.org": "جرب استخدام turn.matrix.org", "OK": "حسنا", - "Unable to capture screen": "غير قادر على التقاط الشاشة" + "Unable to capture screen": "غير قادر على التقاط الشاشة", + "Call Failed": "فشل الاتصال", + "You are already in a call.": "أنت بالفعل في مكالمة.", + "VoIP is unsupported": "تقنية VoIP غير مدعومة", + "You cannot place VoIP calls in this browser.": "لايمكنك اجراء مكالمات VoIP عبر هذا المتصفح.", + "A call is currently being placed!": "يتم حاليًا إجراء مكالمة!", + "A call is already in progress!": "المكالمة جارية بالفعل!", + "Permission Required": "مطلوب صلاحية", + "You do not have permission to start a conference call in this room": "ليس لديك صلاحية لبدء مكالمة جماعية في هذه الغرفة", + "Replying With Files": "الرد مع الملفات", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "في الوقت الحالي ، لا يمكن الرد مع ملف. هل تريد تحميل هذا الملف بدون رد؟", + "The file '%(fileName)s' failed to upload.": "فشل في رفع الملف '%(fileName)s'.", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "إن حجم الملف '%(fileName)s' يتجاوز الحد المسموح به للرفع في السيرفر", + "Upload Failed": "فشل الرفع", + "Server may be unavailable, overloaded, or you hit a bug.": "قد يكون السيرفر غير متوفر، او محملا بشكل زائد او انك طلبت ميزة بها مشكلة.", + "The server does not support the room version specified.": "السيرفر لا يدعم إصدار الغرفة المحدد.", + "Failure to create room": "فشل في انشاء الغرفة", + "Cancel entering passphrase?": "هل تريد إلغاء إدخال عبارة المرور؟", + "Are you sure you want to cancel entering passphrase?": "هل أنت متأكد من أنك تريد إلغاء إدخال عبارة المرور؟", + "Go Back": "الرجوع للخلف", + "Setting up keys": "إعداد المفاتيح", + "Sun": "احد", + "Mon": "اثنين", + "Tue": "ثلاثاء", + "Wed": "اربعاء", + "Thu": "خميس", + "Fri": "جمعة", + "Sat": "سبت", + "Jan": "يناير", + "Feb": "فبراير", + "Mar": "مارس", + "Apr": "ابريل", + "May": "مايو", + "Jun": "يونيو", + "Jul": "يوليو", + "Aug": "اغسطس", + "Sep": "سبتمبر", + "Oct": "اكتوبر", + "Nov": "نوفمبر", + "Dec": "ديسمبر", + "PM": "مساء", + "AM": "صباحا", + "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", + "Who would you like to add to this community?": "هل ترغب في اضافة هذا المجتمع؟", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "تحذير: أي شخص تضيفه إلى مجتمع سيكون مرئيًا للعامة لأي شخص يعرف معرف المجتمع", + "Invite new community members": "دعوى اعضاء جدد للمجتمع", + "Name or Matrix ID": "الاسم او معرف Matrix", + "Invite to Community": "دعوة الى المجتمع", + "Which rooms would you like to add to this community?": "ما هي الغرف التي ترغب في إضافتها إلى هذا المجتمع؟", + "Show these rooms to non-members on the community page and room list?": "هل تريد إظهار هذه الغرف لغير الأعضاء في صفحة المجتمع وقائمة الغرف؟", + "Add rooms to the community": "اضافة غرف الى المجتمع", + "Room name or address": "اسم او عنوان الغرفة", + "Add to community": "اضافة لمجتمع", + "Failed to invite the following users to %(groupId)s:": "فشل في اضافة المستخدمين التاليين الى %(groupId)s:", + "Failed to invite users to community": "فشل دعوة المستخدمين إلى المجتمع", + "Failed to invite users to %(groupId)s": "فشل في دعوة المستخدمين الى %(groupId)s", + "Failed to add the following rooms to %(groupId)s:": "فشل في اضافة الغرف التالية الى %(groupId)s:", + "Unnamed Room": "غرفة بدون اسم", + "Identity server has no terms of service": "سيرفر الهوية ليس لديه شروط للخدمة", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "هذا الحدث يتطلب الوصول الى السيرفر الافتراضي للهوية للتحقق من البريد الالكتروني او رقم الهاتف، ولكن هذا السيرفر ليس لديه اي شروط للخدمة.", + "Only continue if you trust the owner of the server.": "لا تستمر إلا إذا كنت تثق في مالك السيرفر.", + "Trust": "ثِق", + "%(name)s is requesting verification": "%(name)s يطلب التحقق", + "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s ليس لديه الصلاحية لارسال التنبيهات - يرجى فحص اعدادات متصفحك", + "%(brand)s was not given permission to send notifications - please try again": "لم تعطى الصلاحية ل %(brand)s لارسال التنبيهات - يرجى المحاولة ثانية", + "Unable to enable Notifications": "غير قادر على تفعيل التنبيهات", + "This email address was not found": "لم يتم العثور على البريد الالكتروني هذا", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "يبدو ان بريدك الالكتروني غير مرتبط بمعرف Matrix على هذا السيرفر.", + "Use your account to sign in to the latest version": "استخدم حسابك للدخول الى الاصدار الاخير", + "We’re excited to announce Riot is now Element": "نحن سعيدون باعلان ان Riot اصبح الان Element", + "Riot is now Element!": "Riot اصبح الان Element!", + "Learn More": "تعلم المزيد", + "Sign In or Create Account": "قم بتسجيل الدخول او انشاء حساب جديد", + "Use your account or create a new one to continue.": "استخدم حسابك او قم بانشاء حساب اخر للاستمرار.", + "Create Account": "انشاء حساب", + "Sign In": "الدخول", + "Default": "افتراضي", + "Restricted": "مقيد", + "Moderator": "مشرف", + "Admin": "مدير", + "Custom (%(level)s)": "(%(level)s) مخصص", + "Failed to invite": "فشل في الدعوة", + "Operation failed": "فشلت العملية", + "Failed to invite users to the room:": "فشل في دعوة المستخدمين للغرفة:", + "Failed to invite the following users to the %(roomName)s room:": "فشل في دعوة المستخدمين التالية اسمائهم الى الغرفة %(roomName)s:", + "You need to be logged in.": "تحتاج إلى تسجيل الدخول.", + "You need to be able to invite users to do that.": "يجب أن تكون قادرًا على دعوة المستخدمين للقيام بذلك.", + "Unable to create widget.": "غير قادر على إنشاء Widget.", + "Missing roomId.": "معرف الغرفة مفقود.", + "Failed to send request.": "فشل في ارسال الطلب.", + "This room is not recognised.": "لم يتم التعرف على هذه الغرفة.", + "Power level must be positive integer.": "يجب أن يكون مستوى الطاقة عددًا صحيحًا موجبًا.", + "You are not in this room.": "أنت لست في هذه الغرفة.", + "You do not have permission to do that in this room.": "ليس لديك صلاحية للقيام بذلك في هذه الغرفة.", + "Missing room_id in request": "رقم الغرفة مفقود في الطلب", + "Room %(roomId)s not visible": "الغرفة %(roomId)s غير مرئية", + "Missing user_id in request": "رقم المستخدم مفقود في الطلب", + "Messages": "الرسائل", + "Actions": "الإجراءات", + "Advanced": "متقدم", + "Other": "أخرى", + "Command error": "خطأ في الأمر", + "Usage": "الاستخدام", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "ادخل احد الرموز ¯\\_(ツ)_/¯ قبل نص الرسالة", + "Sends a message as plain text, without interpreting it as markdown": "ارسال رسالة كنص، دون تفسيرها على انها معلمات", + "Sends a message as html, without interpreting it as markdown": "ارسال رسالة بشكل HTML، دون تفسيرها على انها معلمات", + "Searches DuckDuckGo for results": "البحث في DuckDuckGo للحصول على نتائج", + "/ddg is not a command": "/ddg ليس امر", + "To use it, just wait for autocomplete results to load and tab through them.": "لاستخدامها، فقط انتظر حتى يكتمل تحميل النتائج والمرور عليها.", + "Upgrades a room to a new version": "ترقية الغرفة الى الاصدار الجديد", + "You do not have the required permissions to use this command.": "ليس لديك الأذونات المطلوبة لاستخدام هذا الأمر.", + "Error upgrading room": "خطأ في ترقية الغرفة", + "Double check that your server supports the room version chosen and try again.": "تحقق مرة أخرى من أن سيرفرك يدعم إصدار الغرفة المختار وحاول مرة أخرى.", + "Changes your display nickname": "يغير لقب العرض الخاص بك", + "Changes your display nickname in the current room only": "يغير لقب العرض الخاص بك في الغرفة الحالية فقط", + "Changes the avatar of the current room": "يغير الصورة الرمزية للغرفة الحالية", + "Changes your avatar in this current room only": "تغيير الصورة الرمزية الخاصة بك في هذه الغرفة الحالية فقط", + "Changes your avatar in all rooms": "يغير صورتك الرمزية في جميع الغرف", + "Gets or sets the room topic": "الحصول على أو تحديد موضوع الغرفة", + "Failed to set topic": "فشل في تحديد الموضوع", + "This room has no topic.": "هذه الغرفة ليس لها موضوع.", + "Sets the room name": "يضبط اسم الغرفة", + "Invites user with given id to current room": "يدعو المستخدم حسب المعرّف المعطى إلى الغرفة الحالية", + "Use an identity server": "خادوم التعريف", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "استخدم سيرفر للهوية للدعوة عبر البريد الالكتروني. انقر على استمرار لاستخدام سيرفر الهوية الافتراضي (%(defaultIdentityServerName)s) او قم بضبط الاعدادات.", + "Use an identity server to invite by email. Manage in Settings.": "استخدم سيرفر الهوية للدعوة عبر البريد الالكتروني. ضبط الاعدادات.", + "Joins room with given address": "الانضمام الى الغرفة بحسب العنوان المعطى", + "Leave room": "اترك الغرفة", + "Unrecognised room address:": "عنوان الغرفة غير معروف:", + "Kicks user with given id": "يطرد المستخدم حسب المعرّف المعطى", + "Bans user with given id": "يحظر المستخدم حسب المعرّف المعطى", + "Unbans user with given ID": "يُلغي الحظر عن المستخدم حسب المعرّف المعطى", + "Ignores a user, hiding their messages from you": "يتجاهل المستخدم، ويخفي رسائله عنك", + "Ignored user": "مستخدم متجاهل", + "You are now ignoring %(userId)s": "انت تقوم الان بتجاهل %(userId)s", + "Stops ignoring a user, showing their messages going forward": "يوقف تجاهل المستخدم ويظهر رسائله من الان فصاعدا", + "Unignored user": "المستخدم غير متجاهل", + "You are no longer ignoring %(userId)s": "انت لم تعد متجاهلا للمستخدم %(userId)s", + "Define the power level of a user": "قم بتعريف مستوى الطاقة للمستخدم", + "Command failed": "فشل الامر", + "Could not find user in room": "لم يستطع ايجاد مستخدم في غرفة", + "Deops user with given id": "يُلغي إدارية المستخدم حسب المعرّف المعطى", + "Opens the Developer Tools dialog": "يفتح نافذة ادوات المطور", + "Adds a custom widget by URL to the room": "يضيف Widget خاص عبر URL الى الغرفة", + "Please supply a widget URL or embed code": "رجاء قم بتحديد Widget URL او قم بتضمين كود", + "Please supply a https:// or http:// widget URL": "Please supply a https:// or http:// widget URL", + "You cannot modify widgets in this room.": "لا يمكنك تعديل الحاجيات في هذه الغرفة.", + "Verifies a user, session, and pubkey tuple": "يتحقق من العناصر: المستخدم والجلسة والمفتاح العام", + "Unknown (user, session) pair:": "زوج (المستخدم، الجلسة)غير معروف:", + "Session already verified!": "تم التحقق من الجلسة بالفعل!", + "WARNING: Session already verified, but keys do NOT MATCH!": "تحذير: تم التحقق من الجلسة بالفعل ، ولكن لا تتطابق المفاتيح!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "تحذير: فشل التحقق من المفتاح! مفتاح التوقيع للمستخدم %(userId)s و الجلسة %(deviceId)s هو \"%(fprint)s\" والتي لا تتوافق مع المفتاح \"%(fingerprint)s\" المعطى. هذا يعني ان اتصالك اصبح مكشوف!", + "Verified key": "مفتاح مؤكد", + "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "مفتاح التوقيع الذي اعطيته يتوافق مع مفتاح التوقيع الذي استلمته من جلسة المستخدم %(userId)s رقم %(deviceId)s. تم تحديد الجلسة على انها مؤكدة.", + "Forces the current outbound group session in an encrypted room to be discarded": "يفرض تجاهل جلسة المجموعة الصادرة الحالية في غرفة مشفرة", + "Sends the given message coloured as a rainbow": "يرسل رسالة معينة ملونة مثل قوس قزح", + "Sends the given emote coloured as a rainbow": "يرسل الرمز المعطى باللون كقوس قزح", + "Displays list of commands with usages and descriptions": "يعرض قائمة الأوامر مع الوصف وطرق الاستخدام", + "Displays information about a user": "يعرض معلومات عن المستخدم", + "Send a bug report with logs": "إرسال تقرير خطأ يحتوي على سجلات الاحداث", + "Logs sent": "تم ارسال سجل الاحداث", + "Opens chat with the given user": "يفتح دردشة من المستخدم المعطى", + "Sends a message to the given user": "يرسل رسالة الى المستخدم المعطى", + "Displays action": "يعرض إجراءً", + "Reason": "السبب", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s قبل دعوة %(displayName)s.", + "%(targetName)s accepted an invitation.": "%(targetName)s قبل الدعوة.", + "%(senderName)s requested a VoIP conference.": "%(senderName)s طلب مكالمة VoIP جماعية.", + "%(senderName)s invited %(targetName)s.": "%(senderName)s دعا %(targetName)s.", + "%(senderName)s banned %(targetName)s.": "%(senderName)s حظر %(targetName)s.", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s غير اسم العرض الخاص به الى %(displayName)s.", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s حدد اسم العرض:%(displayName)s.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s ازال اسم العرض (%(oldDisplayName)s).", + "%(senderName)s removed their profile picture.": "%(senderName)s ازال صورة البروفايل الخاصة به.", + "%(senderName)s changed their profile picture.": "%(senderName)s غير صورة البروفايل الخاصة به.", + "%(senderName)s set a profile picture.": "%(senderName)s غير صورة البروفايل الخاصة به.", + "%(senderName)s made no change.": "%(senderName)s لم يقم باية تعديلات.", + "VoIP conference started.": "بدأ اجتماع VoIP.", + "%(targetName)s joined the room.": "%(targetName)s انضم الى الغرفة.", + "VoIP conference finished.": "انتهى اجتماع VoIP.", + "%(targetName)s rejected the invitation.": "%(targetName)s رفض الدعوة.", + "%(targetName)s left the room.": "%(targetName)s غادر الغرفة.", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s الغى الحظر على %(targetName)s.", + "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s سحب دعوة %(targetName)s.", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s طرد %(targetName)s.", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s غير الموضوع الى \"%(topic)s\".", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s ازال اسم الغرفة.", + "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s غير اسم الغرفة من %(oldRoomName)s الى %(newRoomName)s.", + "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s غير اسم الغرفة الى %(roomName)s.", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s قام بترقية هذه الغرفة.", + "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s قام بجعل هذة الغرفة عامة لكل شخص يعرف الرابط.", + "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s جعل ممكنه لكل من لديه دعوة فقط.", + "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s قام بتغيير قاعدة الانضمام الى %(rule)s", + "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s قام بالسماح للضيوف بالانضمام الى الغرفة.", + "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s قام بإيقاف امكانية انضمام الضيوف الى الغرفة.", + "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s غير طريقة دخول الضيوف الى %(rule)s", + "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s قام بتفعيل الذوق لمجموعة %(groups)s في هذه الغرفة.", + "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s قام بإيقاف الذوق لمجموعة %(groups)s في هذه الغرفة.", + "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s قام بتفعيل الذوق لمجموعة %(newGroups)s و إيقاف الذوق للمجموعة %(oldGroups)s في هذه الغرفة.", + "%(senderDisplayName)s sent an image.": "قام %(senderDisplayName)s بإرسال صورة.", + "%(senderName)s set the main address for this room to %(address)s.": "قام %(senderName)s بتعديل العنوان الرئيسي لهذه الغرفة الى %(address)s.", + "%(senderName)s removed the main address for this room.": "قام %(senderName)s بإزالة العنوان الرئيسي لهذه الغرفة.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "قام %(senderName)s بإضافة العناوين البديلة %(addresses)s لهذه الغرفة.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "قام %(senderName)s بإضافة العنوان البديل %(addresses)s لهذه الغرفة.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "قام %(senderName)s بإزالة العناوين البديلة %(addresses)s لهذه الغرفة.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "قام %(senderName)s بإزالة العنوان البديل %(addresses)s لهذه الغرفة.", + "%(senderName)s changed the alternative addresses for this room.": "قام %(senderName)s بتعديل العناوين البديلة لهذه الغرفة.", + "%(senderName)s changed the main and alternative addresses for this room.": "قام %(senderName)s بتعديل العناوين الرئيسية و البديلة لهذه الغرفة.", + "%(senderName)s changed the addresses for this room.": "قام %(senderName)s بتعديل عناوين هذه الغرفة.", + "Someone": "شخص ما", + "(not supported by this browser)": "(غير مدعوم في هذا المتصفح)", + "%(senderName)s answered the call.": "%(senderName)s رد على المكالمة.", + "(could not connect media)": "(غير قادر على الاتصال بالوسيط)", + "(no answer)": "(لايوجد رد)", + "(unknown failure: %(reason)s)": "(فشل غير معروف:%(reason)s)", + "%(senderName)s ended the call.": "%(senderName)s انهى المكالمة.", + "%(senderName)s placed a voice call.": "أجرى ⁨%(senderName)s⁩ مكالمة صوتية.", + "%(senderName)s placed a voice call. (not supported by this browser)": "أجرى %(senderName)s مكالمة صوتية. (غير متوافقة مع هذا المتصفح)", + "%(senderName)s placed a video call.": "أجرى %(senderName)s مكالمة فيديو.", + "%(senderName)s placed a video call. (not supported by this browser)": "أجرى %(senderName)s مكالمة فيديو. (غير متوافقة مع هذا المتصفح)", + "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "قام %(senderName)s بسحب الدعوة الى %(targetDisplayName)s بالانضمام الى الغرفة.", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "أرسل %(senderName)s دعوة الى %(targetDisplayName)s للانضمام الى الغرفة.", + "%(senderName)s made future room history visible to all room members, from the point they are invited.": "قام %(senderName)s بتعديل رؤية المحادثات السابقة ممكنة لكل الاعضاء من تاريخ دعوتهم بالانضمام.", + "%(senderName)s made future room history visible to all room members, from the point they joined.": "قام %(senderName)s بتعديل رؤية المحادثات السابقة ممكنة لكل الاعضاء من لحظة انضمامهم.", + "%(senderName)s made future room history visible to all room members.": "قام %(senderName)s بتعديل رؤية المحادثات السابقة ممكنة لكل الاعضاء.", + "%(senderName)s made future room history visible to anyone.": "قام %(senderName)s بتعديل رؤية المحادثات السابقة ممكنة لأي شخص.", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "قام %(senderName)s بجعل المحادثات السابقة مرئية لمجهول (%(visibility)s).", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s من %(fromPowerLevel)s الى %(toPowerLevel)s", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "غير %(senderName)s مستوى الطاقة الخاصة ب %(powerLevelDiffText)s.", + "%(senderName)s changed the pinned messages for the room.": "عدل %(senderName)s الرسائل المثبتة للغرفة.", + "%(widgetName)s widget modified by %(senderName)s": "الودجت %(widgetName)s تعدلت بواسطة %(senderName)s", + "%(widgetName)s widget added by %(senderName)s": "الودجت %(widgetName)s اضيفت بواسطة %(senderName)s", + "%(widgetName)s widget removed by %(senderName)s": "الودجت %(widgetName)s حذفت بواسطة %(senderName)s" } From ee850095a2fe35dbfc3ed11e3a7b446e07d69cba Mon Sep 17 00:00:00 2001 From: invertor Date: Fri, 17 Jul 2020 21:03:01 +0000 Subject: [PATCH 090/308] Translated using Weblate (Arabic) Currently translated at 13.8% (328 of 2378 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ar/ --- src/i18n/strings/ar.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ar.json b/src/i18n/strings/ar.json index 5195f8db13..3ac9e79d46 100644 --- a/src/i18n/strings/ar.json +++ b/src/i18n/strings/ar.json @@ -319,5 +319,13 @@ "%(senderName)s changed the pinned messages for the room.": "عدل %(senderName)s الرسائل المثبتة للغرفة.", "%(widgetName)s widget modified by %(senderName)s": "الودجت %(widgetName)s تعدلت بواسطة %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "الودجت %(widgetName)s اضيفت بواسطة %(senderName)s", - "%(widgetName)s widget removed by %(senderName)s": "الودجت %(widgetName)s حذفت بواسطة %(senderName)s" + "%(widgetName)s widget removed by %(senderName)s": "الودجت %(widgetName)s حذفت بواسطة %(senderName)s", + "Whether or not you're logged in (we don't record your username)": "سواءً كنت مسجلا دخولك أم لا (لا نحتفظ باسم المستخدم)", + "Every page you use in the app": "كل صفحة تستخدمها في التطبيق", + "e.g. ": "مثلا <رابط الصفحة الحالية>", + "Your device resolution": "دقة شاشة جهازك", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "على الرغم من كون هذه الصفحة تحوي معلومات تمكن تحديد الهوية، مثل معرف الغرفة والمستخدم والمجموعة، فهذه البيانات يتم حذفها قبل أن ترسل للخادم", + "The remote side failed to pick up": "الطرف الآخر لم يتمكن من الرد", + "Existing Call": "مكالمة موجودة", + "You cannot place a call with yourself.": "لا يمكنك البدء باتصال بك ذاتك" } From 139508e14b78f5e401aa7795cc7d6d229677394a Mon Sep 17 00:00:00 2001 From: "J. A. Durieux" Date: Fri, 17 Jul 2020 20:33:50 +0000 Subject: [PATCH 091/308] Translated using Weblate (Dutch) Currently translated at 85.0% (2022 of 2378 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index a8cefd00f9..bb0fb5def6 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1846,7 +1846,7 @@ "Reactions": "Reacties", " reacted with %(content)s": " heeft gereageerd met %(content)s", "Frequently Used": "Vaak gebruikt", - "Smileys & People": "Smileys en personen", + "Smileys & People": "Emoticons en personen", "Animals & Nature": "Dieren en natuur", "Food & Drink": "Eten en drinken", "Activities": "Activiteiten", @@ -2024,5 +2024,7 @@ "Dark": "Donker", "Set password": "Stel wachtwoord in", "To return to your account in future you need to set a password": "Zonder wachtwoord kunt u later niet tot uw account terugkeren", - "Restart": "Herstarten" + "Restart": "Herstarten", + "People": "Tweegesprekken", + "Set a room address to easily share your room with other people.": "Geef het gesprek een adres om het gemakkelijk met anderen te kunnen delen." } From ffbcd551827eb5a85f389c7c2a967581a80aaee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 17 Jul 2020 20:40:15 +0000 Subject: [PATCH 092/308] Translated using Weblate (Estonian) Currently translated at 95.5% (2272 of 2378 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 4a9f403317..a0560785a7 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -2268,5 +2268,17 @@ "Verify by emoji": "Verifitseeri emoji'de abil", "Edited at %(date)s": "Muutmise kuupäev %(date)s", "Click to view edits": "Muudatuste nägemiseks klõpsi", - "In reply to ": "Vastuseks kasutajale " + "In reply to ": "Vastuseks kasutajale ", + "There are advanced notifications which are not shown here.": "On olemad detailseid teavitusi, mida siin ei kuvata.", + "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Sa võib-olla oled seadistanud nad %(brand)s'ist erinevas kliendis. Sa küll ei saa neid %(brand)s'is muuta, kuid nad kehtivad siiski.", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Sa hetkel kasutad serverit, et olla leitav ja ise leida sinule teadaolevaid inimesi. Alljärgnevalt saad sa muuta oma isikutuvastusserverit.", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Kui sa ei soovi kasutada serverit, et olla leitav ja ise leida sinule teadaolevaid inimesi, siis sisesta alljärgnevalt mõni teine isikutuvastusserver.", + "Identity Server": "Isikutuvastusserver", + "Do not use an identity server": "Ära kasuta isikutuvastusserverit", + "Enter a new identity server": "Sisesta uue isikutuvastusserveri nimi", + "Change": "Muuda", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Robotite, vidinate ja kleepsupakkide jaoks kasuta lõiminguhaldurit (%(serverName)s).", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Robotite, vidinate ja kleepsupakkide seadistamiseks kasuta lõiminguhaldurit.", + "Manage integrations": "Halda lõiminguid", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Lõiminguhalduritel on laiad volitused - nad võivad sinu nimel lugeda seadistusi, kohandada vidinaid, saata jututubade kutseid ning määrata õigusi." } From 3fce8df9eb845780ab47d56cf14810eee1cd6ea2 Mon Sep 17 00:00:00 2001 From: call_xz Date: Fri, 17 Jul 2020 20:43:55 +0000 Subject: [PATCH 093/308] Translated using Weblate (Japanese) Currently translated at 56.7% (1348 of 2378 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ja/ --- src/i18n/strings/ja.json | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 3c786fcfd2..cb250285ef 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -106,7 +106,7 @@ "Download this file": "この添付ファイルをダウンロード", "Call invitation": "通話への招待", "Forget": "忘れる", - "Messages containing keywords": "keywordsを含むメッセージ", + "Messages containing keywords": "キーワード を含むメッセージ", "Error saving email notification preferences": "電子メール通知設定の保存エラー", "Tuesday": "火曜日", "Enter keywords separated by a comma:": "キーワードをコンマで区切って入力:", @@ -712,7 +712,7 @@ "Name": "名前", "You must register to use this functionality": "この機能を使用するには登録する必要があります", "You must join the room to see its files": "そのファイルを見るために部屋に参加する必要があります", - "There are no visible files in this room": "この部屋には目に見えるファイルはありません", + "There are no visible files in this room": "この部屋に表示可能なファイルは存在しません", "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n": "

コミュニティのページのHTML

\n

\n 詳細な説明を使用して、新しいメンバーをコミュニティに紹介する、または配布する\n 重要なリンク\n

\n

\n あなたは 'img'タグを使うことさえできます\n

\n", "Add rooms to the community summary": "コミュニティサマリーに部屋を追加する", "Which rooms would you like to add to this summary?": "このサマリーにどの部屋を追加したいですか?", @@ -772,7 +772,7 @@ "Error whilst fetching joined communities": "参加したコミュニティを取得中にエラーが発生しました", "Create a new community": "新しいコミュニティを作成する", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "ユーザーと部屋をグループ化するコミュニティを作成してください! Matrixユニバースにあなたの空間を目立たせるためにカスタムホームページを作成してください。", - "You have no visible notifications": "表示される通知はありません", + "You have no visible notifications": "通知はありません", "You can't send any messages until you review and agree to our terms and conditions.": "利用規約 を確認して同意するまでは、いかなるメッセージも送信できません。", "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "このホームサーバーが月間アクティブユーザー制限を超えたため、メッセージは送信されませんでした。 サービスを引き続き使用するには、サービス管理者にお問い合わせください。", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "このホームサーバーがリソース制限を超えたため、メッセージは送信されませんでした。 サービスを引き続き使用するには、サービス管理者にお問い合わせください。", @@ -1324,5 +1324,35 @@ "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "このログインを他のセッションで承認し、あなたの認証情報を確認すれば、暗号化されたメッセージへアクセスできるようになります。", "This requires the latest %(brand)s on your other devices:": "最新版の %(brand)s が他のあなたのデバイスで実行されている必要があります:", "or another cross-signing capable Matrix client": "もしくはクロス署名に対応した他の Matrix クライアント", - "Without completing security on this session, it won’t have access to encrypted messages.": "このセッションでのセキュリティを完了させない限り、暗号化されたメッセージにはアクセスできません。" + "Without completing security on this session, it won’t have access to encrypted messages.": "このセッションでのセキュリティを完了させない限り、暗号化されたメッセージにはアクセスできません。", + "Single Sign On": "シングルサインオン", + "Light": "ライト", + "Dark": "ダーク", + "Font size": "フォントサイズ", + "Use custom size": "独自のサイズを使用", + "Use a more compact ‘Modern’ layout": "よりコンパクトで現代的なレイアウトを使用", + "Use a system font": "システムフォントを使用", + "System font name": "システムフォントの名前", + "Enable experimental, compact IRC style layout": "コンパクトな IRC スタイルレイアウトを使用 (実験的機能)", + "Messages containing my username": "自身のユーザー名を含むメッセージ", + "Messages containing @room": "@room を含むメッセージ", + "When rooms are upgraded": "部屋がアップグレードされた時", + "Hey you. You're the best!": "こんにちは、よろしくね!", + "Customise your appearance": "外観のカスタマイズ", + "Appearance Settings only affect this %(brand)s session.": "外観の設定はこの %(brand)s セッションにのみ適用されます。", + "People": "連絡先", + "Notification options": "通知設定", + "Verify User": "ユーザーの検証", + "Your homeserver": "あなたのホームサーバー", + "%(displayName)s cancelled verification.": "%(displayName)s が検証をキャンセルしました。", + "You cancelled verification.": "あなたが検証をキャンセルしました。", + "Verification cancelled": "検証のキャンセル", + "Notification settings": "通知設定", + "Switch to light mode": "ライトテーマに切り替え", + "Switch to dark mode": "ダークテーマに切り替え", + "Switch theme": "テーマを切り替え", + "Security & privacy": "セキュリティとプライバシー", + "All settings": "全ての設定", + "Feedback": "フィードバック", + "User menu": "ユーザーメニュー" } From 209a5d222003c0e864c526d46dff05c04ceb4f89 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jul 2020 15:10:30 -0600 Subject: [PATCH 094/308] 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 095/308] 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 096/308] 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 097/308] 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 098/308] 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} -