Merge branch 'develop' into 19245-improve-styling-of-search-initialization-errors

This commit is contained in:
Travis Ralston 2022-05-09 19:32:43 -06:00
commit 401e124df6
90 changed files with 562 additions and 398 deletions

View file

@ -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)

View file

@ -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",

View file

@ -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;

View file

@ -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;
}

View file

@ -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

View file

@ -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;

View file

@ -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%;
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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;

View file

@ -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,
@ -842,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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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 {

View file

@ -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;
}

View file

@ -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');
}
*/

View file

@ -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();

View file

@ -948,7 +948,7 @@ export default class CallHandler extends EventEmitter {
): Promise<void> {
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;
}

View file

@ -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;
}

View file

@ -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) {

View file

@ -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();

View file

@ -291,7 +291,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
changeText = _t("Use a different passphrase?");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left element-web unattended
// Security conscious readers will note that if you left element-web unattended
// on this screen, this would make it easy for a malicious person to guess
// your passphrase one letter at a time, but they could get this faster by
// just opening the browser's developer tools and reading it.

View file

@ -649,7 +649,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
changeText = _t("Use a different passphrase?");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left element-web unattended
// Security conscious readers will note that if you left element-web unattended
// on this screen, this would make it easy for a malicious person to guess
// your passphrase one letter at a time, but they could get this faster by
// just opening the browser's developer tools and reading it.

View file

@ -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)}
>
<BaseAvatar
idName={userId}
@ -100,7 +102,7 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
}
let introSection;
if (justRegistered) {
if (justRegistered || !!OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) {
introSection = <UserWelcomeTop />;
} else {
const brandingConfig = SdkConfig.getObject("branding");

View file

@ -135,7 +135,7 @@ export default class CountryDropdown extends React.Component<IProps, IState> {
});
// 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 <Dropdown

View file

@ -56,7 +56,7 @@ const getLabel = (hasStoppingErrors: boolean, hasLocationErrors: boolean): strin
return _t('An error occurred while stopping your live location');
}
if (hasLocationErrors) {
return _t('An error occured whilst sharing your live location');
return _t('An error occurred whilst sharing your live location');
}
return _t('You are sharing your live location');
};
@ -68,13 +68,13 @@ const useLivenessMonitor = (liveBeaconIds: BeaconIdentifier[], beacons: Map<Beac
// refresh beacon monitors when the tab becomes active again
const onPageVisibilityChanged = () => {
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]);

View file

@ -29,7 +29,7 @@ import LiveTimeRemaining from './LiveTimeRemaining';
const getLabel = (hasLocationPublishError: boolean, hasStopSharingError: boolean): string => {
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');

View file

@ -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<IProps, IState>
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<IProps, IState>
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<IProps, IState>
});
}
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<IProps, IState>
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<IProps, IState>
if (permalink) {
permalinkButton = (
<IconizedContextMenuOption
iconClassName={showPermalink
? "mx_MessageContextMenu_iconCopy"
: "mx_MessageContextMenu_iconPermalink"
}
onClick={showPermalink ? this.onCopyPermalinkClick : this.onPermalinkClick}
label={showPermalink ? _t('Copy link') : _t('Share')}
iconClassName="mx_MessageContextMenu_iconPermalink"
onClick={this.onShareClick}
label={_t('Share')}
element="a"
{
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
...{
href: permalink,
target: "_blank",
rel: "noreferrer noopener",
@ -508,6 +499,26 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
let copyLinkButton: JSX.Element;
if (link) {
copyLinkButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconCopy"
onClick={this.onCopyLinkClick}
label={_t('Copy link')}
element="a"
{
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
...{
href: link,
target: "_blank",
rel: "noreferrer noopener",
}
}
/>
);
}
let copyButton: JSX.Element;
if (rightClick && getSelectedText()) {
copyButton = (
@ -566,10 +577,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
}
let nativeItemsList: JSX.Element;
if (copyButton) {
if (copyButton || copyLinkButton) {
nativeItemsList = (
<IconizedContextMenuOptionList>
{ copyButton }
{ copyLinkButton }
</IconizedContextMenuOptionList>
);
}

View file

@ -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";

View file

@ -263,7 +263,7 @@ const ExportDialog: React.FC<IProps> = ({ 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<IProps> = ({ room, onFinished }) => {
hasCancel={true}
cancelButton={_t("Continue")}
onCancel={() => setCancelWarning(false)}
onPrimaryButtonClick={confirmCanel}
onPrimaryButtonClick={confirmCancel}
/>
</BaseDialog>
);

View file

@ -29,7 +29,7 @@ interface IProps extends IDialogProps {
error: string;
}>>;
source: string;
continuation: () => void;
continuation: () => Promise<void>;
}
const KeySignatureUploadFailedDialog: React.FC<IProps> = ({

View file

@ -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"),

View file

@ -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

View file

@ -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);

View file

@ -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<IProps,
this.props.onHideTooltip?.(ev);
};
private onFocus = (ev: FocusEvent) => {
// 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<IProps,
{...props}
onMouseOver={this.showTooltip}
onMouseLeave={this.hideTooltip}
onFocus={this.showTooltip}
onFocus={this.onFocus}
onBlur={this.hideTooltip}
aria-label={title}
>

View file

@ -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<IProps, IState> {
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<IProps, IState> {
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;";

View file

@ -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,
);
}

View file

@ -99,7 +99,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
});
// 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) {

View file

@ -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<unknown>;
isUserAvatar?: boolean;
onClick?(ev: MouseEvent<HTMLInputElement>): void;
}
const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => {
const MiniAvatarUploader: React.FC<IProps> = ({
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<IProps> = ({ 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 <React.Fragment>{ children }</React.Fragment>;
const visible = !!label && (hover || show);
@ -63,7 +67,10 @@ const MiniAvatarUploader: React.FC<IProps> = ({ 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);

View file

@ -167,7 +167,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
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);

View file

@ -99,7 +99,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
});
// 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) {

View file

@ -52,7 +52,7 @@ export default class TruncatedList extends React.Component<IProps> {
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;

View file

@ -58,7 +58,6 @@ export const LocationButton: React.FC<IProps> = ({ 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<IProps> = ({ roomId, sender, menuPosition,
return <React.Fragment>
<CollapsibleButton
className={className}
iconClassName="mx_MessageComposer_location"
onClick={openMenu}
title={_t("Location")}
/>

View file

@ -485,14 +485,14 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
return this.wrapImage(contentUrl, thumbnail);
}
// Overidden by MStickerBody
// Overridden by MStickerBody
protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
return <a href={contentUrl} target={this.props.forExport ? "_blank" : undefined} onClick={this.onClick}>
{ children }
</a>;
}
// 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<IBodyProps, IState> {
return <Spinner w={32} h={32} />;
}
// 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;
/*

View file

@ -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<IProps, IState> {
mx_AppsDrawer_2apps: apps.length === 2,
mx_AppsDrawer_3apps: apps.length === 3,
});
const appConatiners =
const appContainers =
<div className="mx_AppsContainer" ref={this.collectResizer}>
{ apps.map((app, i) => {
if (i < 1) return app;
@ -272,7 +272,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
let drawer;
if (widgetIsMaxmised) {
drawer = appConatiners;
drawer = appContainers;
} else {
drawer = <PersistentVResizer
room={this.props.room}
@ -282,7 +282,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier}>
{ appConatiners }
{ appContainers }
</PersistentVResizer>;
}

View file

@ -104,7 +104,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
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({

View file

@ -24,10 +24,11 @@ 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';
import { getAutoCompleteCreator, Part, Type } from '../../../editor/parts';
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
import { renderModel } from '../../../editor/render';
import TypingStore from "../../../stores/TypingStore";
@ -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|:^$');
@ -90,7 +92,7 @@ function selectionEquals(a: Partial<Selection>, b: Selection): boolean {
interface IProps {
model: EditorModel;
room: Room;
threadId: string;
threadId?: string;
placeholder?: string;
label?: string;
initialCaret?: DocumentOffset;
@ -331,26 +333,32 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private onPaste = (event: ClipboardEvent<HTMLDivElement>): 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 });
}
this.modifiedFlag = true;
const range = getRangeForSelection(this.editorRef.current, model, document.getSelection());
replaceRangeAndMoveCaret(range, parts);
if (plainText && range.length > 0 && linkify.test(plainText)) {
formatRangeAsLink(range, plainText);
} else {
replaceRangeAndMoveCaret(range, parts);
}
};
private onInput = (event: Partial<InputEvent>): void => {

View file

@ -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<typeof MenuItem> {
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 <MenuItem
return <IconizedContextMenuOption
{...props}
className={classNames("mx_CallContextMenu_item", className)}
>
{ title }
{ children }
</MenuItem>;
iconClassName={iconClassName}
label={title}
/>;
}
return <AccessibleTooltipButton
{...props}
title={title}
className={className}
className={classNames(className, iconClassName)}
>
{ children }
</AccessibleTooltipButton>;

View file

@ -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
@ -234,7 +234,7 @@ interface IState {
// Position of the context menu
contextMenu?: {
position: Pick<DOMRect, "top" | "left" | "bottom">;
showPermalink?: boolean;
link?: string;
};
isQuoteExpanded?: boolean;
@ -842,26 +842,27 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
};
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<IProps, IState> {
top: ev.clientY,
bottom: ev.clientY,
},
showPermalink: showPermalink,
link: anchorElement?.href || permalink,
},
actionBarFocused: true,
});
@ -924,7 +925,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
onFinished={this.onCloseMenu}
rightClick={true}
reactions={this.state.reactions}
showPermalink={this.state.contextMenu.showPermalink}
link={this.state.contextMenu.link}
/>
);
}

View file

@ -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<IProps> = (props: IProps) => {
title={_t("More options")}
/> }
{ props.isMenuOpen && (
<ContextMenu
<IconizedContextMenu
onFinished={props.toggleButtonMenu}
{...props.menuPosition}
wrapperClassName="mx_MessageComposer_Menu"
compact={true}
>
<OverflowMenuContext.Provider value={props.toggleButtonMenu}>
{ moreButtons }
<IconizedContextMenuOptionList>
{ moreButtons }
</IconizedContextMenuOptionList>
</OverflowMenuContext.Provider>
</ContextMenu>
</IconizedContextMenu>
) }
</UploadButtonContextProvider>;
};
@ -158,7 +162,6 @@ const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) =>
const className = classNames(
"mx_MessageComposer_button",
"mx_MessageComposer_emoji",
{
"mx_MessageComposer_button_highlight": menuDisplayed,
},
@ -169,6 +172,7 @@ const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) =>
return <React.Fragment>
<CollapsibleButton
className={className}
iconClassName="mx_MessageComposer_emoji"
onClick={openMenu}
title={_t("Emoji")}
/>
@ -254,7 +258,8 @@ const UploadButton = () => {
};
return <CollapsibleButton
className="mx_MessageComposer_button mx_MessageComposer_upload"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_upload"
onClick={onClick}
title={_t('Attachment')}
/>;
@ -266,7 +271,8 @@ function showStickersButton(props: IProps): ReactElement {
? <CollapsibleButton
id='stickersButton'
key="controls_stickers"
className="mx_MessageComposer_button mx_MessageComposer_stickers"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_stickers"
onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)}
title={props.isStickerPickerOpen ? _t("Hide stickers") : _t("Sticker")}
/>
@ -281,7 +287,8 @@ function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement {
? null
: <CollapsibleButton
key="voice_message_send"
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_voiceMessage"
onClick={props.onRecordStartEndClick}
title={_t("Voice Message")}
/>
@ -345,7 +352,8 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
return (
<CollapsibleButton
className="mx_MessageComposer_button mx_MessageComposer_poll"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_poll"
onClick={this.onCreateClick}
title={_t("Poll")}
/>

View file

@ -66,7 +66,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
request.on(VerificationRequestEvent.Change, this.checkRequestIsPending);
// We should probably have a separate class managing the active verification toasts,
// rather than monitoring this in the toast component itself, since we'll get problems
// like the toasdt not going away when the verification is cancelled unless it's the
// like the toast not going away when the verification is cancelled unless it's the
// one on the top (ie. the one that's mounted).
// As a quick & dirty fix, check the toast is still relevant when it mounts (this prevents
// a toast hanging around after logging in if you did a verification as part of login).

View file

@ -28,7 +28,7 @@ export enum ComposerType {
interface IBaseComposerInsertPayload extends ActionPayload {
action: Action.ComposerInsert;
timelineRenderingType: TimelineRenderingType;
composerType?: ComposerType; // falsey if should be re-dispatched to the correct composer
composerType?: ComposerType; // falsy if should be re-dispatched to the correct composer
}
interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {

View file

@ -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" && (<HTMLElement>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)) {
@ -176,7 +178,7 @@ function getTextNodeValue(node) {
if (nodeText.length !== 1) {
return nodeText.replace(CARET_NODE_CHAR, "");
} else {
// only contains ZWS, which is ignored, so return emtpy string
// only contains ZWS, which is ignored, so return empty string
return "";
}
} else {
@ -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,

View file

@ -37,7 +37,7 @@ export function formatRange(range: Range, action: Formatting): void {
range.trim();
}
// Edgecase when just selecting whitespace or new line.
// Edge case when just selecting whitespace or new line.
// There should be no reason to format whitespace, so we can just return.
if (range.length === 0) {
return;
@ -216,20 +216,18 @@ 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;
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 + "]" + "()")], -1);
replaceRangeAndMoveCaret(range, [partCreator.plain("[" + range.text + "]" + "(" + (text ?? "") + ")")], -1);
}
}

View file

@ -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)",
@ -2957,11 +2957,11 @@
"View List": "View List",
"Close sidebar": "Close sidebar",
"An error occurred while stopping your live location": "An error occurred while stopping your live location",
"An error occured whilst sharing your live location": "An error occured whilst sharing your live location",
"An error occurred whilst sharing your live location": "An error occurred whilst sharing your live location",
"You are sharing your live location": "You are sharing your live location",
"%(timeRemaining)s left": "%(timeRemaining)s left",
"Live location enabled": "Live location enabled",
"An error occured whilst sharing your live location, please try again": "An error occured whilst sharing your live location, please try again",
"An error occurred whilst sharing your live location, please try again": "An error occurred whilst sharing your live location, please try again",
"An error occurred while stopping your live location, please try again": "An error occurred while stopping your live location, please try again",
"Stop sharing": "Stop sharing",
"Stop sharing and close": "Stop sharing and close",

View file

@ -91,7 +91,7 @@ export default abstract class BaseEventIndexManager {
*
* @param {MatrixEvent} ev The event that should be added to the index.
* @param {IMatrixProfile} profile The profile of the event sender at the
* time of the event receival.
* time the event was received.
*
* @return {Promise} A promise that will resolve when the was queued up for
* addition.

View file

@ -596,7 +596,7 @@ export default class EventIndex extends EventEmitter {
continue;
}
// If all events were already indexed we assume that we catched
// If all events were already indexed we assume that we caught
// up with our index and don't need to crawl the room further.
// Let us delete the checkpoint in that case, otherwise push
// the new checkpoint to be used by the crawler.
@ -612,7 +612,7 @@ export default class EventIndex extends EventEmitter {
this.crawlerCheckpoints.push(newCheckpoint);
}
} catch (e) {
logger.log("EventIndex: Error durring a crawl", e);
logger.log("EventIndex: Error during a crawl", e);
// An error occurred, put the checkpoint back so we
// can retry.
this.crawlerCheckpoints.push(checkpoint);
@ -797,7 +797,7 @@ export default class EventIndex extends EventEmitter {
// to get our events in the BACKWARDS direction but populate them in the
// forwards direction.
// This needs to happen because a fill request might come with an
// exisitng timeline e.g. if you close and re-open the FilePanel.
// existing timeline e.g. if you close and re-open the FilePanel.
if (fromEvent === null) {
matrixEvents.reverse();
direction = direction == EventTimeline.BACKWARDS ? EventTimeline.FORWARDS: EventTimeline.BACKWARDS;

View file

@ -67,7 +67,7 @@ export class EventIndexPeg {
/**
* Initialize the event index.
*
* @returns {boolean} True if the event index was succesfully initialized,
* @returns {boolean} True if the event index was successfully initialized,
* false otherwise.
*/
async initEventIndex() {
@ -118,7 +118,7 @@ export class EventIndexPeg {
}
/**
* Check if event indexing support is installed for the platfrom.
* Check if event indexing support is installed for the platform.
*
* Event indexing might require additional optional modules to be installed,
* this tells us if those are installed. Note that this should only be

View file

@ -75,7 +75,7 @@ export class IntegrationManagerInstance {
client.setTermsInteractionCallback((policyInfo, agreedUrls) => {
// 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',
);

View file

@ -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

View file

@ -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';

View file

@ -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<OwnBeaconStoreState> {
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 {

View file

@ -504,7 +504,7 @@ export class RoomViewStore extends Store<ActionPayload> {
// 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.

View file

@ -159,6 +159,10 @@ export default class VideoChannelStore extends AsyncStoreWithClient<null> {
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<null> {
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<null> {
}
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<null> {
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
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();
};

View file

@ -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.
*

View file

@ -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

View file

@ -91,7 +91,7 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
/**
* 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);

View file

@ -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.

View file

@ -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.

View file

@ -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) + ')';
}

View file

@ -29,7 +29,7 @@ export class LazyValue<T> {
* 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;
}

View file

@ -34,7 +34,7 @@ const keyMap = new EnhancedMap<Object, EnhancedMap<string, unknown>>();
* 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

View file

@ -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,

View file

@ -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 {

View file

@ -120,7 +120,7 @@ a.mx_reply_anchor:hover {
}
.mx_ReplyChain_Export {
margin-top: 0px;
margin-top: 0;
margin-bottom: 5px;
}

View file

@ -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}

View file

@ -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.

View file

@ -168,7 +168,7 @@ describe('<LeftPanelLiveShareWarning />', () => {
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(() => {

View file

@ -359,7 +359,7 @@ describe('<RoomLiveShareWarning />', () => {
// 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();
});

View file

@ -69,7 +69,7 @@ exports[`<LeftPanelLiveShareWarning /> 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
</div>
</AccessibleButton>
</LeftPanelLiveShareWarning>

View file

@ -32,7 +32,7 @@ exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is
<span
className="mx_RoomLiveShareWarning_label"
>
An error occured whilst sharing your live location, please try again
An error occurred whilst sharing your live location, please try again
</span>
<AccessibleButton
className="mx_RoomLiveShareWarning_stopButton"

View file

@ -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((
<BasicMessageComposer model={model} room={room} />
));
}

View file

@ -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(
<WrappedRoomList searchFilter="" onResize={() => {}} />,
<WrappedRoomList {...defaultProps} />,
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',
};

View file

@ -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;
}

View file

@ -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);

View file

@ -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' });
});
});

View file

@ -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: [] });
});
});

View file

@ -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"