From 7e63202f9a93ad83acd1207137d740c369a3d702 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 4 May 2022 17:22:09 +0200 Subject: [PATCH 01/20] Replace compose context menu with IconizedContextMenu (#22046) Signed-off-by: Michael Weimann --- res/css/views/rooms/_MessageComposer.scss | 33 +++---------------- .../views/location/LocationButton.tsx | 2 +- .../views/rooms/CollapsibleButton.tsx | 16 ++++----- .../views/rooms/MessageComposerButtons.tsx | 24 +++++++++----- 4 files changed, 29 insertions(+), 46 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 1fc36a9af9..9619bad3fe 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -178,12 +178,6 @@ limitations under the License. } } -.mx_ContextualMenu { - .mx_MessageComposer_button { - padding-left: calc(var(--size) + 6px); - } -} - .mx_MessageComposer_button { --size: 26px; position: relative; @@ -192,20 +186,16 @@ limitations under the License. line-height: var(--size); width: auto; padding-left: var(--size); + border-radius: 50%; + margin-right: 6px; - &:not(.mx_CallContextMenu_item) { - border-radius: 50%; - margin-right: 6px; - - &:last-child { - margin-right: auto; - } + &:last-child { + margin-right: auto; } &::before { content: ''; position: absolute; - top: 3px; left: 3px; height: 20px; @@ -399,18 +389,3 @@ limitations under the License. left: 0; } } - -.mx_MessageComposer_Menu .mx_CallContextMenu_item { - display: flex; - align-items: center; - max-width: unset; - margin: 7px 7px 7px 16px; // space out the buttons -} - -.mx_MessageComposer_Menu .mx_ContextualMenu { - min-width: 150px; - width: max-content; - padding: 5px 10px 5px 0; - box-shadow: 0px 2px 9px rgba(0, 0, 0, 0.25); - border-radius: 8px; -} diff --git a/src/components/views/location/LocationButton.tsx b/src/components/views/location/LocationButton.tsx index e6a6f8d92e..b119268588 100644 --- a/src/components/views/location/LocationButton.tsx +++ b/src/components/views/location/LocationButton.tsx @@ -58,7 +58,6 @@ export const LocationButton: React.FC = ({ roomId, sender, menuPosition, const className = classNames( "mx_MessageComposer_button", - "mx_MessageComposer_location", { "mx_MessageComposer_button_highlight": menuDisplayed, }, @@ -67,6 +66,7 @@ export const LocationButton: React.FC = ({ roomId, sender, menuPosition, return diff --git a/src/components/views/rooms/CollapsibleButton.tsx b/src/components/views/rooms/CollapsibleButton.tsx index b9e9f083d0..d89d277073 100644 --- a/src/components/views/rooms/CollapsibleButton.tsx +++ b/src/components/views/rooms/CollapsibleButton.tsx @@ -20,27 +20,27 @@ import classNames from 'classnames'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { MenuItem } from "../../structures/ContextMenu"; import { OverflowMenuContext } from './MessageComposerButtons'; +import { IconizedContextMenuOption } from '../context_menus/IconizedContextMenu'; interface ICollapsibleButtonProps extends ComponentProps { title: string; + iconClassName: string; } -export const CollapsibleButton = ({ title, children, className, ...props }: ICollapsibleButtonProps) => { +export const CollapsibleButton = ({ title, children, className, iconClassName, ...props }: ICollapsibleButtonProps) => { const inOverflowMenu = !!useContext(OverflowMenuContext); if (inOverflowMenu) { - return - { title } - { children } - ; + iconClassName={iconClassName} + label={title} + />; } return { children } ; diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 17c87b15ca..fe5c42ea26 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -38,6 +38,7 @@ import MatrixClientContext from '../../../contexts/MatrixClientContext'; import RoomContext from '../../../contexts/RoomContext'; import { useDispatcher } from "../../../hooks/useDispatcher"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; +import IconizedContextMenu, { IconizedContextMenuOptionList } from '../context_menus/IconizedContextMenu'; interface IProps { addEmoji: (emoji: string) => boolean; @@ -108,15 +109,18 @@ const MessageComposerButtons: React.FC = (props: IProps) => { title={_t("More options")} /> } { props.isMenuOpen && ( - - { moreButtons } + + { moreButtons } + - + ) } ; }; @@ -158,7 +162,6 @@ const EmojiButton: React.FC = ({ addEmoji, menuPosition }) => const className = classNames( "mx_MessageComposer_button", - "mx_MessageComposer_emoji", { "mx_MessageComposer_button_highlight": menuDisplayed, }, @@ -169,6 +172,7 @@ const EmojiButton: React.FC = ({ addEmoji, menuPosition }) => return @@ -254,7 +258,8 @@ const UploadButton = () => { }; return ; @@ -266,7 +271,8 @@ function showStickersButton(props: IProps): ReactElement { ? props.setStickerPickerOpen(!props.isStickerPickerOpen)} title={props.isStickerPickerOpen ? _t("Hide stickers") : _t("Sticker")} /> @@ -281,7 +287,8 @@ function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement { ? null : @@ -345,7 +352,8 @@ class PollButton extends React.PureComponent { return ( From e980c146ff2218d199ac8b1aa712446e671ee150 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sat, 7 May 2022 19:08:00 +0000 Subject: [PATCH 02/20] Create a mixin of cancel button (#8526) --- res/css/_common.scss | 13 ++++++++++--- res/css/structures/auth/_CompleteSecurity.scss | 7 +------ res/css/views/dialogs/_CompoundDialog.scss | 7 +------ res/css/views/elements/_EditableItemList.scss | 4 +--- res/css/views/right_panel/_VerificationPanel.scss | 6 +----- res/css/views/voip/_DialPadContextMenu.scss | 7 +------ res/css/views/voip/_DialPadModal.scss | 7 +------ 7 files changed, 16 insertions(+), 35 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 94ec5ea011..9ef9720b6f 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -385,15 +385,22 @@ legend { color: $alert; } -.mx_Dialog_cancelButton { +@define-mixin customisedCancelButton { mask: url('$(res)/img/feather-customised/cancel.svg'); mask-repeat: no-repeat; mask-position: center; mask-size: cover; - width: 14px; - height: 14px; background-color: $dialog-close-fg-color; cursor: pointer; + position: unset; + width: unset; + height: unset; +} + +.mx_Dialog_cancelButton { + @mixin customisedCancelButton; + width: 14px; + height: 14px; position: absolute; top: 10px; right: 0; diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss index bf5aeb15f5..4c3602ac26 100644 --- a/res/css/structures/auth/_CompleteSecurity.scss +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -34,14 +34,9 @@ limitations under the License. } .mx_CompleteSecurity_skip { - mask: url('$(res)/img/feather-customised/cancel.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: cover; + @mixin customisedCancelButton; width: 18px; height: 18px; - background-color: $dialog-close-fg-color; - cursor: pointer; position: absolute; right: 24px; } diff --git a/res/css/views/dialogs/_CompoundDialog.scss b/res/css/views/dialogs/_CompoundDialog.scss index d90c7e0f8e..28e7388e0e 100644 --- a/res/css/views/dialogs/_CompoundDialog.scss +++ b/res/css/views/dialogs/_CompoundDialog.scss @@ -38,14 +38,9 @@ limitations under the License. } .mx_CompoundDialog_cancelButton { - mask: url('$(res)/img/feather-customised/cancel.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: cover; + @mixin customisedCancelButton; width: 20px; height: 20px; - background-color: $dialog-close-fg-color; - cursor: pointer; // Align with middle of title, 34px from right edge position: absolute; diff --git a/res/css/views/elements/_EditableItemList.scss b/res/css/views/elements/_EditableItemList.scss index 91ef20539c..8782456249 100644 --- a/res/css/views/elements/_EditableItemList.scss +++ b/res/css/views/elements/_EditableItemList.scss @@ -25,14 +25,12 @@ limitations under the License. } .mx_EditableItem_delete { + @mixin customisedCancelButton; order: 3; margin-right: 5px; - cursor: pointer; vertical-align: middle; width: 14px; height: 14px; - mask-image: url('$(res)/img/feather-customised/cancel.svg'); - mask-repeat: no-repeat; background-color: $alert; mask-size: 100%; } diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss index 0db93f58cc..ff5b5f0dc1 100644 --- a/res/css/views/right_panel/_VerificationPanel.scss +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -25,14 +25,10 @@ limitations under the License. .mx_UserInfo { .mx_EncryptionPanel_cancel { - mask: url('$(res)/img/feather-customised/cancel.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: cover; + @mixin customisedCancelButton; width: 14px; height: 14px; background-color: $settings-subsection-fg-color; - cursor: pointer; position: absolute; z-index: 100; top: 14px; diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 905486de8a..046db3133e 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -35,15 +35,10 @@ limitations under the License. } .mx_DialPadContextMenu_cancel { + @mixin customisedCancelButton; float: right; - mask: url('$(res)/img/feather-customised/cancel.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: cover; width: 14px; height: 14px; - background-color: $dialog-close-fg-color; - cursor: pointer; } .mx_DialPadContextMenu_header:focus-within { diff --git a/res/css/views/voip/_DialPadModal.scss b/res/css/views/voip/_DialPadModal.scss index ff1ded029c..75ad8a1902 100644 --- a/res/css/views/voip/_DialPadModal.scss +++ b/res/css/views/voip/_DialPadModal.scss @@ -45,15 +45,10 @@ limitations under the License. } .mx_DialPadModal_cancel { + @mixin customisedCancelButton; float: right; - mask: url('$(res)/img/feather-customised/cancel.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: cover; width: 14px; height: 14px; - background-color: $dialog-close-fg-color; - cursor: pointer; margin-right: 16px; } From dfc7224fc7c7d05bc86ff28e0992763829f35681 Mon Sep 17 00:00:00 2001 From: Sinharitik589 <67551927+Sinharitik589@users.noreply.github.com> Date: Mon, 9 May 2022 11:04:56 +0530 Subject: [PATCH 03/20] Converting selected text to MD link when pasting a URL (#8242) * Converting selected text to MD link when pasting a URL * Update src/editor/operations.ts Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> * Converting selected text to MD link when pasting a URL Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/BasicMessageComposer.tsx | 11 +++++++++-- src/editor/operations.ts | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 068d096624..004c20daef 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -24,7 +24,8 @@ import { logger } from "matrix-js-sdk/src/logger"; import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; import { Caret, setSelection } from '../../../editor/caret'; -import { formatRange, replaceRangeAndMoveCaret, toggleInlineFormat } from '../../../editor/operations'; +import { formatRange, formatRangeAsLink, replaceRangeAndMoveCaret, toggleInlineFormat } + from '../../../editor/operations'; import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom'; import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete'; import { getAutoCompleteCreator, Type } from '../../../editor/parts'; @@ -45,6 +46,7 @@ import { ICompletion } from "../../../autocomplete/Autocompleter"; import { getKeyBindingsManager } from '../../../KeyBindingsManager'; import { ALTERNATE_KEY_NAME, KeyBindingAction } from '../../../accessibility/KeyboardShortcuts'; import { _t } from "../../../languageHandler"; +import { linkify } from '../../../linkify-matrix'; // matches emoticons which follow the start of a line or whitespace const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$'); @@ -348,9 +350,14 @@ export default class BasicMessageEditor extends React.Component const text = event.clipboardData.getData("text/plain"); parts = parsePlainTextMessage(text, partCreator, { shouldEscape: false }); } + const textToInsert = event.clipboardData.getData("text/plain"); this.modifiedFlag = true; const range = getRangeForSelection(this.editorRef.current, model, document.getSelection()); - replaceRangeAndMoveCaret(range, parts); + if (textToInsert && linkify.test(textToInsert)) { + formatRangeAsLink(range, textToInsert); + } else { + replaceRangeAndMoveCaret(range, parts); + } }; private onInput = (event: Partial): void => { diff --git a/src/editor/operations.ts b/src/editor/operations.ts index 40a438cc56..f4dd61faa0 100644 --- a/src/editor/operations.ts +++ b/src/editor/operations.ts @@ -216,7 +216,7 @@ export function formatRangeAsCode(range: Range): void { replaceRangeAndExpandSelection(range, parts); } -export function formatRangeAsLink(range: Range) { +export function formatRangeAsLink(range: Range, text?: string) { const { model } = range; const { partCreator } = model; const linkRegex = /\[(.*?)\]\(.*?\)/g; @@ -229,7 +229,7 @@ export function formatRangeAsLink(range: Range) { replaceRangeAndAutoAdjustCaret(range, newParts, true, prefixLength, suffixLength); } else { // We set offset to -1 here so that the caret lands between the brackets - replaceRangeAndMoveCaret(range, [partCreator.plain("[" + range.text + "]" + "()")], -1); + replaceRangeAndMoveCaret(range, [partCreator.plain("[" + range.text + "]" + "(" + (text ?? "") + ")")], -1); } } From b1daf3fec2f0459d088ed604c1413d08b13786c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 9 May 2022 08:25:14 +0200 Subject: [PATCH 04/20] Add a `Copy link` button to the right-click message context-menu labs feature (#8527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Simplify `Share` button Signed-off-by: Šimon Brandner * Add proper `Copy link` button Signed-off-by: Šimon Brandner * i18n Signed-off-by: Šimon Brandner --- .../context_menus/MessageContextMenu.tsx | 52 ++++++++++++------- src/components/views/rooms/EventTile.tsx | 29 ++++++----- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index cf61ee5bfd..86e9d9cc30 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -70,8 +70,8 @@ interface IProps extends IPosition { rightClick?: boolean; // The Relations model from the JS SDK for reactions to `mxEvent` reactions?: Relations; - // A permalink to the event - showPermalink?: boolean; + // A permalink to this event or an href of an anchor element the user has clicked + link?: string; getRelationsForEvent?: GetRelationsForEvent; } @@ -227,7 +227,7 @@ export default class MessageContextMenu extends React.Component this.closeMenu(); }; - private onPermalinkClick = (e: React.MouseEvent): void => { + private onShareClick = (e: React.MouseEvent): void => { e.preventDefault(); Modal.createTrackedDialog('share room message dialog', '', ShareDialog, { target: this.props.mxEvent, @@ -236,9 +236,9 @@ export default class MessageContextMenu extends React.Component this.closeMenu(); }; - private onCopyPermalinkClick = (e: ButtonEvent): void => { + private onCopyLinkClick = (e: ButtonEvent): void => { e.preventDefault(); // So that we don't open the permalink - copyPlaintext(this.getPermalink()); + copyPlaintext(this.props.link); this.closeMenu(); }; @@ -295,11 +295,6 @@ export default class MessageContextMenu extends React.Component }); } - private getPermalink(): string { - if (!this.props.permalinkCreator) return; - return this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); - } - private getUnsentReactions(): MatrixEvent[] { return this.getReactions(e => e.status === EventStatus.NOT_SENT); } @@ -318,11 +313,11 @@ export default class MessageContextMenu extends React.Component public render(): JSX.Element { const cli = MatrixClientPeg.get(); const me = cli.getUserId(); - const { mxEvent, rightClick, showPermalink, eventTileOps, reactions, collapseReplyChain } = this.props; + const { mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain } = this.props; const eventStatus = mxEvent.status; const unsentReactionsCount = this.getUnsentReactions().length; const contentActionable = isContentActionable(mxEvent); - const permalink = this.getPermalink(); + const permalink = this.props.permalinkCreator?.forEvent(this.props.mxEvent.getId()); // status is SENT before remote-echo, null after const isSent = !eventStatus || eventStatus === EventStatus.SENT; const { timelineRenderingType, canReact, canSendMessages } = this.context; @@ -420,17 +415,13 @@ export default class MessageContextMenu extends React.Component if (permalink) { permalinkButton = ( ); } + let copyLinkButton: JSX.Element; + if (link) { + copyLinkButton = ( + + ); + } + let copyButton: JSX.Element; if (rightClick && getSelectedText()) { copyButton = ( @@ -566,10 +577,11 @@ export default class MessageContextMenu extends React.Component } let nativeItemsList: JSX.Element; - if (copyButton) { + if (copyButton || copyLinkButton) { nativeItemsList = ( { copyButton } + { copyLinkButton } ); } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index e8f0199c5f..e54c0900a5 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -234,7 +234,7 @@ interface IState { // Position of the context menu contextMenu?: { position: Pick; - showPermalink?: boolean; + link?: string; }; isQuoteExpanded?: boolean; @@ -842,26 +842,27 @@ export class UnwrappedEventTile extends React.Component { }; private onTimestampContextMenu = (ev: React.MouseEvent): void => { - this.showContextMenu(ev, true); + this.showContextMenu(ev, this.props.permalinkCreator?.forEvent(this.props.mxEvent.getId())); }; - private showContextMenu(ev: React.MouseEvent, showPermalink?: boolean): void { + private showContextMenu(ev: React.MouseEvent, permalink?: string): void { + const clickTarget = ev.target as HTMLElement; + // Return if message right-click context menu isn't enabled if (!SettingsStore.getValue("feature_message_right_click_context_menu")) return; - // Return if we're in a browser and click either an a tag or we have - // selected text, as in those cases we want to use the native browser - // menu - const clickTarget = ev.target as HTMLElement; - if ( - !PlatformPeg.get().allowOverridingNativeContextMenus() && - (clickTarget.tagName === "a" || clickTarget.closest("a") || getSelectedText()) - ) return; + // Try to find an anchor element + const anchorElement = (clickTarget instanceof HTMLAnchorElement) ? clickTarget : clickTarget.closest("a"); // There is no way to copy non-PNG images into clipboard, so we can't // have our own handling for copying images, so we leave it to the // Electron layer (webcontents-handler.ts) - if (ev.target instanceof HTMLImageElement) return; + if (clickTarget instanceof HTMLImageElement) return; + + // Return if we're in a browser and click either an a tag or we have + // selected text, as in those cases we want to use the native browser + // menu + if (!PlatformPeg.get().allowOverridingNativeContextMenus() && (getSelectedText() || anchorElement)) return; // We don't want to show the menu when editing a message if (this.props.editState) return; @@ -875,7 +876,7 @@ export class UnwrappedEventTile extends React.Component { top: ev.clientY, bottom: ev.clientY, }, - showPermalink: showPermalink, + link: anchorElement?.href || permalink, }, actionBarFocused: true, }); @@ -924,7 +925,7 @@ export class UnwrappedEventTile extends React.Component { onFinished={this.onCloseMenu} rightClick={true} reactions={this.state.reactions} - showPermalink={this.state.contextMenu.showPermalink} + link={this.state.contextMenu.link} /> ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4c61d944e8..aa4dcd2cdd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2921,10 +2921,10 @@ "Forward": "Forward", "View source": "View source", "Show preview": "Show preview", - "Copy link": "Copy link", "Source URL": "Source URL", "Collapse reply thread": "Collapse reply thread", "Report": "Report", + "Copy link": "Copy link", "Forget": "Forget", "Mentions only": "Mentions only", "See room timeline (devtools)": "See room timeline (devtools)", From ed086b0608f24761b3a4b592d2dfcc1f528f3952 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 9 May 2022 06:27:13 +0000 Subject: [PATCH 05/20] Set line-height: 1 to RedactedBody inside GenericEventListSummary for IRC/modern layout (#8529) * Move line-height of .mx_EventTile_line from _GroupLayout.scss to _EventTile.scss Specifying mx_EventTile_line's line-height in mx_GroupLayout is too strong for mx_GenericEventListSummary. - Set line-height:1 to mx_RedactedBody inside mx_GenericEventListSummary on IRC/modern layout Signed-off-by: Suguru Hirahara * Use a variable to remove the comment Signed-off-by: Suguru Hirahara * Readability Signed-off-by: Suguru Hirahara --- res/css/views/rooms/_EventTile.scss | 17 +++++++++++++++-- res/css/views/rooms/_GroupLayout.scss | 27 +++++++++++++++++++++------ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 6f2fb70498..133de86ddd 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -50,6 +50,12 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss .mx_EventTile_receiptSending::before { mask-image: url('$(res)/img/element-icons/circle-sending.svg'); } + + &[data-layout=group] { + .mx_EventTile_line { + line-height: var(--GroupLayout-EventTile-line-height); + } + } } .mx_EventTile:not([data-layout=bubble]) { @@ -263,8 +269,15 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss } } -.mx_GenericEventListSummary:not([data-layout=bubble]) .mx_EventTile_line { - padding-left: $left-gutter; +.mx_GenericEventListSummary:not([data-layout=bubble]) { + .mx_EventTile_line { + padding-left: $left-gutter; + line-height: normal; + + .mx_RedactedBody { + line-height: 1; // remove spacing between lines + } + } } .mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line, diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index f08fa1512e..86c0eb176e 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -18,6 +18,8 @@ limitations under the License. $left-gutter: 64px; .mx_GroupLayout { + --GroupLayout-EventTile-line-height: $font-22px; + .mx_EventTile { > .mx_DisambiguatedProfile { line-height: $font-20px; @@ -33,10 +35,14 @@ $left-gutter: 64px; position: absolute; // for modern layout } - .mx_EventTile_line, .mx_EventTile_reply { + .mx_EventTile_line, + .mx_EventTile_reply { padding-top: 1px; padding-bottom: 3px; - line-height: $font-22px; + } + + .mx_EventTile_reply { + line-height: var(--GroupLayout-EventTile-line-height); } } } @@ -47,7 +53,8 @@ $left-gutter: 64px; .mx_EventTile { padding-top: 4px; - .mx_EventTile_line, .mx_EventTile_reply { + .mx_EventTile_line, + .mx_EventTile_reply { padding-top: 0; padding-bottom: 0; } @@ -56,9 +63,12 @@ $left-gutter: 64px; // same as the padding for non-compact .mx_EventTile.mx_EventTile_info padding-top: 0px; font-size: $font-13px; - .mx_EventTile_line, .mx_EventTile_reply { + + .mx_EventTile_line, + .mx_EventTile_reply { line-height: $font-20px; } + .mx_EventTile_avatar { top: 4px; } @@ -71,10 +81,13 @@ $left-gutter: 64px; &.mx_EventTile_emote { // add a bit more space for emotes so that avatars don't collide padding-top: 8px; + .mx_EventTile_avatar { top: 2px; } - .mx_EventTile_line, .mx_EventTile_reply { + + .mx_EventTile_line, + .mx_EventTile_reply { padding-top: 0px; padding-bottom: 1px; } @@ -82,7 +95,9 @@ $left-gutter: 64px; &.mx_EventTile_emote.mx_EventTile_continuation { padding-top: 0; - .mx_EventTile_line, .mx_EventTile_reply { + + .mx_EventTile_line, + .mx_EventTile_reply { padding-top: 0px; padding-bottom: 0px; } From 50a714de4a20200485c234c2addc4c7f0d830981 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 9 May 2022 06:33:34 +0000 Subject: [PATCH 06/20] Add margin-top to the location body which is a sibling of DisambiguatedProfile (#8523) Signed-off-by: Suguru Hirahara --- res/css/views/messages/_MLocationBody.scss | 4 ++++ res/css/views/rooms/_EventTile.scss | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/res/css/views/messages/_MLocationBody.scss b/res/css/views/messages/_MLocationBody.scss index adca4ae4ba..cbbd34526d 100644 --- a/res/css/views/messages/_MLocationBody.scss +++ b/res/css/views/messages/_MLocationBody.scss @@ -38,3 +38,7 @@ limitations under the License. max-width: 100%; width: 450px; } + +.mx_DisambiguatedProfile ~ .mx_MLocationBody { + margin-top: 6px; // See: https://github.com/matrix-org/matrix-react-sdk/pull/8442 +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 133de86ddd..27b5df0a49 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -855,12 +855,6 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss padding-right: 0; } - .mx_ReplyChain { - .mx_MLocationBody { - margin-top: 6px; // See: https://github.com/matrix-org/matrix-react-sdk/pull/8442 - } - } - &:not([data-layout=bubble]) { padding-top: $spacing-16; } From cd8bdc1f549ba687a94946a776969f251de1f4b5 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 9 May 2022 06:35:17 +0000 Subject: [PATCH 07/20] Add box-shadow to the reply preview on the main (left) panel only (#8397) Remove the box-shadow from the preview on the (right) panel for threads and a chat with a maximized widget. Signed-off-by: Suguru Hirahara --- res/css/views/rooms/_ReplyPreview.scss | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss index 598371bd36..50abcc738b 100644 --- a/res/css/views/rooms/_ReplyPreview.scss +++ b/res/css/views/rooms/_ReplyPreview.scss @@ -16,12 +16,10 @@ limitations under the License. .mx_ReplyPreview { border: 1px solid $primary-hairline-color; - background: $background; border-bottom: none; - border-radius: 8px 8px 0 0; + background: $background; max-height: 50vh; overflow: auto; - box-shadow: 0px -16px 32px $composer-shadow-color; .mx_ReplyPreview_section { border-bottom: 1px solid $primary-hairline-color; @@ -53,3 +51,12 @@ limitations under the License. } } } + +.mx_RoomView_body { + .mx_ReplyPreview { + // Add box-shadow to the reply preview on the main (left) panel only. + // It is not added to the preview on the (right) panel for threads and a chat with a maximized widget. + box-shadow: 0px -16px 32px $composer-shadow-color; + border-radius: 8px 8px 0 0; + } +} From aa8e3dbfb45afc04ed19818a4a31cd13f73dc1df Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 9 May 2022 07:55:28 +0100 Subject: [PATCH 08/20] Support Inter on custom themes (#8399) * include Inter natively on legacy & custom themes * fix theming of beta button * fix beta pill skinning properly * switch back to accent-alt for beta pills Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/beta/_BetaCard.scss | 2 +- res/themes/legacy-light/css/_fonts.scss | 55 +++---------------------- 2 files changed, 6 insertions(+), 51 deletions(-) diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss index 658e43f051..d54dd4a4c8 100644 --- a/res/css/views/beta/_BetaCard.scss +++ b/res/css/views/beta/_BetaCard.scss @@ -119,7 +119,7 @@ limitations under the License. font-size: 12px; font-weight: $font-semi-bold; line-height: 15px; - color: #FFFFFF; + color: $button-primary-fg-color; display: inline-block; vertical-align: text-bottom; word-break: keep-all; // avoid multiple lines on CJK language diff --git a/res/themes/legacy-light/css/_fonts.scss b/res/themes/legacy-light/css/_fonts.scss index 1bc9b5a4a3..807d8673ba 100644 --- a/res/themes/legacy-light/css/_fonts.scss +++ b/res/themes/legacy-light/css/_fonts.scss @@ -12,6 +12,11 @@ * and it's better to rely on the browser's built-in obliquing behaviour. */ +// Grab the other fonts from the current theme, so we can override to Inter +// in custom fonts if needed. +@import "../../light/css/_fonts.scss"; + +// Nunito as the default, for old time's sake on legacy themes. /* the 'src' links are relative to the bundle.css, which is in a subdirectory. */ @font-face { @@ -32,53 +37,3 @@ font-weight: 700; src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype'); } - -/* latin-ext */ -@font-face { - font-family: 'Inconsolata'; - font-style: normal; - font-weight: 400; - src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2') format('woff2'); - unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: 'Inconsolata'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -/* latin-ext */ -@font-face { - font-family: 'Inconsolata'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2') format('woff2'); - unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: 'Inconsolata'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - -/* a COLR/CPAL version of Twemoji used for consistent cross-browser emoji - * taken from https://github.com/mozilla/twemoji-colr - * using the fix from https://github.com/mozilla/twemoji-colr/issues/50 to - * work on macOS - */ -/* -// except we now load it dynamically via FontManager to handle browsers -// which can't render COLR/CPAL still -@font-face { - font-family: "Twemoji Mozilla"; - src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2'); -} -*/ \ No newline at end of file From 7e15bef062343c3ff625e46ce1efda085a429c41 Mon Sep 17 00:00:00 2001 From: Kerry Date: Mon, 9 May 2022 10:42:05 +0200 Subject: [PATCH 09/20] Live location sharing: fix code smells - return useEffect unsub, dont map (#8535) Signed-off-by: Kerry Archibald --- src/components/views/beacon/LeftPanelLiveShareWarning.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/beacon/LeftPanelLiveShareWarning.tsx b/src/components/views/beacon/LeftPanelLiveShareWarning.tsx index b93d8de1ae..1acca35246 100644 --- a/src/components/views/beacon/LeftPanelLiveShareWarning.tsx +++ b/src/components/views/beacon/LeftPanelLiveShareWarning.tsx @@ -68,13 +68,13 @@ const useLivenessMonitor = (liveBeaconIds: BeaconIdentifier[], beacons: Map { if (document.visibilityState === 'visible') { - liveBeaconIds.map(identifier => beacons.get(identifier)?.monitorLiveness()); + liveBeaconIds.forEach(identifier => beacons.get(identifier)?.monitorLiveness()); } }; if (liveBeaconIds.length) { document.addEventListener("visibilitychange", onPageVisibilityChanged); } - () => { + return () => { document.removeEventListener("visibilitychange", onPageVisibilityChanged); }; }, [liveBeaconIds, beacons]); From 7e21be06d0693b79e8e0e8a5e98f21922e967abb Mon Sep 17 00:00:00 2001 From: Yaya Usman <38439166+yaya-usman@users.noreply.github.com> Date: Mon, 9 May 2022 15:04:44 +0300 Subject: [PATCH 10/20] Fixes suggested room not ellipsized on shrinking (#8536) * fixes suggested room not ellipsized on shrinking * style-lint check update --- res/css/views/rooms/_RoomTile.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 23fb4f1e9a..ce8fd062d3 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -38,6 +38,10 @@ limitations under the License. margin-right: 10px; } + .mx_RoomTile_details { + min-width: 0; + } + .mx_RoomTile_titleContainer { height: 32px; min-width: 0; From 674aec405032c115b33b124f97a982818c3a637f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 May 2022 13:39:32 +0100 Subject: [PATCH 11/20] Fix regression around pasting links (#8537) * Fix regression around pasting links * Add tests --- .../views/rooms/BasicMessageComposer.tsx | 23 +++--- src/editor/dom.ts | 12 +-- src/editor/operations.ts | 6 +- .../views/rooms/BasicMessageComposer-test.tsx | 65 +++++++++++++++ test/editor/mock.ts | 5 +- test/editor/operations-test.ts | 81 +++++++++++++++++-- 6 files changed, 163 insertions(+), 29 deletions(-) create mode 100644 test/components/views/rooms/BasicMessageComposer-test.tsx diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 004c20daef..667d5a42a4 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -28,7 +28,7 @@ import { formatRange, formatRangeAsLink, replaceRangeAndMoveCaret, toggleInlineF from '../../../editor/operations'; import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom'; import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete'; -import { getAutoCompleteCreator, Type } from '../../../editor/parts'; +import { getAutoCompleteCreator, Part, Type } from '../../../editor/parts'; import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize'; import { renderModel } from '../../../editor/render'; import TypingStore from "../../../stores/TypingStore"; @@ -92,7 +92,7 @@ function selectionEquals(a: Partial, b: Selection): boolean { interface IProps { model: EditorModel; room: Room; - threadId: string; + threadId?: string; placeholder?: string; label?: string; initialCaret?: DocumentOffset; @@ -333,28 +333,29 @@ export default class BasicMessageEditor extends React.Component private onPaste = (event: ClipboardEvent): boolean => { event.preventDefault(); // we always handle the paste ourselves - if (this.props.onPaste && this.props.onPaste(event, this.props.model)) { + if (this.props.onPaste?.(event, this.props.model)) { // to prevent double handling, allow props.onPaste to skip internal onPaste return true; } const { model } = this.props; const { partCreator } = model; + const plainText = event.clipboardData.getData("text/plain"); const partsText = event.clipboardData.getData("application/x-element-composer"); - let parts; + + let parts: Part[]; if (partsText) { const serializedTextParts = JSON.parse(partsText); - const deserializedParts = serializedTextParts.map(p => partCreator.deserializePart(p)); - parts = deserializedParts; + parts = serializedTextParts.map(p => partCreator.deserializePart(p)); } else { - const text = event.clipboardData.getData("text/plain"); - parts = parsePlainTextMessage(text, partCreator, { shouldEscape: false }); + parts = parsePlainTextMessage(plainText, partCreator, { shouldEscape: false }); } - const textToInsert = event.clipboardData.getData("text/plain"); + this.modifiedFlag = true; const range = getRangeForSelection(this.editorRef.current, model, document.getSelection()); - if (textToInsert && linkify.test(textToInsert)) { - formatRangeAsLink(range, textToInsert); + + if (plainText && range.length > 0 && linkify.test(plainText)) { + formatRangeAsLink(range, plainText); } else { replaceRangeAndMoveCaret(range, parts); } diff --git a/src/editor/dom.ts b/src/editor/dom.ts index 6226f74acb..0700ecb482 100644 --- a/src/editor/dom.ts +++ b/src/editor/dom.ts @@ -17,6 +17,8 @@ limitations under the License. import { CARET_NODE_CHAR, isCaretNode } from "./render"; import DocumentOffset from "./offset"; +import EditorModel from "./model"; +import Range from "./range"; type Predicate = (node: Node) => boolean; type Callback = (node: Node) => void; @@ -122,7 +124,7 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) { let foundNode = false; let text = ""; - function enterNodeCallback(node) { + function enterNodeCallback(node: HTMLElement) { if (!foundNode) { if (node === selectionNode) { foundNode = true; @@ -148,12 +150,12 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) { return true; } - function leaveNodeCallback(node) { + function leaveNodeCallback(node: HTMLElement) { // if this is not the last DIV (which are only used as line containers atm) // we don't just check if there is a nextSibling because sometimes the caret ends up // after the last DIV and it creates a newline if you type then, // whereas you just want it to be appended to the current line - if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") { + if (node.tagName === "DIV" && (node.nextSibling)?.tagName === "DIV") { text += "\n"; if (!foundNode) { offsetToNode += 1; @@ -167,7 +169,7 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) { } // get text value of text node, ignoring ZWS if it's a caret node -function getTextNodeValue(node) { +function getTextNodeValue(node: Node): string { const nodeText = node.nodeValue; // filter out ZWS for caret nodes if (isCaretNode(node.parentElement)) { @@ -184,7 +186,7 @@ function getTextNodeValue(node) { } } -export function getRangeForSelection(editor, model, selection) { +export function getRangeForSelection(editor: HTMLDivElement, model: EditorModel, selection: Selection): Range { const focusOffset = getSelectionOffsetAndText( editor, selection.focusNode, diff --git a/src/editor/operations.ts b/src/editor/operations.ts index f4dd61faa0..39cb6a5676 100644 --- a/src/editor/operations.ts +++ b/src/editor/operations.ts @@ -219,14 +219,12 @@ export function formatRangeAsCode(range: Range): void { export function formatRangeAsLink(range: Range, text?: string) { const { model } = range; const { partCreator } = model; - const linkRegex = /\[(.*?)\]\(.*?\)/g; + const linkRegex = /\[(.*?)]\(.*?\)/g; const isFormattedAsLink = linkRegex.test(range.text); if (isFormattedAsLink) { const linkDescription = range.text.replace(linkRegex, "$1"); const newParts = [partCreator.plain(linkDescription)]; - const prefixLength = 1; - const suffixLength = range.length - (linkDescription.length + 2); - replaceRangeAndAutoAdjustCaret(range, newParts, true, prefixLength, suffixLength); + replaceRangeAndMoveCaret(range, newParts, 0); } else { // We set offset to -1 here so that the caret lands between the brackets replaceRangeAndMoveCaret(range, [partCreator.plain("[" + range.text + "]" + "(" + (text ?? "") + ")")], -1); diff --git a/test/components/views/rooms/BasicMessageComposer-test.tsx b/test/components/views/rooms/BasicMessageComposer-test.tsx new file mode 100644 index 0000000000..838119492a --- /dev/null +++ b/test/components/views/rooms/BasicMessageComposer-test.tsx @@ -0,0 +1,65 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { MatrixClient, Room } from 'matrix-js-sdk/src/matrix'; + +import BasicMessageComposer from '../../../../src/components/views/rooms/BasicMessageComposer'; +import * as TestUtils from "../../../test-utils"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import EditorModel from "../../../../src/editor/model"; +import { createPartCreator, createRenderer } from "../../../editor/mock"; + +describe("BasicMessageComposer", () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + + beforeEach(() => { + TestUtils.stubClient(); + }); + + it("should allow a user to paste a URL without it being mangled", () => { + const model = new EditorModel([], pc, renderer); + + const wrapper = render(model); + + wrapper.find(".mx_BasicMessageComposer_input").simulate("paste", { + clipboardData: { + getData: type => { + if (type === "text/plain") { + return "https://element.io"; + } + }, + }, + }); + + expect(model.parts).toHaveLength(1); + expect(model.parts[0].text).toBe("https://element.io"); + }); +}); + +function render(model: EditorModel): ReactWrapper { + const client: MatrixClient = MatrixClientPeg.get(); + + const roomId = '!1234567890:domain'; + const userId = client.getUserId(); + const room = new Room(roomId, client, userId); + + return mount(( + + )); +} diff --git a/test/editor/mock.ts b/test/editor/mock.ts index bc6eafc8cc..bddddbf7cb 100644 --- a/test/editor/mock.ts +++ b/test/editor/mock.ts @@ -18,6 +18,7 @@ import { Room, MatrixClient } from "matrix-js-sdk/src/matrix"; import AutocompleteWrapperModel from "../../src/editor/autocomplete"; import { PartCreator } from "../../src/editor/parts"; +import DocumentPosition from "../../src/editor/position"; class MockAutoComplete { public _updateCallback; @@ -78,11 +79,11 @@ export function createPartCreator(completions = []) { } export function createRenderer() { - const render = (c) => { + const render = (c: DocumentPosition) => { render.caret = c; render.count += 1; }; render.count = 0; - render.caret = null; + render.caret = null as DocumentPosition; return render; } diff --git a/test/editor/operations-test.ts b/test/editor/operations-test.ts index 3e4de22417..6af732e5bd 100644 --- a/test/editor/operations-test.ts +++ b/test/editor/operations-test.ts @@ -17,21 +17,88 @@ limitations under the License. import EditorModel from "../../src/editor/model"; import { createPartCreator, createRenderer } from "./mock"; import { - toggleInlineFormat, - selectRangeOfWordAtCaret, formatRange, formatRangeAsCode, + formatRangeAsLink, + selectRangeOfWordAtCaret, + toggleInlineFormat, } from "../../src/editor/operations"; import { Formatting } from "../../src/components/views/rooms/MessageComposerFormatBar"; import { longestBacktickSequence } from '../../src/editor/deserialize'; const SERIALIZED_NEWLINE = { "text": "\n", "type": "newline" }; -describe('editor/operations: formatting operations', () => { - describe('toggleInlineFormat', () => { - it('works for words', () => { - const renderer = createRenderer(); - const pc = createPartCreator(); +describe("editor/operations: formatting operations", () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + + describe("formatRange", () => { + it.each([ + [Formatting.Bold, "hello **world**!"], + ])("should correctly wrap format %s", (formatting: Formatting, expected: string) => { + const model = new EditorModel([ + pc.plain("hello world!"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(6, false), + model.positionForOffset(11, false)); // around "world" + + expect(range.parts[0].text).toBe("world"); + expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]); + formatRange(range, formatting); + expect(model.serializeParts()).toEqual([{ "text": expected, "type": "plain" }]); + }); + + it("should apply to word range is within if length 0", () => { + const model = new EditorModel([ + pc.plain("hello world!"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(6, false)); + + expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]); + formatRange(range, Formatting.Bold); + expect(model.serializeParts()).toEqual([{ "text": "hello **world!**", "type": "plain" }]); + }); + + it("should do nothing for a range with length 0 at initialisation", () => { + const model = new EditorModel([ + pc.plain("hello world!"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(6, false)); + range.setWasEmpty(false); + + expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]); + formatRange(range, Formatting.Bold); + expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]); + }); + }); + + describe("formatRangeAsLink", () => { + it.each([ + // Caret is denoted by | in the expectation string + ["testing", "[testing](|)", ""], + ["testing", "[testing](foobar|)", "foobar"], + ["[testing]()", "testing|", ""], + ["[testing](foobar)", "testing|", ""], + ])("converts %s -> %s", (input: string, expectation: string, text: string) => { + const model = new EditorModel([ + pc.plain(`foo ${input} bar`), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(4, false), + model.positionForOffset(4 + input.length, false)); // around input + + expect(range.parts[0].text).toBe(input); + formatRangeAsLink(range, text); + expect(renderer.caret.offset).toBe(4 + expectation.indexOf("|")); + expect(model.parts[0].text).toBe("foo " + expectation.replace("|", "") + " bar"); + }); + }); + + describe("toggleInlineFormat", () => { + it("works for words", () => { const model = new EditorModel([ pc.plain("hello world!"), ], pc, renderer); From 8aa303f9b739f6b9d8211eb7d4ab5f675b759b53 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 9 May 2022 15:10:22 +0200 Subject: [PATCH 12/20] Improve welcome screen, add opt-out analytics (#8474) --- src/components/structures/HomePage.tsx | 4 ++- .../views/elements/MiniAvatarUploader.tsx | 29 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index ddba1c81d7..e84d115e43 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -75,6 +75,8 @@ const UserWelcomeTop = () => { hasAvatarLabel={_tDom("Great, that'll help people know it's you")} noAvatarLabel={_tDom("Add a photo so people know it's you.")} setAvatarUrl={url => cli.setAvatarUrl(url)} + isUserAvatar + onClick={ev => PosthogTrackers.trackInteraction("WebHomeMiniAvatarUploadButton", ev)} > = ({ justRegistered = false }) => { } let introSection; - if (justRegistered) { + if (justRegistered || !!OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) { introSection = ; } else { const brandingConfig = SdkConfig.getObject("branding"); diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index 43e66db09c..c501c92ac9 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useContext, useRef, useState } from 'react'; -import { EventType } from 'matrix-js-sdk/src/@types/event'; import classNames from 'classnames'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; +import React, { useContext, useRef, useState, MouseEvent } from 'react'; +import Analytics from "../../../Analytics"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import RoomContext from "../../../contexts/RoomContext"; +import { useTimeout } from "../../../hooks/useTimeout"; +import { TranslatedString } from '../../../languageHandler'; +import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import AccessibleButton from "./AccessibleButton"; import Spinner from "./Spinner"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { useTimeout } from "../../../hooks/useTimeout"; -import Analytics from "../../../Analytics"; -import { TranslatedString } from '../../../languageHandler'; -import RoomContext from "../../../contexts/RoomContext"; -import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; export const AVATAR_SIZE = 52; @@ -34,9 +34,13 @@ interface IProps { noAvatarLabel?: TranslatedString; hasAvatarLabel?: TranslatedString; setAvatarUrl(url: string): Promise; + isUserAvatar?: boolean; + onClick?(ev: MouseEvent): void; } -const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => { +const MiniAvatarUploader: React.FC = ({ + hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, isUserAvatar, children, onClick, +}) => { const cli = useContext(MatrixClientContext); const [busy, setBusy] = useState(false); const [hover, setHover] = useState(false); @@ -54,7 +58,7 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel; const { room } = useContext(RoomContext); - const canSetAvatar = room?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId()); + const canSetAvatar = isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getUserId()); if (!canSetAvatar) return { children }; const visible = !!label && (hover || show); @@ -63,7 +67,10 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva type="file" ref={uploadRef} className="mx_MiniAvatarUploader_input" - onClick={chromeFileInputFix} + onClick={(ev) => { + chromeFileInputFix(ev); + onClick?.(ev); + }} onChange={async (ev) => { if (!ev.target.files?.length) return; setBusy(true); From dc346d5e837bb00c6ddbda12b742e7e744e8c68c Mon Sep 17 00:00:00 2001 From: Kerry Date: Mon, 9 May 2022 15:14:45 +0200 Subject: [PATCH 13/20] test typescriptification - RoomViewStore (#8539) * test/stores/RoomViewStore-test.js -> test/stores/RoomViewStore-test.tsx Signed-off-by: Kerry Archibald * fix tsc issues Signed-off-by: Kerry Archibald --- test/stores/RoomViewStore-test.js | 77 -------------------------- test/stores/RoomViewStore-test.tsx | 86 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 77 deletions(-) delete mode 100644 test/stores/RoomViewStore-test.js create mode 100644 test/stores/RoomViewStore-test.tsx diff --git a/test/stores/RoomViewStore-test.js b/test/stores/RoomViewStore-test.js deleted file mode 100644 index d948fa6496..0000000000 --- a/test/stores/RoomViewStore-test.js +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2017 - 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { RoomViewStore } from '../../src/stores/RoomViewStore'; -import { Action } from '../../src/dispatcher/actions'; -import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg'; -import * as testUtils from '../test-utils'; - -const dispatch = testUtils.getDispatchForStore(RoomViewStore.instance); - -jest.mock('../../src/utils/DMRoomMap', () => { - const mock = { - getUserIdForRoomId: jest.fn(), - getDMRoomsForUserId: jest.fn(), - }; - - return { - shared: jest.fn().mockReturnValue(mock), - sharedInstance: mock, - }; -}); - -describe('RoomViewStore', function() { - beforeEach(function() { - testUtils.stubClient(); - peg.get().credentials = { userId: "@test:example.com" }; - peg.get().on = jest.fn(); - peg.get().off = jest.fn(); - - // Reset the state of the store - RoomViewStore.instance.reset(); - }); - - it('can be used to view a room by ID and join', function(done) { - peg.get().joinRoom = async (roomAddress) => { - expect(roomAddress).toBe("!randomcharacters:aser.ver"); - done(); - }; - - dispatch({ action: Action.ViewRoom, room_id: '!randomcharacters:aser.ver' }); - dispatch({ action: 'join_room' }); - expect(RoomViewStore.instance.isJoining()).toBe(true); - }); - - it('can be used to view a room by alias and join', function(done) { - const token = RoomViewStore.instance.addListener(() => { - // Wait until the room alias has resolved and the room ID is - if (!RoomViewStore.instance.isRoomLoading()) { - expect(RoomViewStore.instance.getRoomId()).toBe("!randomcharacters:aser.ver"); - dispatch({ action: 'join_room' }); - expect(RoomViewStore.instance.isJoining()).toBe(true); - } - }); - - peg.get().getRoomIdForAlias.mockResolvedValue({ room_id: "!randomcharacters:aser.ver" }); - peg.get().joinRoom = async (roomAddress) => { - token.remove(); // stop RVS listener - expect(roomAddress).toBe("#somealias2:aser.ver"); - done(); - }; - - dispatch({ action: Action.ViewRoom, room_alias: '#somealias2:aser.ver' }); - }); -}); diff --git a/test/stores/RoomViewStore-test.tsx b/test/stores/RoomViewStore-test.tsx new file mode 100644 index 0000000000..1513783559 --- /dev/null +++ b/test/stores/RoomViewStore-test.tsx @@ -0,0 +1,86 @@ +/* +Copyright 2017 - 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Room } from 'matrix-js-sdk/src/matrix'; + +import { RoomViewStore } from '../../src/stores/RoomViewStore'; +import { Action } from '../../src/dispatcher/actions'; +import * as testUtils from '../test-utils'; +import { flushPromises, getMockClientWithEventEmitter } from '../test-utils'; + +const dispatch = testUtils.getDispatchForStore(RoomViewStore.instance); + +jest.mock('../../src/utils/DMRoomMap', () => { + const mock = { + getUserIdForRoomId: jest.fn(), + getDMRoomsForUserId: jest.fn(), + }; + + return { + shared: jest.fn().mockReturnValue(mock), + sharedInstance: mock, + }; +}); + +describe('RoomViewStore', function() { + const userId = '@alice:server'; + const mockClient = getMockClientWithEventEmitter({ + joinRoom: jest.fn(), + getRoom: jest.fn(), + getRoomIdForAlias: jest.fn(), + }); + const room = new Room('!room:server', mockClient, userId); + + beforeEach(function() { + jest.clearAllMocks(); + mockClient.credentials = { userId: "@test:example.com" }; + mockClient.joinRoom.mockResolvedValue(room); + mockClient.getRoom.mockReturnValue(room); + + // Reset the state of the store + RoomViewStore.instance.reset(); + }); + + it('can be used to view a room by ID and join', async () => { + dispatch({ action: Action.ViewRoom, room_id: '!randomcharacters:aser.ver' }); + dispatch({ action: 'join_room' }); + await flushPromises(); + expect(mockClient.joinRoom).toHaveBeenCalledWith('!randomcharacters:aser.ver', { viaServers: [] }); + expect(RoomViewStore.instance.isJoining()).toBe(true); + }); + + it('can be used to view a room by alias and join', async () => { + const roomId = "!randomcharacters:aser.ver"; + const alias = "#somealias2:aser.ver"; + + mockClient.getRoomIdForAlias.mockResolvedValue({ room_id: roomId, servers: [] }); + + dispatch({ action: Action.ViewRoom, room_alias: alias }); + await flushPromises(); + await flushPromises(); + + // roomId is set to id of the room alias + expect(RoomViewStore.instance.getRoomId()).toBe(roomId); + + // join the room + dispatch({ action: 'join_room' }); + + expect(RoomViewStore.instance.isJoining()).toBeTruthy(); + await flushPromises(); + + expect(mockClient.joinRoom).toHaveBeenCalledWith(alias, { viaServers: [] }); + }); +}); From 7d1ecfdcbcf56d2b655fd760a7ce8dcd9d05ef21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 9 May 2022 15:23:51 +0200 Subject: [PATCH 14/20] Make date changes more obvious (#6410) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_DateSeparator.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/messages/_DateSeparator.scss b/res/css/views/messages/_DateSeparator.scss index 93a61e2de0..a1672c7279 100644 --- a/res/css/views/messages/_DateSeparator.scss +++ b/res/css/views/messages/_DateSeparator.scss @@ -27,6 +27,7 @@ limitations under the License. flex: 1 1 0; height: 0; border: none; + border-bottom: 1px solid $menu-selected-color; } .mx_DateSeparator > div { From 333dd59f0627965bef2deea282c71c8304f8af70 Mon Sep 17 00:00:00 2001 From: Kerry Date: Mon, 9 May 2022 15:24:12 +0200 Subject: [PATCH 15/20] test typescriptification - RoomList (#8540) * test/components/views/rooms/RoomList-test.js -> test/components/views/rooms/RoomList-test.tsx Signed-off-by: Kerry Archibald * fix ts issues Signed-off-by: Kerry Archibald --- .../{RoomList-test.js => RoomList-test.tsx} | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) rename test/components/views/rooms/{RoomList-test.js => RoomList-test.tsx} (86%) diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.tsx similarity index 86% rename from test/components/views/rooms/RoomList-test.js rename to test/components/views/rooms/RoomList-test.tsx index 0a96d11322..7b9d3ee311 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.tsx @@ -17,7 +17,11 @@ limitations under the License. import React from 'react'; import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; -import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk/src/matrix'; +import { + PendingEventOrdering, + Room, + RoomMember, +} from 'matrix-js-sdk/src/matrix'; import * as TestUtils from '../../../test-utils'; import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; @@ -29,6 +33,8 @@ import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayout import RoomList from "../../../../src/components/views/rooms/RoomList"; import RoomSublist from "../../../../src/components/views/rooms/RoomSublist"; import RoomTile from "../../../../src/components/views/rooms/RoomTile"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from '../../../test-utils'; +import ResizeNotifier from '../../../../src/utils/ResizeNotifier'; function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; @@ -38,7 +44,7 @@ describe('RoomList', () => { function createRoom(opts) { const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), { // The room list now uses getPendingEvents(), so we need a detached ordering. - pendingEventOrdering: "detached", + pendingEventOrdering: PendingEventOrdering.Detached, }); if (opts) { Object.assign(room, opts); @@ -47,25 +53,38 @@ describe('RoomList', () => { } let parentDiv = null; - let client = null; let root = null; const myUserId = '@me:domain'; const movingRoomId = '!someroomid'; - let movingRoom; - let otherRoom; + let movingRoom: Room | undefined; + let otherRoom: Room | undefined; - let myMember; - let myOtherMember; + let myMember: RoomMember | undefined; + let myOtherMember: RoomMember | undefined; + + const client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(myUserId), + getRooms: jest.fn(), + getVisibleRooms: jest.fn(), + getRoom: jest.fn(), + }); + + const defaultProps = { + onKeyDown: jest.fn(), + onFocus: jest.fn(), + onBlur: jest.fn(), + onResize: jest.fn(), + resizeNotifier: {} as unknown as ResizeNotifier, + isMinimized: false, + activeSpace: '', + }; beforeEach(async function(done) { RoomListStoreClass.TEST_MODE = true; + jest.clearAllMocks(); - TestUtils.stubClient(); - client = MatrixClientPeg.get(); client.credentials = { userId: myUserId }; - //revert this to prototype method as the test-utils monkey-patches this to return a hardcoded value - client.getUserId = MatrixClient.prototype.getUserId; DMRoomMap.makeShared(); @@ -74,7 +93,7 @@ describe('RoomList', () => { const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList); root = ReactDOM.render( - {}} />, + , parentDiv, ); ReactTestUtils.findRenderedComponentWithType(root, RoomList); @@ -99,7 +118,7 @@ describe('RoomList', () => { }[userId]); // Mock the matrix client - client.getRooms = () => [ + const mockRooms = [ movingRoom, otherRoom, createRoom({ tags: { 'm.favourite': { order: 0.1 } }, name: 'Some other room' }), @@ -107,14 +126,15 @@ describe('RoomList', () => { createRoom({ tags: { 'm.lowpriority': {} }, name: 'Some unimportant room' }), createRoom({ tags: { 'custom.tag': {} }, name: 'Some room customly tagged' }), ]; - client.getVisibleRooms = client.getRooms; + client.getRooms.mockReturnValue(mockRooms); + client.getVisibleRooms.mockReturnValue(mockRooms); const roomMap = {}; client.getRooms().forEach((r) => { roomMap[r.roomId] = r; }); - client.getRoom = (roomId) => roomMap[roomId]; + client.getRoom.mockImplementation((roomId) => roomMap[roomId]); // Now that everything has been set up, prepare and update the store await RoomListStore.instance.makeReady(client); @@ -171,6 +191,7 @@ describe('RoomList', () => { movingRoom.tags = { [oldTagId]: {} }; } else if (oldTagId === DefaultTagID.DM) { // Mock inverse m.direct + // @ts-ignore forcing private property DMRoomMap.shared().roomToUser = { [movingRoom.roomId]: '@someotheruser:domain', }; From 19efa093e0650b529427c835388998ad0a7cf519 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 9 May 2022 09:46:08 -0400 Subject: [PATCH 16/20] Handle Jitsi Meet crashes more gracefully (#8541) --- src/stores/VideoChannelStore.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/stores/VideoChannelStore.ts b/src/stores/VideoChannelStore.ts index 62e3571aa3..8ea50265c7 100644 --- a/src/stores/VideoChannelStore.ts +++ b/src/stores/VideoChannelStore.ts @@ -159,6 +159,10 @@ export default class VideoChannelStore extends AsyncStoreWithClient { messaging.on(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio); messaging.on(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo); messaging.on(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo); + // Empirically, it's possible for Jitsi Meet to crash instantly at startup, + // sending a hangup event that races with the rest of this method, so we also + // need to add the hangup listener now rather than later + messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); this.emit(VideoChannelEvent.StartConnect, roomId); @@ -186,6 +190,7 @@ export default class VideoChannelStore extends AsyncStoreWithClient { messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio); messaging.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo); messaging.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo); + messaging.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); this.emit(VideoChannelEvent.Disconnect, roomId); @@ -193,7 +198,6 @@ export default class VideoChannelStore extends AsyncStoreWithClient { } this.connected = true; - messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); this.matrixClient.getRoom(roomId).on(RoomEvent.MyMembership, this.onMyMembership); window.addEventListener("beforeunload", this.setDisconnected); @@ -258,6 +262,9 @@ export default class VideoChannelStore extends AsyncStoreWithClient { private onHangup = async (ev: CustomEvent) => { this.ack(ev); + // In case this hangup is caused by Jitsi Meet crashing at startup, + // wait for the connection event in order to avoid racing + if (!this.connected) await waitForEvent(this, VideoChannelEvent.Connect); await this.setDisconnected(); }; From e05a3e644659c58921635e96265b7d033c0d4d41 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 9 May 2022 16:18:15 +0200 Subject: [PATCH 17/20] fix: update matrix-analytics-events (#8543) --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 808ede86b9..0ab61a579f 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "linkifyjs": "4.0.0-beta.4", "lodash": "^4.17.20", "maplibre-gl": "^1.15.2", - "matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#4aef17b56798639906f26a8739043a3c5c5fde7e", + "matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#a0687ca6fbdb7258543d49b99fb88b9201e900b0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "^0.0.1-beta.7", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", diff --git a/yarn.lock b/yarn.lock index 85cfe26d24..be813be18b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6644,9 +6644,9 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -"matrix-analytics-events@github:matrix-org/matrix-analytics-events.git#4aef17b56798639906f26a8739043a3c5c5fde7e": +"matrix-analytics-events@github:matrix-org/matrix-analytics-events.git#a0687ca6fbdb7258543d49b99fb88b9201e900b0": version "0.0.1" - resolved "https://codeload.github.com/matrix-org/matrix-analytics-events/tar.gz/4aef17b56798639906f26a8739043a3c5c5fde7e" + resolved "https://codeload.github.com/matrix-org/matrix-analytics-events/tar.gz/a0687ca6fbdb7258543d49b99fb88b9201e900b0" matrix-encrypt-attachment@^1.0.3: version "1.0.3" From 617c0e3c50c2d58ffedb32dd047a9d6345fbf861 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 May 2022 17:37:21 +0100 Subject: [PATCH 18/20] Add cypress badge (#8544) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1312e56a5b..7966c9ea44 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![npm](https://img.shields.io/npm/v/matrix-react-sdk)](https://www.npmjs.com/package/matrix-react-sdk) ![Tests](https://github.com/matrix-org/matrix-react-sdk/actions/workflows/tests.yml/badge.svg) ![Static Analysis](https://github.com/matrix-org/matrix-react-sdk/actions/workflows/static_analysis.yaml/badge.svg) +[![matrix-react-sdk](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ppvnzg/develop&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/ppvnzg/runs) [![Weblate](https://translate.element.io/widgets/element-web/-/matrix-react-sdk/svg-badge.svg)](https://translate.element.io/engage/element-web/) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=coverage)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk) From 89d7760f3643b70b037c8146e8d9b18d8a31ef4f Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 9 May 2022 18:02:03 -0400 Subject: [PATCH 19/20] Don't leave button tooltips open when closing modals (#8546) --- .../views/elements/AccessibleTooltipButton.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 1e0abe1fe9..b52212b29d 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { SyntheticEvent } from 'react'; +import React, { SyntheticEvent, FocusEvent } from 'react'; import AccessibleButton from "./AccessibleButton"; import Tooltip, { Alignment } from './Tooltip'; @@ -68,6 +68,12 @@ export default class AccessibleTooltipButton extends React.PureComponent { + // We only show the tooltip if focus arrived here from some other + // element, to avoid leaving tooltips hanging around when a modal closes + if (ev.relatedTarget) this.showTooltip(); + }; + render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, onHideTooltip, @@ -84,7 +90,7 @@ export default class AccessibleTooltipButton extends React.PureComponent From 548290b0069d59cdd44b39829c3c21eb22015ec7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 9 May 2022 16:52:05 -0600 Subject: [PATCH 20/20] Run a minor code quality checker over the repo (#8524) * Run a minor code quality checker over the repo Largely targeted at spelling of common words and misc code issues. * Update snapshots --- src/BasePlatform.ts | 2 +- src/CallHandler.tsx | 2 +- src/HtmlUtils.tsx | 2 +- src/ImageUtils.ts | 2 +- src/Rooms.ts | 2 +- .../views/dialogs/security/CreateKeyBackupDialog.tsx | 2 +- .../views/dialogs/security/CreateSecretStorageDialog.tsx | 2 +- src/components/views/auth/CountryDropdown.tsx | 2 +- src/components/views/beacon/LeftPanelLiveShareWarning.tsx | 2 +- src/components/views/beacon/RoomLiveShareWarning.tsx | 2 +- src/components/views/dialogs/BulkRedactDialog.tsx | 2 +- src/components/views/dialogs/ExportDialog.tsx | 4 ++-- .../views/dialogs/KeySignatureUploadFailedDialog.tsx | 2 +- src/components/views/dialogs/ShareDialog.tsx | 2 +- .../views/dialogs/security/RestoreKeyBackupDialog.tsx | 2 +- src/components/views/elements/AccessibleButton.tsx | 2 +- src/components/views/elements/AppTile.tsx | 6 +++--- src/components/views/elements/ErrorBoundary.tsx | 2 +- src/components/views/elements/LanguageDropdown.tsx | 2 +- src/components/views/elements/ReplyChain.tsx | 2 +- .../views/elements/SpellCheckLanguagesDropdown.tsx | 2 +- src/components/views/elements/TruncatedList.tsx | 2 +- src/components/views/messages/MImageBody.tsx | 8 ++++---- src/components/views/rooms/AppsDrawer.tsx | 8 ++++---- src/components/views/rooms/AuxPanel.tsx | 2 +- src/components/views/rooms/EventTile.tsx | 2 +- src/components/views/toasts/VerificationRequestToast.tsx | 2 +- src/dispatcher/payloads/ComposerInsertPayload.ts | 2 +- src/editor/dom.ts | 2 +- src/editor/operations.ts | 2 +- src/i18n/strings/en_EN.json | 4 ++-- src/indexing/BaseEventIndexManager.ts | 2 +- src/indexing/EventIndex.ts | 6 +++--- src/indexing/EventIndexPeg.ts | 4 ++-- src/integrations/IntegrationManagerInstance.ts | 2 +- src/notifications/VectorPushRulesDefinitions.ts | 4 ++-- src/settings/watchers/ThemeWatcher.ts | 2 +- src/stores/OwnBeaconStore.ts | 4 ++-- src/stores/RoomViewStore.tsx | 2 +- src/stores/WidgetEchoStore.ts | 4 ++-- src/stores/spaces/flattenSpaceHierarchy.ts | 2 +- src/stores/widgets/WidgetMessagingStore.ts | 2 +- src/utils/DMRoomMap.ts | 2 +- src/utils/ErrorUtils.tsx | 2 +- src/utils/FileUtils.ts | 2 +- src/utils/LazyValue.ts | 2 +- src/utils/Singleflight.ts | 2 +- src/utils/beacon/useBeacon.ts | 2 +- src/utils/direct-messages.ts | 2 +- src/utils/exportUtils/exportCustomCSS.css | 2 +- src/utils/permalinks/ElementPermalinkConstructor.ts | 2 +- src/utils/permalinks/Permalinks.ts | 2 +- .../views/beacon/LeftPanelLiveShareWarning-test.tsx | 2 +- .../components/views/beacon/RoomLiveShareWarning-test.tsx | 2 +- .../__snapshots__/LeftPanelLiveShareWarning-test.tsx.snap | 2 +- .../__snapshots__/RoomLiveShareWarning-test.tsx.snap | 2 +- 56 files changed, 72 insertions(+), 72 deletions(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index b7f52d3895..95f3459763 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -237,7 +237,7 @@ export default abstract class BasePlatform { } /** - * Restarts the application, without neccessarily reloading + * Restarts the application, without necessarily reloading * any application code */ abstract reload(); diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 2b934251b7..787f602fb7 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -948,7 +948,7 @@ export default class CallHandler extends EventEmitter { ): Promise { if (consultFirst) { // if we're consulting, we just start by placing a call to the transfer - // target (passing the transferee so the actual tranfer can happen later) + // target (passing the transferee so the actual transfer can happen later) this.dialNumber(destination, call); return; } diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 7f92653c30..6b9724caab 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -187,7 +187,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to delete attribs.target; } } else { - // Delete the href attrib if it is falsey + // Delete the href attrib if it is falsy delete attribs.href; } diff --git a/src/ImageUtils.ts b/src/ImageUtils.ts index 9bfab37193..acf8daa607 100644 --- a/src/ImageUtils.ts +++ b/src/ImageUtils.ts @@ -25,7 +25,7 @@ limitations under the License. * reflect the actual height the scaled thumbnail occupies. * * This is very useful for calculating how much height a thumbnail will actually - * consume in the timeline, when performing scroll offset calcuations + * consume in the timeline, when performing scroll offset calculations * (e.g. scroll locking) */ export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) { diff --git a/src/Rooms.ts b/src/Rooms.ts index 32419080ac..b7b2f64a77 100644 --- a/src/Rooms.ts +++ b/src/Rooms.ts @@ -52,7 +52,7 @@ export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolea // Used to split rooms via tags const tagNames = Object.keys(room.tags); // Used for 1:1 direct chats - // Show 1:1 chats in seperate "Direct Messages" section as long as they haven't + // Show 1:1 chats in separate "Direct Messages" section as long as they haven't // been moved to a different tag section const totalMemberCount = room.currentState.getJoinedMemberCount() + room.currentState.getInvitedMemberCount(); diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index 560eaadab1..dea6c87941 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -291,7 +291,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { }); // default value here too, otherwise we need to handle null / undefined - // values between mounting and the initial value propgating + // values between mounting and the initial value propagating const value = this.props.value || this.state.defaultCountry.iso2; return { if (hasLocationPublishError) { - return _t('An error occured whilst sharing your live location, please try again'); + return _t('An error occurred whilst sharing your live location, please try again'); } if (hasStopSharingError) { return _t('An error occurred while stopping your live location, please try again'); diff --git a/src/components/views/dialogs/BulkRedactDialog.tsx b/src/components/views/dialogs/BulkRedactDialog.tsx index 86c0f4033f..3d8e6967f1 100644 --- a/src/components/views/dialogs/BulkRedactDialog.tsx +++ b/src/components/views/dialogs/BulkRedactDialog.tsx @@ -25,7 +25,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t } from '../../../languageHandler'; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; -import { IDialogProps } from "../dialogs/IDialogProps"; +import { IDialogProps } from "./IDialogProps"; import BaseDialog from "../dialogs/BaseDialog"; import InfoDialog from "../dialogs/InfoDialog"; import DialogButtons from "../elements/DialogButtons"; diff --git a/src/components/views/dialogs/ExportDialog.tsx b/src/components/views/dialogs/ExportDialog.tsx index 5c1bf48cac..1a8d1020cd 100644 --- a/src/components/views/dialogs/ExportDialog.tsx +++ b/src/components/views/dialogs/ExportDialog.tsx @@ -263,7 +263,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { else onFinished(false); }; - const confirmCanel = async () => { + const confirmCancel = async () => { await exporter?.cancelExport(); setExportCancelled(true); setExporting(false); @@ -346,7 +346,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { hasCancel={true} cancelButton={_t("Continue")} onCancel={() => setCancelWarning(false)} - onPrimaryButtonClick={confirmCanel} + onPrimaryButtonClick={confirmCancel} /> ); diff --git a/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx b/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx index 381c96dc66..72ce6bd3ba 100644 --- a/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx +++ b/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx @@ -29,7 +29,7 @@ interface IProps extends IDialogProps { error: string; }>>; source: string; - continuation: () => void; + continuation: () => Promise; } const KeySignatureUploadFailedDialog: React.FC = ({ diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index cb06d09d80..f07964650f 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -52,7 +52,7 @@ const socials = [ }, { name: 'Reddit', img: require("../../../../res/img/social/reddit.png"), - url: (url) => `http://www.reddit.com/submit?url=${url}`, + url: (url) => `https://www.reddit.com/submit?url=${url}`, }, { name: 'email', img: require("../../../../res/img/social/email-1.png"), diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx index 719c31c48f..0fedcf3d93 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx @@ -44,7 +44,7 @@ enum ProgressState { } interface IProps extends IDialogProps { - // if false, will close the dialog as soon as the restore completes succesfully + // if false, will close the dialog as soon as the restore completes successfully // default: true showSummary?: boolean; // If specified, gather the key from the user but then call the function with the backup diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 3db6d0dfb0..36600cbfb3 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -96,7 +96,7 @@ export default function AccessibleButton({ // that might receive focus as a result of the AccessibleButtonClick action // It's because we are using html buttons at a few places e.g. inside dialogs // And divs which we report as role button to assistive technologies. - // Browsers handle space and enter keypresses differently and we are only adjusting to the + // Browsers handle space and enter key presses differently and we are only adjusting to the // inconsistencies here newProps.onKeyDown = (e) => { const action = getKeyBindingsManager().getAccessibilityAction(e); diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 1eba26a3d4..f202a1e570 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -57,7 +57,7 @@ interface IProps { // which bypasses permission prompts as it was added explicitly by that user room?: Room; threadId?: string | null; - // Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer. + // Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer container. // This should be set to true when there is only one widget in the app drawer, otherwise it should be false. fullWidth?: boolean; // Optional. If set, renders a smaller view of the widget @@ -288,7 +288,7 @@ export default class AppTile extends React.Component { private setupSgListeners() { this.sgWidget.on("preparing", this.onWidgetPreparing); this.sgWidget.on("ready", this.onWidgetReady); - // emits when the capabilites have been setup or changed + // emits when the capabilities have been set up or changed this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified); } @@ -543,7 +543,7 @@ export default class AppTile extends React.Component { const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox " + "allow-same-origin allow-scripts allow-presentation allow-downloads"; - // Additional iframe feature pemissions + // Additional iframe feature permissions // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/) const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write;"; diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx index 91e56b82be..4d281de12c 100644 --- a/src/components/views/elements/ErrorBoundary.tsx +++ b/src/components/views/elements/ErrorBoundary.tsx @@ -53,7 +53,7 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> { // in their own `console.error` invocation. logger.error(error); logger.error( - "The above error occured while React was rendering the following components:", + "The above error occurred while React was rendering the following components:", componentStack, ); } diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx index 7d19dbfce1..cf1dfedcce 100644 --- a/src/components/views/elements/LanguageDropdown.tsx +++ b/src/components/views/elements/LanguageDropdown.tsx @@ -99,7 +99,7 @@ export default class LanguageDropdown extends React.Component { }); // default value here too, otherwise we need to handle null / undefined - // values between mounting and the initial value propgating + // values between mounting and the initial value propagating let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); let value = null; if (language) { diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 0272980ef0..dd7cf76855 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -167,7 +167,7 @@ export default class ReplyChain extends React.Component { await this.matrixClient.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId); } catch (e) { // if it fails catch the error and return early, there's no point trying to find the event in this case. - // Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved). + // Return null as it is falsy and thus should be treated as an error (as the event cannot be resolved). return null; } return this.room.findEventById(eventId); diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index 126898a4ff..a269ac0010 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -99,7 +99,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component { return this.props.getChildren(start, end); } else { // XXX: I'm not sure why anything would pass null into this, it seems - // like a bizzare case to handle, but I'm preserving the behaviour. + // like a bizarre case to handle, but I'm preserving the behaviour. // (see commit 38d5c7d5c5d5a34dc16ef5d46278315f5c57f542) return React.Children.toArray(this.props.children).filter((c) => { return c != null; diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 73c59472fd..118de8e8fe 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -485,14 +485,14 @@ export default class MImageBody extends React.Component { return this.wrapImage(contentUrl, thumbnail); } - // Overidden by MStickerBody + // Overridden by MStickerBody protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element { return { children } ; } - // Overidden by MStickerBody + // Overridden by MStickerBody protected getPlaceholder(width: number, height: number): JSX.Element { const blurhash = this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]; @@ -506,12 +506,12 @@ export default class MImageBody extends React.Component { return ; } - // Overidden by MStickerBody + // Overridden by MStickerBody protected getTooltip(): JSX.Element { return null; } - // Overidden by MStickerBody + // Overridden by MStickerBody protected getFileBody(): string | JSX.Element { if (this.props.forExport) return null; /* diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx index 22afea2490..601cc9ee34 100644 --- a/src/components/views/rooms/AppsDrawer.tsx +++ b/src/components/views/rooms/AppsDrawer.tsx @@ -49,7 +49,7 @@ interface IState { // @ts-ignore - TS wants a string key, but we know better apps: {[id: Container]: IApp[]}; resizingVertical: boolean; // true when changing the height of the apps drawer - resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets + resizingHorizontal: boolean; // true when changing the distribution of the width between widgets resizing: boolean; } @@ -259,7 +259,7 @@ export default class AppsDrawer extends React.Component { mx_AppsDrawer_2apps: apps.length === 2, mx_AppsDrawer_3apps: apps.length === 3, }); - const appConatiners = + const appContainers =
{ apps.map((app, i) => { if (i < 1) return app; @@ -272,7 +272,7 @@ export default class AppsDrawer extends React.Component { let drawer; if (widgetIsMaxmised) { - drawer = appConatiners; + drawer = appContainers; } else { drawer = { handleWrapperClass="mx_AppsContainer_resizerHandleContainer" className="mx_AppsContainer_resizer" resizeNotifier={this.props.resizeNotifier}> - { appConatiners } + { appContainers } ; } diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index a7f9a12229..c1325658f9 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -104,7 +104,7 @@ export default class AuxPanel extends React.Component { const severity = ev.getContent().severity || "normal"; const stateKey = ev.getStateKey(); - // We want a non-empty title but can accept falsey values (e.g. + // We want a non-empty title but can accept falsy values (e.g. // zero) if (title && value !== undefined) { counters.push({ diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index e54c0900a5..160bb9fff5 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -212,7 +212,7 @@ interface IProps { // whether or not to display thread info showThreadInfo?: boolean; - // if specified and `true`, the message his behing + // if specified and `true`, the message is being // hidden for moderation from other users but is // displayed to the current user either because they're // the author or they are a moderator diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index b424f43900..3786f5c858 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -66,7 +66,7 @@ export default class VerificationRequestToast extends React.PureComponent { // To avoid visual glitching of two modals stacking briefly, we customise the // terms dialog sizing when it will appear for the integration manager so that - // it gets the same basic size as the IM's own modal. + // it gets the same basic size as the integration manager's own modal. return dialogTermsInteractionCallback( policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager', ); diff --git a/src/notifications/VectorPushRulesDefinitions.ts b/src/notifications/VectorPushRulesDefinitions.ts index 85cefd5194..1d6aff8a13 100644 --- a/src/notifications/VectorPushRulesDefinitions.ts +++ b/src/notifications/VectorPushRulesDefinitions.ts @@ -132,7 +132,7 @@ export const VectorPushRulesDefinitions = { }), // Messages just sent to a group chat room - // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined + // 1:1 room messages are caught by the .m.rule.room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. ".m.rule.message": new VectorPushRuleDefinition({ description: _td("Messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js @@ -144,7 +144,7 @@ export const VectorPushRulesDefinitions = { }), // Encrypted messages just sent to a group chat room - // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined + // Encrypted 1:1 room messages are caught by the .m.rule.encrypted_room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. ".m.rule.encrypted": new VectorPushRuleDefinition({ description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js diff --git a/src/settings/watchers/ThemeWatcher.ts b/src/settings/watchers/ThemeWatcher.ts index a8994d6f74..041435c7cd 100644 --- a/src/settings/watchers/ThemeWatcher.ts +++ b/src/settings/watchers/ThemeWatcher.ts @@ -100,7 +100,7 @@ export default class ThemeWatcher { // itself completely redundant since we just override the result here and we're // now effectively just using the ThemeController as a place to store the static // variable. The system theme setting probably ought to have an equivalent - // controller that honours the same flag, although probablt better would be to + // controller that honours the same flag, although probably better would be to // have the theme logic in one place rather than split between however many // different places. if (ThemeController.isLogin) return 'light'; diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index 885c15b69d..1dbf8668fc 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -43,7 +43,7 @@ import { TimedGeoUri, watchPosition, } from "../utils/beacon"; -import { getCurrentPosition } from "../utils/beacon/geolocation"; +import { getCurrentPosition } from "../utils/beacon"; const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconInfoOwner === userId; @@ -456,7 +456,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { private onWatchedPosition = (position: GeolocationPosition) => { const timedGeoPosition = mapGeolocationPositionToTimedGeo(position); - // if this is our first position, publish immediateley + // if this is our first position, publish immediately if (!this.lastPublishedPositionTimestamp) { this.publishLocationToBeacons(timedGeoPosition); } else { diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 77909ae612..29e974498d 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -504,7 +504,7 @@ export class RoomViewStore extends Store { // since we should still consider a join to be in progress until the room // & member events come down the sync. // - // This flag remains true after the room has been sucessfully joined, + // This flag remains true after the room has been successfully joined, // (this store doesn't listen for the appropriate member events) // so you should always observe the joined state from the member event // if a room object is present. diff --git a/src/stores/WidgetEchoStore.ts b/src/stores/WidgetEchoStore.ts index 34b9b4aa47..2923d46b09 100644 --- a/src/stores/WidgetEchoStore.ts +++ b/src/stores/WidgetEchoStore.ts @@ -44,9 +44,9 @@ class WidgetEchoStore extends EventEmitter { } /** - * Gets the widgets for a room, substracting those that are pending deletion. + * Gets the widgets for a room, subtracting those that are pending deletion. * Widgets that are pending addition are not included, since widgets are - * represted as MatrixEvents, so to do this we'd have to create fake MatrixEvents, + * represented as MatrixEvents, so to do this we'd have to create fake MatrixEvents, * and we don't really need the actual widget events anyway since we just want to * show a spinner / prevent widgets being added twice. * diff --git a/src/stores/spaces/flattenSpaceHierarchy.ts b/src/stores/spaces/flattenSpaceHierarchy.ts index 9d94cd4a8d..138947c395 100644 --- a/src/stores/spaces/flattenSpaceHierarchy.ts +++ b/src/stores/spaces/flattenSpaceHierarchy.ts @@ -38,7 +38,7 @@ const traverseSpaceDescendants = ( }; /** - * Helper function to traverse space heirachy and flatten + * Helper function to traverse space hierarchy and flatten * @param spaceEntityMap ie map of rooms or dm userIds * @param spaceDescendantMap map of spaces and their children * @returns set of all rooms diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts index d954af6d60..bcc46c0e43 100644 --- a/src/stores/widgets/WidgetMessagingStore.ts +++ b/src/stores/widgets/WidgetMessagingStore.ts @@ -91,7 +91,7 @@ export class WidgetMessagingStore extends AsyncStoreWithClient { /** * Gets the widget messaging class for a given widget UID. * @param {string} widgetUid The widget UID. - * @returns {ClientWidgetApi} The widget API, or a falsey value if not found. + * @returns {ClientWidgetApi} The widget API, or a falsy value if not found. */ public getMessagingForUid(widgetUid: string): ClientWidgetApi { return this.widgetMap.get(widgetUid); diff --git a/src/utils/DMRoomMap.ts b/src/utils/DMRoomMap.ts index ee4837cfb6..811522a667 100644 --- a/src/utils/DMRoomMap.ts +++ b/src/utils/DMRoomMap.ts @@ -142,7 +142,7 @@ export default class DMRoomMap { /** * Gets the DM room which the given IDs share, if any. * @param {string[]} ids The identifiers (user IDs and email addresses) to look for. - * @returns {Room} The DM room which all IDs given share, or falsey if no common room. + * @returns {Room} The DM room which all IDs given share, or falsy if no common room. */ public getDMRoomForIdentifiers(ids: string[]): Room { // TODO: [Canonical DMs] Handle lookups for email addresses. diff --git a/src/utils/ErrorUtils.tsx b/src/utils/ErrorUtils.tsx index 52c9c470f8..6af61aca2e 100644 --- a/src/utils/ErrorUtils.tsx +++ b/src/utils/ErrorUtils.tsx @@ -25,7 +25,7 @@ import { _t, _td, Tags, TranslatedString } from '../languageHandler'; * * @param {string} limitType The limit_type from the error * @param {string} adminContact The admin_contact from the error - * @param {Object} strings Translateable string for different + * @param {Object} strings Translatable string for different * limit_type. Must include at least the empty string key * which is the default. Strings may include an 'a' tag * for the admin contact link. diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 21d2928073..e934389152 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -64,7 +64,7 @@ export function presentableTextForFile( // big a file they are downloading. // The content.info also contains a MIME-type but we don't display // it since it is "ugly", users generally aren't aware what it - // means and the type of the attachment can usually be inferrered + // means and the type of the attachment can usually be inferred // from the file extension. text += ' (' + filesize(content.info.size) + ')'; } diff --git a/src/utils/LazyValue.ts b/src/utils/LazyValue.ts index 9cdcda489a..70ffb1106c 100644 --- a/src/utils/LazyValue.ts +++ b/src/utils/LazyValue.ts @@ -29,7 +29,7 @@ export class LazyValue { * Whether or not a cached value is present. */ public get present(): boolean { - // we use a tracking variable just in case the final value is falsey + // we use a tracking variable just in case the final value is falsy return this.done; } diff --git a/src/utils/Singleflight.ts b/src/utils/Singleflight.ts index 07d82efa3e..93822594a2 100644 --- a/src/utils/Singleflight.ts +++ b/src/utils/Singleflight.ts @@ -34,7 +34,7 @@ const keyMap = new EnhancedMap>(); * second call comes through late. There are various functions named "forget" * to have the cache be cleared of a result. * - * Singleflights in our usecase are tied to an instance of something, combined + * Singleflights in our use case are tied to an instance of something, combined * with a string key to differentiate between multiple possible actions. This * means that a "save" key will be scoped to the instance which defined it and * not leak between other instances. This is done to avoid having to concatenate diff --git a/src/utils/beacon/useBeacon.ts b/src/utils/beacon/useBeacon.ts index 91b00104a1..e1dcfc4975 100644 --- a/src/utils/beacon/useBeacon.ts +++ b/src/utils/beacon/useBeacon.ts @@ -54,7 +54,7 @@ export const useBeacon = (beaconInfoEvent: MatrixEvent): Beacon | undefined => { } }, [beaconInfoEvent, matrixClient]); - // beacon update will fire when this beacon is superceded + // beacon update will fire when this beacon is superseded // check the updated event id for equality to the matrix event const beaconInstanceEventId = useEventEmitterState( beacon, diff --git a/src/utils/direct-messages.ts b/src/utils/direct-messages.ts index 0932cc9aaf..e67c01c7ca 100644 --- a/src/utils/direct-messages.ts +++ b/src/utils/direct-messages.ts @@ -175,7 +175,7 @@ export class ThreepidMember extends Member { this.id = id; } - // This is a getter that would be falsey on all other implementations. Until we have + // This is a getter that would be falsy on all other implementations. Until we have // better type support in the react-sdk we can use this trick to determine the kind // of 3PID we're dealing with, if any. get isEmail(): boolean { diff --git a/src/utils/exportUtils/exportCustomCSS.css b/src/utils/exportUtils/exportCustomCSS.css index 0a0a2c2005..a62f890649 100644 --- a/src/utils/exportUtils/exportCustomCSS.css +++ b/src/utils/exportUtils/exportCustomCSS.css @@ -120,7 +120,7 @@ a.mx_reply_anchor:hover { } .mx_ReplyChain_Export { - margin-top: 0px; + margin-top: 0; margin-bottom: 5px; } diff --git a/src/utils/permalinks/ElementPermalinkConstructor.ts b/src/utils/permalinks/ElementPermalinkConstructor.ts index b901581ca6..01525081a6 100644 --- a/src/utils/permalinks/ElementPermalinkConstructor.ts +++ b/src/utils/permalinks/ElementPermalinkConstructor.ts @@ -80,7 +80,7 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor { } /** - * Parses an app route (`(user|room)/identifer`) to a Matrix entity + * Parses an app route (`(user|room)/identifier`) to a Matrix entity * (room, user). * @param {string} route The app route * @returns {PermalinkParts} diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index 4ab355fe78..d4d6627051 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -274,7 +274,7 @@ export function makeUserPermalink(userId: string): string { export function makeRoomPermalink(roomId: string): string { if (!roomId) { - throw new Error("can't permalink a falsey roomId"); + throw new Error("can't permalink a falsy roomId"); } // If the roomId isn't actually a room ID, don't try to list the servers. diff --git a/test/components/views/beacon/LeftPanelLiveShareWarning-test.tsx b/test/components/views/beacon/LeftPanelLiveShareWarning-test.tsx index a05fbf348f..8a10f73575 100644 --- a/test/components/views/beacon/LeftPanelLiveShareWarning-test.tsx +++ b/test/components/views/beacon/LeftPanelLiveShareWarning-test.tsx @@ -168,7 +168,7 @@ describe('', () => { const component = getComponent(); // error mode expect(component.find('.mx_LeftPanelLiveShareWarning').at(0).text()).toEqual( - 'An error occured whilst sharing your live location', + 'An error occurred whilst sharing your live location', ); act(() => { diff --git a/test/components/views/beacon/RoomLiveShareWarning-test.tsx b/test/components/views/beacon/RoomLiveShareWarning-test.tsx index 13ae42fce4..2a6956c92b 100644 --- a/test/components/views/beacon/RoomLiveShareWarning-test.tsx +++ b/test/components/views/beacon/RoomLiveShareWarning-test.tsx @@ -359,7 +359,7 @@ describe('', () => { // renders wire error ui expect(component.find('.mx_RoomLiveShareWarning_label').text()).toEqual( - 'An error occured whilst sharing your live location, please try again', + 'An error occurred whilst sharing your live location, please try again', ); expect(findByTestId(component, 'room-live-share-wire-error-close-button').length).toBeTruthy(); }); diff --git a/test/components/views/beacon/__snapshots__/LeftPanelLiveShareWarning-test.tsx.snap b/test/components/views/beacon/__snapshots__/LeftPanelLiveShareWarning-test.tsx.snap index d1d6dd56c0..0dd32a9387 100644 --- a/test/components/views/beacon/__snapshots__/LeftPanelLiveShareWarning-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/LeftPanelLiveShareWarning-test.tsx.snap @@ -69,7 +69,7 @@ exports[` when user has live location monitor rende role="button" tabIndex={0} > - An error occured whilst sharing your live location + An error occurred whilst sharing your live location
diff --git a/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap b/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap index c9695ddf58..8701c83c91 100644 --- a/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap @@ -32,7 +32,7 @@ exports[` when user has live beacons and geolocation is - An error occured whilst sharing your live location, please try again + An error occurred whilst sharing your live location, please try again