Merge branch 'develop' into 19245-improve-styling-of-search-initialization-errors
This commit is contained in:
commit
401e124df6
90 changed files with 562 additions and 398 deletions
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -29,7 +29,7 @@ interface IProps extends IDialogProps {
|
|||
error: string;
|
||||
}>>;
|
||||
source: string;
|
||||
continuation: () => void;
|
||||
continuation: () => Promise<void>;
|
||||
}
|
||||
|
||||
const KeySignatureUploadFailedDialog: React.FC<IProps> = ({
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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;";
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
/*
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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")}
|
||||
/>
|
||||
|
|
|
@ -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).
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue