From 98808aabcab35f38ceb0c8d8f7ae6e3fc59cb3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 18:53:29 +0200 Subject: [PATCH 01/42] Set contentEditable for PillParts to false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 351df5062f..39e92ded1c 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -249,6 +249,7 @@ abstract class PillPart extends BasePart implements IPillPart { toDOMNode() { const container = document.createElement("span"); container.setAttribute("spellcheck", "false"); + container.setAttribute("contentEditable", "false"); container.className = this.className; container.appendChild(document.createTextNode(this.text)); this.setAvatar(container); From 5423421240cf1abbe749eb0dde82c8255753784a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 19:09:39 +0200 Subject: [PATCH 02/42] Give singletonRoomViewStore a type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/RoomViewStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 10f42f3166..1a85ff59b1 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -429,7 +429,7 @@ class RoomViewStore extends Store { } } -let singletonRoomViewStore = null; +let singletonRoomViewStore: RoomViewStore = null; if (!singletonRoomViewStore) { singletonRoomViewStore = new RoomViewStore(); } From 667abca31f42bfba9c055e521afb1540429dd840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 20:02:32 +0200 Subject: [PATCH 03/42] Handle pill onclick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_BasicMessageComposer.scss | 1 + src/editor/parts.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index e1ba468204..d87444441a 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -47,6 +47,7 @@ limitations under the License. &.mx_BasicMessageComposer_input_shouldShowPillAvatar { span.mx_UserPill, span.mx_RoomPill { position: relative; + cursor: pointer; // avatar psuedo element &::before { diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 39e92ded1c..8f662f9367 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -25,6 +25,10 @@ import AutocompleteWrapperModel, { UpdateQuery, } from "./autocomplete"; import * as Avatar from "../Avatar"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { Action } from "../dispatcher/actions"; +import singletonRoomViewStore from "../stores/RoomViewStore"; +import { MatrixClientPeg } from "../MatrixClientPeg"; interface ISerializedPart { type: Type.Plain | Type.Newline | Type.Command | Type.PillCandidate; @@ -74,6 +78,7 @@ interface IPillCandidatePart extends Omit { type: Type.AtRoomPill | Type.RoomPill | Type.UserPill; resourceId: string; + onClick?(): void; } export type Part = IBasePart | IPillCandidatePart | IPillPart; @@ -250,6 +255,7 @@ abstract class PillPart extends BasePart implements IPillPart { const container = document.createElement("span"); container.setAttribute("spellcheck", "false"); container.setAttribute("contentEditable", "false"); + container.onclick = this.onClick; container.className = this.className; container.appendChild(document.createTextNode(this.text)); this.setAvatar(container); @@ -304,6 +310,8 @@ abstract class PillPart extends BasePart implements IPillPart { abstract get className(): string; + abstract onClick?(): void; + abstract setAvatar(node: HTMLElement): void; } @@ -365,6 +373,9 @@ class RoomPillPart extends PillPart { get className() { return "mx_RoomPill mx_Pill"; } + + // FIXME: We do this to shut up the linter, is there a way to do this properly + onClick = undefined; } class AtRoomPillPart extends RoomPillPart { @@ -403,6 +414,13 @@ class UserPillPart extends PillPart { this._setAvatarVars(node, avatarUrl, initialLetter); } + onClick = () => { + defaultDispatcher.dispatch({ + action: Action.ViewUser, + member: MatrixClientPeg.get().getRoom(singletonRoomViewStore.getRoomId()).getMember(this.resourceId), + }); + }; + get type(): IPillPart["type"] { return Type.UserPill; } From 780f9b6add39c4ca9cb9df80c1c22509fbbffc30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:29:41 +0200 Subject: [PATCH 04/42] Handle pill deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/rooms/BasicMessageComposer.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 3258674cf6..d707a25e44 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -507,6 +507,7 @@ export default class BasicMessageEditor extends React.Component handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { this.formatBarRef.current.hide(); + handled = this.fakeDeletion(event.key === Key.BACKSPACE ? "deleteContentBackward" : "deleteContentForward"); } if (handled) { @@ -515,6 +516,29 @@ export default class BasicMessageEditor extends React.Component } }; + /** + * Because pills have contentEditable="false" there is no event emitted when + * the user tries to delete them. Therefore we need to fake what would + * normally happen + * @param direction in which to delete + * @returns handled + */ + private fakeDeletion(direction: "deleteContentForward" | "deleteContentBackward" ): boolean { + const selection = document.getSelection(); + // Use the default handling for ranges + if (selection.type === "Range") return false; + + this.modifiedFlag = true; + const { caret, text } = getCaretOffsetAndText(this.editorRef.current, selection); + + // Do the deletion itself + if (direction === "deleteContentBackward") caret.offset--; + const newText = text.slice(0, caret.offset) + text.slice(caret.offset + 1); + + this.props.model.update(newText, direction, caret); + return true; + } + private async tabCompleteName(): Promise { try { await new Promise(resolve => this.setState({ showVisualBell: false }, resolve)); From 113b6319b129ab550669ed506b5f4b10e8b7ed60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:52:01 +0200 Subject: [PATCH 05/42] This looks a bit nicer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 8f662f9367..3d50c39cd0 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -27,7 +27,7 @@ import AutocompleteWrapperModel, { import * as Avatar from "../Avatar"; import defaultDispatcher from "../dispatcher/dispatcher"; import { Action } from "../dispatcher/actions"; -import singletonRoomViewStore from "../stores/RoomViewStore"; +import RoomViewStore from "../stores/RoomViewStore"; import { MatrixClientPeg } from "../MatrixClientPeg"; interface ISerializedPart { @@ -417,7 +417,7 @@ class UserPillPart extends PillPart { onClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, - member: MatrixClientPeg.get().getRoom(singletonRoomViewStore.getRoomId()).getMember(this.resourceId), + member: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()).getMember(this.resourceId), }); }; From 4ef8c9fd297a616658152bba6e0406a301cbe948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:52:32 +0200 Subject: [PATCH 06/42] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index ddcb9057ec..f3b580cdca 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -240,7 +240,7 @@ export default class CallPreview extends React.Component { this.scheduledUpdate.mark(); }; - private onRoomViewStoreUpdate = (payload) => { + private onRoomViewStoreUpdate = () => { if (RoomViewStore.getRoomId() === this.state.roomId) return; const roomId = RoomViewStore.getRoomId(); From d7811d9db7f1ac6f2d5a75efb49fd3bd38fbdfb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:59:31 +0200 Subject: [PATCH 07/42] Maybe this shuts it up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/ActiveRoomObserver.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ActiveRoomObserver.ts b/src/ActiveRoomObserver.ts index 1126dc9496..c7423fab8f 100644 --- a/src/ActiveRoomObserver.ts +++ b/src/ActiveRoomObserver.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { EventSubscription } from 'fbemitter'; import RoomViewStore from './stores/RoomViewStore'; type Listener = (isActive: boolean) => void; @@ -30,7 +31,7 @@ type Listener = (isActive: boolean) => void; export class ActiveRoomObserver { private listeners: {[key: string]: Listener[]} = {}; private _activeRoomId = RoomViewStore.getRoomId(); - private readonly roomStoreToken: string; + private readonly roomStoreToken: EventSubscription; constructor() { // TODO: We could self-destruct when the last listener goes away, or at least stop listening. From b79f2d06991711a2233e1832e7a37a4d614c7b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:21:59 +0200 Subject: [PATCH 08/42] Fix the ugly solution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 3d50c39cd0..af741c4502 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -78,7 +78,6 @@ interface IPillCandidatePart extends Omit { type: Type.AtRoomPill | Type.RoomPill | Type.UserPill; resourceId: string; - onClick?(): void; } export type Part = IBasePart | IPillCandidatePart | IPillPart; @@ -310,7 +309,7 @@ abstract class PillPart extends BasePart implements IPillPart { abstract get className(): string; - abstract onClick?(): void; + protected onClick?: () => void; abstract setAvatar(node: HTMLElement): void; } @@ -373,9 +372,6 @@ class RoomPillPart extends PillPart { get className() { return "mx_RoomPill mx_Pill"; } - - // FIXME: We do this to shut up the linter, is there a way to do this properly - onClick = undefined; } class AtRoomPillPart extends RoomPillPart { @@ -414,7 +410,7 @@ class UserPillPart extends PillPart { this._setAvatarVars(node, avatarUrl, initialLetter); } - onClick = () => { + protected onClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, member: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()).getMember(this.resourceId), From 48a6a83745a7f799a0647509b4a796ca4cbfb8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:41:58 +0200 Subject: [PATCH 09/42] Set cursor for each pill type separately MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_BasicMessageComposer.scss | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index d87444441a..544a96daba 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -47,7 +47,6 @@ limitations under the License. &.mx_BasicMessageComposer_input_shouldShowPillAvatar { span.mx_UserPill, span.mx_RoomPill { position: relative; - cursor: pointer; // avatar psuedo element &::before { @@ -66,6 +65,14 @@ limitations under the License. font-size: $font-10-4px; } } + + span.mx_UserPill { + cursor: pointer; + } + + span.mx_RoomPill { + cursor: default; + } } &.mx_BasicMessageComposer_input_disabled { From 069c1f466520ddfe424b5b1b58e9054e01b1f7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:52:05 +0200 Subject: [PATCH 10/42] Make code a bit cleaner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/BasicMessageComposer.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index d707a25e44..81211c57b7 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -507,7 +507,7 @@ export default class BasicMessageEditor extends React.Component handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { this.formatBarRef.current.hide(); - handled = this.fakeDeletion(event.key === Key.BACKSPACE ? "deleteContentBackward" : "deleteContentForward"); + handled = this.fakeDeletion(event.key === Key.BACKSPACE); } if (handled) { @@ -523,7 +523,7 @@ export default class BasicMessageEditor extends React.Component * @param direction in which to delete * @returns handled */ - private fakeDeletion(direction: "deleteContentForward" | "deleteContentBackward" ): boolean { + private fakeDeletion(backward: boolean): boolean { const selection = document.getSelection(); // Use the default handling for ranges if (selection.type === "Range") return false; @@ -532,10 +532,10 @@ export default class BasicMessageEditor extends React.Component const { caret, text } = getCaretOffsetAndText(this.editorRef.current, selection); // Do the deletion itself - if (direction === "deleteContentBackward") caret.offset--; + if (backward) caret.offset--; const newText = text.slice(0, caret.offset) + text.slice(caret.offset + 1); - this.props.model.update(newText, direction, caret); + this.props.model.update(newText, backward ? "deleteContentBackward" : "deleteContentForward", caret); return true; } From 3515b2ca05e25d43f1d1ff3bb803e319f5b93c63 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Jul 2021 12:51:27 +0100 Subject: [PATCH 11/42] Fix edge case behaviour caused by our weird reuse of DOM nodes between owners --- src/editor/parts.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index af741c4502..c1724c09bb 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -269,6 +269,9 @@ abstract class PillPart extends BasePart implements IPillPart { if (node.className !== this.className) { node.className = this.className; } + if (node.onclick !== this.onClick) { + node.onclick = this.onClick; + } this.setAvatar(node); } From 8139aeb073a6a3b018e10614a4a39fd0a1841623 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Jul 2021 12:51:49 +0100 Subject: [PATCH 12/42] skip loading room & finding member, use existing member field --- src/editor/parts.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index c1724c09bb..688116ab90 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -27,8 +27,6 @@ import AutocompleteWrapperModel, { import * as Avatar from "../Avatar"; import defaultDispatcher from "../dispatcher/dispatcher"; import { Action } from "../dispatcher/actions"; -import RoomViewStore from "../stores/RoomViewStore"; -import { MatrixClientPeg } from "../MatrixClientPeg"; interface ISerializedPart { type: Type.Plain | Type.Newline | Type.Command | Type.PillCandidate; @@ -416,7 +414,7 @@ class UserPillPart extends PillPart { protected onClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, - member: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()).getMember(this.resourceId), + member: this.member, }); }; From 51f0f5718a8882897d99174a0879e610cc158223 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Jul 2021 13:26:34 +0100 Subject: [PATCH 13/42] improve types --- .../views/rooms/BasicMessageComposer.tsx | 10 +- .../views/rooms/EditMessageComposer.tsx | 8 +- .../views/rooms/SendMessageComposer.tsx | 6 +- src/editor/autocomplete.ts | 24 +-- src/editor/caret.ts | 6 +- src/editor/deserialize.ts | 4 +- src/editor/diff.ts | 2 +- src/editor/history.ts | 14 +- src/editor/offset.ts | 5 +- src/editor/operations.ts | 26 +-- src/editor/parts.ts | 162 +++++++++--------- src/editor/position.ts | 14 +- src/editor/range.ts | 20 +-- src/editor/render.ts | 32 ++-- src/editor/serialize.ts | 49 +++--- 15 files changed, 196 insertions(+), 186 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 81211c57b7..bf6a6a27d2 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -32,7 +32,7 @@ import { } from '../../../editor/operations'; import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom'; import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete'; -import { getAutoCompleteCreator } from '../../../editor/parts'; +import { getAutoCompleteCreator, Type } from '../../../editor/parts'; import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize'; import { renderModel } from '../../../editor/render'; import TypingStore from "../../../stores/TypingStore"; @@ -157,7 +157,7 @@ export default class BasicMessageEditor extends React.Component range.expandBackwardsWhile((index, offset) => { const part = model.parts[index]; n -= 1; - return n >= 0 && (part.type === "plain" || part.type === "pill-candidate"); + return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate); }); const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text); if (emoticonMatch) { @@ -548,9 +548,9 @@ export default class BasicMessageEditor extends React.Component const range = model.startRange(position); range.expandBackwardsWhile((index, offset, part) => { return part.text[offset] !== " " && part.text[offset] !== "+" && ( - part.type === "plain" || - part.type === "pill-candidate" || - part.type === "command" + part.type === Type.Plain || + part.type === Type.PillCandidate || + part.type === Type.Command ); }); const { partCreator } = model; diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index e4b13e2155..b7e067ee93 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -25,7 +25,7 @@ import { getCaretOffsetAndText } from '../../../editor/dom'; import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize'; import { findEditableEvent } from '../../../utils/EventUtils'; import { parseEvent } from '../../../editor/deserialize'; -import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import BasicMessageComposer from "./BasicMessageComposer"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -242,12 +242,12 @@ export default class EditMessageComposer extends React.Component const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { - if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { + if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { return true; } if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") - && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + && (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) { return true; } } @@ -268,7 +268,7 @@ export default class EditMessageComposer extends React.Component private getSlashCommand(): [Command, string, string] { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command - if (part.type === "user-pill") { + if (part.type === Type.UserPill) { return text + part.resourceId; } return text + part.text; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 0639c20fef..76e33ce4b7 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -31,7 +31,7 @@ import { textSerialize, unescapeMessage, } from '../../../editor/serialize'; -import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyThread from "../elements/ReplyThread"; import { findEditableEvent } from '../../../utils/EventUtils'; @@ -240,14 +240,14 @@ export default class SendMessageComposer extends React.Component { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { - if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { + if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { return true; } // be extra resilient when somehow the AutocompleteWrapperModel or // CommandPartCreator fails to insert a command part, so we don't send // a command as a message if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") - && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + && (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) { return true; } } diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index 518c77fa6c..bf8f457d0c 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -43,7 +43,7 @@ export default class AutocompleteWrapperModel { ) { } - public onEscape(e: KeyboardEvent) { + public onEscape(e: KeyboardEvent): void { this.getAutocompleterComponent().onEscape(e); this.updateCallback({ replaceParts: [this.partCreator.plain(this.queryPart.text)], @@ -51,27 +51,27 @@ export default class AutocompleteWrapperModel { }); } - public close() { + public close(): void { this.updateCallback({ close: true }); } - public hasSelection() { + public hasSelection(): boolean { return this.getAutocompleterComponent().hasSelection(); } - public hasCompletions() { + public hasCompletions(): boolean { const ac = this.getAutocompleterComponent(); return ac && ac.countCompletions() > 0; } - public onEnter() { + public onEnter(): void { this.updateCallback({ close: true }); } /** * If there is no current autocompletion, start one and move to the first selection. */ - public async startSelection() { + public async startSelection(): Promise { const acComponent = this.getAutocompleterComponent(); if (acComponent.countCompletions() === 0) { // Force completions to show for the text currently entered @@ -81,15 +81,15 @@ export default class AutocompleteWrapperModel { } } - public selectPreviousSelection() { + public selectPreviousSelection(): void { this.getAutocompleterComponent().moveSelection(-1); } - public selectNextSelection() { + public selectNextSelection(): void { this.getAutocompleterComponent().moveSelection(+1); } - public onPartUpdate(part: Part, pos: DocumentPosition) { + public onPartUpdate(part: Part, pos: DocumentPosition): Promise { // 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; @@ -97,7 +97,7 @@ export default class AutocompleteWrapperModel { return this.updateQuery(part.text); } - public onComponentSelectionChange(completion: ICompletion) { + public onComponentSelectionChange(completion: ICompletion): void { if (!completion) { this.updateCallback({ replaceParts: [this.queryPart], @@ -109,14 +109,14 @@ export default class AutocompleteWrapperModel { } } - public onComponentConfirm(completion: ICompletion) { + public onComponentConfirm(completion: ICompletion): void { this.updateCallback({ replaceParts: this.partForCompletion(completion), close: true, }); } - private partForCompletion(completion: ICompletion) { + private partForCompletion(completion: ICompletion): Part[] { const { completionId } = completion; const text = completion.completion; switch (completion.type) { diff --git a/src/editor/caret.ts b/src/editor/caret.ts index 67d10ddbb5..2b5035b567 100644 --- a/src/editor/caret.ts +++ b/src/editor/caret.ts @@ -19,7 +19,7 @@ import { needsCaretNodeBefore, needsCaretNodeAfter } from "./render"; import Range from "./range"; import EditorModel from "./model"; import DocumentPosition, { IPosition } from "./position"; -import { Part } from "./parts"; +import { Part, Type } from "./parts"; export type Caret = Range | DocumentPosition; @@ -113,7 +113,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) { // to find newline parts for (let i = 0; i <= partIndex; ++i) { const part = parts[i]; - if (part.type === "newline") { + if (part.type === Type.Newline) { lineIndex += 1; nodeIndex = -1; prevPart = null; @@ -128,7 +128,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) { // and not an adjacent caret node if (i < partIndex) { const nextPart = parts[i + 1]; - const isLastOfLine = !nextPart || nextPart.type === "newline"; + const isLastOfLine = !nextPart || nextPart.type === Type.Newline; if (needsCaretNodeAfter(part, isLastOfLine)) { nodeIndex += 1; } diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index eb8adfda9d..beef3be5cf 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { walkDOMDepthFirst } from "./dom"; import { checkBlockNode } from "../HtmlUtils"; import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks"; -import { PartCreator } from "./parts"; +import { PartCreator, Type } from "./parts"; import SdkConfig from "../SdkConfig"; function parseAtRoomMentions(text: string, partCreator: PartCreator) { @@ -200,7 +200,7 @@ function prefixQuoteLines(isFirstNode, parts, partCreator) { parts.splice(0, 0, partCreator.plain(QUOTE_LINE_PREFIX)); } for (let i = 0; i < parts.length; i += 1) { - if (parts[i].type === "newline") { + if (parts[i].type === Type.Newline) { parts.splice(i + 1, 0, partCreator.plain(QUOTE_LINE_PREFIX)); i += 1; } diff --git a/src/editor/diff.ts b/src/editor/diff.ts index de8efc9c21..5cf94560ce 100644 --- a/src/editor/diff.ts +++ b/src/editor/diff.ts @@ -21,7 +21,7 @@ export interface IDiff { at?: number; } -function firstDiff(a: string, b: string) { +function firstDiff(a: string, b: string): number { const compareLen = Math.min(a.length, b.length); for (let i = 0; i < compareLen; ++i) { if (a[i] !== b[i]) { diff --git a/src/editor/history.ts b/src/editor/history.ts index 350ba6c99a..7764dbf682 100644 --- a/src/editor/history.ts +++ b/src/editor/history.ts @@ -36,7 +36,7 @@ export default class HistoryManager { private addedSinceLastPush = false; private removedSinceLastPush = false; - clear() { + public clear(): void { this.stack = []; this.newlyTypedCharCount = 0; this.currentIndex = -1; @@ -103,7 +103,7 @@ export default class HistoryManager { } // needs to persist parts and caret position - tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff) { + public tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff): boolean { // ignore state restoration echos. // these respect the inputType values of the input event, // but are actually passed in from MessageEditor calling model.reset() @@ -121,22 +121,22 @@ export default class HistoryManager { return shouldPush; } - ensureLastChangesPushed(model: EditorModel) { + public ensureLastChangesPushed(model: EditorModel): void { if (this.changedSinceLastPush) { this.pushState(model, this.lastCaret); } } - canUndo() { + public canUndo(): boolean { return this.currentIndex >= 1 || this.changedSinceLastPush; } - canRedo() { + public canRedo(): boolean { return this.currentIndex < (this.stack.length - 1); } // returns state that should be applied to model - undo(model: EditorModel) { + public undo(model: EditorModel): IHistory { if (this.canUndo()) { this.ensureLastChangesPushed(model); this.currentIndex -= 1; @@ -145,7 +145,7 @@ export default class HistoryManager { } // returns state that should be applied to model - redo() { + public redo(): IHistory { if (this.canRedo()) { this.changedSinceLastPush = false; this.currentIndex += 1; diff --git a/src/editor/offset.ts b/src/editor/offset.ts index 413a22c71b..2e6e0ffe21 100644 --- a/src/editor/offset.ts +++ b/src/editor/offset.ts @@ -15,16 +15,17 @@ limitations under the License. */ import EditorModel from "./model"; +import DocumentPosition from "./position"; export default class DocumentOffset { constructor(public offset: number, public readonly atNodeEnd: boolean) { } - asPosition(model: EditorModel) { + public asPosition(model: EditorModel): DocumentPosition { return model.positionForOffset(this.offset, this.atNodeEnd); } - add(delta: number, atNodeEnd = false) { + public add(delta: number, atNodeEnd = false): DocumentOffset { return new DocumentOffset(this.offset + delta, atNodeEnd); } } diff --git a/src/editor/operations.ts b/src/editor/operations.ts index a738f2d111..2ff09ccce6 100644 --- a/src/editor/operations.ts +++ b/src/editor/operations.ts @@ -15,13 +15,13 @@ limitations under the License. */ import Range from "./range"; -import { Part } from "./parts"; +import { Part, Type } from "./parts"; /** * Some common queries and transformations on the editor model */ -export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) { +export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]): void { const { model } = range; model.transform(() => { const oldLen = range.length; @@ -32,7 +32,7 @@ export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) { }); } -export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) { +export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]): void { const { model } = range; model.transform(() => { const oldLen = range.length; @@ -43,29 +43,29 @@ export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) { }); } -export function rangeStartsAtBeginningOfLine(range: Range) { +export function rangeStartsAtBeginningOfLine(range: Range): boolean { const { model } = range; const startsWithPartial = range.start.offset !== 0; const isFirstPart = range.start.index === 0; - const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline"; + const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === Type.Newline; return !startsWithPartial && (isFirstPart || previousIsNewline); } -export function rangeEndsAtEndOfLine(range: Range) { +export function rangeEndsAtEndOfLine(range: Range): boolean { const { model } = range; const lastPart = model.parts[range.end.index]; 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"; + const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === Type.Newline; return !endsWithPartial && (isLastPart || nextIsNewline); } -export function formatRangeAsQuote(range: Range) { +export function formatRangeAsQuote(range: Range): void { const { model, parts } = range; const { partCreator } = model; for (let i = 0; i < parts.length; ++i) { const part = parts[i]; - if (part.type === "newline") { + if (part.type === Type.Newline) { parts.splice(i + 1, 0, partCreator.plain("> ")); } } @@ -81,10 +81,10 @@ export function formatRangeAsQuote(range: Range) { replaceRangeAndExpandSelection(range, parts); } -export function formatRangeAsCode(range: Range) { +export function formatRangeAsCode(range: Range): void { const { model, parts } = range; const { partCreator } = model; - const needsBlock = parts.some(p => p.type === "newline"); + const needsBlock = parts.some(p => p.type === Type.Newline); if (needsBlock) { parts.unshift(partCreator.plain("```"), partCreator.newline()); if (!rangeStartsAtBeginningOfLine(range)) { @@ -105,9 +105,9 @@ export function formatRangeAsCode(range: Range) { // parts helper methods const isBlank = part => !part.text || !/\S/.test(part.text); -const isNL = part => part.type === "newline"; +const isNL = part => part.type === Type.Newline; -export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix) { +export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix): void { const { model, parts } = range; const { partCreator } = model; diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 688116ab90..4e0235bdf7 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -41,7 +41,7 @@ interface ISerializedPillPart { export type SerializedPart = ISerializedPart | ISerializedPillPart; -enum Type { +export enum Type { Plain = "plain", Newline = "newline", Command = "command", @@ -59,12 +59,12 @@ interface IBasePart { createAutoComplete(updateCallback: UpdateCallback): void; serialize(): SerializedPart; - remove(offset: number, len: number): string; + remove(offset: number, len: number): string | undefined; split(offset: number): IBasePart; validateAndInsert(offset: number, str: string, inputType: string): boolean; - appendUntilRejected(str: string, inputType: string): string; - updateDOMNode(node: Node); - canUpdateDOMNode(node: Node); + appendUntilRejected(str: string, inputType: string): string | undefined; + updateDOMNode(node: Node): void; + canUpdateDOMNode(node: Node): boolean; toDOMNode(): Node; } @@ -87,19 +87,19 @@ abstract class BasePart { this._text = text; } - acceptsInsertion(chr: string, offset: number, inputType: string) { + protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean { return true; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return true; } - merge(part: Part) { + public merge(part: Part): boolean { return false; } - split(offset: number) { + public split(offset: number): IBasePart { const splitText = this.text.substr(offset); this._text = this.text.substr(0, offset); return new PlainPart(splitText); @@ -107,7 +107,7 @@ abstract 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: number, len: number) { + public remove(offset: number, len: number): string | undefined { // validate const strWithRemoval = this.text.substr(0, offset) + this.text.substr(offset + len); for (let i = offset; i < (len + offset); ++i) { @@ -120,7 +120,7 @@ abstract class BasePart { } // append str, returns the remaining string if a character was rejected. - appendUntilRejected(str: string, inputType: string) { + public appendUntilRejected(str: string, inputType: string): string | undefined { const offset = this.text.length; for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); @@ -134,7 +134,7 @@ abstract 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: number, str: string, inputType: string) { + public validateAndInsert(offset: number, str: string, inputType: string): boolean { for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); if (!this.acceptsInsertion(chr, offset + i, inputType)) { @@ -147,42 +147,42 @@ abstract class BasePart { return true; } - createAutoComplete(updateCallback: UpdateCallback): void {} + public createAutoComplete(updateCallback: UpdateCallback): void {} - trim(len: number) { + protected trim(len: number): string { const remaining = this._text.substr(len); this._text = this._text.substr(0, len); return remaining; } - get text() { + public get text(): string { return this._text; } - abstract get type(): Type; + public abstract get type(): Type; - get canEdit() { + public get canEdit(): boolean { return true; } - toString() { + public toString(): string { return `${this.type}(${this.text})`; } - serialize(): SerializedPart { + public serialize(): SerializedPart { return { type: this.type as ISerializedPart["type"], text: this.text, }; } - abstract updateDOMNode(node: Node); - abstract canUpdateDOMNode(node: Node); - abstract toDOMNode(): Node; + public abstract updateDOMNode(node: Node): void; + public abstract canUpdateDOMNode(node: Node): boolean; + public abstract toDOMNode(): Node; } abstract class PlainBasePart extends BasePart { - acceptsInsertion(chr: string, offset: number, inputType: string) { + protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean { if (chr === "\n") { return false; } @@ -205,11 +205,11 @@ abstract class PlainBasePart extends BasePart { return true; } - toDOMNode() { + public toDOMNode(): Node { return document.createTextNode(this.text); } - merge(part) { + public merge(part): boolean { if (part.type === this.type) { this._text = this.text + part.text; return true; @@ -217,38 +217,38 @@ abstract class PlainBasePart extends BasePart { return false; } - updateDOMNode(node: Node) { + public updateDOMNode(node: Node): void { if (node.textContent !== this.text) { node.textContent = this.text; } } - canUpdateDOMNode(node: Node) { + public canUpdateDOMNode(node: Node): boolean { return node.nodeType === Node.TEXT_NODE; } } // exported for unit tests, should otherwise only be used through PartCreator export class PlainPart extends PlainBasePart implements IBasePart { - get type(): IBasePart["type"] { + public get type(): IBasePart["type"] { return Type.Plain; } } -abstract class PillPart extends BasePart implements IPillPart { +export abstract class PillPart extends BasePart implements IPillPart { constructor(public resourceId: string, label) { super(label); } - acceptsInsertion(chr: string) { + protected acceptsInsertion(chr: string): boolean { return chr !== " "; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return position !== 0; //if you remove initial # or @, pill should become plain } - toDOMNode() { + public toDOMNode(): Node { const container = document.createElement("span"); container.setAttribute("spellcheck", "false"); container.setAttribute("contentEditable", "false"); @@ -259,7 +259,7 @@ abstract class PillPart extends BasePart implements IPillPart { return container; } - updateDOMNode(node: HTMLElement) { + public updateDOMNode(node: HTMLElement): void { const textNode = node.childNodes[0]; if (textNode.textContent !== this.text) { textNode.textContent = this.text; @@ -273,7 +273,7 @@ abstract class PillPart extends BasePart implements IPillPart { this.setAvatar(node); } - canUpdateDOMNode(node: HTMLElement) { + public canUpdateDOMNode(node: HTMLElement): boolean { return node.nodeType === Node.ELEMENT_NODE && node.nodeName === "SPAN" && node.childNodes.length === 1 && @@ -281,7 +281,7 @@ abstract class PillPart extends BasePart implements IPillPart { } // helper method for subclasses - _setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) { + protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string): void { const avatarBackground = `url('${avatarUrl}')`; const avatarLetter = `'${initialLetter}'`; // check if the value is changing, @@ -294,7 +294,7 @@ abstract class PillPart extends BasePart implements IPillPart { } } - serialize(): ISerializedPillPart { + public serialize(): ISerializedPillPart { return { type: this.type, text: this.text, @@ -302,43 +302,43 @@ abstract class PillPart extends BasePart implements IPillPart { }; } - get canEdit() { + public get canEdit(): boolean { return false; } - abstract get type(): IPillPart["type"]; + public abstract get type(): IPillPart["type"]; - abstract get className(): string; + protected abstract get className(): string; protected onClick?: () => void; - abstract setAvatar(node: HTMLElement): void; + protected abstract setAvatar(node: HTMLElement): void; } class NewlinePart extends BasePart implements IBasePart { - acceptsInsertion(chr: string, offset: number) { + protected acceptsInsertion(chr: string, offset: number): boolean { return offset === 0 && chr === "\n"; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return true; } - toDOMNode() { + public toDOMNode(): Node { return document.createElement("br"); } - merge() { + public merge(): boolean { return false; } - updateDOMNode() {} + public updateDOMNode(): void {} - canUpdateDOMNode(node: HTMLElement) { + public canUpdateDOMNode(node: HTMLElement): boolean { return node.tagName === "BR"; } - get type(): IBasePart["type"] { + public get type(): IBasePart["type"] { return Type.Newline; } @@ -346,7 +346,7 @@ class NewlinePart extends BasePart implements IBasePart { // rather than trying to append to it, which is what we want. // As a newline can also be only one character, it makes sense // as it can only be one character long. This caused #9741. - get canEdit() { + public get canEdit(): boolean { return false; } } @@ -356,21 +356,21 @@ class RoomPillPart extends PillPart { super(resourceId, label); } - setAvatar(node: HTMLElement) { + protected setAvatar(node: HTMLElement): void { let initialLetter = ""; let avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop"); if (!avatarUrl) { 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); + this.setAvatarVars(node, avatarUrl, initialLetter); } - get type(): IPillPart["type"] { + public get type(): IPillPart["type"] { return Type.RoomPill; } - get className() { + protected get className() { return "mx_RoomPill mx_Pill"; } } @@ -380,11 +380,11 @@ class AtRoomPillPart extends RoomPillPart { super(text, text, room); } - get type(): IPillPart["type"] { + public get type(): IPillPart["type"] { return Type.AtRoomPill; } - serialize(): ISerializedPillPart { + public serialize(): ISerializedPillPart { return { type: this.type, text: this.text, @@ -397,7 +397,7 @@ class UserPillPart extends PillPart { super(userId, displayName); } - setAvatar(node: HTMLElement) { + protected setAvatar(node: HTMLElement): void { if (!this.member) { return; } @@ -408,21 +408,21 @@ class UserPillPart extends PillPart { if (avatarUrl === defaultAvatarUrl) { initialLetter = Avatar.getInitialLetter(name); } - this._setAvatarVars(node, avatarUrl, initialLetter); + this.setAvatarVars(node, avatarUrl, initialLetter); } - protected onClick = () => { + protected onClick = (): void => { defaultDispatcher.dispatch({ action: Action.ViewUser, member: this.member, }); }; - get type(): IPillPart["type"] { + public get type(): IPillPart["type"] { return Type.UserPill; } - get className() { + protected get className() { return "mx_UserPill mx_Pill"; } } @@ -432,11 +432,11 @@ class PillCandidatePart extends PlainBasePart implements IPillCandidatePart { super(text); } - createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel { + public createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel { return this.autoCompleteCreator.create(updateCallback); } - acceptsInsertion(chr: string, offset: number, inputType: string) { + protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean { if (offset === 0) { return true; } else { @@ -444,11 +444,11 @@ class PillCandidatePart extends PlainBasePart implements IPillCandidatePart { } } - merge() { + public merge(): boolean { return false; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return true; } @@ -479,17 +479,21 @@ interface IAutocompleteCreator { export class PartCreator { protected readonly autoCompleteCreator: IAutocompleteCreator; - constructor(private room: Room, private client: MatrixClient, autoCompleteCreator: AutoCompleteCreator = null) { + constructor( + private readonly room: Room, + private readonly 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?.(this) }; } - setAutoCompleteCreator(autoCompleteCreator: AutoCompleteCreator) { + public setAutoCompleteCreator(autoCompleteCreator: AutoCompleteCreator): void { this.autoCompleteCreator.create = autoCompleteCreator(this); } - createPartForInput(input: string, partIndex: number, inputType?: string): Part { + public createPartForInput(input: string, partIndex: number, inputType?: string): Part { switch (input[0]) { case "#": case "@": @@ -503,11 +507,11 @@ export class PartCreator { } } - createDefaultPart(text: string) { + public createDefaultPart(text: string): Part { return this.plain(text); } - deserializePart(part: SerializedPart): Part { + public deserializePart(part: SerializedPart): Part { switch (part.type) { case Type.Plain: return this.plain(part.text); @@ -524,19 +528,19 @@ export class PartCreator { } } - plain(text: string) { + public plain(text: string): PlainPart { return new PlainPart(text); } - newline() { + public newline(): NewlinePart { return new NewlinePart("\n"); } - pillCandidate(text: string) { + public pillCandidate(text: string): PillCandidatePart { return new PillCandidatePart(text, this.autoCompleteCreator); } - roomPill(alias: string, roomId?: string) { + public roomPill(alias: string, roomId?: string): RoomPillPart { let room; if (roomId || alias[0] !== "#") { room = this.client.getRoom(roomId || alias); @@ -549,16 +553,20 @@ export class PartCreator { return new RoomPillPart(alias, room ? room.name : alias, room); } - atRoomPill(text: string) { + public atRoomPill(text: string): AtRoomPillPart { return new AtRoomPillPart(text, this.room); } - userPill(displayName: string, userId: string) { + public userPill(displayName: string, userId: string): UserPillPart { const member = this.room.getMember(userId); return new UserPillPart(userId, displayName, member); } - createMentionParts(insertTrailingCharacter: boolean, displayName: string, userId: string) { + public createMentionParts( + insertTrailingCharacter: boolean, + displayName: string, + userId: string, + ): [UserPillPart, PlainPart] { const pill = this.userPill(displayName, userId); const postfix = this.plain(insertTrailingCharacter ? ": " : " "); return [pill, postfix]; @@ -583,7 +591,7 @@ export class CommandPartCreator extends PartCreator { } public deserializePart(part: SerializedPart): Part { - if (part.type === "command") { + if (part.type === Type.Command) { return this.command(part.text); } else { return super.deserializePart(part); diff --git a/src/editor/position.ts b/src/editor/position.ts index 37d2a07b43..50dc283eb3 100644 --- a/src/editor/position.ts +++ b/src/editor/position.ts @@ -30,7 +30,7 @@ export default class DocumentPosition implements IPosition { constructor(public readonly index: number, public readonly offset: number) { } - compare(otherPos: DocumentPosition) { + public compare(otherPos: DocumentPosition): number { if (this.index === otherPos.index) { return this.offset - otherPos.offset; } else { @@ -38,7 +38,7 @@ export default class DocumentPosition implements IPosition { } } - iteratePartsBetween(other: DocumentPosition, model: EditorModel, callback: Callback) { + public iteratePartsBetween(other: DocumentPosition, model: EditorModel, callback: Callback): void { if (this.index === -1 || other.index === -1) { return; } @@ -57,7 +57,7 @@ export default class DocumentPosition implements IPosition { } } - forwardsWhile(model: EditorModel, predicate: Predicate) { + public forwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition { if (this.index === -1) { return this; } @@ -82,7 +82,7 @@ export default class DocumentPosition implements IPosition { } } - backwardsWhile(model: EditorModel, predicate: Predicate) { + public backwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition { if (this.index === -1) { return this; } @@ -107,7 +107,7 @@ export default class DocumentPosition implements IPosition { } } - asOffset(model: EditorModel) { + public asOffset(model: EditorModel): DocumentOffset { if (this.index === -1) { return new DocumentOffset(0, true); } @@ -121,7 +121,7 @@ export default class DocumentPosition implements IPosition { return new DocumentOffset(offset, atEnd); } - isAtEnd(model: EditorModel) { + public isAtEnd(model: EditorModel): boolean { if (model.parts.length === 0) { return true; } @@ -130,7 +130,7 @@ export default class DocumentPosition implements IPosition { return this.index === lastPartIdx && this.offset === lastPart.text.length; } - isAtStart() { + public isAtStart(): boolean { return this.index === 0 && this.offset === 0; } } diff --git a/src/editor/range.ts b/src/editor/range.ts index 634805702f..13776177a7 100644 --- a/src/editor/range.ts +++ b/src/editor/range.ts @@ -32,23 +32,23 @@ export default class Range { this._end = bIsLarger ? positionB : positionA; } - moveStart(delta: number) { + public moveStart(delta: number): void { this._start = this._start.forwardsWhile(this.model, () => { delta -= 1; return delta >= 0; }); } - trim() { + public trim(): void { this._start = this._start.forwardsWhile(this.model, whitespacePredicate); this._end = this._end.backwardsWhile(this.model, whitespacePredicate); } - expandBackwardsWhile(predicate: Predicate) { + public expandBackwardsWhile(predicate: Predicate): void { this._start = this._start.backwardsWhile(this.model, predicate); } - get text() { + public get text(): string { let text = ""; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { const t = part.text.substring(startIdx, endIdx); @@ -63,7 +63,7 @@ export default class Range { * @param {Part[]} parts the parts to replace the range with * @return {Number} the net amount of characters added, can be negative. */ - replace(parts: Part[]) { + public replace(parts: Part[]): number { const newLength = parts.reduce((sum, part) => sum + part.text.length, 0); let oldLength = 0; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { @@ -77,8 +77,8 @@ export default class Range { * Returns a copy of the (partial) parts within the range. * For partial parts, only the text is adjusted to the part that intersects with the range. */ - get parts() { - const parts = []; + public get parts(): Part[] { + const parts: Part[] = []; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { const serializedPart = part.serialize(); serializedPart.text = part.text.substring(startIdx, endIdx); @@ -88,7 +88,7 @@ export default class Range { return parts; } - get length() { + public get length(): number { let len = 0; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { len += endIdx - startIdx; @@ -96,11 +96,11 @@ export default class Range { return len; } - get start() { + public get start(): DocumentPosition { return this._start; } - get end() { + public get end(): DocumentPosition { return this._end; } } diff --git a/src/editor/render.ts b/src/editor/render.ts index 0e0b7d2145..d9997de855 100644 --- a/src/editor/render.ts +++ b/src/editor/render.ts @@ -15,19 +15,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Part } from "./parts"; +import { Part, Type } from "./parts"; import EditorModel from "./model"; -export function needsCaretNodeBefore(part: Part, prevPart: Part) { - const isFirst = !prevPart || prevPart.type === "newline"; +export function needsCaretNodeBefore(part: Part, prevPart: Part): boolean { + const isFirst = !prevPart || prevPart.type === Type.Newline; return !part.canEdit && (isFirst || !prevPart.canEdit); } -export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean) { +export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean): boolean { return !part.canEdit && isLastOfLine; } -function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement) { +function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement): void { const next = node.nextSibling; if (next) { node.parentElement.insertBefore(nodeToInsert, next); @@ -44,25 +44,25 @@ export const CARET_NODE_CHAR = "\ufeff"; // a caret node is a node that allows the caret to be placed // where otherwise it wouldn't be possible // (e.g. next to a pill span without adjacent text node) -function createCaretNode() { +function createCaretNode(): HTMLElement { const span = document.createElement("span"); span.className = "caretNode"; span.appendChild(document.createTextNode(CARET_NODE_CHAR)); return span; } -function updateCaretNode(node: HTMLElement) { +function updateCaretNode(node: HTMLElement): void { // 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: HTMLElement) { +export function isCaretNode(node: HTMLElement): boolean { return node && node.tagName === "SPAN" && node.className === "caretNode"; } -function removeNextSiblings(node: ChildNode) { +function removeNextSiblings(node: ChildNode): void { if (!node) { return; } @@ -74,7 +74,7 @@ function removeNextSiblings(node: ChildNode) { } } -function removeChildren(parent: HTMLElement) { +function removeChildren(parent: HTMLElement): void { const firstChild = parent.firstChild; if (firstChild) { removeNextSiblings(firstChild); @@ -82,7 +82,7 @@ function removeChildren(parent: HTMLElement) { } } -function reconcileLine(lineContainer: ChildNode, parts: Part[]) { +function reconcileLine(lineContainer: ChildNode, parts: Part[]): void { let currentNode; let prevPart; const lastPart = parts[parts.length - 1]; @@ -131,13 +131,13 @@ function reconcileLine(lineContainer: ChildNode, parts: Part[]) { removeNextSiblings(currentNode); } -function reconcileEmptyLine(lineContainer) { +function reconcileEmptyLine(lineContainer: HTMLElement): void { // empty div needs to have a BR in it to give it height let foundBR = false; let partNode = lineContainer.firstChild; while (partNode) { const nextNode = partNode.nextSibling; - if (!foundBR && partNode.tagName === "BR") { + if (!foundBR && (partNode as HTMLElement).tagName === "BR") { foundBR = true; } else { partNode.remove(); @@ -149,9 +149,9 @@ function reconcileEmptyLine(lineContainer) { } } -export function renderModel(editor: HTMLDivElement, model: EditorModel) { +export function renderModel(editor: HTMLDivElement, model: EditorModel): void { const lines = model.parts.reduce((linesArr, part) => { - if (part.type === "newline") { + if (part.type === Type.Newline) { linesArr.push([]); } else { const lastLine = linesArr[linesArr.length - 1]; @@ -175,7 +175,7 @@ export function renderModel(editor: HTMLDivElement, model: EditorModel) { if (parts.length) { reconcileLine(lineContainer, parts); } else { - reconcileEmptyLine(lineContainer); + reconcileEmptyLine(lineContainer as HTMLElement); } }); if (lines.length) { diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index f68173ae29..38a73cc945 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -22,30 +22,31 @@ import { AllHtmlEntities } from 'html-entities'; import SettingsStore from '../settings/SettingsStore'; import SdkConfig from '../SdkConfig'; import cheerio from 'cheerio'; +import { Type } from './parts'; -export function mdSerialize(model: EditorModel) { +export function mdSerialize(model: EditorModel): string { return model.parts.reduce((html, part) => { switch (part.type) { - case "newline": + case Type.Newline: return html + "\n"; - case "plain": - case "command": - case "pill-candidate": - case "at-room-pill": + case Type.Plain: + case Type.Command: + case Type.PillCandidate: + case Type.AtRoomPill: return html + part.text; - case "room-pill": + case Type.RoomPill: // Here we use the resourceId for compatibility with non-rich text clients // See https://github.com/vector-im/element-web/issues/16660 return html + `[${part.resourceId.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`; - case "user-pill": + case Type.UserPill: return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`; } }, ""); } -export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}) { +export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}): string { let md = mdSerialize(model); // copy of raw input to remove unwanted math later const orig = md; @@ -156,31 +157,31 @@ export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } } } -export function textSerialize(model: EditorModel) { +export function textSerialize(model: EditorModel): string { return model.parts.reduce((text, part) => { switch (part.type) { - case "newline": + case Type.Newline: return text + "\n"; - case "plain": - case "command": - case "pill-candidate": - case "at-room-pill": + case Type.Plain: + case Type.Command: + case Type.PillCandidate: + case Type.AtRoomPill: return text + part.text; - case "room-pill": + case Type.RoomPill: // Here we use the resourceId for compatibility with non-rich text clients // See https://github.com/vector-im/element-web/issues/16660 return text + `${part.resourceId}`; - case "user-pill": + case Type.UserPill: return text + `${part.text}`; } }, ""); } -export function containsEmote(model: EditorModel) { +export function containsEmote(model: EditorModel): boolean { return startsWith(model, "/me ", false); } -export function startsWith(model: EditorModel, prefix: string, caseSensitive = true) { +export function startsWith(model: EditorModel, prefix: string, caseSensitive = true): boolean { const firstPart = model.parts[0]; // part type will be "plain" while editing, // and "command" while composing a message. @@ -190,26 +191,26 @@ export function startsWith(model: EditorModel, prefix: string, caseSensitive = t text = text.toLowerCase(); } - return firstPart && (firstPart.type === "plain" || firstPart.type === "command") && text.startsWith(prefix); + return firstPart && (firstPart.type === Type.Plain || firstPart.type === Type.Command) && text.startsWith(prefix); } -export function stripEmoteCommand(model: EditorModel) { +export function stripEmoteCommand(model: EditorModel): EditorModel { // trim "/me " return stripPrefix(model, "/me "); } -export function stripPrefix(model: EditorModel, prefix: string) { +export function stripPrefix(model: EditorModel, prefix: string): EditorModel { model = model.clone(); model.removeText({ index: 0, offset: 0 }, prefix.length); return model; } -export function unescapeMessage(model: EditorModel) { +export function unescapeMessage(model: EditorModel): EditorModel { const { parts } = model; if (parts.length) { const firstPart = parts[0]; // only unescape \/ to / at start of editor - if (firstPart.type === "plain" && firstPart.text.startsWith("\\/")) { + if (firstPart.type === Type.Plain && firstPart.text.startsWith("\\/")) { model = model.clone(); model.removeText({ index: 0, offset: 0 }, 1); } From bc701a182b959108b4742f7e61af2cd110e547dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 4 Aug 2021 09:48:41 +0200 Subject: [PATCH 14/42] Add Figma colors to dark theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index e4ea2bb57e..3f7763ddc1 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -1,5 +1,22 @@ -// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0 +// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A741 +$accent: #0DBD8B; +$alert: #FF5B55; +$links: #0086e6; +$primary-content: #ffffff; +$secondary-content: #A9B2BC; +$tertiary-content: #8E99A4; +$quaternary-content: #6F7882; +$quinary-content: #394049; $system-dark: #21262C; +$background: #15191E; +$panels: rgba($system-dark, 0.9); +$panel-base: #8D97A5; // This color is not intended for use in the app +$panel-selected: rgba($panel-base, 0.3); +$panel-hover: rgba($panel-base, 0.1); +$panel-actions: rgba($panel-base, 0.2); +$space-nav: rgba($panel-base, 0.1); + +// TODO: Move userId colors here // unified palette // try to use these colors when possible @@ -115,8 +132,7 @@ $eventtile-meta-color: $roomtopic-color; $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; -$quinary-content-color: #394049; -$toast-bg-color: $quinary-content-color; +$toast-bg-color: $quinary-content; // ******************** From fbb79a17e34d865d3912e6a8b1a3367c7b1515fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 4 Aug 2021 09:49:05 +0200 Subject: [PATCH 15/42] Add Figma colors to light theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/light/css/_light.scss | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index aa17dddc56..fb85ec7984 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -12,8 +12,25 @@ $font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial' $monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', 'monospace', 'Noto Color Emoji'; -// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0 +// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A120 +$accent: #0DBD8B; +$alert: #FF5B55; +$links: #0086e6; +$primary-content: #17191C; +$secondary-content: #737D8C; +$tertiary-content: #8D97A5; +$quaternary-content: #c1c6cd; +$quinary-content: #E3E8F0; $system-light: #F4F6FA; +$background: #ffffff; +$panels: rgba($system-light, 0.9); +$panel-base: #8D97A5; // This color is not intended for use in the app +$panel-selected: rgba($panel-base, 0.3); +$panel-hover: rgba($panel-base, 0.1); +$panel-actions: rgba($panel-base, 0.2); +$space-nav: rgba($panel-base, 0.15); + +// TODO: Move userId colors here // unified palette // try to use these colors when possible From 3c12a5a99510b2613ecd11803929ed9447591eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 4 Aug 2021 10:06:11 +0200 Subject: [PATCH 16/42] Some de-duplication for the dark theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 3f7763ddc1..96ee1c77f8 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -20,16 +20,16 @@ $space-nav: rgba($panel-base, 0.1); // unified palette // try to use these colors when possible -$bg-color: #15191E; +$bg-color: $background; $base-color: $bg-color; -$base-text-color: #ffffff; +$base-text-color: $primary-content; $header-panel-bg-color: #20252B; $header-panel-border-color: #000000; $header-panel-text-primary-color: #B9BEC6; $header-panel-text-secondary-color: #c8c8cd; -$text-primary-color: #ffffff; +$text-primary-color: $primary-content; $text-secondary-color: #B9BEC6; -$quaternary-fg-color: #6F7882; +$quaternary-fg-color: $quaternary-content; $search-bg-color: #181b21; $search-placeholder-color: #61708b; $room-highlight-color: #343a46; @@ -40,8 +40,8 @@ $primary-bg-color: $bg-color; $muted-fg-color: $header-panel-text-primary-color; // additional text colors -$secondary-fg-color: #A9B2BC; -$tertiary-fg-color: #8E99A4; +$secondary-fg-color: $secondary-content; +$tertiary-fg-color: $tertiary-content; // used for dialog box text $light-fg-color: $header-panel-text-secondary-color; @@ -96,7 +96,7 @@ $menu-bg-color: $header-panel-bg-color; $menu-box-shadow-color: $bg-color; $menu-selected-color: $room-highlight-color; -$avatar-initial-color: #ffffff; +$avatar-initial-color: $primary-content; $avatar-bg-color: $bg-color; $h3-color: $primary-fg-color; @@ -125,7 +125,7 @@ $roomheader-addroom-fg-color: $text-primary-color; $groupFilterPanel-button-color: $header-panel-text-primary-color; $groupheader-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color; -$icon-button-color: #8E99A4; +$icon-button-color: $tertiary-content; $roomtopic-color: $text-secondary-color; $eventtile-meta-color: $roomtopic-color; @@ -137,7 +137,7 @@ $toast-bg-color: $quinary-content; // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #394049; +$dialpad-button-bg-color: $quinary-content; $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $bg-color; @@ -180,12 +180,12 @@ $tab-label-icon-bg-color: $text-primary-color; $tab-label-active-icon-bg-color: $text-primary-color; // Buttons -$button-primary-fg-color: #ffffff; +$button-primary-fg-color: $primary-content; $button-primary-bg-color: $accent-color; $button-secondary-bg-color: transparent; -$button-danger-fg-color: #ffffff; +$button-danger-fg-color: $primary-content; $button-danger-bg-color: $notice-primary-color; -$button-danger-disabled-fg-color: #ffffff; +$button-danger-disabled-fg-color: $primary-content; $button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color $button-link-fg-color: $accent-color; $button-link-bg-color: transparent; @@ -217,17 +217,17 @@ $reaction-row-button-selected-border-color: $accent-color; $kbd-border-color: #000000; $tooltip-timeline-bg-color: $groupFilterPanel-bg-color; -$tooltip-timeline-fg-color: #ffffff; +$tooltip-timeline-fg-color: $primary-content; $interactive-tooltip-bg-color: $base-color; -$interactive-tooltip-fg-color: #ffffff; +$interactive-tooltip-fg-color: $primary-content; $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; -$message-body-panel-bg-color: #394049; // "Dark Tile" +$message-body-panel-bg-color: $quinary-content; // "Dark Tile" $message-body-panel-icon-fg-color: $secondary-fg-color; $message-body-panel-icon-bg-color: $system-dark; // "System Dark" From c89560691d20c09571d3a89c029e934c9e80f4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 4 Aug 2021 10:10:23 +0200 Subject: [PATCH 17/42] Some de-duplication for the light theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/light/css/_light.scss | 63 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index fb85ec7984..6e354ba6d8 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -24,28 +24,27 @@ $quinary-content: #E3E8F0; $system-light: #F4F6FA; $background: #ffffff; $panels: rgba($system-light, 0.9); -$panel-base: #8D97A5; // This color is not intended for use in the app -$panel-selected: rgba($panel-base, 0.3); -$panel-hover: rgba($panel-base, 0.1); -$panel-actions: rgba($panel-base, 0.2); -$space-nav: rgba($panel-base, 0.15); +$panel-selected: rgba($tertiary-content, 0.3); +$panel-hover: rgba($tertiary-content, 0.1); +$panel-actions: rgba($tertiary-content, 0.2); +$space-nav: rgba($tertiary-content, 0.15); // TODO: Move userId colors here // unified palette // try to use these colors when possible -$accent-color: #0DBD8B; +$accent-color: $accent; $accent-bg-color: rgba(3, 179, 129, 0.16); $notice-primary-color: #ff4b55; $notice-primary-bg-color: rgba(255, 75, 85, 0.16); $primary-fg-color: #2e2f32; -$secondary-fg-color: #737D8C; +$secondary-fg-color: $secondary-content; $tertiary-fg-color: #8D99A5; -$quaternary-fg-color: #C1C6CD; +$quaternary-fg-color: $quaternary-content; $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) -$primary-bg-color: #ffffff; +$primary-bg-color: $background; $muted-fg-color: #61708b; // Commonly used in headings and relevant alt text // used for dialog box text @@ -55,7 +54,7 @@ $light-fg-color: #747474; $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) -$accent-fg-color: #ffffff; +$accent-fg-color: $background; $accent-color-50pct: rgba($accent-color, 0.5); $accent-color-darker: #92caad; $accent-color-alt: #238CF5; @@ -99,7 +98,7 @@ $primary-hairline-color: transparent; // used for the border of input text fields $input-border-color: #e7e7e7; -$input-darker-bg-color: #e3e8f0; +$input-darker-bg-color: $quinary-content; $input-darker-fg-color: #9fa9ba; $input-lighter-bg-color: #f2f5f8; $input-lighter-fg-color: $input-darker-fg-color; @@ -107,7 +106,7 @@ $input-focused-border-color: #238cf5; $input-valid-border-color: $accent-color; $input-invalid-border-color: $warning-color; -$field-focused-label-bg-color: #ffffff; +$field-focused-label-bg-color: $background; $button-bg-color: $accent-color; $button-fg-color: white; @@ -129,8 +128,8 @@ $menu-bg-color: #fff; $menu-box-shadow-color: rgba(118, 131, 156, 0.6); $menu-selected-color: #f5f8fa; -$avatar-initial-color: #ffffff; -$avatar-bg-color: #ffffff; +$avatar-initial-color: $background; +$avatar-bg-color: $background; $h3-color: #3d3b39; @@ -180,7 +179,7 @@ $roomheader-addroom-fg-color: #5c6470; $groupFilterPanel-button-color: #91A1C0; $groupheader-button-color: #91A1C0; $rightpanel-button-color: #91A1C0; -$icon-button-color: #C1C6CD; +$icon-button-color: $quaternary-content; $roomtopic-color: #9e9e9e; $eventtile-meta-color: $roomtopic-color; @@ -192,12 +191,12 @@ $voipcall-plinth-color: $system-light; // ******************** -$theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #e3e8f0; +$theme-button-bg-color: $quinary-content; +$dialpad-button-bg-color: $quinary-content; $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons -$roomlist-filter-active-bg-color: #ffffff; +$roomlist-filter-active-bg-color: $background; $roomlist-bg-color: rgba(245, 245, 245, 0.90); $roomlist-header-color: $tertiary-fg-color; $roomsublist-divider-color: $primary-fg-color; @@ -211,13 +210,13 @@ $roomtile-selected-bg-color: #FFF; $presence-online: $accent-color; $presence-away: #d9b072; -$presence-offline: #E3E8F0; +$presence-offline: $quinary-content; // ******************** $username-variant1-color: #368bd6; $username-variant2-color: #ac3ba8; -$username-variant3-color: #0DBD8B; +$username-variant3-color: $accent; $username-variant4-color: #e64f7a; $username-variant5-color: #ff812d; $username-variant6-color: #2dc2c5; @@ -269,24 +268,24 @@ $e2e-warning-color: #ba6363; /*** ImageView ***/ $lightbox-bg-color: #454545; -$lightbox-fg-color: #ffffff; -$lightbox-border-color: #ffffff; +$lightbox-fg-color: $background; +$lightbox-border-color: $background; // Tabbed views $tab-label-fg-color: #45474a; -$tab-label-active-fg-color: #ffffff; +$tab-label-active-fg-color: $background; $tab-label-bg-color: transparent; $tab-label-active-bg-color: $accent-color; $tab-label-icon-bg-color: #454545; $tab-label-active-icon-bg-color: $tab-label-active-fg-color; // Buttons -$button-primary-fg-color: #ffffff; +$button-primary-fg-color: $background; $button-primary-bg-color: $accent-color; $button-secondary-bg-color: $accent-fg-color; -$button-danger-fg-color: #ffffff; +$button-danger-fg-color: $background; $button-danger-bg-color: $notice-primary-color; -$button-danger-disabled-fg-color: #ffffff; +$button-danger-disabled-fg-color: $background; $button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color $button-link-fg-color: $accent-color; $button-link-bg-color: transparent; @@ -311,7 +310,7 @@ $memberstatus-placeholder-color: $muted-fg-color; $authpage-bg-color: #2e3649; $authpage-modal-bg-color: rgba(245, 245, 245, 0.90); -$authpage-body-bg-color: #ffffff; +$authpage-body-bg-color: $background; $authpage-focus-bg-color: #dddddd; $authpage-lang-color: #4e5054; $authpage-primary-color: #232f32; @@ -335,17 +334,17 @@ $kbd-border-color: $reaction-row-button-border-color; $inverted-bg-color: #27303a; $tooltip-timeline-bg-color: $inverted-bg-color; -$tooltip-timeline-fg-color: #ffffff; +$tooltip-timeline-fg-color: $background; $interactive-tooltip-bg-color: #27303a; -$interactive-tooltip-fg-color: #ffffff; +$interactive-tooltip-fg-color: $background; $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; -$message-body-panel-bg-color: #E3E8F0; // "Separator" +$message-body-panel-bg-color: $quinary-content; // "Separator" $message-body-panel-icon-fg-color: $secondary-fg-color; $message-body-panel-icon-bg-color: $system-light; @@ -354,7 +353,7 @@ $message-body-panel-icon-bg-color: $system-light; $voice-record-stop-symbol-color: #ff4b55; $voice-record-live-circle-color: #ff4b55; -$voice-record-stop-border-color: #E3E8F0; // "Separator" +$voice-record-stop-border-color: $quinary-content; // "Separator" $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; $voice-record-icon-color: $tertiary-fg-color; $voice-playback-button-bg-color: $message-body-panel-icon-bg-color; @@ -374,7 +373,7 @@ $eventbubble-self-bg: #F0FBF8; $eventbubble-others-bg: $system-light; $eventbubble-bg-hover: #FAFBFD; $eventbubble-avatar-outline: $primary-bg-color; -$eventbubble-reply-color: #C1C6CD; +$eventbubble-reply-color: $quaternary-content; // ***** Mixins! ***** From 422c27fcefa4812dbeb5199b7d91afad3689947c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 6 Aug 2021 07:46:01 +0200 Subject: [PATCH 18/42] Reorder code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 4e0235bdf7..277b4bb526 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -397,6 +397,14 @@ class UserPillPart extends PillPart { super(userId, displayName); } + public get type(): IPillPart["type"] { + return Type.UserPill; + } + + protected get className() { + return "mx_UserPill mx_Pill"; + } + protected setAvatar(node: HTMLElement): void { if (!this.member) { return; @@ -417,14 +425,6 @@ class UserPillPart extends PillPart { member: this.member, }); }; - - public get type(): IPillPart["type"] { - return Type.UserPill; - } - - protected get className() { - return "mx_UserPill mx_Pill"; - } } class PillCandidatePart extends PlainBasePart implements IPillCandidatePart { From 49f41498eab67a419d005367a240f4f702cbd807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 6 Aug 2021 08:19:49 +0200 Subject: [PATCH 19/42] Remove dupe import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/ActiveRoomObserver.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ActiveRoomObserver.ts b/src/ActiveRoomObserver.ts index 681b6f4568..c7423fab8f 100644 --- a/src/ActiveRoomObserver.ts +++ b/src/ActiveRoomObserver.ts @@ -16,7 +16,6 @@ limitations under the License. import { EventSubscription } from 'fbemitter'; import RoomViewStore from './stores/RoomViewStore'; -import { EventSubscription } from 'fbemitter'; type Listener = (isActive: boolean) => void; From b1fe59eea3114d817ca0d8788dbfbdc869f5b154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 6 Aug 2021 08:34:46 +0200 Subject: [PATCH 20/42] Revert changes which didn't make much sense and remove some comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 4 ++-- res/themes/light/css/_light.scss | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 96ee1c77f8..9a85c9d2b0 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -96,7 +96,7 @@ $menu-bg-color: $header-panel-bg-color; $menu-box-shadow-color: $bg-color; $menu-selected-color: $room-highlight-color; -$avatar-initial-color: $primary-content; +$avatar-initial-color: #ffffff; $avatar-bg-color: $bg-color; $h3-color: $primary-fg-color; @@ -227,7 +227,7 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; -$message-body-panel-bg-color: $quinary-content; // "Dark Tile" +$message-body-panel-bg-color: $quinary-content; $message-body-panel-icon-fg-color: $secondary-fg-color; $message-body-panel-icon-bg-color: $system-dark; // "System Dark" diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 6e354ba6d8..d86caf4caf 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -216,7 +216,7 @@ $presence-offline: $quinary-content; $username-variant1-color: #368bd6; $username-variant2-color: #ac3ba8; -$username-variant3-color: $accent; +$username-variant3-color: #0DBD8B; $username-variant4-color: #e64f7a; $username-variant5-color: #ff812d; $username-variant6-color: #2dc2c5; @@ -268,8 +268,8 @@ $e2e-warning-color: #ba6363; /*** ImageView ***/ $lightbox-bg-color: #454545; -$lightbox-fg-color: $background; -$lightbox-border-color: $background; +$lightbox-fg-color: #ffffff; +$lightbox-border-color: #ffffff; // Tabbed views $tab-label-fg-color: #45474a; @@ -280,7 +280,7 @@ $tab-label-icon-bg-color: #454545; $tab-label-active-icon-bg-color: $tab-label-active-fg-color; // Buttons -$button-primary-fg-color: $background; +$button-primary-fg-color: #ffffff; $button-primary-bg-color: $accent-color; $button-secondary-bg-color: $accent-fg-color; $button-danger-fg-color: $background; @@ -344,7 +344,7 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; -$message-body-panel-bg-color: $quinary-content; // "Separator" +$message-body-panel-bg-color: $quinary-content; $message-body-panel-icon-fg-color: $secondary-fg-color; $message-body-panel-icon-bg-color: $system-light; @@ -353,7 +353,7 @@ $message-body-panel-icon-bg-color: $system-light; $voice-record-stop-symbol-color: #ff4b55; $voice-record-live-circle-color: #ff4b55; -$voice-record-stop-border-color: $quinary-content; // "Separator" +$voice-record-stop-border-color: $quinary-content; $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; $voice-record-icon-color: $tertiary-fg-color; $voice-playback-button-bg-color: $message-body-panel-icon-bg-color; From d285a45da5a561cfbddb14a6380b1d77c0aab7e4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 12:28:06 +0100 Subject: [PATCH 21/42] Fix stray tabIndex on AutoHideScrollbar component in FF --- src/components/structures/AutoHideScrollbar.tsx | 4 +++- src/components/structures/LeftPanel.tsx | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 184d883dda..a60df45770 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -61,7 +61,9 @@ export default class AutoHideScrollbar extends React.Component { style={style} className={["mx_AutoHideScrollbar", className].join(" ")} onWheel={onWheel} - tabIndex={tabIndex} + // Firefox sometimes makes this element focusable due to + // overflow:scroll;, so force it out of tab order by default. + tabIndex={tabIndex ?? -1} > { children } ); diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 3bd2c68c6c..ff5d15d44d 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -392,9 +392,6 @@ export default class LeftPanel extends React.Component { From 38953452506c1fd8fdc65d130478cdef738f3846 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 12:28:20 +0100 Subject: [PATCH 22/42] Fix disabled state on AccessibleButton not being exposed via ARIA --- src/components/views/elements/AccessibleButton.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 8bb6341c3d..0ce9a3a030 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -67,7 +67,9 @@ export default function AccessibleButton({ ...restProps }: IProps) { const newProps: IAccessibleButtonProps = restProps; - if (!disabled) { + if (disabled) { + newProps["aria-disabled"] = true; + } else { newProps.onClick = onClick; // We need to consume enter onKeyDown and space onKeyUp // otherwise we are risking also activating other keyboard focusable elements @@ -118,7 +120,7 @@ export default function AccessibleButton({ ); // React.createElement expects InputHTMLAttributes - return React.createElement(element, restProps, children); + return React.createElement(element, newProps, children); } AccessibleButton.defaultProps = { From 1a1b1738c127bc24e4f8e7036fd0d8e3d6921c48 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 12:28:46 +0100 Subject: [PATCH 23/42] Add aria label to clickable notification badge on space panel --- src/components/views/spaces/SpaceTreeLevel.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index bb2184853e..0862458d9e 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -77,11 +77,17 @@ export const SpaceButton: React.FC = ({ let notifBadge; if (notificationState) { + let ariaLabel = _t("Jump to first unread room."); + if (space.getMyMembership() === "invite") { + ariaLabel = _t("Jump to first invite."); + } + notifBadge =
SpaceStore.instance.setActiveRoomInSpace(space || null)} forceCount={false} notification={notificationState} + aria-label={ariaLabel} />
; } From facc882a1110b002aae4349b966881e86da1ab28 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 12:50:32 +0100 Subject: [PATCH 24/42] i18n and add space null guard for home space --- src/components/views/spaces/SpaceTreeLevel.tsx | 2 +- src/i18n/strings/en_EN.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 0862458d9e..5d2f58fab2 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -78,7 +78,7 @@ export const SpaceButton: React.FC = ({ let notifBadge; if (notificationState) { let ariaLabel = _t("Jump to first unread room."); - if (space.getMyMembership() === "invite") { + if (space?.getMyMembership() === "invite") { ariaLabel = _t("Jump to first invite."); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9f30776f44..09cb877d33 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1072,6 +1072,8 @@ "Preview Space": "Preview Space", "Allow people to preview your space before they join.": "Allow people to preview your space before they join.", "Recommended for public spaces.": "Recommended for public spaces.", + "Jump to first unread room.": "Jump to first unread room.", + "Jump to first invite.": "Jump to first invite.", "Expand": "Expand", "Collapse": "Collapse", "Space options": "Space options", @@ -1665,8 +1667,6 @@ "Activity": "Activity", "A-Z": "A-Z", "List options": "List options", - "Jump to first unread room.": "Jump to first unread room.", - "Jump to first invite.": "Jump to first invite.", "Show %(count)s more|other": "Show %(count)s more", "Show %(count)s more|one": "Show %(count)s more", "Show less": "Show less", From 41475a3b948d99cb1955c4e9e4e2bf960fc879f3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 14:12:55 +0100 Subject: [PATCH 25/42] fix keyboard focus issue around the space hierarchy --- res/css/structures/_SpaceRoomDirectory.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index cb91aa3c7d..03ff1d5ae5 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -269,7 +269,7 @@ limitations under the License. } } - &:hover { + &:hover, &:focus-within { background-color: $groupFilterPanel-bg-color; .mx_AccessibleButton { From 54fb24f359bdb9fd7e2f2f76871a9ac52d8f9df2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 14:21:41 +0100 Subject: [PATCH 26/42] Fix dropdown keyboard accessibility when filter is disabled --- src/components/views/elements/Dropdown.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index dddcceb97c..1f3ceabb83 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -204,7 +204,7 @@ export default class Dropdown extends React.Component { this.props.onOptionChange(dropdownKey); }; - private onInputKeyDown = (e: React.KeyboardEvent) => { + private onKeyDown = (e: React.KeyboardEvent) => { let handled = true; // These keys don't generate keypress events and so needs to be on keyup @@ -320,7 +320,6 @@ export default class Dropdown extends React.Component { type="text" autoFocus={true} className="mx_Dropdown_option" - onKeyDown={this.onInputKeyDown} onChange={this.onInputChange} value={this.state.searchQuery} role="combobox" @@ -358,7 +357,7 @@ export default class Dropdown extends React.Component { // Note the menu sits inside the AccessibleButton div so it's anchored // to the input, but overflows below it. The root contains both. - return
+ return
Date: Fri, 6 Aug 2021 14:21:56 +0100 Subject: [PATCH 27/42] Fix dropdown negative wraparound for keyboard accessibility --- src/components/views/elements/Dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index 1f3ceabb83..7ad27bc93b 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -269,7 +269,7 @@ export default class Dropdown extends React.Component { private prevOption(optionKey: string): string { const keys = Object.keys(this.childrenByKey); const index = keys.indexOf(optionKey); - return keys[(index - 1) % keys.length]; + return keys[index <= 0 ? keys.length - 1 : (index - 1) % keys.length]; } private scrollIntoView(node: Element) { From 3fd2c005161c005827ea479c83a85a0f12442346 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 14:46:02 +0100 Subject: [PATCH 28/42] Improve aria labels around spaces avatar uploader --- src/components/views/spaces/SpaceBasicSettings.tsx | 7 ++++++- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx index 9d3696c5a9..4f305edd8b 100644 --- a/src/components/views/spaces/SpaceBasicSettings.tsx +++ b/src/components/views/spaces/SpaceBasicSettings.tsx @@ -65,6 +65,7 @@ export const SpaceAvatar = ({ }} kind="link" className="mx_SpaceBasicSettings_avatar_remove" + aria-label={_t("Delete avatar")} > { _t("Delete") } @@ -72,7 +73,11 @@ export const SpaceAvatar = ({ } else { avatarSection =
avatarUploadRef.current?.click()} /> - avatarUploadRef.current?.click()} kind="link"> + avatarUploadRef.current?.click()} + kind="link" + aria-label={_t("Upload avatar")} + > { _t("Upload") } ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 09cb877d33..3aab3cb68c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1014,7 +1014,9 @@ "Your server isn't responding to some requests.": "Your server isn't responding to some requests.", "Decline (%(counter)s)": "Decline (%(counter)s)", "Accept to continue:": "Accept to continue:", + "Delete avatar": "Delete avatar", "Delete": "Delete", + "Upload avatar": "Upload avatar", "Upload": "Upload", "Name": "Name", "Description": "Description", @@ -2718,7 +2720,6 @@ "Everyone": "Everyone", "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!": "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!", "Long Description (HTML)": "Long Description (HTML)", - "Upload avatar": "Upload avatar", "Community %(groupId)s not found": "Community %(groupId)s not found", "This homeserver does not support communities": "This homeserver does not support communities", "Failed to load %(groupId)s": "Failed to load %(groupId)s", From 6fddfe0d59da12421d873312788a3349d44574d9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 14:48:46 +0100 Subject: [PATCH 29/42] Fix dropdown keyboard selection accessibility --- src/components/views/elements/Dropdown.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index 7ad27bc93b..b4f382c9c3 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -18,7 +18,7 @@ limitations under the License. import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react'; import classnames from 'classnames'; -import AccessibleButton from './AccessibleButton'; +import AccessibleButton, { ButtonEvent } from './AccessibleButton'; import { _t } from '../../../languageHandler'; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -178,7 +178,7 @@ export default class Dropdown extends React.Component { this.ignoreEvent = ev; }; - private onInputClick = (ev: React.MouseEvent) => { + private onAccessibleButtonClick = (ev: ButtonEvent) => { if (this.props.disabled) return; if (!this.state.expanded) { @@ -186,6 +186,10 @@ export default class Dropdown extends React.Component { expanded: true, }); ev.preventDefault(); + } else if ((ev as React.KeyboardEvent).key === Key.ENTER) { + // the accessible button consumes enter onKeyDown for firing onClick, so handle it here + this.props.onOptionChange(this.state.highlightedOption); + this.close(); } }; @@ -328,6 +332,7 @@ export default class Dropdown extends React.Component { aria-owns={`${this.props.id}_listbox`} aria-disabled={this.props.disabled} aria-label={this.props.label} + onKeyDown={this.onKeyDown} /> ); } @@ -357,16 +362,17 @@ export default class Dropdown extends React.Component { // Note the menu sits inside the AccessibleButton div so it's anchored // to the input, but overflows below it. The root contains both. - return
+ return
{ currentValue } From 0cad9ed14a7714bce46cdea8effc9ebf9bddd93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 8 Aug 2021 11:15:18 +0200 Subject: [PATCH 30/42] Add _CallViewButtons.scss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/_components.scss | 1 + .../views/voip/CallView/_CallViewButtons.scss | 102 ++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 res/css/views/voip/CallView/_CallViewButtons.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index af161c92c6..e1e6b607df 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -270,6 +270,7 @@ @import "./views/toasts/_IncomingCallToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; +@import "./views/voip/CallView/_CallViewButtons.scss"; @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallPreview.scss"; @import "./views/voip/_CallView.scss"; diff --git a/res/css/views/voip/CallView/_CallViewButtons.scss b/res/css/views/voip/CallView/_CallViewButtons.scss new file mode 100644 index 0000000000..8e343f0ff3 --- /dev/null +++ b/res/css/views/voip/CallView/_CallViewButtons.scss @@ -0,0 +1,102 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +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_CallViewButtons { + position: absolute; + display: flex; + justify-content: center; + bottom: 5px; + opacity: 1; + transition: opacity 0.5s; + z-index: 200; // To be above _all_ feeds + + &.mx_CallViewButtons_hidden { + opacity: 0.001; // opacity 0 can cause a re-layout + pointer-events: none; + } + + .mx_CallViewButtons_button { + cursor: pointer; + margin-left: 2px; + margin-right: 2px; + + + &::before { + content: ''; + display: inline-block; + + height: 48px; + width: 48px; + + background-repeat: no-repeat; + background-size: contain; + background-position: center; + } + + + &.mx_CallViewButtons_dialpad::before { + background-image: url('$(res)/img/voip/dialpad.svg'); + } + + &.mx_CallViewButtons_button_micOn::before { + background-image: url('$(res)/img/voip/mic-on.svg'); + } + + &.mx_CallViewButtons_button_micOff::before { + background-image: url('$(res)/img/voip/mic-off.svg'); + } + + &.mx_CallViewButtons_button_vidOn::before { + background-image: url('$(res)/img/voip/vid-on.svg'); + } + + &.mx_CallViewButtons_button_vidOff::before { + background-image: url('$(res)/img/voip/vid-off.svg'); + } + + &.mx_CallViewButtons_button_screensharingOn::before { + background-image: url('$(res)/img/voip/screensharing-on.svg'); + } + + &.mx_CallViewButtons_button_screensharingOff::before { + background-image: url('$(res)/img/voip/screensharing-off.svg'); + } + + &.mx_CallViewButtons_button_sidebarOn::before { + background-image: url('$(res)/img/voip/sidebar-on.svg'); + } + + &.mx_CallViewButtons_button_sidebarOff::before { + background-image: url('$(res)/img/voip/sidebar-off.svg'); + } + + &.mx_CallViewButtons_button_hangup::before { + background-image: url('$(res)/img/voip/hangup.svg'); + } + + &.mx_CallViewButtons_button_more::before { + background-image: url('$(res)/img/voip/more.svg'); + } + + &.mx_CallViewButtons_button_invisible { + visibility: hidden; + pointer-events: none; + position: absolute; + } + } +} From 9f28c30145d8aeddc5aa05ddc4028678c3b93db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 8 Aug 2021 11:16:55 +0200 Subject: [PATCH 31/42] Add CallViewButtons.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/voip/CallView/CallViewButtons.tsx | 315 ++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 src/components/views/voip/CallView/CallViewButtons.tsx diff --git a/src/components/views/voip/CallView/CallViewButtons.tsx b/src/components/views/voip/CallView/CallViewButtons.tsx new file mode 100644 index 0000000000..8c48bd767d --- /dev/null +++ b/src/components/views/voip/CallView/CallViewButtons.tsx @@ -0,0 +1,315 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +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 AccessibleTooltipButton from "../../elements/AccessibleTooltipButton"; +import CallContextMenu from "../../context_menus/CallContextMenu"; +import DialpadContextMenu from "../../context_menus/DialpadContextMenu"; +import AccessibleButton from "../../elements/AccessibleButton"; +import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import { Alignment } from "../../elements/Tooltip"; +import { + alwaysAboveLeftOf, + alwaysAboveRightOf, + ChevronFace, + ContextMenuTooltipButton, +} from '../../../structures/ContextMenu'; +import { _t } from "../../../../languageHandler"; + +// Height of the header duplicated from CSS because we need to subtract it from our max +// height to get the max height of the video +const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px) + +const TOOLTIP_Y_OFFSET = -24; + +const CONTROLS_HIDE_DELAY = 2000; + +interface IProps { + call: MatrixCall; + pipMode: boolean; + handlers: { + onHangupClick: () => void; + onScreenshareClick: () => void; + onToggleSidebarClick: () => void; + onMicMuteClick: () => void; + onVidMuteClick: () => void; + }; + buttonsState: { + micMuted: boolean; + vidMuted: boolean; + sidebarShown: boolean; + screensharing: boolean; + }; + buttonsVisibility: { + screensharing: boolean; + vidMute: boolean; + sidebar: boolean; + dialpad: boolean; + contextMenu: boolean; + }; +} + +interface IState { + visible: boolean; + showDialpad: boolean; + hoveringControls: boolean; + showMoreMenu: boolean; +} + +export default class CallViewButtons extends React.Component { + private dialpadButton = createRef(); + private contextMenuButton = createRef(); + private controlsHideTimer: number = null; + + constructor(props: IProps) { + super(props); + + this.state = { + showDialpad: false, + hoveringControls: false, + showMoreMenu: false, + visible: true, + }; + } + + public componentDidMount(): void { + this.showControls(); + } + + public showControls(): void { + if (this.state.showMoreMenu || this.state.showDialpad) return; + + if (!this.state.visible) { + this.setState({ + visible: true, + }); + } + if (this.controlsHideTimer !== null) { + clearTimeout(this.controlsHideTimer); + } + this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY); + } + + private onControlsHideTimer = (): void => { + if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return; + this.controlsHideTimer = null; + this.setState({ visible: false }); + }; + + private onMouseEnter = (): void => { + this.setState({ hoveringControls: true }); + }; + + private onMouseLeave = (): void => { + this.setState({ hoveringControls: false }); + }; + + private onDialpadClick = (): void => { + if (!this.state.showDialpad) { + this.setState({ showDialpad: true }); + this.showControls(); + } else { + this.setState({ showDialpad: false }); + } + }; + + private onMoreClick = (): void => { + this.setState({ showMoreMenu: true }); + this.showControls(); + }; + + private closeDialpad = (): void => { + this.setState({ showDialpad: false }); + }; + + private closeContextMenu = (): void => { + this.setState({ showMoreMenu: false }); + }; + + public render(): JSX.Element { + const micClasses = classNames("mx_CallViewButtons_button", { + mx_CallViewButtons_button_micOn: !this.props.buttonsState.micMuted, + mx_CallViewButtons_button_micOff: this.props.buttonsState.micMuted, + }); + + const vidClasses = classNames("mx_CallViewButtons_button", { + mx_CallViewButtons_button_vidOn: !this.props.buttonsState.vidMuted, + mx_CallViewButtons_button_vidOff: this.props.buttonsState.vidMuted, + }); + + const screensharingClasses = classNames("mx_CallViewButtons_button", { + mx_CallViewButtons_button_screensharingOn: this.props.buttonsState.screensharing, + mx_CallViewButtons_button_screensharingOff: !this.props.buttonsState.screensharing, + }); + + const sidebarButtonClasses = classNames("mx_CallViewButtons_button", { + mx_CallViewButtons_button_sidebarOn: this.props.buttonsState.sidebarShown, + mx_CallViewButtons_button_sidebarOff: !this.props.buttonsState.sidebarShown, + }); + + // Put the other states of the mic/video icons in the document to make sure they're cached + // (otherwise the icon disappears briefly when toggled) + const micCacheClasses = classNames("mx_CallViewButtons_button", "mx_CallViewButtons_button_invisible", { + mx_CallViewButtons_button_micOn: this.props.buttonsState.micMuted, + mx_CallViewButtons_button_micOff: !this.props.buttonsState.micMuted, + }); + + const vidCacheClasses = classNames("mx_CallViewButtons_button", "mx_CallViewButtons_button_invisible", { + mx_CallViewButtons_button_vidOn: this.props.buttonsState.micMuted, + mx_CallViewButtons_button_vidOff: !this.props.buttonsState.micMuted, + }); + + const callControlsClasses = classNames("mx_CallViewButtons", { + mx_CallViewButtons_hidden: !this.state.visible, + }); + + let vidMuteButton; + if (this.props.buttonsVisibility.vidMute) { + vidMuteButton = ( + + ); + } + + let screensharingButton; + if (this.props.buttonsVisibility.screensharing) { + screensharingButton = ( + + ); + } + + let sidebarButton; + if (this.props.buttonsVisibility.sidebar) { + sidebarButton = ( + + ); + } + + let contextMenuButton; + if (this.props.buttonsVisibility.contextMenu) { + contextMenuButton = ( + + ); + } + let dialpadButton; + if (this.props.buttonsVisibility.dialpad) { + dialpadButton = ( + + ); + } + + let dialPad; + if (this.state.showDialpad) { + dialPad = ; + } + + let contextMenu; + if (this.state.showMoreMenu) { + contextMenu = ; + } + + return ( +
+ { dialPad } + { contextMenu } + { dialpadButton } + + { vidMuteButton } +
+
+ { screensharingButton } + { sidebarButton } + { contextMenuButton } + +
+ ); + } +} From d0e76a0ecd99d067baa92632062b360b414271a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 8 Aug 2021 11:20:17 +0200 Subject: [PATCH 32/42] Use CallViewButtons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallView.scss | 109 +-------- src/components/views/voip/CallView.tsx | 305 ++++--------------------- 2 files changed, 46 insertions(+), 368 deletions(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 7752edddfa..498dd8e096 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -47,11 +47,11 @@ limitations under the License. height: 180px; } - .mx_CallView_callControls { + .mx_CallViewButtons { bottom: 0px; } - .mx_CallView_callControls_button { + .mx_CallViewButtons_button { &::before { width: 36px; height: 36px; @@ -199,20 +199,6 @@ limitations under the License. } } -.mx_CallView_callControls { - position: absolute; - display: flex; - justify-content: center; - bottom: 5px; - opacity: 1; - transition: opacity 0.5s; - z-index: 200; // To be above _all_ feeds -} - -.mx_CallView_callControls_hidden { - opacity: 0.001; // opacity 0 can cause a re-layout - pointer-events: none; -} .mx_CallView_presenting { opacity: 1; @@ -232,94 +218,3 @@ limitations under the License. opacity: 0.001; // opacity 0 can cause a re-layout pointer-events: none; } - -.mx_CallView_callControls_button { - cursor: pointer; - margin-left: 2px; - margin-right: 2px; - - - &::before { - content: ''; - display: inline-block; - - height: 48px; - width: 48px; - - background-repeat: no-repeat; - background-size: contain; - background-position: center; - } -} - -.mx_CallView_callControls_dialpad { - &::before { - background-image: url('$(res)/img/voip/dialpad.svg'); - } -} - -.mx_CallView_callControls_button_micOn { - &::before { - background-image: url('$(res)/img/voip/mic-on.svg'); - } -} - -.mx_CallView_callControls_button_micOff { - &::before { - background-image: url('$(res)/img/voip/mic-off.svg'); - } -} - -.mx_CallView_callControls_button_vidOn { - &::before { - background-image: url('$(res)/img/voip/vid-on.svg'); - } -} - -.mx_CallView_callControls_button_vidOff { - &::before { - background-image: url('$(res)/img/voip/vid-off.svg'); - } -} - -.mx_CallView_callControls_button_screensharingOn { - &::before { - background-image: url('$(res)/img/voip/screensharing-on.svg'); - } -} - -.mx_CallView_callControls_button_screensharingOff { - &::before { - background-image: url('$(res)/img/voip/screensharing-off.svg'); - } -} - -.mx_CallView_callControls_button_sidebarOn { - &::before { - background-image: url('$(res)/img/voip/sidebar-on.svg'); - } -} - -.mx_CallView_callControls_button_sidebarOff { - &::before { - background-image: url('$(res)/img/voip/sidebar-off.svg'); - } -} - -.mx_CallView_callControls_button_hangup { - &::before { - background-image: url('$(res)/img/voip/hangup.svg'); - } -} - -.mx_CallView_callControls_button_more { - &::before { - background-image: url('$(res)/img/voip/more.svg'); - } -} - -.mx_CallView_callControls_button_invisible { - visibility: hidden; - pointer-events: none; - position: absolute; -} diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index d3371b8456..a6ae71713b 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,15 +27,7 @@ import { CallEvent, CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/we import classNames from 'classnames'; import AccessibleButton from '../elements/AccessibleButton'; import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard'; -import { - alwaysAboveLeftOf, - alwaysAboveRightOf, - ChevronFace, - ContextMenuTooltipButton, -} from '../../structures/ContextMenu'; -import CallContextMenu from '../context_menus/CallContextMenu'; import { avatarUrlForMember } from '../../../Avatar'; -import DialpadContextMenu from '../context_menus/DialpadContextMenu'; import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker"; @@ -43,8 +35,7 @@ import Modal from '../../../Modal'; import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes'; import CallViewSidebar from './CallViewSidebar'; import CallViewHeader from './CallView/CallViewHeader'; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import { Alignment } from "../elements/Tooltip"; +import CallViewButtons from "./CallView/CallViewButtons"; interface IProps { // The call for us to display @@ -83,8 +74,6 @@ interface IState { sidebarShown: boolean; } -const tooltipYOffset = -24; - function getFullScreenElement() { return ( document.fullscreenElement || @@ -113,18 +102,11 @@ function exitFullscreen() { if (exitMethod) exitMethod.call(document); } -const CONTROLS_HIDE_DELAY = 2000; -// Height of the header duplicated from CSS because we need to subtract it from our max -// height to get the max height of the video -const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px) - @replaceableComponent("views.voip.CallView") export default class CallView extends React.Component { private dispatcherRef: string; private contentRef = createRef(); - private controlsHideTimer: number = null; - private dialpadButton = createRef(); - private contextMenuButton = createRef(); + private buttonsRef = createRef(); constructor(props: IProps) { super(props); @@ -240,16 +222,8 @@ export default class CallView extends React.Component { }); }; - private onControlsHideTimer = () => { - if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return; - this.controlsHideTimer = null; - this.setState({ - controlsVisible: false, - }); - }; - private onMouseMove = () => { - this.showControls(); + this.buttonsRef.current?.showControls(); }; private getOrderedFeeds(feeds: Array): { primary: CallFeed, secondary: Array } { @@ -275,29 +249,6 @@ export default class CallView extends React.Component { return { primary, secondary }; } - private showControls(): void { - if (this.state.showMoreMenu || this.state.showDialpad) return; - - if (!this.state.controlsVisible) { - this.setState({ - controlsVisible: true, - }); - } - if (this.controlsHideTimer !== null) { - clearTimeout(this.controlsHideTimer); - } - this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY); - } - - private onDialpadClick = (): void => { - if (!this.state.showDialpad) { - this.setState({ showDialpad: true }); - this.showControls(); - } else { - this.setState({ showDialpad: false }); - } - }; - private onMicMuteClick = (): void => { const newVal = !this.state.micMuted; @@ -328,19 +279,6 @@ export default class CallView extends React.Component { }); }; - private onMoreClick = (): void => { - this.setState({ showMoreMenu: true }); - this.showControls(); - }; - - private closeDialpad = (): void => { - this.setState({ showDialpad: false }); - }; - - private closeContextMenu = (): void => { - this.setState({ showMoreMenu: false }); - }; - // we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire // Note that this assumes we always have a CallView on screen at any given time // CallHandler would probably be a better place for this @@ -353,7 +291,7 @@ export default class CallView extends React.Component { if (ctrlCmdOnly) { this.onMicMuteClick(); // show the controls to give feedback - this.showControls(); + this.buttonsRef.current?.showControls(); handled = true; } break; @@ -362,7 +300,7 @@ export default class CallView extends React.Component { if (ctrlCmdOnly) { this.onVidMuteClick(); // show the controls to give feedback - this.showControls(); + this.buttonsRef.current?.showControls(); handled = true; } break; @@ -374,15 +312,6 @@ export default class CallView extends React.Component { } }; - private onCallControlsMouseEnter = (): void => { - this.setState({ hoveringControls: true }); - this.showControls(); - }; - - private onCallControlsMouseLeave = (): void => { - this.setState({ hoveringControls: false }); - }; - private onCallResumeClick = (): void => { const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call); CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId); @@ -401,206 +330,60 @@ export default class CallView extends React.Component { }; private onToggleSidebar = (): void => { - this.setState({ - sidebarShown: !this.state.sidebarShown, - }); + this.setState({ sidebarShown: !this.state.sidebarShown }); }; private renderCallControls(): JSX.Element { - const micClasses = classNames({ - mx_CallView_callControls_button: true, - mx_CallView_callControls_button_micOn: !this.state.micMuted, - mx_CallView_callControls_button_micOff: this.state.micMuted, - }); - - const vidClasses = classNames({ - mx_CallView_callControls_button: true, - mx_CallView_callControls_button_vidOn: !this.state.vidMuted, - mx_CallView_callControls_button_vidOff: this.state.vidMuted, - }); - - const screensharingClasses = classNames({ - mx_CallView_callControls_button: true, - mx_CallView_callControls_button_screensharingOn: this.state.screensharing, - mx_CallView_callControls_button_screensharingOff: !this.state.screensharing, - }); - - const sidebarButtonClasses = classNames({ - mx_CallView_callControls_button: true, - mx_CallView_callControls_button_sidebarOn: this.state.sidebarShown, - mx_CallView_callControls_button_sidebarOff: !this.state.sidebarShown, - }); - - // Put the other states of the mic/video icons in the document to make sure they're cached - // (otherwise the icon disappears briefly when toggled) - const micCacheClasses = classNames({ - mx_CallView_callControls_button: true, - mx_CallView_callControls_button_micOn: this.state.micMuted, - mx_CallView_callControls_button_micOff: !this.state.micMuted, - mx_CallView_callControls_button_invisible: true, - }); - - const vidCacheClasses = classNames({ - mx_CallView_callControls_button: true, - mx_CallView_callControls_button_vidOn: this.state.micMuted, - mx_CallView_callControls_button_vidOff: !this.state.micMuted, - mx_CallView_callControls_button_invisible: true, - }); - - const callControlsClasses = classNames({ - mx_CallView_callControls: true, - mx_CallView_callControls_hidden: !this.state.controlsVisible, - }); - // We don't support call upgrades (yet) so hide the video mute button in voice calls - let vidMuteButton; - if (this.props.call.type === CallType.Video) { - vidMuteButton = ( - - ); - } - + const vidMuteButtonShown = this.props.call.type === CallType.Video; // Screensharing is possible, if we can send a second stream and // identify it using SDPStreamMetadata or if we can replace the already // existing usermedia track by a screensharing track. We also need to be // connected to know the state of the other side - let screensharingButton; - if ( + const screensharingButtonShown = ( (this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) && this.props.call.state === CallState.Connected - ) { - screensharingButton = ( - - ); - } - + ); // To show the sidebar we need secondary feeds, if we don't have them, // we can hide this button. If we are in PiP, sidebar is also hidden, so // we can hide the button too - let sidebarButton; - if ( - !this.props.pipMode && - ( - this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare || - this.props.call.isScreensharing() - ) - ) { - sidebarButton = ( - - ); - } - + const sidebarButtonShown = ( + this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare || + this.props.call.isScreensharing() + ); // The dial pad & 'more' button actions are only relevant in a connected call - let contextMenuButton; - if (this.state.callState === CallState.Connected) { - contextMenuButton = ( - - ); - } - let dialpadButton; - if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) { - dialpadButton = ( - - ); - } - - let dialPad; - if (this.state.showDialpad) { - dialPad = ; - } - - let contextMenu; - if (this.state.showMoreMenu) { - contextMenu = ; - } + const contextMenuButtonShown = this.state.callState === CallState.Connected; + const dialpadButtonShown = ( + this.state.callState === CallState.Connected && + this.props.call.opponentSupportsDTMF() + ); return ( -
- { dialPad } - { contextMenu } - { dialpadButton } - - { vidMuteButton } -
-
- { screensharingButton } - { sidebarButton } - { contextMenuButton } - -
+ ); } From eb49960497f4214918f8cdea78761885f55702d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 8 Aug 2021 11:20:52 +0200 Subject: [PATCH 33/42] 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 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f19de74685..826fe5ff88 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -905,6 +905,16 @@ "sends snowfall": "sends snowfall", "Sends the given message with a space themed effect": "Sends the given message with a space themed effect", "sends space invaders": "sends space invaders", + "unknown person": "unknown person", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", + "You held the call Switch": "You held the call Switch", + "You held the call Resume": "You held the call Resume", + "%(peerName)s held the call": "%(peerName)s held the call", + "Connecting": "Connecting", + "You are presenting": "You are presenting", + "%(sharerName)s is presenting": "%(sharerName)s is presenting", + "Your camera is turned off": "Your camera is turned off", + "Your camera is still enabled": "Your camera is still enabled", "Start the camera": "Start the camera", "Stop the camera": "Stop the camera", "Stop sharing your screen": "Stop sharing your screen", @@ -916,16 +926,6 @@ "Unmute the microphone": "Unmute the microphone", "Mute the microphone": "Mute the microphone", "Hangup": "Hangup", - "unknown person": "unknown person", - "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", - "You held the call Switch": "You held the call Switch", - "You held the call Resume": "You held the call Resume", - "%(peerName)s held the call": "%(peerName)s held the call", - "Connecting": "Connecting", - "You are presenting": "You are presenting", - "%(sharerName)s is presenting": "%(sharerName)s is presenting", - "Your camera is turned off": "Your camera is turned off", - "Your camera is still enabled": "Your camera is still enabled", "Video Call": "Video Call", "Voice Call": "Voice Call", "Fill Screen": "Fill Screen", From 09f20bcda78eb797c8654b11ee6d14c5d1d3e837 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Aug 2021 10:29:55 +0100 Subject: [PATCH 34/42] Make space hierarchy a treeview --- src/accessibility/RovingTabIndex.tsx | 52 ++- .../structures/SpaceRoomDirectory.tsx | 418 +++++++++++------- src/components/views/spaces/SpacePanel.tsx | 1 + .../views/spaces/SpaceTreeLevel.tsx | 2 +- 4 files changed, 289 insertions(+), 184 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 87f525bdfc..4e2279e09f 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -150,13 +150,14 @@ const reducer = (state: IState, action: IAction) => { interface IProps { handleHomeEnd?: boolean; + handleUpDown?: boolean; children(renderProps: { onKeyDownHandler(ev: React.KeyboardEvent); }); onKeyDown?(ev: React.KeyboardEvent, state: IState); } -export const RovingTabIndexProvider: React.FC = ({ children, handleHomeEnd, onKeyDown }) => { +export const RovingTabIndexProvider: React.FC = ({ children, handleHomeEnd, handleUpDown, onKeyDown }) => { const [state, dispatch] = useReducer>(reducer, { activeRef: null, refs: [], @@ -167,21 +168,50 @@ export const RovingTabIndexProvider: React.FC = ({ children, handleHomeE const onKeyDownHandler = useCallback((ev) => { let handled = false; // Don't interfere with input default keydown behaviour - if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") { + if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") { // check if we actually have any items switch (ev.key) { case Key.HOME: - handled = true; - // move focus to first item - if (context.state.refs.length > 0) { - context.state.refs[0].current.focus(); + if (handleHomeEnd) { + handled = true; + // move focus to first item + if (context.state.refs.length > 0) { + context.state.refs[0].current.focus(); + } } break; + case Key.END: - handled = true; - // move focus to last item - if (context.state.refs.length > 0) { - context.state.refs[context.state.refs.length - 1].current.focus(); + if (handleHomeEnd) { + handled = true; + // move focus to last item + if (context.state.refs.length > 0) { + context.state.refs[context.state.refs.length - 1].current.focus(); + } + } + break; + + case Key.ARROW_UP: + if (handleUpDown) { + handled = true; + if (context.state.refs.length > 0) { + const idx = context.state.refs.indexOf(context.state.activeRef); + if (idx > 1) { + context.state.refs[idx - 1].current.focus(); + } + } + } + break; + + case Key.ARROW_DOWN: + if (handleUpDown) { + handled = true; + if (context.state.refs.length > 0) { + const idx = context.state.refs.indexOf(context.state.activeRef); + if (idx < context.state.refs.length - 1) { + context.state.refs[idx + 1].current.focus(); + } + } } break; } @@ -193,7 +223,7 @@ export const RovingTabIndexProvider: React.FC = ({ children, handleHomeE } else if (onKeyDown) { return onKeyDown(ev, context.state); } - }, [context.state, onKeyDown, handleHomeEnd]); + }, [context.state, onKeyDown, handleHomeEnd, handleUpDown]); return { children({ onKeyDownHandler }) } diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index d8cc9593f0..b2d5e787f5 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode, useMemo, useState } from "react"; +import React, { ReactNode, KeyboardEvent, useMemo, useState } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces"; @@ -46,6 +46,8 @@ import { getDisplayAliasForAliasSet } from "../../Rooms"; import { useDispatcher } from "../../hooks/useDispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { Action } from "../../dispatcher/actions"; +import { Key } from "../../Keyboard"; +import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex"; interface IHierarchyProps { space: Room; @@ -80,6 +82,7 @@ const Tile: React.FC = ({ || (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room")); const [showChildren, toggleShowChildren] = useStateToggle(true); + const [onFocus, isActive, ref] = useRovingTabIndex(); const onPreviewClick = (ev: ButtonEvent) => { ev.preventDefault(); @@ -94,11 +97,21 @@ const Tile: React.FC = ({ let button; if (joinedRoom) { - button = + button = { _t("View") } ; } else if (onJoinClick) { - button = + button = { _t("Join") } ; } @@ -106,13 +119,13 @@ const Tile: React.FC = ({ let checkbox; if (onToggleClick) { if (hasPermissions) { - checkbox = ; + checkbox = ; } else { checkbox = { ev.stopPropagation(); }} > - + ; } } @@ -185,19 +198,65 @@ const Tile: React.FC = ({ toggleShowChildren(); }} />; + if (showChildren) { - childSection =
+ const onChildrenKeyDown = (e) => { + if (e.key === Key.ARROW_LEFT) { + e.preventDefault(); + e.stopPropagation(); + ref.current?.focus(); + } + }; + + childSection =
{ children }
; } } + const onKeyDown = children ? (e) => { + let handled = false; + + switch (e.key) { + case Key.ARROW_LEFT: + if (showChildren) { + handled = true; + toggleShowChildren(); + } + break; + + case Key.ARROW_RIGHT: + handled = true; + if (showChildren) { + (ref.current?.nextElementSibling?.firstElementChild as HTMLElement)?.focus(); + } else { + toggleShowChildren(); + } + break; + } + + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + } : undefined; + return <> { content } { childToggle } @@ -414,176 +473,191 @@ export const SpaceHierarchy: React.FC = ({ return

{ _t("Your server does not support showing space hierarchies.") }

; } - let content; - if (roomsMap) { - const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length; - const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at - - let countsStr; - if (numSpaces > 1) { - countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces }); - } else if (numSpaces > 0) { - countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces }); - } else { - countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces }); + const onKeyDown = (ev: KeyboardEvent, state: IState) => { + if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceRoomDirectory_search")) { + state.refs[0]?.current?.focus(); } - - let manageButtons; - if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { - const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { - return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][]; - }); - - const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { - return parentChildMap.get(parentId)?.get(childId)?.content.suggested; - }); - - const disabled = !selectedRelations.length || removing || saving; - - let Button: React.ComponentType> = AccessibleButton; - let props = {}; - if (!selectedRelations.length) { - Button = AccessibleTooltipButton; - props = { - tooltip: _t("Select a room below first"), - yOffset: -40, - }; - } - - manageButtons = <> - - - ; - } - - let results; - if (roomsMap.size) { - const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); - - results = <> - { - setError(""); - if (!selected.has(parentId)) { - setSelected(new Map(selected.set(parentId, new Set([childId])))); - return; - } - - const parentSet = selected.get(parentId); - if (!parentSet.has(childId)) { - setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId])))); - return; - } - - parentSet.delete(childId); - setSelected(new Map(selected.set(parentId, new Set(parentSet)))); - } : undefined} - onViewRoomClick={(roomId, autoJoin) => { - showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); - }} - /> - { children &&
} - ; - } else { - results =
-

{ _t("No results found") }

-
{ _t("You may want to try a different search or check for typos.") }
-
; - } - - content = <> -
- { countsStr } - - { additionalButtons } - { manageButtons } - -
- { error &&
- { error } -
} - - { results } - { children } - - ; - } else { - content = ; - } + }; // TODO loading state/error state - return <> - + return + { ({ onKeyDownHandler }) => { + let content; + if (roomsMap) { + const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length; + const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at - { content } - ; + let countsStr; + if (numSpaces > 1) { + countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces }); + } else if (numSpaces > 0) { + countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces }); + } else { + countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces }); + } + + let manageButtons; + if (space.getMyMembership() === "join" && + space.currentState.maySendStateEvent(EventType.SpaceChild, userId) + ) { + const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { + return [ + ...selected.get(parentId).values(), + ].map(childId => [parentId, childId]) as [string, string][]; + }); + + const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { + return parentChildMap.get(parentId)?.get(childId)?.content.suggested; + }); + + const disabled = !selectedRelations.length || removing || saving; + + let Button: React.ComponentType> = AccessibleButton; + let props = {}; + if (!selectedRelations.length) { + Button = AccessibleTooltipButton; + props = { + tooltip: _t("Select a room below first"), + yOffset: -40, + }; + } + + manageButtons = <> + + + ; + } + + let results; + if (roomsMap.size) { + const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); + + results = <> + { + setError(""); + if (!selected.has(parentId)) { + setSelected(new Map(selected.set(parentId, new Set([childId])))); + return; + } + + const parentSet = selected.get(parentId); + if (!parentSet.has(childId)) { + setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId])))); + return; + } + + parentSet.delete(childId); + setSelected(new Map(selected.set(parentId, new Set(parentSet)))); + } : undefined} + onViewRoomClick={(roomId, autoJoin) => { + showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); + }} + /> + { children &&
} + ; + } else { + results =
+

{ _t("No results found") }

+
{ _t("You may want to try a different search or check for typos.") }
+
; + } + + content = <> +
+ { countsStr } + + { additionalButtons } + { manageButtons } + +
+ { error &&
+ { error } +
} + + { results } + { children } + + ; + } else { + content = ; + } + + return <> + + + { content } + ; + } } +
; }; interface IProps { diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 58e1db4b1d..38d530bee5 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -272,6 +272,7 @@ const SpacePanel = () => {
    { (provided, snapshot) => ( diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 5d2f58fab2..5b32722504 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -328,7 +328,7 @@ const SpaceTreeLevel: React.FC = ({ isNested, parents, }) => { - return
      + return
        { spaces.map(s => { return ( Date: Mon, 9 Aug 2021 14:01:34 +0100 Subject: [PATCH 35/42] iterate spaces treeview stuff --- res/css/structures/_SpaceRoomDirectory.scss | 4 ++ src/accessibility/RovingTabIndex.tsx | 2 +- .../structures/SpaceRoomDirectory.tsx | 72 ++++++++++--------- src/components/views/spaces/SpacePanel.tsx | 18 +++-- .../views/spaces/SpaceTreeLevel.tsx | 7 +- 5 files changed, 57 insertions(+), 46 deletions(-) diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index 03ff1d5ae5..88e6a3f494 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -278,6 +278,10 @@ limitations under the License. } } + li.mx_SpaceRoomDirectory_roomTileWrapper { + list-style: none; + } + .mx_SpaceRoomDirectory_roomTile, .mx_SpaceRoomDirectory_subspace_children { &::before { diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 4e2279e09f..68e10049fd 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -196,7 +196,7 @@ export const RovingTabIndexProvider: React.FC = ({ children, handleHomeE handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); - if (idx > 1) { + if (idx > 0) { context.state.refs[idx - 1].current.focus(); } } diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index b2d5e787f5..bcd1a4252e 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode, KeyboardEvent, useMemo, useState } from "react"; +import React, { ReactNode, KeyboardEvent, useMemo, useState, KeyboardEventHandler } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces"; @@ -185,8 +185,9 @@ const Tile: React.FC = ({
; - let childToggle; - let childSection; + let childToggle: JSX.Element; + let childSection: JSX.Element; + let onKeyDown: KeyboardEventHandler; if (children) { // the chevron is purposefully a div rather than a button as it should be ignored for a11y childToggle =
= ({ { children }
; } + + onKeyDown = (e) => { + let handled = false; + + switch (e.key) { + case Key.ARROW_LEFT: + if (showChildren) { + handled = true; + toggleShowChildren(); + } + break; + + case Key.ARROW_RIGHT: + handled = true; + if (showChildren) { + const childSection = ref.current?.nextElementSibling; + childSection?.querySelector(".mx_SpaceRoomDirectory_roomTile")?.focus(); + } else { + toggleShowChildren(); + } + break; + } + + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + }; } - const onKeyDown = children ? (e) => { - let handled = false; - - switch (e.key) { - case Key.ARROW_LEFT: - if (showChildren) { - handled = true; - toggleShowChildren(); - } - break; - - case Key.ARROW_RIGHT: - handled = true; - if (showChildren) { - (ref.current?.nextElementSibling?.firstElementChild as HTMLElement)?.focus(); - } else { - toggleShowChildren(); - } - break; - } - - if (handled) { - e.preventDefault(); - e.stopPropagation(); - } - } : undefined; - - return <> + return
  • = ({ inputRef={ref} onFocus={onFocus} tabIndex={isActive ? 0 : -1} - aria-expanded={children ? showChildren : undefined} - role="treeitem" > { content } { childToggle } { childSection } - ; +
  • ; }; export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => { diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 38d530bee5..4e3455e1fb 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -100,9 +100,12 @@ const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => { return SpaceStore.instance.allRoomsInHome; }); - return
  • + return
  • SpaceStore.instance.setActiveSpace(null)} @@ -142,9 +145,12 @@ const CreateSpaceButton = ({ openMenu(); }; - return
  • + return
  • = ({ onClick={onClick} onContextMenu={openMenu} forceHide={!isNarrow || menuDisplayed} - role="treeitem" inputRef={handle} > { children } @@ -290,7 +289,7 @@ export class SpaceItem extends React.PureComponent { /> : null; return ( -
  • +
  • { avatarSize={isNested ? 24 : 32} onClick={this.onClick} onKeyDown={this.onKeyDown} - aria-expanded={!collapsed} - ContextMenuComponent={this.props.space.getMyMembership() === "join" - ? SpaceContextMenu : undefined} + ContextMenuComponent={this.props.space.getMyMembership() === "join" ? SpaceContextMenu : undefined} > { toggleCollapseButton } From 857bb9db44ee2c29428c8413a866791f88b3d0e2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Aug 2021 09:46:25 +0100 Subject: [PATCH 36/42] Add some treeview labels --- src/components/structures/SpaceRoomDirectory.tsx | 7 ++++++- src/components/views/spaces/SpacePanel.tsx | 1 + src/i18n/strings/en_EN.json | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index bcd1a4252e..27b70c6841 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -639,7 +639,12 @@ export const SpaceHierarchy: React.FC = ({ { error &&
    { error }
    } - + { results } { children } diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 4e3455e1fb..40016af36f 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -279,6 +279,7 @@ const SpacePanel = () => { className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })} onKeyDown={onKeyDownHandler} role="tree" + aria-label={_t("Spaces")} > { (provided, snapshot) => ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3aab3cb68c..6dec6b51ce 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2829,6 +2829,7 @@ "Mark as suggested": "Mark as suggested", "No results found": "No results found", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", + "Space": "Space", "Search names and descriptions": "Search names and descriptions", "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", "Create room": "Create room", @@ -3110,7 +3111,6 @@ "Page Down": "Page Down", "Esc": "Esc", "Enter": "Enter", - "Space": "Space", "End": "End", "[number]": "[number]" } From 5bc165f2ac1475c0536f1867e4e35d2a56603b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 11 Aug 2021 18:33:16 +0200 Subject: [PATCH 37/42] Make scrollbar dot transparent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_EventTile.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 1c9d8e87d9..56cede0895 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -489,6 +489,10 @@ $hover-select-border: 4px; // https://github.com/vector-im/vector-web/issues/754 overflow-x: overlay; overflow-y: visible; + + &::-webkit-scrollbar-corner { + background: transparent; + } } } From 012f2c9e7e9702d10bf2cda96b3d897fc796cd4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 11 Aug 2021 19:16:55 +0200 Subject: [PATCH 38/42] This doesn't need to be here as it was moved into CallViewButtons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index fbb69a51ec..a6ae71713b 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -135,7 +135,6 @@ export default class CallView extends React.Component { public componentDidMount() { this.dispatcherRef = dis.register(this.onAction); document.addEventListener('keydown', this.onNativeKeyDown); - this.showControls(); } public componentWillUnmount() { From cf8ee19e23e494e834b25c82ef7e3c2c3b83c13c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Aug 2021 19:25:17 +0100 Subject: [PATCH 39/42] Fix Netflify builds from fork PRs Some absolutely horrenous hacks to upload the context as an artifact then download it, unzip it and set the PR number as a variable we can use, because GitHub Actions just doesn't offer any other way of doing this. Maybe we'd be better off going back to Netlify... --- .github/workflows/layered-build.yaml | 12 ++++++++++++ .github/workflows/netflify.yaml | 25 +++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/workflows/layered-build.yaml b/.github/workflows/layered-build.yaml index 7235b4020f..1474338a16 100644 --- a/.github/workflows/layered-build.yaml +++ b/.github/workflows/layered-build.yaml @@ -16,4 +16,16 @@ jobs: path: element-web/webapp # We'll only use this in a triggered job, then we're done with it retention-days: 1 + - uses: actions/github-script@v3.1.0 + with: + script: | + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/context.json', JSON.stringify(context)); + - name: Upload Context + uses: actions/upload-artifact@v2 + with: + name: context.json + path: context.json + # We'll only use this in a triggered job, then we're done with it + retention-days: 1 diff --git a/.github/workflows/netflify.yaml b/.github/workflows/netflify.yaml index ccccfe7e07..007488cae9 100644 --- a/.github/workflows/netflify.yaml +++ b/.github/workflows/netflify.yaml @@ -33,7 +33,28 @@ jobs: }); var fs = require('fs'); fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); - - run: unzip previewbuild.zip && rm previewbuild.zip + + var contextArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "context.json" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: contextArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/context.json.zip', Buffer.from(download.data)); + - name: Extract Artifacts + run: unzip -d webapp previewbuild.zip && rm previewbuild.zip && unzip context.json && rm context.json.zip + - name: 'Read Context' + id: readctx + uses: actions/github-script@v3.1.0 + with: + script: | + var fs = require('fs'); + var ctx = JSON.parse(fs.readFileSync('${{github.workspace}}/context.json')); + console.log(`::set-output name=prnumber::${ctx.payload.pull_request.number}`); - name: Deploy to Netlify id: netlify uses: nwtgck/actions-netlify@v1.2 @@ -51,7 +72,7 @@ jobs: uses: phulsechinmay/rewritable-pr-comment@v0.3.0 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_ID: ${{ github.event.workflow_run.pull_requests[0].number }} + ISSUE_ID: ${{ steps.readctx.outputs.prnumber }} message: | Preview: ${{ steps.netlify.outputs.deploy-url }} ⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts. From bbdee0d83bf83288559568079c00532344ea5168 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Aug 2021 19:41:37 +0100 Subject: [PATCH 40/42] publish the right directory --- .github/workflows/netflify.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/netflify.yaml b/.github/workflows/netflify.yaml index 007488cae9..444333fdfb 100644 --- a/.github/workflows/netflify.yaml +++ b/.github/workflows/netflify.yaml @@ -59,7 +59,7 @@ jobs: id: netlify uses: nwtgck/actions-netlify@v1.2 with: - publish-dir: . + publish-dir: webapp deploy-message: "Deploy from GitHub Actions" # These don't work because we're in workflow_run enable-pull-request-comment: false From 1fe5ace8edfe57d82f988e1b088deb3cedb7414d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Aug 2021 21:10:22 +0100 Subject: [PATCH 41/42] Edit PR Description instead of commenting We could include the magic comments in the PR template so the various automated comments were always in the same order, if we wanted. --- .github/workflows/netflify.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/netflify.yaml b/.github/workflows/netflify.yaml index 444333fdfb..d9ec1f842d 100644 --- a/.github/workflows/netflify.yaml +++ b/.github/workflows/netflify.yaml @@ -68,12 +68,13 @@ jobs: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} timeout-minutes: 1 - - name: Comment on PR - uses: phulsechinmay/rewritable-pr-comment@v0.3.0 - with: + - name: Edit PR Description + uses: velas/pr-description@v1.0.1 + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_ID: ${{ steps.readctx.outputs.prnumber }} - message: | + with: + pull-request-number: ${{ steps.readctx.outputs.prnumber }} + description-message: | Preview: ${{ steps.netlify.outputs.deploy-url }} ⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts. From 8016b340b00d7a1ca264b82db2adf43f64bfe843 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Aug 2021 21:20:28 +0100 Subject: [PATCH 42/42] Just upload the PR object itself We don't know what secret info might end up in the context --- .github/workflows/layered-build.yaml | 8 ++++---- .github/workflows/netflify.yaml | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/layered-build.yaml b/.github/workflows/layered-build.yaml index 1474338a16..c9d7e89a75 100644 --- a/.github/workflows/layered-build.yaml +++ b/.github/workflows/layered-build.yaml @@ -20,12 +20,12 @@ jobs: with: script: | var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/context.json', JSON.stringify(context)); - - name: Upload Context + fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request)); + - name: Upload PR Info uses: actions/upload-artifact@v2 with: - name: context.json - path: context.json + name: pr.json + path: pr.json # We'll only use this in a triggered job, then we're done with it retention-days: 1 diff --git a/.github/workflows/netflify.yaml b/.github/workflows/netflify.yaml index 444333fdfb..3cb4543820 100644 --- a/.github/workflows/netflify.yaml +++ b/.github/workflows/netflify.yaml @@ -34,27 +34,27 @@ jobs: var fs = require('fs'); fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); - var contextArtifact = artifacts.data.artifacts.filter((artifact) => { - return artifact.name == "context.json" + var prInfoArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "pr.json" })[0]; var download = await github.actions.downloadArtifact({ owner: context.repo.owner, repo: context.repo.repo, - artifact_id: contextArtifact.id, + artifact_id: prInfoArtifact.id, archive_format: 'zip', }); var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/context.json.zip', Buffer.from(download.data)); + fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data)); - name: Extract Artifacts - run: unzip -d webapp previewbuild.zip && rm previewbuild.zip && unzip context.json && rm context.json.zip - - name: 'Read Context' + run: unzip -d webapp previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip + - name: 'Read PR Info' id: readctx uses: actions/github-script@v3.1.0 with: script: | var fs = require('fs'); - var ctx = JSON.parse(fs.readFileSync('${{github.workspace}}/context.json')); - console.log(`::set-output name=prnumber::${ctx.payload.pull_request.number}`); + var pr = JSON.parse(fs.readFileSync('${{github.workspace}}/pr.json')); + console.log(`::set-output name=prnumber::${pr.number}`); - name: Deploy to Netlify id: netlify uses: nwtgck/actions-netlify@v1.2