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

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