From c05eceef7f3560adad8354c0aef0f32d38684718 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 16 Feb 2021 17:56:09 +0000 Subject: [PATCH 001/185] Rework composer autocomplete to be smarter and not trap tab --- src/components/views/rooms/Autocomplete.tsx | 34 ++++++++++++------- .../views/rooms/BasicMessageComposer.tsx | 26 ++++++++++---- src/editor/autocomplete.ts | 5 ++- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 15af75084a..e62a5a9bd6 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -24,8 +24,6 @@ import {Room} from 'matrix-js-sdk/src/models/room'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; -const COMPOSER_SELECTED = 0; - export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`; interface IProps { @@ -68,7 +66,7 @@ export default class Autocomplete extends React.PureComponent { completionList: [], // how far down the completion list we are (THIS IS 1-INDEXED!) - selectionOffset: COMPOSER_SELECTED, + selectionOffset: 1, // whether we should show completions if they're available shouldShowCompletions: true, @@ -112,7 +110,7 @@ export default class Autocomplete extends React.PureComponent { completions: [], completionList: [], // Reset selected completion - selectionOffset: COMPOSER_SELECTED, + selectionOffset: 1, // Hide the autocomplete box hide: true, }); @@ -148,26 +146,31 @@ export default class Autocomplete extends React.PureComponent { const completionList = flatMap(completions, (provider) => provider.completions); // Reset selection when completion list becomes empty. - let selectionOffset = COMPOSER_SELECTED; + let selectionOffset = 1; if (completionList.length > 0) { /* If the currently selected completion is still in the completion list, try to find it and jump to it. If not, select composer. */ - const currentSelection = this.state.selectionOffset === 0 ? null : + const currentSelection = this.state.selectionOffset <= 1 ? null : this.state.completionList[this.state.selectionOffset - 1].completion; selectionOffset = completionList.findIndex( (completion) => completion.completion === currentSelection); if (selectionOffset === -1) { - selectionOffset = COMPOSER_SELECTED; + selectionOffset = 1; } else { selectionOffset++; // selectionOffset is 1-indexed! } } - let hide = this.state.hide; + let hide = true; // If `completion.command.command` is truthy, then a provider has matched with the query const anyMatches = completions.some((completion) => !!completion.command.command); - hide = !anyMatches; + if (anyMatches) { + hide = false; + if (this.props.onSelectionChange) { + this.props.onSelectionChange(this.state.completionList[selectionOffset - 1], selectionOffset - 1); + } + } this.setState({ completions, @@ -193,8 +196,8 @@ export default class Autocomplete extends React.PureComponent { if (completionCount === 0) return; // there are no items to move the selection through // Note: selectionOffset 0 represents the unsubstituted text, while 1 means first pill selected - const index = (this.state.selectionOffset + delta + completionCount + 1) % (completionCount + 1); - this.setSelection(index); + const index = (this.state.selectionOffset + delta + completionCount - 1) % completionCount; + this.setSelection(1 + index); } onEscape(e: KeyboardEvent): boolean { @@ -213,7 +216,7 @@ export default class Autocomplete extends React.PureComponent { hide = () => { this.setState({ hide: true, - selectionOffset: 0, + selectionOffset: 1, completions: [], completionList: [], }); @@ -232,8 +235,13 @@ export default class Autocomplete extends React.PureComponent { }); } + onConfirmCompletion = () => { + this.onCompletionClicked(this.state.selectionOffset); + } + onCompletionClicked = (selectionOffset: number): boolean => { - if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) { + const count = this.countCompletions(); + if (count === 0 || selectionOffset < 1 || selectionOffset > count) { return false; } diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 017ce77166..3c82f75b33 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -126,6 +126,7 @@ export default class BasicMessageEditor extends React.Component super(props); this.state = { showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"), + showVisualBell: false, }; this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, @@ -201,7 +202,11 @@ export default class BasicMessageEditor extends React.Component if (isEmpty) { this.formatBarRef.current.hide(); } - this.setState({autoComplete: this.props.model.autoComplete}); + this.setState({ + autoComplete: this.props.model.autoComplete, + // if a change is happening then clear the showVisualBell + showVisualBell: diff ? false : this.state.showVisualBell, + }); this.historyManager.tryPush(this.props.model, selection, inputType, diff); let isTyping = !this.props.model.isEmpty; @@ -490,6 +495,7 @@ export default class BasicMessageEditor extends React.Component } break; case Key.TAB: + case Key.ENTER: if (!metaOrAltPressed) { autoComplete.onTab(event); handled = true; @@ -504,7 +510,7 @@ export default class BasicMessageEditor extends React.Component default: return; // don't preventDefault on anything else } - } else if (event.key === Key.TAB) { + } else if (!this.props.model.isEmpty && !this.state.showVisualBell && event.key === Key.TAB) { this.tabCompleteName(event); handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { @@ -545,6 +551,8 @@ export default class BasicMessageEditor extends React.Component this.setState({showVisualBell: true}); model.autoComplete.close(); } + } else { + this.setState({showVisualBell: true}); } } catch (err) { console.error(err); @@ -562,7 +570,7 @@ export default class BasicMessageEditor extends React.Component private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number) => { this.modifiedFlag = true; - this.props.model.autoComplete.onComponentSelectionChange(completion); + // this.props.model.autoComplete.onComponentSelectionChange(completion); this.setState({completionIndex}); }; @@ -679,6 +687,11 @@ export default class BasicMessageEditor extends React.Component }; const {completionIndex} = this.state; + const hasAutocomplete = Boolean(this.state.autoComplete); + let activeDescendant; + if (hasAutocomplete && completionIndex >= 0) { + activeDescendant = generateCompletionDomId(completionIndex); + } return (
{ autoComplete } @@ -697,10 +710,11 @@ export default class BasicMessageEditor extends React.Component aria-label={this.props.label} role="textbox" aria-multiline="true" - aria-autocomplete="both" + aria-autocomplete="list" aria-haspopup="listbox" - aria-expanded={Boolean(this.state.autoComplete)} - aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined} + aria-expanded={hasAutocomplete} + aria-owns="mx_Autocomplete" + aria-activedescendant={activeDescendant} dir="auto" />
); diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index d8cea961d4..4633cd56c2 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -74,10 +74,9 @@ export default class AutocompleteWrapperModel { 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); + await acComponent.onConfirmCompletion(); + this.updateCallback({close: true}); } } From 9e2974d84d70cbdf4e01e8c813b53a3d73fd6133 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Feb 2021 10:25:25 +0000 Subject: [PATCH 002/185] Improve composer keyboard trapping --- src/components/structures/LoggedInView.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index c76cd7cee7..b04d840fb0 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -495,24 +495,24 @@ class LoggedInView extends React.Component { if (handled) { ev.stopPropagation(); ev.preventDefault(); - } else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) { + } else if (!isModifier && !ev.ctrlKey && !ev.metaKey) { // The above condition is crafted to _allow_ characters with Shift // already pressed (but not the Shift key down itself). - const isClickShortcut = ev.target !== document.body && (ev.key === Key.SPACE || ev.key === Key.ENTER); - // Do not capture the context menu key to improve keyboard accessibility - if (ev.key === Key.CONTEXT_MENU) { - return; - } + // We explicitly allow alt to be held due to it being a common accent modifier. + // XXX: Forwarding Dead keys in this way does not work as intended but better to at least + // move focus to the composer so the user can re-type the dead key correctly. + const isPrintable = ev.key.length === 1 || ev.key === "Dead"; - if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) { + // If the user is entering a printable character outside of an input field + // redirect it to the composer for them. + if (!isClickShortcut && isPrintable && !canElementReceiveInput(ev.target)) { // synchronous dispatch so we focus before key generates input dis.fire(Action.FocusComposer, true); ev.stopPropagation(); - // we should *not* preventDefault() here as - // that would prevent typing in the now-focussed composer + // we should *not* preventDefault() here as that would prevent typing in the now-focused composer } } }; From 9463fda1c10a26542bbfdd6ae0556ed8aa0fbb02 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Feb 2021 10:55:24 +0000 Subject: [PATCH 003/185] Improve VoiceOver & WebKit accessibility support Based on https://bugs.webkit.org/show_bug.cgi?id=167671#c15 (workaround) --- src/autocomplete/AutocompleteProvider.tsx | 17 +++++------------ src/autocomplete/CommandProvider.tsx | 2 +- src/autocomplete/CommunityProvider.tsx | 2 +- src/autocomplete/DuckDuckGoProvider.tsx | 2 +- src/autocomplete/EmojiProvider.tsx | 2 +- src/autocomplete/NotifProvider.tsx | 2 +- src/autocomplete/RoomProvider.tsx | 2 +- src/autocomplete/UserProvider.tsx | 2 +- src/components/views/rooms/Autocomplete.tsx | 4 ++-- 9 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index a40ce7144d..cc958546e1 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -27,11 +27,11 @@ export interface ICommand { }; } -export default class AutocompleteProvider { +export default abstract class AutocompleteProvider { commandRegex: RegExp; forcedCommandRegex: RegExp; - constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) { + protected constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) { if (commandRegex) { if (!commandRegex.global) { throw new Error('commandRegex must have global flag set'); @@ -93,18 +93,11 @@ export default class AutocompleteProvider { }; } - async getCompletions(query: string, selection: ISelectionRange, force = false): Promise { - return []; - } + abstract getCompletions(query: string, selection: ISelectionRange, force: boolean): Promise; - getName(): string { - return 'Default Provider'; - } + abstract getName(): string; - renderCompletions(completions: React.ReactNode[]): React.ReactNode | null { - console.error('stub; should be implemented in subclasses'); - return null; - } + abstract renderCompletions(completions: React.ReactNode[]): React.ReactNode | null; // Whether we should provide completions even if triggered forcefully, without a sigil. shouldForceComplete(): boolean { diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index c2d1290e08..7698dfcd15 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -91,7 +91,7 @@ export default class CommandProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index ebf5d536ec..8e2d2789cd 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -112,7 +112,7 @@ export default class CommunityProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/DuckDuckGoProvider.tsx b/src/autocomplete/DuckDuckGoProvider.tsx index e63f7255dc..a16c82aaf9 100644 --- a/src/autocomplete/DuckDuckGoProvider.tsx +++ b/src/autocomplete/DuckDuckGoProvider.tsx @@ -99,7 +99,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 705474f8d0..4a237fe091 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -140,7 +140,7 @@ export default class EmojiProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index ef1823c0ca..e948f8a985 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -66,7 +66,7 @@ export default class NotifProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 74deacf61f..6614615436 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -123,7 +123,7 @@ export default class RoomProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 32eea55b0b..6d909d38ad 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -178,7 +178,7 @@ export default class UserProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index e62a5a9bd6..cdea607cea 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -298,7 +298,7 @@ export default class Autocomplete extends React.PureComponent { return completions.length > 0 ? ( -
+
{ completionResult.provider.getName() }
{ completionResult.provider.renderCompletions(completions) }
@@ -306,7 +306,7 @@ export default class Autocomplete extends React.PureComponent { }).filter((completion) => !!completion); return !this.state.hide && renderedCompletions.length > 0 ? ( -
+
{ renderedCompletions }
) : null; From 131f499c25828b0c52401ca7556c14b78f23bbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 27 Feb 2021 08:04:20 +0100 Subject: [PATCH 004/185] Add Confirm Public Encrypted Room dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/room/SecurityRoomSettingsTab.js | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js index f72e78fa3f..b1be77064a 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js @@ -147,7 +147,7 @@ export default class SecurityRoomSettingsTab extends React.Component { }); }; - _onRoomAccessRadioToggle = (roomAccess) => { + _setRoomAccess = (roomAccess) => { // join_rule // INVITE | PUBLIC // ----------------------+---------------- @@ -191,6 +191,29 @@ export default class SecurityRoomSettingsTab extends React.Component { console.error(e); this.setState({guestAccess: beforeGuestAccess}); }); + } + + _onRoomAccessRadioToggle = async (roomAccess) => { + if ( + this.state.encrypted && + this.state.joinRule != "public" && + roomAccess != "invite_only" + ) { + Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, { + title: _t('Confirm making this room public?'), + description: _t( + "Making end-to-end encrypted rooms public renders the " + + "encryption pointless, wastes processing power, and can cause " + + "performance problems. Please consider creating a separate " + + "unencrypted public room.", + ), + onFinished: (confirm) => { + if (confirm) this._setRoomAccess(roomAccess); + }, + }); + } else { + this._setRoomAccess(roomAccess); + } }; _onHistoryRadioToggle = (history) => { From 0d9bc009689ad2df73cc8a601ca70a4f7c3e2ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 27 Feb 2021 08:04:30 +0100 Subject: [PATCH 005/185] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5bbbdf60b5..10d0b56e7f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1337,6 +1337,8 @@ "Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room", "Enable encryption?": "Enable encryption?", "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.": "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.", + "Confirm making this room public?": "Confirm making this room public?", + "Making end-to-end encrypted rooms public renders the encryption pointless, wastes processing power, and can cause performance problems. Please consider creating a separate unencrypted public room.": "Making end-to-end encrypted rooms public renders the encryption pointless, wastes processing power, and can cause performance problems. Please consider creating a separate unencrypted public room.", "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.", "Click here to fix": "Click here to fix", "To link to this room, please add an address.": "To link to this room, please add an address.", From 5d6bc9a88663463587768c49495e8c03e8f77a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 28 Feb 2021 08:29:31 +0100 Subject: [PATCH 006/185] Add second Confirm Public Encrypted Room dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/room/SecurityRoomSettingsTab.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js index b1be77064a..e3c23c890c 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js @@ -91,7 +91,24 @@ export default class SecurityRoomSettingsTab extends React.Component { if (refreshWhenTypes.includes(e.getType())) this.forceUpdate(); }; - _onEncryptionChange = (e) => { + _onEncryptionChange = async (e) => { + if (this.state.joinRule == "public") { + const {finished} = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, { + title: _t('Enable encryption in a public room?'), + description: _t( + "Note that enabling encryption in public rooms renders the " + + "encryption pointless, wastes processing power, and can cause " + + "performance problems. Please consider creating a separate " + + "encrypted room.", + ), + }); + const [confirm] = await finished; + if (!confirm) { + this.setState({encrypted: false}); + return; + } + } + Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, { title: _t('Enable encryption?'), description: _t( From 5a55b0dcf03e1a86a1a5eb3a33a9664b186d75bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 28 Feb 2021 08:29:37 +0100 Subject: [PATCH 007/185] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 10d0b56e7f..42ad88c01e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1335,6 +1335,8 @@ "Roles & Permissions": "Roles & Permissions", "Permissions": "Permissions", "Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room", + "Enable encryption in a public room?": "Enable encryption in a public room?", + "Note that enabling encryption in public rooms renders the encryption pointless, wastes processing power, and can cause performance problems. Please consider creating a separate encrypted room.": "Note that enabling encryption in public rooms renders the encryption pointless, wastes processing power, and can cause performance problems. Please consider creating a separate encrypted room.", "Enable encryption?": "Enable encryption?", "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.": "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.", "Confirm making this room public?": "Confirm making this room public?", From 24428783f53dfe88b79477374cc8f34e88fc98c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 28 Feb 2021 09:54:05 +0100 Subject: [PATCH 008/185] Remove unnecessary async MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/room/SecurityRoomSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js index e3c23c890c..ccc5470265 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js @@ -210,7 +210,7 @@ export default class SecurityRoomSettingsTab extends React.Component { }); } - _onRoomAccessRadioToggle = async (roomAccess) => { + _onRoomAccessRadioToggle = (roomAccess) => { if ( this.state.encrypted && this.state.joinRule != "public" && From 60e7089c770592b90194f478c57a3c587a71bad0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 11 May 2021 11:14:21 +0100 Subject: [PATCH 009/185] post-merge fixes, the new keybindings stuff made it messy --- src/KeyBindingsDefaults.ts | 14 ++-- src/KeyBindingsManager.ts | 12 ++- .../views/rooms/BasicMessageComposer.tsx | 75 ++++++++++--------- src/editor/autocomplete.ts | 6 +- 4 files changed, 52 insertions(+), 55 deletions(-) diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts index 63c4ac0f86..1270491d08 100644 --- a/src/KeyBindingsDefaults.ts +++ b/src/KeyBindingsDefaults.ts @@ -161,31 +161,29 @@ const messageComposerBindings = (): KeyBinding[] => { const autocompleteBindings = (): KeyBinding[] => { return [ { - action: AutocompleteAction.CompleteOrNextSelection, + action: AutocompleteAction.ForceComplete, keyCombo: { key: Key.TAB, }, }, { - action: AutocompleteAction.CompleteOrNextSelection, + action: AutocompleteAction.ForceComplete, keyCombo: { key: Key.TAB, ctrlKey: true, }, }, { - action: AutocompleteAction.CompleteOrPrevSelection, + action: AutocompleteAction.Complete, keyCombo: { - key: Key.TAB, - shiftKey: true, + key: Key.ENTER, }, }, { - action: AutocompleteAction.CompleteOrPrevSelection, + action: AutocompleteAction.Complete, keyCombo: { - key: Key.TAB, + key: Key.ENTER, ctrlKey: true, - shiftKey: true, }, }, { diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index aac14bde20..4a84b13257 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -52,13 +52,11 @@ export enum MessageComposerAction { /** Actions for text editing autocompletion */ export enum AutocompleteAction { - /** - * Select previous selection or, if the autocompletion window is not shown, open the window and select the first - * selection. - */ - CompleteOrPrevSelection = 'ApplySelection', - /** Select next selection or, if the autocompletion window is not shown, open it and select the first selection */ - CompleteOrNextSelection = 'CompleteOrNextSelection', + /** Accepts chosen autocomplete selection */ + Complete = 'Complete', + /** Accepts chosen autocomplete selection or, + * if the autocompletion window is not shown, open the window and select the first selection */ + ForceComplete = 'ForceComplete', /** Move to the previous autocomplete selection */ PrevSelection = 'PrevSelection', /** Move to the next autocomplete selection */ diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 5377e08b5e..a2cae654f3 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -434,6 +434,45 @@ export default class BasicMessageEditor extends React.Component private onKeyDown = (event: React.KeyboardEvent) => { const model = this.props.model; let handled = false; + + const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event); + if (model.autoComplete && model.autoComplete.hasCompletions()) { + const autoComplete = model.autoComplete; + switch (autocompleteAction) { + case AutocompleteAction.ForceComplete: + case AutocompleteAction.Complete: + autoComplete.confirmCompletion(); + handled = true; + break; + case AutocompleteAction.PrevSelection: + autoComplete.selectPreviousSelection(); + handled = true; + break; + case AutocompleteAction.NextSelection: + autoComplete.selectNextSelection(); + handled = true; + break; + case AutocompleteAction.Cancel: + autoComplete.onEscape(event); + handled = true; + break; + default: + return; // don't preventDefault on anything else + } + } else if (autocompleteAction === AutocompleteAction.ForceComplete) { + // there is no current autocomplete window, try to open it + this.tabCompleteName(); + handled = true; + } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { + this.formatBarRef.current.hide(); + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + return; + } + const action = getKeyBindingsManager().getMessageComposerAction(event); switch (action) { case MessageComposerAction.FormatBold: @@ -485,42 +524,6 @@ export default class BasicMessageEditor extends React.Component handled = true; break; } - if (handled) { - event.preventDefault(); - event.stopPropagation(); - return; - } - - const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event); - if (model.autoComplete && model.autoComplete.hasCompletions()) { - const autoComplete = model.autoComplete; - switch (autocompleteAction) { - case AutocompleteAction.CompleteOrPrevSelection: - case AutocompleteAction.PrevSelection: - autoComplete.selectPreviousSelection(); - handled = true; - break; - case AutocompleteAction.CompleteOrNextSelection: - case AutocompleteAction.NextSelection: - autoComplete.selectNextSelection(); - handled = true; - break; - case AutocompleteAction.Cancel: - autoComplete.onEscape(event); - handled = true; - break; - default: - return; // don't preventDefault on anything else - } - } else if (autocompleteAction === AutocompleteAction.CompleteOrPrevSelection - || autocompleteAction === AutocompleteAction.CompleteOrNextSelection) { - // there is no current autocomplete window, try to open it - this.tabCompleteName(); - handled = true; - } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { - this.formatBarRef.current.hide(); - } - if (handled) { event.preventDefault(); event.stopPropagation(); diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index 86260963e5..fe09406ca1 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -64,7 +64,8 @@ export default class AutocompleteWrapperModel { return ac && ac.countCompletions() > 0; } - public onEnter() { + public async confirmCompletion() { + await this.getAutocompleterComponent().onConfirmCompletion(); this.updateCallback({close: true}); } @@ -76,9 +77,6 @@ export default class AutocompleteWrapperModel { if (acComponent.countCompletions() === 0) { // Force completions to show for the text currently entered await acComponent.forceComplete(); - } else { - await acComponent.onConfirmCompletion(); - this.updateCallback({close: true}); } } From 78f569de94b623021aed496a6c848b4d6ec6e4bb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 12:56:23 +0100 Subject: [PATCH 010/185] delint --- src/autocomplete/AutocompleteProvider.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index 38dd34a047..1924ea48a7 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -98,9 +98,7 @@ export default abstract class AutocompleteProvider { selection: ISelectionRange, force: boolean, limit: number, - ): Promise { - return []; - } + ): Promise; abstract getName(): string; From 28eaac0ef808bb5e7604faef3aa240a6c4c2ae54 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 22:25:19 +0100 Subject: [PATCH 011/185] remove dead code and fix some types --- src/components/views/rooms/Autocomplete.tsx | 8 ++++---- .../views/rooms/BasicMessageComposer.tsx | 5 ++--- src/editor/autocomplete.ts | 20 ------------------- src/editor/model.ts | 2 +- 4 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 1eb2be473b..d9b4268917 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -33,9 +33,9 @@ interface IProps { // the query string for which to show autocomplete suggestions query: string; // method invoked with range and text content when completion is confirmed - onConfirm: (ICompletion) => void; + onConfirm: (completion: ICompletion) => void; // method invoked when selected (if any) completion changes - onSelectionChange?: (ICompletion, number) => void; + onSelectionChange?: (partIndex: number) => void; selection: ISelectionRange; // The room in which we're autocompleting room: Room; @@ -172,7 +172,7 @@ export default class Autocomplete extends React.PureComponent { if (anyMatches) { hide = false; if (this.props.onSelectionChange) { - this.props.onSelectionChange(this.state.completionList[selectionOffset - 1], selectionOffset - 1); + this.props.onSelectionChange(selectionOffset - 1); } } @@ -258,7 +258,7 @@ export default class Autocomplete extends React.PureComponent { setSelection(selectionOffset: number) { this.setState({selectionOffset, hide: false}); if (this.props.onSelectionChange) { - this.props.onSelectionChange(this.state.completionList[selectionOffset - 1], selectionOffset - 1); + this.props.onSelectionChange(selectionOffset - 1); } } diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index a2cae654f3..16332fda8f 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -575,10 +575,9 @@ export default class BasicMessageEditor extends React.Component this.props.model.autoComplete.onComponentConfirm(completion); }; - private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number) => { + private onAutoCompleteSelectionChange = (completionIndex: number) => { this.modifiedFlag = true; - // this.props.model.autoComplete.onComponentSelectionChange(completion); - this.setState({completionIndex}); + this.setState({ completionIndex }); }; private configureEmoticonAutoReplace = () => { diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index fe09406ca1..ea943c3f2c 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -32,7 +32,6 @@ export type GetAutocompleterComponent = () => Autocomplete; export type UpdateQuery = (test: string) => Promise; export default class AutocompleteWrapperModel { - private queryPart: Part; private partIndex: number; constructor( @@ -45,10 +44,6 @@ export default class AutocompleteWrapperModel { public onEscape(e: KeyboardEvent) { this.getAutocompleterComponent().onEscape(e); - this.updateCallback({ - replaceParts: [this.partCreator.plain(this.queryPart.text)], - close: true, - }); } public close() { @@ -89,25 +84,10 @@ export default class AutocompleteWrapperModel { } public onPartUpdate(part: Part, 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); } - public onComponentSelectionChange(completion: ICompletion) { - if (!completion) { - this.updateCallback({ - replaceParts: [this.queryPart], - }); - } else { - this.updateCallback({ - replaceParts: this.partForCompletion(completion), - }); - } - } - public onComponentConfirm(completion: ICompletion) { this.updateCallback({ replaceParts: this.partForCompletion(completion), diff --git a/src/editor/model.ts b/src/editor/model.ts index f1b6f90957..90765da047 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -237,7 +237,7 @@ export default class EditorModel { } } } - // not _autoComplete, only there if active part is autocomplete part + // not autoComplete, only there if active part is autocomplete part if (this.autoComplete) { return this.autoComplete.onPartUpdate(part, pos); } From 02c13788236b13dbcbcfe490e497a784c766996d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 22 May 2021 17:19:07 +0200 Subject: [PATCH 012/185] Fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/room/SecurityRoomSettingsTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index b165cec8cd..73cf70b91a 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -121,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component { + private onEncryptionChange = async (e: React.ChangeEvent) => { if (this.state.joinRule == "public") { const {finished} = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, { title: _t('Enable encryption in a public room?'), @@ -239,7 +239,7 @@ export default class SecurityRoomSettingsTab extends React.Component { + private onRoomAccessRadioToggle = (roomAccess) => { if ( this.state.encrypted && this.state.joinRule != "public" && From 27ee7c583656ec3e12cf4330d56647d86e6a3914 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 24 Jun 2021 17:51:11 +0100 Subject: [PATCH 013/185] Move backdrop filter to a canvas based solution --- res/css/structures/_LeftPanel.scss | 1 - res/css/structures/_MatrixChat.scss | 10 +++++ res/css/structures/_SpacePanel.scss | 1 + res/themes/light/css/_mods.scss | 21 --------- src/components/structures/BackdropPanel.tsx | 50 +++++++++++++++++++++ src/components/structures/LeftPanel.tsx | 34 +++++--------- src/components/structures/LoggedInView.tsx | 19 +++++++- src/components/views/spaces/SpacePanel.tsx | 25 ++++++++++- src/stores/OwnProfileStore.ts | 17 +++++++ 9 files changed, 129 insertions(+), 49 deletions(-) create mode 100644 src/components/structures/BackdropPanel.tsx diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index c7dd678c07..d9db8b4fed 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -43,7 +43,6 @@ $roomListCollapsedWidth: 68px; // 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_LeftPanel_roomListContainer { - background-color: $roomlist-bg-color; flex: 1 0 0; min-width: 0; // Create another flexbox (this time a column) for the room list components diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index a220c5d505..2e5ffbcd82 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -35,6 +35,16 @@ limitations under the License. height: 100%; } +.mx_BackdropPanel { + position: absolute; + top: 0; + left: 0; + width: 100%; + min-height: 100%; + z-index: 0; + opacity: .15; +} + .mx_MatrixToolbar { order: 1; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index e64057d16c..21c1bed18f 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -27,6 +27,7 @@ $activeBorderColor: $secondary-fg-color; background-color: $groupFilterPanel-bg-color; padding: 0; margin: 0; + position: relative; // Create another flexbox so the Panel fills the container display: flex; diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss index fbca58dfb1..15f6d4b0fe 100644 --- a/res/themes/light/css/_mods.scss +++ b/res/themes/light/css/_mods.scss @@ -4,27 +4,6 @@ // set the user avatar (if any) as a background so // it can be blurred by the tag panel and room list -@supports (backdrop-filter: none) { - .mx_LeftPanel { - background-image: var(--avatar-url, unset); - background-repeat: no-repeat; - background-size: cover; - background-position: left top; - } - - .mx_GroupFilterPanel { - backdrop-filter: blur($groupFilterPanel-background-blur-amount); - } - - .mx_SpacePanel { - backdrop-filter: blur($groupFilterPanel-background-blur-amount); - } - - .mx_LeftPanel .mx_LeftPanel_roomListContainer { - backdrop-filter: blur($roomlist-background-blur-amount); - } -} - .mx_RoomSublist_showNButton { background-color: transparent !important; } diff --git a/src/components/structures/BackdropPanel.tsx b/src/components/structures/BackdropPanel.tsx new file mode 100644 index 0000000000..daaae418c4 --- /dev/null +++ b/src/components/structures/BackdropPanel.tsx @@ -0,0 +1,50 @@ +import React, { createRef } from "react"; + +interface IProps { + width?: number; + height?: number; + backgroundImage?: ImageBitmap; + blur?: string; +} + + +export default class BackdropPanel extends React.PureComponent { + private canvasRef: React.RefObject = createRef(); + private ctx: CanvasRenderingContext2D; + + static defaultProps = { + blur: "60px", + } + + public componentDidMount() { + this.ctx = this.canvasRef.current.getContext("2d"); + } + + public componentDidUpdate() { + if (this.props.backgroundImage) { + requestAnimationFrame(this.refreshBackdropImage); + } + } + + private refreshBackdropImage = (): void => { + const { width, height, backgroundImage } = this.props; + this.canvasRef.current.width = width; + this.canvasRef.current.height = height; + + const destinationX = width - backgroundImage.width; + const destinationY = height - backgroundImage.height; + + this.ctx.filter = `blur(${this.props.blur})`; + this.ctx.drawImage( + backgroundImage, + Math.min(destinationX, 0), + Math.min(destinationY, 0), + Math.max(width, backgroundImage.width), + Math.max(height, backgroundImage.height), + ); + } + + public render() { + return ; + } +} diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index af22db1350..f71817480e 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -36,18 +36,18 @@ import SettingsStore from "../../settings/SettingsStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore"; import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; -import { OwnProfileStore } from "../../stores/OwnProfileStore"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; import {replaceableComponent} from "../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../customisations/Media"; import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; import UIStore from "../../stores/UIStore"; +import BackdropPanel from "./BackdropPanel"; interface IProps { isMinimized: boolean; resizeNotifier: ResizeNotifier; + backgroundImage?: ImageBitmap; } interface IState { @@ -85,16 +85,14 @@ export default class LeftPanel extends React.Component { BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); - OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); - this.bgImageWatcherRef = SettingsStore.watchSetting( - "RoomList.backgroundImage", null, this.onBackgroundImageUpdate); this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); }); } public componentDidMount() { + UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current); UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); UIStore.instance.on("ListContainer", this.refreshStickyHeaders); // Using the passive option to not block the main thread @@ -104,10 +102,8 @@ export default class LeftPanel extends React.Component { public componentWillUnmount() { SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef); - SettingsStore.unwatchSetting(this.bgImageWatcherRef); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); - OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); UIStore.instance.stopTrackingElementDimensions("ListContainer"); UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders); @@ -144,23 +140,6 @@ export default class LeftPanel extends React.Component { } }; - private onBackgroundImageUpdate = () => { - // Note: we do this in the LeftPanel as it uses this variable most prominently. - const avatarSize = 32; // arbitrary - let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); - const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage"); - if (settingBgMxc) { - avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize); - } - - const avatarUrlProp = `url(${avatarUrl})`; - if (!avatarUrl) { - document.body.style.removeProperty("--avatar-url"); - } else if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) { - document.body.style.setProperty("--avatar-url", avatarUrlProp); - } - }; - private handleStickyHeaders(list: HTMLDivElement) { if (this.isDoingStickyHeaders) return; this.isDoingStickyHeaders = true; @@ -453,8 +432,15 @@ export default class LeftPanel extends React.Component { "mx_AutoHideScrollbar", ); + const panelDimensions = UIStore.instance.getElementDimensions("LeftPanel"); + return (
+ {leftLeftPanel}