Merge remote-tracking branch 'origin/release-v3.43.0'

# Conflicts:
#	CHANGELOG.md
#	package.json
#	src/components/views/messages/MessageActionBar.tsx
This commit is contained in:
Michael Telatynski 2022-04-26 11:49:16 +01:00
commit 6b0156d876
680 changed files with 12433 additions and 5108 deletions

View file

@ -22,7 +22,6 @@ import classNames from "classnames";
import FocusLock from "react-focus-lock";
import { Writeable } from "../../@types/common";
import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore";
import { checkInputableElement, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
@ -105,7 +104,6 @@ interface IState {
// Generic ContextMenu Portal wrapper
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
@replaceableComponent("structures.ContextMenu")
export default class ContextMenu extends React.PureComponent<IProps, IState> {
private readonly initialFocus: HTMLElement;
@ -431,7 +429,7 @@ export type AboveLeftOf = IPosition & {
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
export const aboveLeftOf = (
elementRect: DOMRect,
elementRect: Pick<DOMRect, "right" | "top" | "bottom">,
chevronFace = ChevronFace.None,
vPadding = 0,
): AboveLeftOf => {
@ -452,9 +450,37 @@ export const aboveLeftOf = (
return menuOptions;
};
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the right of elementRect,
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowRightOf?)
export const aboveRightOf = (
elementRect: Pick<DOMRect, "left" | "top" | "bottom">,
chevronFace = ChevronFace.None,
vPadding = 0,
): AboveLeftOf => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
const buttonLeft = elementRect.left + window.pageXOffset;
const buttonBottom = elementRect.bottom + window.pageYOffset;
const buttonTop = elementRect.top + window.pageYOffset;
// Align the left edge of the menu to the left edge of the button
menuOptions.left = buttonLeft;
// Align the menu vertically on whichever side of the button has more space available.
if (buttonBottom < UIStore.instance.windowHeight / 2) {
menuOptions.top = buttonBottom + vPadding;
} else {
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
}
return menuOptions;
};
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
// and always above elementRect
export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
export const alwaysAboveLeftOf = (
elementRect: Pick<DOMRect, "right" | "bottom" | "top">,
chevronFace = ChevronFace.None,
vPadding = 0,
) => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
const buttonRight = elementRect.right + window.pageXOffset;
@ -474,7 +500,11 @@ export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFac
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the right of elementRect
// and always above elementRect
export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
export const alwaysAboveRightOf = (
elementRect: Pick<DOMRect, "left" | "top">,
chevronFace = ChevronFace.None,
vPadding = 0,
) => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
const buttonLeft = elementRect.left + window.pageXOffset;

View file

@ -37,7 +37,7 @@ interface IProps {
// Whether to wrap the page in a scrollbar
scrollbar?: boolean;
// Map of keys to replace with values, e.g {$placeholder: "value"}
replaceMap?: Map<string, string>;
replaceMap?: Record<string, string>;
}
interface IState {
@ -57,8 +57,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
};
}
protected translate(s: string): string {
// default implementation - skins may wish to extend this
private translate(s: string): string {
return sanitizeHtml(_t(s));
}

View file

@ -29,7 +29,6 @@ import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler';
import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuildsNotice";
import BaseCard from "../views/right_panel/BaseCard";
import { replaceableComponent } from "../../utils/replaceableComponent";
import ResizeNotifier from '../../utils/ResizeNotifier';
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
@ -51,7 +50,6 @@ interface IState {
/*
* Component which shows the filtered file using a TimelinePanel
*/
@replaceableComponent("structures.FilePanel")
class FilePanel extends React.Component<IProps, IState> {
static contextType = RoomContext;

View file

@ -16,14 +16,11 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../utils/replaceableComponent";
interface IProps {
title: React.ReactNode;
message: React.ReactNode;
}
@replaceableComponent("structures.GenericErrorPage")
export default class GenericErrorPage extends React.PureComponent<IProps> {
render() {
return <div className='mx_GenericErrorPage'>

View file

@ -21,7 +21,6 @@ import AutoHideScrollbar from './AutoHideScrollbar';
import { getHomePageUrl } from "../../utils/pages";
import { _tDom } from "../../languageHandler";
import SdkConfig from "../../SdkConfig";
import * as sdk from "../../index";
import dis from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import BaseAvatar from "../views/avatars/BaseAvatar";
@ -33,6 +32,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import MiniAvatarUploader, { AVATAR_SIZE } from "../views/elements/MiniAvatarUploader";
import Analytics from "../../Analytics";
import PosthogTrackers from "../../PosthogTrackers";
import EmbeddedPage from "./EmbeddedPage";
const onClickSendDm = () => {
Analytics.trackEvent('home_page', 'button', 'dm');
@ -94,8 +94,6 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
const pageUrl = getHomePageUrl(config);
if (pageUrl) {
// FIXME: Using an import will result in wrench-element-tests failures
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
}

View file

@ -23,7 +23,6 @@ import {
import { _t } from "../../languageHandler";
import { HostSignupStore } from "../../stores/HostSignupStore";
import SdkConfig from "../../SdkConfig";
import { replaceableComponent } from "../../utils/replaceableComponent";
interface IProps {
onClick?(): void;
@ -31,7 +30,6 @@ interface IProps {
interface IState {}
@replaceableComponent("structures.HostSignupAction")
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
private openDialog = async () => {
this.props.onClick?.();

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { ComponentProps, createRef } from "react";
import AutoHideScrollbar from "./AutoHideScrollbar";
import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
interface IProps extends Omit<ComponentProps<typeof AutoHideScrollbar>, "onWheel"> {
@ -40,7 +39,6 @@ interface IState {
rightIndicatorOffset: string;
}
@replaceableComponent("structures.IndicatorScrollbar")
export default class IndicatorScrollbar extends React.Component<IProps, IState> {
private autoHideScrollbar = createRef<AutoHideScrollbar>();
private scrollElement: HTMLDivElement;

View file

@ -28,7 +28,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import getEntryComponentForLoginType, { IStageComponent } from '../views/auth/InteractiveAuthEntryComponents';
import Spinner from "../views/elements/Spinner";
import { replaceableComponent } from "../../utils/replaceableComponent";
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
@ -89,7 +88,6 @@ interface IState {
submitButtonEnabled: boolean;
}
@replaceableComponent("structures.InteractiveAuthComponent")
export default class InteractiveAuthComponent extends React.Component<IProps, IState> {
private readonly authLogic: InteractiveAuth;
private readonly intervalId: number = null;

View file

@ -27,7 +27,6 @@ import { Action } from "../../dispatcher/actions";
import RoomSearch from "./RoomSearch";
import ResizeNotifier from "../../utils/ResizeNotifier";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
@ -61,7 +60,6 @@ interface IState {
activeSpace: SpaceKey;
}
@replaceableComponent("structures.LeftPanel")
export default class LeftPanel extends React.Component<IProps, IState> {
private listContainerRef = createRef<HTMLDivElement>();
private roomSearchRef = createRef<RoomSearch>();

View file

@ -52,7 +52,6 @@ import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { getKeyBindingsManager } from '../../KeyBindingsManager';
import { IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel";
import { replaceableComponent } from "../../utils/replaceableComponent";
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
import { OwnProfileStore } from '../../stores/OwnProfileStore';
@ -63,7 +62,7 @@ import ToastContainer from './ToastContainer';
import UserView from "./UserView";
import BackdropPanel from "./BackdropPanel";
import { mediaFromMxc } from "../../customisations/Media";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import { UserTab } from "../views/dialogs/UserTab";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import RightPanelStore from '../../stores/right-panel/RightPanelStore';
import { TimelineRenderingType } from "../../contexts/RoomContext";
@ -127,7 +126,6 @@ interface IState {
*
* Components mounted below us can access the matrix client via the react context.
*/
@replaceableComponent("structures.LoggedInView")
class LoggedInView extends React.Component<IProps, IState> {
static displayName = 'LoggedInView';

View file

@ -19,7 +19,6 @@ import React from 'react';
import { NumberSize, Resizable } from 're-resizable';
import { Direction } from "re-resizable/lib/resizer";
import { replaceableComponent } from "../../utils/replaceableComponent";
import ResizeNotifier from "../../utils/ResizeNotifier";
interface IProps {
@ -28,7 +27,6 @@ interface IProps {
panel?: JSX.Element;
}
@replaceableComponent("structures.MainSplit")
export default class MainSplit extends React.Component<IProps> {
private onResizeStart = (): void => {
this.props.resizeNotifier.startResizing();

View file

@ -33,7 +33,7 @@ import { throttle } from "lodash";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { RoomType } from "matrix-js-sdk/src/@types/event";
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by various components
import 'focus-visible';
// what-input helps improve keyboard accessibility
import 'what-input';
@ -84,19 +84,18 @@ import {
UPDATE_STATUS_INDICATOR,
} from "../../stores/notifications/RoomNotificationStateStore";
import { SettingLevel } from "../../settings/SettingLevel";
import { leaveRoomBehaviour } from "../../utils/membership";
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
import { UIFeature } from "../../settings/UIFeature";
import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages";
import { replaceableComponent } from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { RoomUpdateCause } from "../../stores/room-list/models";
import SecurityCustomisations from "../../customisations/Security";
import Spinner from "../views/elements/Spinner";
import QuestionDialog from "../views/dialogs/QuestionDialog";
import UserSettingsDialog, { UserTab } from '../views/dialogs/UserSettingsDialog';
import UserSettingsDialog from '../views/dialogs/UserSettingsDialog';
import { UserTab } from "../views/dialogs/UserTab";
import CreateRoomDialog from '../views/dialogs/CreateRoomDialog';
import RoomDirectory from './RoomDirectory';
import KeySignatureUploadFailedDialog from "../views/dialogs/KeySignatureUploadFailedDialog";
@ -131,6 +130,7 @@ import { ViewStartChatOrReusePayload } from '../../dispatcher/payloads/ViewStart
import { IConfigOptions } from "../../IConfigOptions";
import { SnakedObject } from "../../utils/SnakedObject";
import InfoDialog from '../views/dialogs/InfoDialog';
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
// legacy export
export { default as Views } from "../../Views";
@ -208,7 +208,6 @@ interface IState {
forceTimeline?: boolean; // see props
}
@replaceableComponent("structures.MatrixChat")
export default class MatrixChat extends React.PureComponent<IProps, IState> {
static displayName = "MatrixChat";
@ -2097,12 +2096,3 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
</ErrorBoundary>;
}
}
export function isLoggedIn(): boolean {
// JRS: Maybe we should move the step that writes this to the window out of
// `element-web` and into this file? Better yet, we should probably create a
// store to hold this state.
// See also https://github.com/vector-im/element-web/issues/15034.
const app = window.matrixChat;
return app && (app as MatrixChat).state.view === Views.LOGGED_IN;
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react';
import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import { Room } from 'matrix-js-sdk/src/models/room';
@ -31,13 +31,12 @@ import SettingsStore from '../../settings/SettingsStore';
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import { Layout } from "../../settings/enums/Layout";
import { _t } from "../../languageHandler";
import EventTile, { UnwrappedEventTile, haveTileForEvent, IReadReceiptProps } from "../views/rooms/EventTile";
import EventTile, { UnwrappedEventTile, IReadReceiptProps } from "../views/rooms/EventTile";
import { hasText } from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
import HistoryTile from "../views/rooms/HistoryTile";
import { replaceableComponent } from "../../utils/replaceableComponent";
import defaultDispatcher from '../../dispatcher/dispatcher';
import CallEventGrouper from "./CallEventGrouper";
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
@ -51,8 +50,9 @@ import Spinner from "../views/elements/Spinner";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import EditorStateTransfer from "../../utils/EditorStateTransfer";
import { Action } from '../../dispatcher/actions';
import { getEventDisplayInfo } from "../../utils/EventUtils";
import { getEventDisplayInfo } from "../../utils/EventRenderingUtils";
import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker";
import { haveRendererForEvent } from "../../events/EventTileFactory";
import { editorRoomKey } from "../../Editing";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
@ -97,7 +97,7 @@ export function shouldFormContinuation(
timelineRenderingType !== TimelineRenderingType.Thread) return false;
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
if (!haveTileForEvent(prevEvent, showHiddenEvents)) return false;
if (!haveRendererForEvent(prevEvent, showHiddenEvents)) return false;
return true;
}
@ -170,9 +170,6 @@ interface IProps {
// callback which is called when the panel is scrolled.
onScroll?(event: Event): void;
// callback which is called when the user interacts with the room timeline
onUserScroll(event: SyntheticEvent): void;
// callback which is called when more content is needed.
onFillRequest?(backwards: boolean): Promise<boolean>;
@ -200,7 +197,6 @@ interface IReadReceiptForUser {
/* (almost) stateless UI component which builds the event tiles in the room timeline.
*/
@replaceableComponent("structures.MessagePanel")
export default class MessagePanel extends React.Component<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
@ -246,7 +242,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// displayed event in the current render cycle.
private readReceiptsByUserId: Record<string, IReadReceiptForUser> = {};
private readonly showHiddenEventsInTimeline: boolean;
private readonly _showHiddenEvents: boolean;
private readonly threadsEnabled: boolean;
private isMounted = false;
@ -274,7 +270,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// Cache these settings on mount since Settings is expensive to query,
// and we check this in a hot code path. This is also cached in our
// RoomContext, however we still need a fallback for roomless MessagePanels.
this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline");
this.threadsEnabled = SettingsStore.getValue("feature_thread");
this.showTypingNotificationsWatcherRef =
@ -469,7 +465,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
};
public get showHiddenEvents(): boolean {
return this.context?.showHiddenEventsInTimeline ?? this.showHiddenEventsInTimeline;
return this.context?.showHiddenEvents ?? this._showHiddenEvents;
}
// TODO: Implement granular (per-room) hide options
@ -492,7 +488,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
return true;
}
if (!haveTileForEvent(mxEv, this.showHiddenEvents)) {
if (!haveRendererForEvent(mxEv, this.showHiddenEvents)) {
return false; // no tile = no show
}
@ -752,7 +748,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
const willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEv.getDate() || new Date());
lastInSection = willWantDateSeparator ||
mxEv.getSender() !== nextEv.getSender() ||
getEventDisplayInfo(nextEv).isInfoMessage ||
getEventDisplayInfo(nextEv, this.showHiddenEvents).isInfoMessage ||
!shouldFormContinuation(
mxEv, nextEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType,
);
@ -1031,7 +1027,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
ref={this.scrollPanel}
className={classes}
onScroll={this.props.onScroll}
onUserScroll={this.props.onUserScroll}
onFillRequest={this.props.onFillRequest}
onUnfillRequest={this.props.onUnfillRequest}
style={style}

View file

@ -19,7 +19,6 @@ import * as React from "react";
import { ComponentClass } from "../../@types/common";
import NonUrgentToastStore from "../../stores/NonUrgentToastStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { replaceableComponent } from "../../utils/replaceableComponent";
interface IProps {
}
@ -28,7 +27,6 @@ interface IState {
toasts: ComponentClass[];
}
@replaceableComponent("structures.NonUrgentToastContainer")
export default class NonUrgentToastContainer extends React.PureComponent<IProps, IState> {
public constructor(props, context) {
super(props, context);

View file

@ -20,7 +20,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../languageHandler';
import { MatrixClientPeg } from "../../MatrixClientPeg";
import BaseCard from "../views/right_panel/BaseCard";
import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { Layout } from "../../settings/enums/Layout";
@ -38,7 +37,6 @@ interface IState {
/*
* Component which shows the global notification list using a TimelinePanel
*/
@replaceableComponent("structures.NotificationPanel")
export default class NotificationPanel extends React.PureComponent<IProps, IState> {
static contextType = RoomContext;

View file

@ -28,7 +28,6 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SettingsStore from "../../settings/SettingsStore";
import MemberList from "../views/rooms/MemberList";
import UserInfo from "../views/right_panel/UserInfo";
@ -60,7 +59,6 @@ interface IState {
cardState?: IRightPanelCardState;
}
@replaceableComponent("structures.RightPanel")
export default class RightPanel extends React.Component<IProps, IState> {
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
@ -233,6 +231,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
mxEvent={cardState.threadHeadEvent}
initialEvent={cardState.initialEvent}
isInitialEventHighlighted={cardState.isInitialEventHighlighted}
initialEventScrollIntoView={cardState.initialEventScrollIntoView}
permalinkCreator={this.props.permalinkCreator}
e2eStatus={this.props.e2eStatus}
/>;

View file

@ -24,18 +24,14 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
import NetworkDropdown, { ALL_ROOMS, Protocols } from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { mediaFromMxc } from "../../customisations/Media";
import { IDialogProps } from "../views/dialogs/IDialogProps";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import BaseAvatar from "../views/avatars/BaseAvatar";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import QuestionDialog from "../views/dialogs/QuestionDialog";
import BaseDialog from "../views/dialogs/BaseDialog";
@ -46,9 +42,7 @@ import { getDisplayAliasForAliasSet } from "../../Rooms";
import { Action } from "../../dispatcher/actions";
import PosthogTrackers from "../../PosthogTrackers";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800;
import { PublicRoomTile } from "../views/rooms/PublicRoomTile";
const LAST_SERVER_KEY = "mx_last_room_directory_server";
const LAST_INSTANCE_KEY = "mx_last_room_directory_instance";
@ -71,7 +65,6 @@ interface IState {
filterString: string;
}
@replaceableComponent("structures.RoomDirectory")
export default class RoomDirectory extends React.Component<IProps, IState> {
private unmounted = false;
private nextBatch: string = null;
@ -251,7 +244,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
* HS admins to do this through the RoomSettings interface, but
* this needs SPEC-417.
*/
private removeFromDirectory(room: IPublicRoomsChunkRoom) {
private removeFromDirectory = (room: IPublicRoomsChunkRoom) => {
const alias = getDisplayAliasForRoom(room);
const name = room.name || alias || _t('Unnamed room');
@ -291,14 +284,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
});
},
});
}
private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: React.MouseEvent) => {
// If room was shift-clicked, remove it from the room directory
if (ev.shiftKey) {
ev.preventDefault();
this.removeFromDirectory(room);
}
};
private onOptionChange = (server: string, instanceId?: string) => {
@ -406,21 +391,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
}
};
private onPreviewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
this.showRoom(room, null, false, true);
ev.stopPropagation();
};
private onViewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
this.showRoom(room);
ev.stopPropagation();
};
private onJoinClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
this.showRoom(room, null, true);
ev.stopPropagation();
};
private onCreateRoomClick = (ev: ButtonEvent) => {
this.onFinished();
dis.dispatch({
@ -435,7 +405,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
this.showRoom(null, alias, autoJoin);
}
private showRoom(room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
private showRoom = (room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) => {
this.onFinished();
const payload: ViewRoomPayload = {
action: Action.ViewRoom,
@ -479,112 +449,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
payload.room_id = room.room_id;
}
dis.dispatch(payload);
}
private createRoomCells(room: IPublicRoomsChunkRoom) {
const client = MatrixClientPeg.get();
const clientRoom = client.getRoom(room.room_id);
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
const isGuest = client.isGuest();
let previewButton;
let joinOrViewButton;
// Element Web currently does not allow guests to join rooms, so we
// instead show them preview buttons for all rooms. If the room is not
// world readable, a modal will appear asking you to register first. If
// it is readable, the preview appears as normal.
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
previewButton = (
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>
{ _t("Preview") }
</AccessibleButton>
);
}
if (hasJoinedRoom) {
joinOrViewButton = (
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>
{ _t("View") }
</AccessibleButton>
);
} else if (!isGuest) {
joinOrViewButton = (
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>
{ _t("Join") }
</AccessibleButton>
);
}
let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
}
let topic = room.topic || '';
// Additional truncation based on line numbers is done via CSS,
// but to ensure that the DOM is not polluted with a huge string
// we give it a hard limit before rendering.
if (topic.length > MAX_TOPIC_LENGTH) {
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyAndSanitizeHtml(topic);
let avatarUrl = null;
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
return <div
key={room.room_id}
role="listitem"
className="mx_RoomDirectory_listItem"
>
<div
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomAvatar"
>
<BaseAvatar
width={32}
height={32}
resizeMethod='crop'
name={name}
idName={name}
url={avatarUrl}
/>
</div>
<div
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomDescription"
>
<div className="mx_RoomDirectory_name">
{ name }
</div>&nbsp;
<div
className="mx_RoomDirectory_topic"
dangerouslySetInnerHTML={{ __html: topic }}
/>
<div className="mx_RoomDirectory_alias">
{ getDisplayAliasForRoom(room) }
</div>
</div>
<div
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomMemberCount"
>
{ room.num_joined_members }
</div>
<div
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_preview"
>
{ previewButton }
</div>
<div
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_join"
>
{ joinOrViewButton }
</div>
</div>;
}
};
private stringLooksLikeId(s: string, fieldType: IFieldType) {
let pat = /^#[^\s]+:[^\s]/;
if (fieldType && fieldType.regexp) {
@ -622,7 +487,14 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
content = <Spinner />;
} else {
const cells = (this.state.publicRooms || [])
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), []);
.map(room =>
<PublicRoomTile
key={room.room_id}
room={room}
showRoom={this.showRoom}
removeFromDirectory={this.removeFromDirectory}
/>,
);
// we still show the scrollpanel, at least for now, because
// otherwise we don't fetch more because we don't get a fill
// request from the scrollpanel because there isn't one

View file

@ -26,7 +26,6 @@ import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { isMac, Key } from "../../Keyboard";
@ -50,7 +49,6 @@ interface IState {
spotlightBetaEnabled: boolean;
}
@replaceableComponent("structures.RoomSearch")
export default class RoomSearch extends React.PureComponent<IProps, IState> {
private readonly dispatcherRef: string;
private readonly betaRef: string;

View file

@ -24,7 +24,6 @@ import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher';
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
import { Action } from "../../dispatcher/actions";
import { replaceableComponent } from "../../utils/replaceableComponent";
import NotificationBadge from "../views/rooms/NotificationBadge";
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
import AccessibleButton from "../views/elements/AccessibleButton";
@ -82,7 +81,6 @@ interface IState {
isResending: boolean;
}
@replaceableComponent("structures.RoomStatusBar")
export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
private unmounted = false;
public static contextType = MatrixClientContext;

View file

@ -23,7 +23,7 @@ limitations under the License.
import React, { createRef } from 'react';
import classNames from 'classnames';
import { IRecommendedVersion, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import { EventSubscription } from "fbemitter";
import { ISearchResults } from 'matrix-js-sdk/src/@types/search';
import { logger } from "matrix-js-sdk/src/logger";
@ -35,6 +35,7 @@ import { throttle } from "lodash";
import { MatrixError } from 'matrix-js-sdk/src/http-api';
import { ClientEvent } from "matrix-js-sdk/src/client";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
import shouldHideEvent from '../../shouldHideEvent';
import { _t } from '../../languageHandler';
@ -48,14 +49,13 @@ import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore';
import { RoomViewStore } from '../../stores/RoomViewStore';
import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/enums/Layout";
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { haveTileForEvent } from "../views/rooms/EventTile";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import MatrixClientContext, { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext";
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
@ -86,7 +86,6 @@ import { getKeyBindingsManager } from '../../KeyBindingsManager';
import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
import { replaceableComponent } from "../../utils/replaceableComponent";
import EditorStateTransfer from "../../utils/EditorStateTransfer";
import ErrorDialog from '../views/dialogs/ErrorDialog';
import SearchResultTile from '../views/rooms/SearchResultTile';
@ -109,6 +108,7 @@ import { DoAfterSyncPreparedPayload } from '../../dispatcher/payloads/DoAfterSyn
import FileDropTarget from './FileDropTarget';
import Measured from '../views/elements/Measured';
import { FocusComposerPayload } from '../../dispatcher/payloads/FocusComposerPayload';
import { haveRendererForEvent } from "../../events/EventTileFactory";
const DEBUG = false;
let debuglog = function(msg: string) {};
@ -156,6 +156,8 @@ export interface IRoomState {
initialEventPixelOffset?: number;
// Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean;
// Whether to scroll the event into view
initialEventScrollIntoView?: boolean;
replyToEvent?: MatrixEvent;
numUnreadMessages: number;
searchTerm?: string;
@ -197,7 +199,7 @@ export interface IRoomState {
showTwelveHourTimestamps: boolean;
readMarkerInViewThresholdMs: number;
readMarkerOutOfViewThresholdMs: number;
showHiddenEventsInTimeline: boolean;
showHiddenEvents: boolean;
showReadReceipts: boolean;
showRedactions: boolean;
showJoinLeaves: boolean;
@ -220,7 +222,6 @@ export interface IRoomState {
narrow: boolean;
}
@replaceableComponent("structures.RoomView")
export class RoomView extends React.Component<IRoomProps, IRoomState> {
private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription;
@ -270,7 +271,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
showHiddenEvents: SettingsStore.getValue("showHiddenEventsInTimeline"),
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
@ -298,7 +299,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
context.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
context.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
// Start listening for RoomViewStore updates
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
this.roomStoreToken = RoomViewStore.instance.addListener(this.onRoomViewStoreUpdate);
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
@ -327,7 +328,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.setState({ readMarkerOutOfViewThresholdMs: value as number }),
),
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[,,, value]) =>
this.setState({ showHiddenEventsInTimeline: value as boolean }),
this.setState({ showHiddenEvents: value as boolean }),
),
];
}
@ -389,7 +390,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return;
}
if (!initial && this.state.roomId !== RoomViewStore.getRoomId()) {
if (!initial && this.state.roomId !== RoomViewStore.instance.getRoomId()) {
// RoomView explicitly does not support changing what room
// is being viewed: instead it should just be re-mounted when
// switching rooms. Therefore, if the room ID changes, we
@ -404,28 +405,29 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return;
}
const roomId = RoomViewStore.getRoomId();
const roomId = RoomViewStore.instance.getRoomId();
const newState: Pick<IRoomState, any> = {
// This convoluted type signature ensures we get IntelliSense *and* correct typing
const newState: Partial<IRoomState> & Pick<IRoomState, any> = {
roomId,
roomAlias: RoomViewStore.getRoomAlias(),
roomLoading: RoomViewStore.isRoomLoading(),
roomLoadError: RoomViewStore.getRoomLoadError(),
joining: RoomViewStore.isJoining(),
replyToEvent: RoomViewStore.getQuotingEvent(),
roomAlias: RoomViewStore.instance.getRoomAlias(),
roomLoading: RoomViewStore.instance.isRoomLoading(),
roomLoadError: RoomViewStore.instance.getRoomLoadError(),
joining: RoomViewStore.instance.isJoining(),
replyToEvent: RoomViewStore.instance.getQuotingEvent(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.instance.shouldPeek(),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
showRedactions: SettingsStore.getValue("showRedactions", roomId),
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
wasContextSwitch: RoomViewStore.instance.getWasContextSwitch(),
initialEventId: null, // default to clearing this, will get set later in the method if needed
showRightPanel: RightPanelStore.instance.isOpenForRoom(roomId),
};
const initialEventId = RoomViewStore.getInitialEventId();
const initialEventId = RoomViewStore.instance.getInitialEventId();
if (initialEventId) {
const room = this.context.getRoom(roomId);
let initialEvent = room?.findEventById(initialEventId);
@ -445,22 +447,29 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
);
}
// If we have an initial event, we want to reset the event pixel offset to ensure it ends up
// visible
newState.initialEventPixelOffset = null;
const thread = initialEvent?.getThread();
if (thread && !initialEvent?.isThreadRoot) {
showThread({
rootEvent: thread.rootEvent,
initialEvent,
highlighted: RoomViewStore.isInitialEventHighlighted(),
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
});
} else {
newState.initialEventId = initialEventId;
newState.isInitialEventHighlighted = RoomViewStore.isInitialEventHighlighted();
newState.isInitialEventHighlighted = RoomViewStore.instance.isInitialEventHighlighted();
newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView();
if (thread && initialEvent?.isThreadRoot) {
showThread({
rootEvent: thread.rootEvent,
initialEvent,
highlighted: RoomViewStore.isInitialEventHighlighted(),
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
});
}
}
@ -760,19 +769,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
}
private onUserScroll = () => {
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: this.state.room.roomId,
event_id: this.state.initialEventId,
highlighted: false,
replyingToEvent: this.state.replyToEvent,
metricsTrigger: undefined, // room doesn't change
});
}
};
private onRightPanelStoreUpdate = () => {
this.setState({
showRightPanel: RightPanelStore.instance.isOpenForRoom(this.state.roomId),
@ -1303,6 +1299,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.updateTopUnreadMessagesBar();
};
private resetJumpToEvent = (eventId?: string) => {
if (this.state.initialEventId && this.state.initialEventScrollIntoView &&
this.state.initialEventId === eventId) {
debuglog("Removing scroll_into_view flag from initial event");
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: this.state.room.roomId,
event_id: this.state.initialEventId,
highlighted: this.state.isInitialEventHighlighted,
scroll_into_view: false,
replyingToEvent: this.state.replyToEvent,
metricsTrigger: undefined, // room doesn't change
});
}
};
private injectSticker(url: string, info: object, text: string, threadId: string | null) {
if (this.context.isGuest()) {
dis.dispatch({ action: 'require_registration' });
@ -1347,7 +1359,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.handleSearchResult(searchPromise);
};
private handleSearchResult(searchPromise: Promise<any>): Promise<boolean> {
private handleSearchResult(searchPromise: Promise<ISearchResults>): Promise<boolean> {
// keep a record of the current search id, so that if the search terms
// change before we get a response, we can ignore the results.
const localSearchId = this.searchId;
@ -1356,7 +1368,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
searchInProgress: true,
});
return searchPromise.then((results) => {
return searchPromise.then(async (results) => {
debuglog("search complete");
if (this.unmounted ||
this.state.timelineRenderingType !== TimelineRenderingType.Search ||
@ -1383,6 +1395,20 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return b.length - a.length;
});
if (this.context.supportsExperimentalThreads()) {
// Process all thread roots returned in this batch of search results
// XXX: This won't work for results coming from Seshat which won't include the bundled relationship
for (const result of results.results) {
for (const event of result.context.getTimeline()) {
const bundledRelationship = event
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
if (!bundledRelationship || event.getThread()) continue;
const room = this.context.getRoom(event.getRoomId());
event.setThread(room.findThreadForEvent(event) ?? room.createThread(event, [], true));
}
}
}
this.setState({
searchHighlights: highlights,
searchResults: results,
@ -1454,7 +1480,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
continue;
}
if (!haveTileForEvent(mxEv, this.state.showHiddenEventsInTimeline)) {
if (!haveRendererForEvent(mxEv, this.state.showHiddenEvents)) {
// XXX: can this ever happen? It will make the result count
// not match the displayed count.
continue;
@ -2053,9 +2079,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
hidden={hideMessagePanel}
highlightedEventId={highlightedEventId}
eventId={this.state.initialEventId}
eventScrollIntoView={this.state.initialEventScrollIntoView}
eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={this.onMessageListScroll}
onUserScroll={this.onUserScroll}
onEventScrolledIntoView={this.resetJumpToEvent}
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
showUrlPreview={this.state.showUrlPreview}
className={this.messagePanelClassNames}

View file

@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
import React, { createRef, CSSProperties, ReactNode, KeyboardEvent } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import ResizeNotifier from "../../utils/ResizeNotifier";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
@ -110,10 +109,6 @@ interface IProps {
/* onScroll: a callback which is called whenever any scroll happens.
*/
onScroll?(event: Event): void;
/* onUserScroll: callback which is called when the user interacts with the room timeline
*/
onUserScroll?(event: SyntheticEvent): void;
}
/* This component implements an intelligent scrolling list.
@ -170,7 +165,6 @@ interface IPreventShrinkingState {
offsetNode: HTMLElement;
}
@replaceableComponent("structures.ScrollPanel")
export default class ScrollPanel extends React.Component<IProps> {
static defaultProps = {
stickyBottom: true,
@ -595,29 +589,21 @@ export default class ScrollPanel extends React.Component<IProps> {
* @param {object} ev the keyboard event
*/
public handleScrollKey = (ev: KeyboardEvent) => {
let isScrolling = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev);
switch (roomAction) {
case KeyBindingAction.ScrollUp:
this.scrollRelative(-1);
isScrolling = true;
break;
case KeyBindingAction.ScrollDown:
this.scrollRelative(1);
isScrolling = true;
break;
case KeyBindingAction.JumpToFirstMessage:
this.scrollToTop();
isScrolling = true;
break;
case KeyBindingAction.JumpToLatestMessage:
this.scrollToBottom();
isScrolling = true;
break;
}
if (isScrolling && this.props.onUserScroll) {
this.props.onUserScroll(ev);
}
};
/* Scroll the panel to bring the DOM node with the scroll token
@ -967,7 +953,6 @@ export default class ScrollPanel extends React.Component<IProps> {
<AutoHideScrollbar
wrappedRef={this.collectScroll}
onScroll={this.onScroll}
onWheel={this.props.onUserScroll}
className={`mx_ScrollPanel ${this.props.className}`}
style={this.props.style}
>

View file

@ -20,7 +20,6 @@ import { throttle } from 'lodash';
import classNames from 'classnames';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import { replaceableComponent } from "../../utils/replaceableComponent";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
@ -43,7 +42,6 @@ interface IState {
blurred: boolean;
}
@replaceableComponent("structures.SearchBox")
export default class SearchBox extends React.Component<IProps, IState> {
private search = createRef<HTMLInputElement>();

View file

@ -60,7 +60,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
import { IOOBData } from "../../stores/ThreepidInviteStore";
import { awaitRoomDownSync } from "../../utils/RoomUpgrade";
import RoomViewStore from "../../stores/RoomViewStore";
import { RoomViewStore } from "../../stores/RoomViewStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
@ -371,7 +371,7 @@ export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
metricsTrigger: "SpaceHierarchy",
});
}, err => {
RoomViewStore.showJoinRoomError(err, roomId);
RoomViewStore.instance.showJoinRoomError(err, roomId);
});
return prom;

View file

@ -22,7 +22,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../languageHandler';
import AutoHideScrollbar from './AutoHideScrollbar';
import { replaceableComponent } from "../../utils/replaceableComponent";
import AccessibleButton from "../views/elements/AccessibleButton";
import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers";
@ -64,7 +63,6 @@ interface IState {
activeTabIndex: number;
}
@replaceableComponent("structures.TabbedView")
export default class TabbedView extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -37,7 +37,7 @@ import SdkConfig from '../../SdkConfig';
import Modal from '../../Modal';
import BetaFeedbackDialog from '../views/dialogs/BetaFeedbackDialog';
import { Action } from '../../dispatcher/actions';
import { UserTab } from '../views/dialogs/UserSettingsDialog';
import { UserTab } from '../views/dialogs/UserTab';
import dis from '../../dispatcher/dispatcher';
interface IProps {
@ -163,7 +163,9 @@ const EmptyThread: React.FC<EmptyThreadIProps> = ({ hasThreads, filterOption, sh
body = <>
<p>{ _t("Threads help keep your conversations on-topic and easy to track.") }</p>
<p className="mx_ThreadPanel_empty_tip">
{ _t('<b>Tip:</b> Use "Reply in thread" when hovering over a message.', {}, {
{ _t('<b>Tip:</b> Use “%(replyInThread)s” when hovering over a message.', {
replyInThread: _t("Reply in thread"),
}, {
b: sub => <b>{ sub }</b>,
}) }
</p>
@ -250,7 +252,7 @@ const ThreadPanel: React.FC<IProps> = ({
<RoomContext.Provider value={{
...roomContext,
timelineRenderingType: TimelineRenderingType.ThreadsList,
showHiddenEventsInTimeline: true,
showHiddenEvents: true,
narrow,
}}>
<BaseCard

View file

@ -25,7 +25,6 @@ import classNames from "classnames";
import BaseCard from "../views/right_panel/BaseCard";
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
import { replaceableComponent } from "../../utils/replaceableComponent";
import ResizeNotifier from '../../utils/ResizeNotifier';
import MessageComposer from '../views/rooms/MessageComposer';
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
@ -51,7 +50,7 @@ import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import Measured from '../views/elements/Measured';
import PosthogTrackers from "../../PosthogTrackers";
import { ButtonEvent } from "../views/elements/AccessibleButton";
import RoomViewStore from '../../stores/RoomViewStore';
import { RoomViewStore } from '../../stores/RoomViewStore';
interface IProps {
room: Room;
@ -62,6 +61,7 @@ interface IProps {
e2eStatus?: E2EStatus;
initialEvent?: MatrixEvent;
isInitialEventHighlighted?: boolean;
initialEventScrollIntoView?: boolean;
}
interface IState {
@ -73,7 +73,6 @@ interface IState {
narrow: boolean;
}
@replaceableComponent("structures.ThreadView")
export default class ThreadView extends React.Component<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
@ -112,7 +111,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
room.removeListener(ThreadEvent.New, this.onNewThread);
SettingsStore.unwatchSetting(this.layoutWatcherRef);
const hasRoomChanged = RoomViewStore.getRoomId() !== roomId;
const hasRoomChanged = RoomViewStore.instance.getRoomId() !== roomId;
if (this.props.isInitialEventHighlighted && !hasRoomChanged) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@ -217,13 +216,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
};
private resetHighlightedEvent = (): void => {
if (this.props.initialEvent && this.props.isInitialEventHighlighted) {
private resetJumpToEvent = (event?: string): void => {
if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
event === this.props.initialEvent?.getId()) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: this.props.room.roomId,
event_id: this.props.initialEvent?.getId(),
highlighted: false,
highlighted: this.props.isInitialEventHighlighted,
scroll_into_view: false,
replyingToEvent: this.state.replyToEvent,
metricsTrigger: undefined, // room doesn't change
});
@ -374,7 +375,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
editState={this.state.editState}
eventId={this.props.initialEvent?.getId()}
highlightedEventId={highlightedEventId}
onUserScroll={this.resetHighlightedEvent}
eventScrollIntoView={this.props.initialEventScrollIntoView}
onEventScrolledIntoView={this.resetJumpToEvent}
onPaginationRequest={this.onPaginationRequest}
/>
</div> }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, ReactNode, SyntheticEvent } from 'react';
import React, { createRef, ReactNode } from 'react';
import ReactDOM from "react-dom";
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
@ -40,8 +40,6 @@ import dis from "../../dispatcher/dispatcher";
import { Action } from '../../dispatcher/actions';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import { haveTileForEvent } from "../views/rooms/EventTile";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
import MessagePanel from "./MessagePanel";
import { IScrollState } from "./ScrollPanel";
@ -55,6 +53,7 @@ import CallEventGrouper, { buildCallEventGroupers } from "./CallEventGrouper";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { haveRendererForEvent } from "../../events/EventTileFactory";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@ -92,6 +91,9 @@ interface IProps {
// id of an event to jump to. If not given, will go to the end of the live timeline.
eventId?: string;
// whether we should scroll the event into view
eventScrollIntoView?: boolean;
// where to position the event given by eventId, in pixels from the bottom of the viewport.
// If not given, will try to put the event half way down the viewport.
eventPixelOffset?: number;
@ -125,8 +127,7 @@ interface IProps {
// callback which is called when the panel is scrolled.
onScroll?(event: Event): void;
// callback which is called when the user interacts with the room timeline
onUserScroll?(event: SyntheticEvent): void;
onEventScrolledIntoView?(eventId?: string): void;
// callback which is called when the read-up-to mark is updated.
onReadMarkerUpdated?(): void;
@ -211,7 +212,6 @@ interface IEventIndexOpts {
*
* Also responsible for handling and sending read receipts.
*/
@replaceableComponent("structures.TimelinePanel")
class TimelinePanel extends React.Component<IProps, IState> {
static contextType = RoomContext;
@ -329,9 +329,11 @@ class TimelinePanel extends React.Component<IProps, IState> {
const differentEventId = newProps.eventId != this.props.eventId;
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
if (differentEventId || differentHighlightedEventId) {
logger.log("TimelinePanel switching to eventId " + newProps.eventId +
" (was " + this.props.eventId + ")");
const differentAvoidJump = newProps.eventScrollIntoView && !this.props.eventScrollIntoView;
if (differentEventId || differentHighlightedEventId || differentAvoidJump) {
logger.log("TimelinePanel switching to " +
"eventId " + newProps.eventId + " (was " + this.props.eventId + "), " +
"scrollIntoView: " + newProps.eventScrollIntoView + " (was " + this.props.eventScrollIntoView + ")");
return this.initTimeline(newProps);
}
}
@ -1125,7 +1127,41 @@ class TimelinePanel extends React.Component<IProps, IState> {
offsetBase = 0.5;
}
return this.loadTimeline(initialEvent, pixelOffset, offsetBase);
return this.loadTimeline(initialEvent, pixelOffset, offsetBase, props.eventScrollIntoView);
}
private scrollIntoView(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
const doScroll = () => {
if (eventId) {
debuglog("TimelinePanel scrolling to eventId " + eventId +
" at position " + (offsetBase * 100) + "% + " + pixelOffset);
this.messagePanel.current.scrollToEvent(
eventId,
pixelOffset,
offsetBase,
);
} else {
debuglog("TimelinePanel scrolling to bottom");
this.messagePanel.current.scrollToBottom();
}
};
debuglog("TimelinePanel scheduling scroll to event");
this.props.onEventScrolledIntoView?.(eventId);
// Ensure the correct scroll position pre render, if the messages have already been loaded to DOM,
// to avoid it jumping around
doScroll();
// Ensure the correct scroll position post render for correct behaviour.
//
// requestAnimationFrame runs our code immediately after the DOM update but before the next repaint.
//
// If the messages have just been loaded for the first time, this ensures we'll repeat setting the
// correct scroll position after React has re-rendered the TimelinePanel and MessagePanel and
// updated the DOM.
window.requestAnimationFrame(() => {
doScroll();
});
}
/**
@ -1141,8 +1177,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
* @param {number?} offsetBase the reference point for the pixelOffset. 0
* means the top of the container, 1 means the bottom, and fractional
* values mean somewhere in the middle. If omitted, it defaults to 0.
*
* @param {boolean?} scrollIntoView whether to scroll the event into view.
*/
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void {
this.timelineWindow = new TimelineWindow(
MatrixClientPeg.get(), this.props.timelineSet,
{ windowLimit: this.props.timelineCap });
@ -1177,11 +1215,9 @@ class TimelinePanel extends React.Component<IProps, IState> {
"messagePanel didn't load");
return;
}
if (eventId) {
this.messagePanel.current.scrollToEvent(eventId, pixelOffset,
offsetBase);
} else {
this.messagePanel.current.scrollToBottom();
if (scrollIntoView) {
this.scrollIntoView(eventId, pixelOffset, offsetBase);
}
if (this.props.sendReadReceiptOnLoad) {
@ -1475,7 +1511,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
const shouldIgnore = !!ev.status || // local echo
(ignoreOwn && ev.getSender() === myUserId); // own message
const isWithoutTile = !haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline) ||
const isWithoutTile = !haveRendererForEvent(ev, this.context?.showHiddenEvents) ||
shouldHideEvent(ev, this.context);
if (isWithoutTile || !node) {
@ -1632,7 +1668,6 @@ class TimelinePanel extends React.Component<IProps, IState> {
ourUserId={MatrixClientPeg.get().credentials.userId}
stickyBottom={stickyBottom}
onScroll={this.onMessageListScroll}
onUserScroll={this.props.onUserScroll}
onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest}
isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour}

View file

@ -18,14 +18,12 @@ import * as React from "react";
import classNames from "classnames";
import ToastStore, { IToast } from "../../stores/ToastStore";
import { replaceableComponent } from "../../utils/replaceableComponent";
interface IState {
toasts: IToast<any>[];
countSeen: number;
}
@replaceableComponent("structures.ToastContainer")
export default class ToastContainer extends React.Component<{}, IState> {
constructor(props, context) {
super(props, context);
@ -81,7 +79,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
titleElement = (
<div className="mx_Toast_title">
<h2>{ title }</h2>
<span>{ countIndicator }</span>
<span className="mx_Toast_title_countIndicator">{ countIndicator }</span>
</div>
);
}

View file

@ -27,7 +27,6 @@ import { Action } from "../../dispatcher/actions";
import ProgressBar from "../views/elements/ProgressBar";
import AccessibleButton from "../views/elements/AccessibleButton";
import { IUpload } from "../../models/IUpload";
import { replaceableComponent } from "../../utils/replaceableComponent";
import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps {
@ -40,7 +39,6 @@ interface IState {
uploadsHere: IUpload[];
}
@replaceableComponent("structures.UploadBar")
export default class UploadBar extends React.Component<IProps, IState> {
static contextType = MatrixClientContext;

View file

@ -25,7 +25,7 @@ import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { _t } from "../../languageHandler";
import { ChevronFace, ContextMenuButton } from "./ContextMenu";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import { UserTab } from "../views/dialogs/UserTab";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import Modal from "../../Modal";
@ -53,7 +53,6 @@ import { UIFeature } from "../../settings/UIFeature";
import HostSignupAction from "./HostSignupAction";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { replaceableComponent } from "../../utils/replaceableComponent";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
@ -143,7 +142,6 @@ const below = (rect: PartialDOMRect) => {
};
};
@replaceableComponent("structures.UserMenu")
export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef: string;
private themeWatcherRef: string;

View file

@ -23,7 +23,6 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
import HomePage from "./HomePage";
import { replaceableComponent } from "../../utils/replaceableComponent";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import MainSplit from "./MainSplit";
import RightPanel from "./RightPanel";
@ -41,7 +40,6 @@ interface IState {
member?: RoomMember;
}
@replaceableComponent("structures.UserView")
export default class UserView extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -24,12 +24,12 @@ import { _t } from "../../languageHandler";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { canEditContent } from "../../utils/EventUtils";
import { MatrixClientPeg } from '../../MatrixClientPeg';
import { replaceableComponent } from "../../utils/replaceableComponent";
import { IDialogProps } from "../views/dialogs/IDialogProps";
import BaseDialog from "../views/dialogs/BaseDialog";
import { DevtoolsContext } from "../views/dialogs/devtools/BaseTool";
import { StateEventEditor } from "../views/dialogs/devtools/RoomState";
import { stringify, TimelineEventEditor } from "../views/dialogs/devtools/Event";
import CopyableText from "../views/elements/CopyableText";
interface IProps extends IDialogProps {
mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu
@ -39,7 +39,6 @@ interface IState {
isEditing: boolean;
}
@replaceableComponent("structures.ViewSource")
export default class ViewSource extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
@ -65,29 +64,58 @@ export default class ViewSource extends React.Component<IProps, IState> {
// @ts-ignore
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
const originalEventSource = mxEvent.event;
const copyOriginalFunc = (): string => {
return stringify(originalEventSource);
};
if (isEncrypted) {
const copyDecryptedFunc = (): string => {
return stringify(decryptedEventSource);
};
return (
<>
<details open className="mx_ViewSource_details">
<summary>
<span className="mx_ViewSource_heading">{ _t("Decrypted event source") }</span>
<span className="mx_ViewSource_heading">
{ _t("Decrypted event source") }
</span>
</summary>
<SyntaxHighlight language="json">{ stringify(decryptedEventSource) }</SyntaxHighlight>
<div className="mx_ViewSource_container">
<CopyableText getTextToCopy={copyDecryptedFunc}>
<SyntaxHighlight language="json">
{ stringify(decryptedEventSource) }
</SyntaxHighlight>
</CopyableText>
</div>
</details>
<details className="mx_ViewSource_details">
<summary>
<span className="mx_ViewSource_heading">{ _t("Original event source") }</span>
<span className="mx_ViewSource_heading">
{ _t("Original event source") }
</span>
</summary>
<SyntaxHighlight language="json">{ stringify(originalEventSource) }</SyntaxHighlight>
<div className="mx_ViewSource_container">
<CopyableText getTextToCopy={copyOriginalFunc}>
<SyntaxHighlight language="json">
{ stringify(originalEventSource) }
</SyntaxHighlight>
</CopyableText>
</div>
</details>
</>
);
} else {
return (
<>
<div className="mx_ViewSource_heading">{ _t("Original event source") }</div>
<SyntaxHighlight language="json">{ stringify(originalEventSource) }</SyntaxHighlight>
<div className="mx_ViewSource_heading">
{ _t("Original event source") }
</div>
<div className="mx_ViewSource_container">
<CopyableText getTextToCopy={copyOriginalFunc}>
<SyntaxHighlight language="json">
{ stringify(originalEventSource) }
</SyntaxHighlight>
</CopyableText>
</div>
</>
);
}
@ -139,8 +167,16 @@ export default class ViewSource extends React.Component<IProps, IState> {
return (
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
<div>
<div>{ _t("Room ID: %(roomId)s", { roomId }) }</div>
<div>{ _t("Event ID: %(eventId)s", { eventId }) }</div>
<div>
<CopyableText getTextToCopy={() => roomId} border={false}>
{ _t("Room ID: %(roomId)s", { roomId }) }
</CopyableText>
</div>
<div>
<CopyableText getTextToCopy={() => eventId} border={false}>
{ _t("Event ID: %(eventId)s", { eventId }) }
</CopyableText>
</div>
<div className="mx_ViewSource_separator" />
{ isEditing ? this.editSourceContent() : this.viewSourceContent() }
</div>

View file

@ -19,7 +19,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from '../../views/elements/AccessibleButton';
import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody";
import AuthPage from "../../views/auth/AuthPage";
@ -33,7 +32,6 @@ interface IState {
lostKeys: boolean;
}
@replaceableComponent("structures.auth.CompleteSecurity")
export default class CompleteSecurity extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -19,7 +19,6 @@ import React from 'react';
import AuthPage from '../../views/auth/AuthPage';
import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
onFinished: () => void;
@ -27,7 +26,6 @@ interface IProps {
tokenLogin?: boolean;
}
@replaceableComponent("structures.auth.E2eSetup")
export default class E2eSetup extends React.Component<IProps> {
render() {
return (

View file

@ -28,7 +28,6 @@ import AuthPage from "../../views/auth/AuthPage";
import ServerPicker from "../../views/elements/ServerPicker";
import EmailField from "../../views/auth/EmailField";
import PassphraseField from '../../views/auth/PassphraseField';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
import InlineSpinner from '../../views/elements/InlineSpinner';
import Spinner from "../../views/elements/Spinner";
@ -81,7 +80,6 @@ enum ForgotPasswordField {
PasswordConfirm = 'field_password_confirm',
}
@replaceableComponent("structures.auth.ForgotPassword")
export default class ForgotPassword extends React.Component<IProps, IState> {
private reset: PasswordReset;
@ -224,8 +222,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
}
private onInputChanged = (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => {
let value = ev.currentTarget.value;
if (stateKey === "email") value = value.trim();
this.setState({
[stateKey]: ev.currentTarget.value,
[stateKey]: value,
} as any);
};

View file

@ -34,7 +34,6 @@ import InlineSpinner from "../../views/elements/InlineSpinner";
import Spinner from "../../views/elements/Spinner";
import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from "../../views/elements/ServerPicker";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader";
import AccessibleButton from '../../views/elements/AccessibleButton';
@ -103,7 +102,6 @@ interface IState {
/*
* A wire component which glues together login UI components and Login logic
*/
@replaceableComponent("structures.auth.LoginComponent")
export default class LoginComponent extends React.PureComponent<IProps, IState> {
private unmounted = false;
private loginLogic: Login;

View file

@ -30,7 +30,6 @@ import Login, { ISSOFlow } from "../../../Login";
import dis from "../../../dispatcher/dispatcher";
import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from '../../views/elements/ServerPicker';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RegistrationForm from '../../views/auth/RegistrationForm';
import AccessibleButton from '../../views/elements/AccessibleButton';
import AuthBody from "../../views/auth/AuthBody";
@ -110,7 +109,6 @@ interface IState {
ssoFlow?: ISSOFlow;
}
@replaceableComponent("structures.auth.Registration")
export default class Registration extends React.Component<IProps, IState> {
loginLogic: Login;

View file

@ -25,7 +25,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import EncryptionPanel from "../../views/right_panel/EncryptionPanel";
import AccessibleButton from '../../views/elements/AccessibleButton';
import Spinner from '../../views/elements/Spinner';
@ -49,7 +48,6 @@ interface IState {
lostKeys: boolean;
}
@replaceableComponent("structures.auth.SetupEncryptionBody")
export default class SetupEncryptionBody extends React.Component<IProps, IState> {
constructor(props) {
super(props);

View file

@ -27,7 +27,6 @@ import { ISSOFlow, LoginFlow, sendLoginRequest } from "../../../Login";
import AuthPage from "../../views/auth/AuthPage";
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform";
import SSOButtons from "../../views/elements/SSOButtons";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import ConfirmWipeDeviceDialog from '../../views/dialogs/ConfirmWipeDeviceDialog';
import Field from '../../views/elements/Field';
import AccessibleButton from '../../views/elements/AccessibleButton';
@ -70,7 +69,6 @@ interface IState {
flows: LoginFlow[];
}
@replaceableComponent("structures.auth.SoftLogout")
export default class SoftLogout extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);

View file

@ -0,0 +1,26 @@
/*
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.
*/
// We're importing via require specifically so the svg becomes a URI rather than a DOM element.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const matrixSvg = require('../../../res/img/matrix.svg').default;
/**
* Intended to replace $matrixLogo in the welcome page.
*/
export const MATRIX_LOGO_HTML = `<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">
<img width="79" height="34" alt="Matrix" style="padding-left: 1px;vertical-align: middle" src="${matrixSvg}"/>
</a>`;

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { ReactNode } from "react";
import PlayPauseButton from "./PlayPauseButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils";
import DurationClock from "./DurationClock";
import { _t } from "../../../languageHandler";
@ -25,7 +24,6 @@ import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock";
import AudioPlayerBase from "./AudioPlayerBase";
@replaceableComponent("views.audio_messages.AudioPlayer")
export default class AudioPlayer extends AudioPlayerBase {
protected renderFileSize(): string {
const bytes = this.props.playback.sizeBytes;

View file

@ -19,7 +19,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { Playback, PlaybackState } from "../../../audio/Playback";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { _t } from "../../../languageHandler";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@ -39,7 +38,6 @@ interface IState {
error?: boolean;
}
@replaceableComponent("views.audio_messages.AudioPlayerBase")
export default abstract class AudioPlayerBase<T extends IProps = IProps> extends React.PureComponent<T, IState> {
protected seekRef: RefObject<SeekBar> = createRef();
protected playPauseRef: RefObject<PlayPauseButton> = createRef();

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { HTMLProps } from "react";
import { formatSeconds } from "../../../DateUtils";
import { replaceableComponent } from "../../../utils/replaceableComponent";
export interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live"> {
seconds: number;
@ -27,7 +26,6 @@ export interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live"> {
* Simply converts seconds into minutes and seconds. Note that hours will not be
* displayed, making it possible to see "82:29".
*/
@replaceableComponent("views.audio_messages.Clock")
export default class Clock extends React.Component<IProps> {
public constructor(props) {
super(props);

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { Playback } from "../../../audio/Playback";
@ -31,7 +30,6 @@ interface IState {
/**
* A clock which shows a clip's maximum duration.
*/
@replaceableComponent("views.audio_messages.DurationClock")
export default class DurationClock extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import { IRecordingUpdate, VoiceRecording } from "../../../audio/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { MarkedExecution } from "../../../utils/MarkedExecution";
@ -32,7 +31,6 @@ interface IState {
/**
* A clock for a live recording.
*/
@replaceableComponent("views.audio_messages.LiveRecordingClock")
export default class LiveRecordingClock extends React.PureComponent<IProps, IState> {
private seconds = 0;
private scheduledUpdate = new MarkedExecution(

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
import Waveform from "./Waveform";
import { MarkedExecution } from "../../../utils/MarkedExecution";
@ -33,7 +32,6 @@ interface IState {
/**
* A waveform which shows the waveform of a live recording
*/
@replaceableComponent("views.audio_messages.LiveRecordingWaveform")
export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> {
public static defaultProps = {
progress: 1,

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { ReactNode } from "react";
import classNames from "classnames";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { _t } from "../../../languageHandler";
import { Playback, PlaybackState } from "../../../audio/Playback";
@ -35,7 +34,6 @@ interface IProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButto
* Displays a play/pause button (activating the play/pause function of the recorder)
* to be displayed in reference to a recording.
*/
@replaceableComponent("views.audio_messages.PlayPauseButton")
export default class PlayPauseButton extends React.PureComponent<IProps> {
public constructor(props) {
super(props);

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { Playback, PlaybackState } from "../../../audio/Playback";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
@ -39,7 +38,6 @@ interface IState {
/**
* A clock for a playback of a recording.
*/
@replaceableComponent("views.audio_messages.PlaybackClock")
export default class PlaybackClock extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
import Waveform from "./Waveform";
import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/Playback";
@ -34,7 +33,6 @@ interface IState {
/**
* A waveform which shows the waveform of a previously recorded recording
*/
@replaceableComponent("views.audio_messages.PlaybackWaveform")
export default class PlaybackWaveform extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);

View file

@ -18,7 +18,6 @@ import React, { ReactNode } from "react";
import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AudioPlayerBase, { IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase";
import SeekBar from "./SeekBar";
import PlaybackWaveform from "./PlaybackWaveform";
@ -30,7 +29,6 @@ interface IProps extends IAudioPlayerBaseProps {
withWaveform?: boolean;
}
@replaceableComponent("views.audio_messages.RecordingPlayback")
export default class RecordingPlayback extends AudioPlayerBase<IProps> {
// This component is rendered in two ways: the composer and timeline. They have different
// rendering properties (specifically the difference of a waveform or not).

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
import { Playback, PlaybackState } from "../../../audio/Playback";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import { percentageOf } from "../../../utils/numbers";
@ -43,7 +42,6 @@ interface ISeekCSS extends CSSProperties {
const ARROW_SKIP_SECONDS = 5; // arbitrary
@replaceableComponent("views.audio_messages.SeekBar")
export default class SeekBar extends React.PureComponent<IProps, IState> {
// We use an animation frame request to avoid overly spamming prop updates, even if we aren't
// really using anything demanding on the CSS front.

View file

@ -17,8 +17,6 @@ limitations under the License.
import React, { CSSProperties } from "react";
import classNames from "classnames";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface WaveformCSSProperties extends CSSProperties {
'--barHeight': number;
}
@ -39,7 +37,6 @@ interface IState {
* For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be
* "filled", as a demonstration of the progress property.
*/
@replaceableComponent("views.audio_messages.Waveform")
export default class Waveform extends React.PureComponent<IProps, IState> {
public static defaultProps = {
progress: 1,

View file

@ -16,9 +16,6 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthBody")
export default class AuthBody extends React.PureComponent {
public render(): React.ReactNode {
return <div className="mx_AuthBody">

View file

@ -19,9 +19,7 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthFooter")
export default class AuthFooter extends React.Component {
public render(): React.ReactNode {
return (

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthHeaderLogo from "./AuthHeaderLogo";
import LanguageSelector from "./LanguageSelector";
@ -25,7 +24,6 @@ interface IProps {
disableLanguageSelector?: boolean;
}
@replaceableComponent("views.auth.AuthHeader")
export default class AuthHeader extends React.Component<IProps> {
public render(): React.ReactNode {
return (

View file

@ -16,9 +16,6 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthHeaderLogo")
export default class AuthHeaderLogo extends React.PureComponent {
public render(): React.ReactNode {
return <div className="mx_AuthHeaderLogo">

View file

@ -18,10 +18,8 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthFooter from "./AuthFooter";
@replaceableComponent("views.auth.AuthPage")
export default class AuthPage extends React.PureComponent {
public render(): React.ReactNode {
return (

View file

@ -18,7 +18,6 @@ import React, { createRef } from 'react';
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
const DIV_ID = 'mx_recaptcha';
@ -35,7 +34,6 @@ interface ICaptchaFormState {
/**
* A pure UI component which displays a captcha form.
*/
@replaceableComponent("views.auth.CaptchaForm")
export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICaptchaFormState> {
static defaultProps = {
onCaptchaResponse: () => {},

View file

@ -16,9 +16,6 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.CompleteSecurityBody")
export default class CompleteSecurityBody extends React.PureComponent {
public render(): React.ReactNode {
return <div className="mx_CompleteSecurityBody">

View file

@ -19,7 +19,6 @@ import React from 'react';
import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber';
import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Dropdown from "../elements/Dropdown";
const COUNTRIES_BY_ISO2 = {};
@ -53,7 +52,6 @@ interface IState {
defaultCountry: PhoneNumberCountryDefinition;
}
@replaceableComponent("views.auth.CountryDropdown")
export default class CountryDropdown extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { PureComponent, RefCallback, RefObject } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Field, { IInputProps } from "../elements/Field";
import { _t, _td } from "../../../languageHandler";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
@ -39,7 +38,6 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
onValidate?(result: IValidationResult): void;
}
@replaceableComponent("views.auth.EmailField")
class EmailField extends PureComponent<IProps> {
static defaultProps = {
label: _td("Email"),

View file

@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { LocalisedPolicy, Policies } from '../../../Terms';
import Field from '../elements/Field';
import CaptchaForm from "./CaptchaForm";
@ -93,7 +92,6 @@ interface IPasswordAuthEntryState {
password: string;
}
@replaceableComponent("views.auth.PasswordAuthEntry")
export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswordAuthEntryState> {
static LOGIN_TYPE = AuthType.Password;
@ -191,7 +189,6 @@ interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
}
/* eslint-enable camelcase */
@replaceableComponent("views.auth.RecaptchaAuthEntry")
export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps> {
static LOGIN_TYPE = AuthType.Recaptcha;
@ -262,7 +259,6 @@ interface ITermsAuthEntryState {
errorText?: string;
}
@replaceableComponent("views.auth.TermsAuthEntry")
export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITermsAuthEntryState> {
static LOGIN_TYPE = AuthType.Terms;
@ -409,7 +405,6 @@ interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
};
}
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
static LOGIN_TYPE = AuthType.Email;
@ -472,7 +467,6 @@ interface IMsisdnAuthEntryState {
errorText: string;
}
@replaceableComponent("views.auth.MsisdnAuthEntry")
export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> {
static LOGIN_TYPE = AuthType.Msisdn;
@ -623,7 +617,6 @@ interface ISSOAuthEntryState {
attemptFailed: boolean;
}
@replaceableComponent("views.auth.SSOAuthEntry")
export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEntryState> {
static LOGIN_TYPE = AuthType.Sso;
static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable;
@ -743,7 +736,6 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
}
}
@replaceableComponent("views.auth.FallbackAuthEntry")
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
private popupWindow: Window;
private fallbackButton = createRef<HTMLButtonElement>();

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { PureComponent, RefCallback, RefObject } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Field, { IInputProps } from "../elements/Field";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t, _td } from "../../../languageHandler";
@ -35,7 +34,6 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
onValidate?(result: IValidationResult);
}
@replaceableComponent("views.auth.EmailField")
class PassphraseConfirmField extends PureComponent<IProps> {
static defaultProps = {
label: _td("Confirm password"),

View file

@ -22,7 +22,6 @@ import SdkConfig from "../../../SdkConfig";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t, _td } from "../../../languageHandler";
import Field, { IInputProps } from "../elements/Field";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends Omit<IInputProps, "onValidate"> {
autoFocus?: boolean;
@ -41,7 +40,6 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
onValidate?(result: IValidationResult);
}
@replaceableComponent("views.auth.PassphraseField")
class PassphraseField extends PureComponent<IProps> {
static defaultProps = {
label: _td("Password"),

View file

@ -24,7 +24,6 @@ import AccessibleButton from "../elements/AccessibleButton";
import withValidation, { IValidationResult } from "../elements/Validation";
import Field from "../elements/Field";
import CountryDropdown from "./CountryDropdown";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import EmailField from "./EmailField";
// For validating phone numbers without country codes
@ -66,7 +65,6 @@ enum LoginField {
* A pure UI component which displays a username/password form.
* The email/username/phone fields are fully-controlled, the password field is not.
*/
@replaceableComponent("views.auth.PasswordLogin")
export default class PasswordLogin extends React.PureComponent<IProps, IState> {
static defaultProps = {
onUsernameChanged: function() {},

View file

@ -31,7 +31,6 @@ import EmailField from "./EmailField";
import PassphraseField from "./PassphraseField";
import Field from '../elements/Field';
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import CountryDropdown from "./CountryDropdown";
import PassphraseConfirmField from "./PassphraseConfirmField";
@ -92,7 +91,6 @@ interface IState {
/*
* A pure UI component which displays a registration form.
*/
@replaceableComponent("views.auth.RegistrationForm")
export default class RegistrationForm extends React.PureComponent<IProps, IState> {
static defaultProps = {
onValidationChange: logger.error,

View file

@ -17,14 +17,14 @@ limitations under the License.
import React from 'react';
import classNames from "classnames";
import * as sdk from "../../../index";
import SdkConfig from '../../../SdkConfig';
import AuthPage from "./AuthPage";
import { _td } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import LanguageSelector from "./LanguageSelector";
import EmbeddedPage from "../../structures/EmbeddedPage";
import { MATRIX_LOGO_HTML } from "../../structures/static-page-vars";
// translatable strings for Welcome pages
_td("Sign in with SSO");
@ -33,12 +33,8 @@ interface IProps {
}
@replaceableComponent("views.auth.Welcome")
export default class Welcome extends React.PureComponent<IProps> {
public render(): React.ReactNode {
// FIXME: Using an import will result in wrench-element-tests failures
const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage");
const pagesConfig = SdkConfig.getObject("embedded_pages");
let pageUrl = null;
if (pagesConfig) {
@ -59,6 +55,8 @@ export default class Welcome extends React.PureComponent<IProps> {
replaceMap={{
"$riot:ssoUrl": "#/start_sso",
"$riot:casUrl": "#/start_cas",
"$matrixLogo": MATRIX_LOGO_HTML,
"[matrix]": MATRIX_LOGO_HTML,
}}
/>
<LanguageSelector />

View file

@ -32,7 +32,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
import TooltipTarget from "../elements/TooltipTarget";
@ -78,7 +77,6 @@ function tooltipText(variant: Icon) {
}
}
@replaceableComponent("views.avatars.DecoratedRoomAvatar")
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
private _dmUser: User;
private isUnmounted = false;

View file

@ -23,9 +23,8 @@ import { logger } from "matrix-js-sdk/src/logger";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import BaseAvatar from "./BaseAvatar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { CardContext } from '../right_panel/BaseCard';
import { CardContext } from '../right_panel/context';
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import SettingsStore from "../../../settings/SettingsStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -52,7 +51,6 @@ interface IState {
imageUrl?: string;
}
@replaceableComponent("views.avatars.MemberAvatar")
export default class MemberAvatar extends React.PureComponent<IProps, IState> {
public static defaultProps = {
width: 40,

View file

@ -28,7 +28,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar';
import DMRoomMap from "../../../utils/DMRoomMap";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { IOOBData } from '../../../stores/ThreepidInviteStore';
@ -52,7 +51,6 @@ interface IState {
urls: string[];
}
@replaceableComponent("views.avatars.RoomAvatar")
export default class RoomAvatar extends React.Component<IProps, IState> {
public static defaultProps = {
width: 36,

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, { useContext } from 'react';
import maplibregl from 'maplibre-gl';
import {
Beacon,
BeaconEvent,
} from 'matrix-js-sdk/src/matrix';
import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import SmartMarker from '../location/SmartMarker';
interface Props {
map: maplibregl.Map;
beacon: Beacon;
}
/**
* Updates a map SmartMarker with latest location from given beacon
*/
const BeaconMarker: React.FC<Props> = ({ map, beacon }) => {
const latestLocationState = useEventEmitterState(
beacon,
BeaconEvent.LocationUpdate,
() => beacon.latestLocationState,
);
const matrixClient = useContext(MatrixClientContext);
const room = matrixClient.getRoom(beacon.roomId);
if (!latestLocationState || !beacon.isLive) {
return null;
}
const geoUri = latestLocationState?.uri;
const markerRoomMember = beacon.beaconInfo.assetType === LocationAssetType.Self ?
room.getMember(beacon.beaconInfoOwner) :
undefined;
return <SmartMarker
map={map}
id={beacon.identifier}
geoUri={geoUri}
roomMember={markerRoomMember}
useMemberColor
/>;
};
export default BeaconMarker;

View file

@ -0,0 +1,84 @@
/*
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, { HTMLProps } from 'react';
import classNames from 'classnames';
import { Beacon } from 'matrix-js-sdk/src/matrix';
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
import { _t } from '../../../languageHandler';
import LiveTimeRemaining from './LiveTimeRemaining';
import { BeaconDisplayStatus } from './displayStatus';
import { getBeaconExpiryTimestamp } from '../../../utils/beacon';
import { formatTime } from '../../../DateUtils';
interface Props {
displayStatus: BeaconDisplayStatus;
displayLiveTimeRemaining?: boolean;
beacon?: Beacon;
label?: string;
}
const BeaconExpiryTime: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
const expiryTime = formatTime(new Date(getBeaconExpiryTimestamp(beacon)));
return <span className='mx_BeaconStatus_expiryTime'>{ _t('Live until %(expiryTime)s', { expiryTime }) }</span>;
};
const BeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> =
({
beacon,
displayStatus,
displayLiveTimeRemaining,
label,
className,
children,
...rest
}) => {
const isIdle = displayStatus === BeaconDisplayStatus.Loading ||
displayStatus === BeaconDisplayStatus.Stopped;
return <div
{...rest}
className={classNames('mx_BeaconStatus', `mx_BeaconStatus_${displayStatus}`, className)}
>
<StyledLiveBeaconIcon
className='mx_BeaconStatus_icon'
withError={displayStatus === BeaconDisplayStatus.Error}
isIdle={isIdle}
/>
<div className='mx_BeaconStatus_description'>
{ displayStatus === BeaconDisplayStatus.Loading && <span>{ _t('Loading live location...') }</span> }
{ displayStatus === BeaconDisplayStatus.Stopped && <span>{ _t('Live location ended') }</span> }
{ displayStatus === BeaconDisplayStatus.Error && <span>{ _t('Live location error') }</span> }
{ displayStatus === BeaconDisplayStatus.Active && beacon && <>
<>
{ label }
{ displayLiveTimeRemaining ?
<LiveTimeRemaining beacon={beacon} /> :
<BeaconExpiryTime beacon={beacon} />
}
</>
</>
}
</div>
{ children }
</div>;
};
export default BeaconStatus;

View file

@ -0,0 +1,116 @@
/*
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 { MatrixClient } from 'matrix-js-sdk/src/client';
import {
Beacon,
Room,
} from 'matrix-js-sdk/src/matrix';
import maplibregl from 'maplibre-gl';
import { useLiveBeacons } from '../../../utils/beacon/useLiveBeacons';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import BaseDialog from "../dialogs/BaseDialog";
import { IDialogProps } from "../dialogs/IDialogProps";
import Map from '../location/Map';
import ZoomButtons from '../location/ZoomButtons';
import BeaconMarker from './BeaconMarker';
import { Bounds, getBeaconBounds } from '../../../utils/beacon/bounds';
import { getGeoUri } from '../../../utils/beacon';
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
interface IProps extends IDialogProps {
roomId: Room['roomId'];
matrixClient: MatrixClient;
// open the map centered on this beacon's location
focusBeacon?: Beacon;
}
const getBoundsCenter = (bounds: Bounds): string | undefined => {
if (!bounds) {
return;
}
return getGeoUri({
latitude: (bounds.north + bounds.south) / 2,
longitude: (bounds.east + bounds.west) / 2,
timestamp: Date.now(),
});
};
/**
* Dialog to view live beacons maximised
*/
const BeaconViewDialog: React.FC<IProps> = ({
focusBeacon,
roomId,
matrixClient,
onFinished,
}) => {
const liveBeacons = useLiveBeacons(roomId, matrixClient);
const bounds = getBeaconBounds(liveBeacons);
const centerGeoUri = focusBeacon?.latestLocationState?.uri || getBoundsCenter(bounds);
return (
<BaseDialog
className='mx_BeaconViewDialog'
onFinished={onFinished}
fixedWidth={false}
>
<MatrixClientContext.Provider value={matrixClient}>
{ !!bounds ? <Map
id='mx_BeaconViewDialog'
bounds={bounds}
centerGeoUri={centerGeoUri}
interactive
className="mx_BeaconViewDialog_map"
>
{
({ map }: { map: maplibregl.Map}) =>
<>
{ liveBeacons.map(beacon => <BeaconMarker
key={beacon.identifier}
map={map}
beacon={beacon}
/>) }
<ZoomButtons map={map} />
</>
}
</Map> :
<div
data-test-id='beacon-view-dialog-map-fallback'
className='mx_BeaconViewDialog_map mx_BeaconViewDialog_mapFallback'
>
<LocationIcon className='mx_BeaconViewDialog_mapFallbackIcon' />
<span className='mx_BeaconViewDialog_mapFallbackMessage'>{ _t('No live locations') }</span>
<AccessibleButton
kind='primary'
onClick={onFinished}
data-test-id='beacon-view-dialog-fallback-close'
>
{ _t('Close') }
</AccessibleButton>
</div>
}
</MatrixClientContext.Provider>
</BaseDialog>
);
};
export default BeaconViewDialog;

View file

@ -0,0 +1,75 @@
/*
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, { useCallback, useEffect, useState } from 'react';
import { BeaconEvent, Beacon } from 'matrix-js-sdk/src/matrix';
import { formatDuration } from '../../../DateUtils';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import { useInterval } from '../../../hooks/useTimeout';
import { _t } from '../../../languageHandler';
import { getBeaconMsUntilExpiry } from '../../../utils/beacon';
const MINUTE_MS = 60000;
const HOUR_MS = MINUTE_MS * 60;
const getUpdateInterval = (ms: number) => {
// every 10 mins when more than an hour
if (ms > HOUR_MS) {
return MINUTE_MS * 10;
}
// every minute when more than a minute
if (ms > MINUTE_MS) {
return MINUTE_MS;
}
// otherwise every second
return 1000;
};
const useMsRemaining = (beacon: Beacon): number => {
const beaconInfo = useEventEmitterState(
beacon,
BeaconEvent.Update,
() => beacon.beaconInfo,
);
const [msRemaining, setMsRemaining] = useState(() => getBeaconMsUntilExpiry(beaconInfo));
useEffect(() => {
setMsRemaining(getBeaconMsUntilExpiry(beaconInfo));
}, [beaconInfo]);
const updateMsRemaining = useCallback(() => {
const ms = getBeaconMsUntilExpiry(beaconInfo);
setMsRemaining(ms);
}, [beaconInfo]);
useInterval(updateMsRemaining, getUpdateInterval(msRemaining));
return msRemaining;
};
const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
const msRemaining = useMsRemaining(beacon);
const timeRemaining = formatDuration(msRemaining);
const liveTimeRemaining = _t(`%(timeRemaining)s left`, { timeRemaining });
return <span
data-test-id='room-live-share-expiry'
className="mx_LiveTimeRemaining"
>{ liveTimeRemaining }</span>;
};
export default LiveTimeRemaining;

View file

@ -0,0 +1,89 @@
/*
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 { Beacon } from 'matrix-js-sdk/src/matrix';
import React, { HTMLProps } from 'react';
import { _t } from '../../../languageHandler';
import { useOwnLiveBeacons } from '../../../utils/beacon';
import BeaconStatus from './BeaconStatus';
import { BeaconDisplayStatus } from './displayStatus';
import AccessibleButton from '../elements/AccessibleButton';
interface Props {
displayStatus: BeaconDisplayStatus;
beacon?: Beacon;
}
/**
* Wraps BeaconStatus with more capabilities
* for errors and actions available for users own live beacons
*/
const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
beacon, displayStatus, className, ...rest
}) => {
const {
hasWireError,
hasStopSharingError,
stoppingInProgress,
onStopSharing,
onResetWireError,
} = useOwnLiveBeacons([beacon?.identifier]);
// combine display status with errors that only occur for user's own beacons
const ownDisplayStatus = hasWireError || hasStopSharingError ?
BeaconDisplayStatus.Error :
displayStatus;
return <BeaconStatus
className='mx_MBeaconBody_chin'
beacon={beacon}
displayStatus={ownDisplayStatus}
label={_t('Live location enabled')}
displayLiveTimeRemaining
{...rest}
>
{ ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton
data-test-id='beacon-status-stop-beacon'
kind='link'
onClick={onStopSharing}
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
disabled={stoppingInProgress}
>
{ _t('Stop') }
</AccessibleButton>
}
{ hasWireError && <AccessibleButton
data-test-id='beacon-status-reset-wire-error'
kind='link'
onClick={onResetWireError}
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
>
{ _t('Retry') }
</AccessibleButton>
}
{ hasStopSharingError && <AccessibleButton
data-test-id='beacon-status-stop-beacon-retry'
kind='link'
onClick={onStopSharing}
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
>
{ _t('Retry') }
</AccessibleButton> }
</BeaconStatus>;
};
export default OwnBeaconStatus;

View file

@ -14,126 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useEffect, useState } from 'react';
import React from 'react';
import classNames from 'classnames';
import { Room, Beacon } from 'matrix-js-sdk/src/matrix';
import { Room } from 'matrix-js-sdk/src/matrix';
import { formatDuration } from '../../../DateUtils';
import { _t } from '../../../languageHandler';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import { useInterval } from '../../../hooks/useTimeout';
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
import { getBeaconMsUntilExpiry, sortBeaconsByLatestExpiry } from '../../../utils/beacon';
import { useOwnLiveBeacons } from '../../../utils/beacon';
import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner';
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
import { Icon as CloseIcon } from '../../../../res/img/image-view/close.svg';
const MINUTE_MS = 60000;
const HOUR_MS = MINUTE_MS * 60;
const getUpdateInterval = (ms: number) => {
// every 10 mins when more than an hour
if (ms > HOUR_MS) {
return MINUTE_MS * 10;
}
// every minute when more than a minute
if (ms > MINUTE_MS) {
return MINUTE_MS;
}
// otherwise every second
return 1000;
};
const useMsRemaining = (beacon: Beacon): number => {
const [msRemaining, setMsRemaining] = useState(() => getBeaconMsUntilExpiry(beacon));
useEffect(() => {
setMsRemaining(getBeaconMsUntilExpiry(beacon));
}, [beacon]);
const updateMsRemaining = useCallback(() => {
const ms = getBeaconMsUntilExpiry(beacon);
setMsRemaining(ms);
}, [beacon]);
useInterval(updateMsRemaining, getUpdateInterval(msRemaining));
return msRemaining;
};
/**
* It's technically possible to have multiple live beacons in one room
* Select the latest expiry to display,
* and kill all beacons on stop sharing
*/
type LiveBeaconsState = {
beacon?: Beacon;
onStopSharing?: () => void;
onResetWireError?: () => void;
stoppingInProgress?: boolean;
hasStopSharingError?: boolean;
hasWireError?: boolean;
};
const useLiveBeacons = (liveBeaconIds: string[], roomId: string): LiveBeaconsState => {
const [stoppingInProgress, setStoppingInProgress] = useState(false);
const [error, setError] = useState<Error>();
const hasWireError = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.WireError,
() =>
OwnBeaconStore.instance.hasWireErrors(roomId),
);
// reset stopping in progress on change in live ids
useEffect(() => {
setStoppingInProgress(false);
setError(undefined);
}, [liveBeaconIds]);
// select the beacon with latest expiry to display expiry time
const beacon = liveBeaconIds.map(beaconId => OwnBeaconStore.instance.getBeaconById(beaconId))
.sort(sortBeaconsByLatestExpiry)
.shift();
const onStopSharing = async () => {
setStoppingInProgress(true);
try {
await Promise.all(liveBeaconIds.map(beaconId => OwnBeaconStore.instance.stopBeacon(beaconId)));
} catch (error) {
// only clear loading in case of error
// to avoid flash of not-loading state
// after beacons have been stopped but we wait for sync
setError(error);
setStoppingInProgress(false);
}
};
const onResetWireError = () => {
liveBeaconIds.map(beaconId => OwnBeaconStore.instance.resetWireError(beaconId));
};
return {
onStopSharing,
onResetWireError,
beacon,
stoppingInProgress,
hasWireError,
hasStopSharingError: !!error,
};
};
const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
const msRemaining = useMsRemaining(beacon);
const timeRemaining = formatDuration(msRemaining);
const liveTimeRemaining = _t(`%(timeRemaining)s left`, { timeRemaining });
return <span
data-test-id='room-live-share-expiry'
className="mx_RoomLiveShareWarning_expiry"
>{ liveTimeRemaining }</span>;
};
import LiveTimeRemaining from './LiveTimeRemaining';
const getLabel = (hasWireError: boolean, hasStopSharingError: boolean): string => {
if (hasWireError) {
@ -157,7 +50,7 @@ const RoomLiveShareWarningInner: React.FC<RoomLiveShareWarningInnerProps> = ({ l
stoppingInProgress,
hasStopSharingError,
hasWireError,
} = useLiveBeacons(liveBeaconIds, roomId);
} = useOwnLiveBeacons(liveBeaconIds);
if (!beacon) {
return null;
@ -188,6 +81,7 @@ const RoomLiveShareWarningInner: React.FC<RoomLiveShareWarningInnerProps> = ({ l
{ !stoppingInProgress && !hasError && <LiveTimeRemaining beacon={beacon} /> }
<AccessibleButton
className='mx_RoomLiveShareWarning_stopButton'
data-test-id='room-live-share-primary-button'
onClick={onButtonClick}
kind='danger'

View file

@ -22,11 +22,19 @@ import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-loca
interface Props extends React.SVGProps<SVGSVGElement> {
// use error styling when true
withError?: boolean;
isIdle?: boolean;
}
const StyledLiveBeaconIcon: React.FC<Props> = ({ className, withError, ...props }) =>
const StyledLiveBeaconIcon: React.FC<Props> = ({ className, withError, isIdle, ...props }) =>
<LiveLocationIcon
{...props}
className={classNames('mx_StyledLiveBeaconIcon', className, { 'mx_StyledLiveBeaconIcon_error': withError })}
className={classNames(
'mx_StyledLiveBeaconIcon',
className,
{
'mx_StyledLiveBeaconIcon_error': withError,
'mx_StyledLiveBeaconIcon_idle': isIdle,
})}
/>;
export default StyledLiveBeaconIcon;

View file

@ -0,0 +1,42 @@
/*
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 { BeaconLocationState } from "matrix-js-sdk/src/content-helpers";
export enum BeaconDisplayStatus {
Loading = 'Loading',
Error = 'Error',
Stopped = 'Stopped',
Active = 'Active',
}
export const getBeaconDisplayStatus = (
isLive: boolean,
latestLocationState?: BeaconLocationState,
error?: Error): BeaconDisplayStatus => {
if (error) {
return BeaconDisplayStatus.Error;
}
if (!isLive) {
return BeaconDisplayStatus.Stopped;
}
if (!latestLocationState) {
return BeaconDisplayStatus.Loading;
}
if (latestLocationState) {
return BeaconDisplayStatus.Active;
}
};

View file

@ -21,13 +21,11 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import { _t } from '../../../languageHandler';
import ContextMenu, { IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
import CallHandler from '../../../CallHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps {
call: MatrixCall;
}
@replaceableComponent("views.context_menus.CallContextMenu")
export default class CallContextMenu extends React.Component<IProps> {
static propTypes = {
// js-sdk User object. Not required because it might not exist.

View file

@ -22,7 +22,6 @@ import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import ContextMenu, { IProps as IContextMenuProps } from '../../structures/ContextMenu';
import Field from "../elements/Field";
import DialPad from '../voip/DialPad';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps {
call: MatrixCall;
@ -32,7 +31,6 @@ interface IState {
value: string;
}
@replaceableComponent("views.context_menus.DialpadContextMenu")
export default class DialpadContextMenu extends React.Component<IProps, IState> {
private numberEntryFieldRef: React.RefObject<Field> = createRef();

View file

@ -16,8 +16,6 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
element: React.ReactNode;
// Function to be called when the parent window is resized
@ -30,7 +28,6 @@ interface IProps {
* This component can be used to display generic HTML content in a contextual
* menu.
*/
@replaceableComponent("views.context_menus.GenericElementContextMenu")
export default class GenericElementContextMenu extends React.Component<IProps> {
constructor(props: IProps) {
super(props);

View file

@ -16,13 +16,10 @@ limitations under the License.
import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
message: string;
}
@replaceableComponent("views.context_menus.GenericTextContextMenu")
export default class GenericTextContextMenu extends React.Component<IProps> {
public render(): JSX.Element {
return <div className="mx_Tooltip mx_Tooltip_visible" style={{ display: "block" }}>

View file

@ -1,6 +1,7 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2021 - 2022 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,12 +16,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactElement } from 'react';
import React, { createRef } from 'react';
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { Relations } from 'matrix-js-sdk/src/models/relations';
import { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
import { M_LOCATION } from 'matrix-js-sdk/src/@types/location';
import { M_POLL_START } from "matrix-events-sdk";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
@ -30,79 +30,81 @@ import Modal from '../../../Modal';
import Resend from '../../../Resend';
import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
import { canEditContent, canForward, editEvent, isContentActionable, isLocationEvent } from '../../../utils/EventUtils';
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
import { ReadPinsEventId } from "../right_panel/types";
import { Action } from "../../../dispatcher/actions";
import ReportEventDialog from '../dialogs/ReportEventDialog';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { ButtonEvent } from '../elements/AccessibleButton';
import { copyPlaintext, getSelectedText } from '../../../utils/strings';
import ContextMenu, { toRightOf } from '../../structures/ContextMenu';
import ReactionPicker from '../emojipicker/ReactionPicker';
import ViewSource from '../../structures/ViewSource';
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
import ShareDialog from '../dialogs/ShareDialog';
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { ChevronFace, IPosition } from '../../structures/ContextMenu';
import { IPosition, ChevronFace } from '../../structures/ContextMenu';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import EndPollDialog from '../dialogs/EndPollDialog';
import { isPollEnded } from '../messages/MPollBody';
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload";
import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenReportEventDialogPayload";
import { createMapSiteLink } from '../../../utils/location';
export function canCancel(status: EventStatus): boolean {
return status === EventStatus.QUEUED || status === EventStatus.NOT_SENT || status === EventStatus.ENCRYPTING;
}
export interface IEventTileOps {
isWidgetHidden(): boolean;
unhideWidget(): void;
}
export interface IOperableEventTile {
getEventTileOps(): IEventTileOps;
}
interface IProps extends IPosition {
chevronFace: ChevronFace;
/* the MatrixEvent associated with the context menu */
mxEvent: MatrixEvent;
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
// An optional EventTileOps implementation that can be used to unhide preview widgets
eventTileOps?: IEventTileOps;
// Callback called when the menu is dismissed
permalinkCreator?: RoomPermalinkCreator;
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
collapseReplyChain?(): void;
/* callback called when the menu is dismissed */
onFinished(): void;
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
// If the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding)
onCloseDialog?(): void;
getRelationsForEvent?: (
eventId: string,
relationType: string,
eventType: string
) => Relations;
// True if the menu is being used as a right click menu
rightClick?: boolean;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations;
// A permalink to the event
showPermalink?: boolean;
getRelationsForEvent?: GetRelationsForEvent;
}
interface IState {
canRedact: boolean;
canPin: boolean;
reactionPickerDisplayed: boolean;
}
@replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
state = {
canRedact: false,
canPin: false,
};
private reactButtonRef = createRef<any>(); // XXX Ref to a functional component
componentDidMount() {
constructor(props: IProps) {
super(props);
this.state = {
canRedact: false,
canPin: false,
reactionPickerDisplayed: false,
};
}
public componentDidMount() {
MatrixClientPeg.get().on(RoomMemberEvent.PowerLevel, this.checkPermissions);
this.checkPermissions();
}
componentWillUnmount() {
public componentWillUnmount(): void {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener(RoomMemberEvent.PowerLevel, this.checkPermissions);
@ -155,9 +157,10 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
};
private onReportEventClick = (): void => {
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
mxEvent: this.props.mxEvent,
}, 'mx_Dialog_reportEvent');
dis.dispatch<OpenReportEventDialogPayload>({
action: Action.OpenReportEventDialog,
event: this.props.mxEvent,
});
this.closeMenu();
};
@ -178,8 +181,8 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
};
private onForwardClick = (): void => {
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
matrixClient: MatrixClientPeg.get(),
dis.dispatch<OpenForwardDialogPayload>({
action: Action.OpenForwardDialog,
event: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator,
});
@ -234,11 +237,45 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu();
};
private onCopyPermalinkClick = (e: ButtonEvent): void => {
e.preventDefault(); // So that we don't open the permalink
copyPlaintext(this.getPermalink());
this.closeMenu();
};
private onCollapseReplyChainClick = (): void => {
this.props.collapseReplyChain();
this.closeMenu();
};
private onCopyClick = (): void => {
copyPlaintext(getSelectedText());
this.closeMenu();
};
private onEditClick = (): void => {
editEvent(this.props.mxEvent, this.context.timelineRenderingType, this.props.getRelationsForEvent);
this.closeMenu();
};
private onReplyClick = (): void => {
dis.dispatch({
action: 'reply_to_event',
event: this.props.mxEvent,
context: this.context.timelineRenderingType,
});
this.closeMenu();
};
private onReactClick = (): void => {
this.setState({ reactionPickerDisplayed: true });
};
private onCloseReactionPicker = (): void => {
this.setState({ reactionPickerDisplayed: false });
this.closeMenu();
};
private onEndPollClick = (): void => {
const matrixClient = MatrixClientPeg.get();
Modal.createTrackedDialog('End Poll', '', EndPollDialog, {
@ -259,11 +296,16 @@ 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);
}
private viewInRoom = () => {
private viewInRoom = (): void => {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
event_id: this.props.mxEvent.getId(),
@ -274,39 +316,35 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu();
};
render() {
public render(): JSX.Element {
const cli = MatrixClientPeg.get();
const me = cli.getUserId();
const mxEvent = this.props.mxEvent;
const { mxEvent, rightClick, showPermalink, eventTileOps, reactions, collapseReplyChain } = this.props;
const eventStatus = mxEvent.status;
const unsentReactionsCount = this.getUnsentReactions().length;
let openInMapSiteButton: JSX.Element;
let endPollButton: JSX.Element;
let resendReactionsButton: JSX.Element;
let redactButton: JSX.Element;
let forwardButton: JSX.Element;
let pinButton: JSX.Element;
let unhidePreviewButton: JSX.Element;
let externalURLButton: JSX.Element;
let quoteButton: JSX.Element;
let collapseReplyChain: JSX.Element;
let redactItemList: JSX.Element;
const contentActionable = isContentActionable(mxEvent);
const permalink = this.getPermalink();
// status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
if (!mxEvent.isRedacted()) {
if (unsentReactionsCount !== 0) {
resendReactionsButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconResend"
label={_t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount })}
onClick={this.onResendReactionsClick}
/>
);
}
const { timelineRenderingType, canReact, canSendMessages } = this.context;
const isThread = (
timelineRenderingType === TimelineRenderingType.Thread ||
timelineRenderingType === TimelineRenderingType.ThreadsList
);
const isThreadRootEvent = isThread && mxEvent?.getThread()?.rootEvent === mxEvent;
let resendReactionsButton: JSX.Element;
if (!mxEvent.isRedacted() && unsentReactionsCount !== 0) {
resendReactionsButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconResend"
label={_t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount })}
onClick={this.onResendReactionsClick}
/>
);
}
let redactButton: JSX.Element;
if (isSent && this.state.canRedact) {
redactButton = (
<IconizedContextMenuOption
@ -317,6 +355,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
let openInMapSiteButton: JSX.Element;
if (this.canOpenInMapSite(mxEvent)) {
const mapSiteLink = createMapSiteLink(mxEvent);
openInMapSiteButton = (
@ -336,26 +375,26 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
if (isContentActionable(mxEvent)) {
if (canForward(mxEvent)) {
forwardButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconForward"
label={_t("Forward")}
onClick={this.onForwardClick}
/>
);
}
let forwardButton: JSX.Element;
if (contentActionable && canForward(mxEvent)) {
forwardButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconForward"
label={_t("Forward")}
onClick={this.onForwardClick}
/>
);
}
if (this.state.canPin) {
pinButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPin"
label={this.isPinned() ? _t('Unpin') : _t('Pin')}
onClick={this.onPinClick}
/>
);
}
let pinButton: JSX.Element;
if (contentActionable && this.state.canPin) {
pinButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPin"
label={this.isPinned() ? _t('Unpin') : _t('Pin')}
onClick={this.onPinClick}
/>
);
}
let viewSourceButton: JSX.Element;
@ -369,40 +408,42 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
if (this.props.eventTileOps) {
if (this.props.eventTileOps.isWidgetHidden()) {
unhidePreviewButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconUnhidePreview"
label={_t("Show preview")}
onClick={this.onUnhidePreviewClick}
/>
);
}
let unhidePreviewButton: JSX.Element;
if (eventTileOps?.isWidgetHidden()) {
unhidePreviewButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconUnhidePreview"
label={_t("Show preview")}
onClick={this.onUnhidePreviewClick}
/>
);
}
let permalink: string | null = null;
let permalinkButton: ReactElement | null = null;
if (this.props.permalinkCreator) {
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
}
permalinkButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPermalink"
onClick={this.onPermalinkClick}
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",
let permalinkButton: JSX.Element;
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')}
element="a"
{
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
...{
href: permalink,
target: "_blank",
rel: "noreferrer noopener",
}
}
/>
);
}
let endPollButton: JSX.Element;
if (this.canEndPoll(mxEvent)) {
endPollButton = (
<IconizedContextMenuOption
@ -413,7 +454,8 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
if (this.props.eventTileOps) { // this event is rendered using TextualBody
let quoteButton: JSX.Element;
if (eventTileOps) { // this event is rendered using TextualBody
quoteButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconQuote"
@ -424,7 +466,9 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
}
// Bridges can provide a 'external_url' to link back to the source.
if (typeof (mxEvent.getContent().external_url) === "string" &&
let externalURLButton: JSX.Element;
if (
typeof (mxEvent.getContent().external_url) === "string" &&
isUrlPermitted(mxEvent.getContent().external_url)
) {
externalURLButton = (
@ -445,8 +489,9 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
if (this.props.collapseReplyChain) {
collapseReplyChain = (
let collapseReplyChainButton: JSX.Element;
if (collapseReplyChain) {
collapseReplyChainButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconCollapse"
label={_t("Collapse reply thread")}
@ -466,20 +511,86 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}
const { timelineRenderingType } = this.context;
const isThread = (
timelineRenderingType === TimelineRenderingType.Thread ||
timelineRenderingType === TimelineRenderingType.ThreadsList
);
const isThreadRootEvent = isThread && this.props.mxEvent.isThreadRoot;
let copyButton: JSX.Element;
if (rightClick && getSelectedText()) {
copyButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconCopy"
label={_t("Copy")}
triggerOnMouseDown={true} // We use onMouseDown so that the selection isn't cleared when we click
onClick={this.onCopyClick}
/>
);
}
const commonItemsList = (
<IconizedContextMenuOptionList>
{ isThreadRootEvent && <IconizedContextMenuOption
let editButton: JSX.Element;
if (rightClick && canEditContent(mxEvent)) {
editButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconEdit"
label={_t("Edit")}
onClick={this.onEditClick}
/>
);
}
let replyButton: JSX.Element;
if (rightClick && contentActionable && canSendMessages) {
replyButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconReply"
label={_t("Reply")}
onClick={this.onReplyClick}
/>
);
}
let reactButton;
if (rightClick && contentActionable && canReact) {
reactButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconReact"
label={_t("React")}
onClick={this.onReactClick}
inputRef={this.reactButtonRef}
/>
);
}
let viewInRoomButton: JSX.Element;
if (isThreadRootEvent) {
viewInRoomButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconViewInRoom"
label={_t("View in room")}
onClick={this.viewInRoom}
/> }
/>
);
}
let nativeItemsList: JSX.Element;
if (copyButton) {
nativeItemsList = (
<IconizedContextMenuOptionList>
{ copyButton }
</IconizedContextMenuOptionList>
);
}
let quickItemsList: JSX.Element;
if (editButton || replyButton || reactButton) {
quickItemsList = (
<IconizedContextMenuOptionList>
{ reactButton }
{ replyButton }
{ editButton }
</IconizedContextMenuOptionList>
);
}
const commonItemsList = (
<IconizedContextMenuOptionList>
{ viewInRoomButton }
{ openInMapSiteButton }
{ endPollButton }
{ quoteButton }
@ -491,10 +602,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
{ unhidePreviewButton }
{ viewSourceButton }
{ resendReactionsButton }
{ collapseReplyChain }
{ collapseReplyChainButton }
</IconizedContextMenuOptionList>
);
let redactItemList: JSX.Element;
if (redactButton) {
redactItemList = (
<IconizedContextMenuOptionList red>
@ -502,33 +614,40 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
</IconizedContextMenuOptionList>
);
}
let reactionPicker: JSX.Element;
if (this.state.reactionPickerDisplayed) {
const buttonRect = (this.reactButtonRef.current as HTMLElement)?.getBoundingClientRect();
reactionPicker = (
<ContextMenu
{...toRightOf(buttonRect)}
onFinished={this.closeMenu}
managed={false}
>
<ReactionPicker
mxEvent={mxEvent}
onFinished={this.onCloseReactionPicker}
reactions={reactions}
/>
</ContextMenu>
);
}
return (
<IconizedContextMenu
{...this.props}
className="mx_MessageContextMenu"
compact={true}
>
{ commonItemsList }
{ redactItemList }
</IconizedContextMenu>
<React.Fragment>
<IconizedContextMenu
{...this.props}
className="mx_MessageContextMenu"
compact={true}
>
{ nativeItemsList }
{ quickItemsList }
{ commonItemsList }
{ redactItemList }
</IconizedContextMenu>
{ reactionPicker }
</React.Fragment>
);
}
}
function canForward(event: MatrixEvent): boolean {
return !(
isLocationEvent(event) ||
M_POLL_START.matches(event.getType())
);
}
function isLocationEvent(event: MatrixEvent): boolean {
const eventType = event.getType();
return (
M_LOCATION.matches(eventType) ||
(
eventType === EventType.RoomMessage &&
M_LOCATION.matches(event.getContent().msgtype)
)
);
}

View file

@ -37,7 +37,7 @@ import Modal from "../../../Modal";
import ExportDialog from "../dialogs/ExportDialog";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { usePinnedEvents } from "../right_panel/PinnedMessagesCard";
import RoomViewStore from "../../../stores/RoomViewStore";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
@ -328,7 +328,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
};
const ensureViewingRoom = (ev: ButtonEvent) => {
if (RoomViewStore.getRoomId() === room.roomId) return;
if (RoomViewStore.instance.getRoomId() === room.roomId) return;
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
@ -373,7 +373,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
ev.stopPropagation();
Modal.createDialog(DevtoolsDialog, {
roomId: RoomViewStore.getRoomId(),
roomId: RoomViewStore.instance.getRoomId(),
}, "mx_DevtoolsDialog_wrapper");
onFinished();
}}

View file

@ -22,7 +22,6 @@ import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { _t } from "../../../languageHandler";
import {
leaveSpace,
shouldShowSpaceSettings,
showCreateNewRoom,
showCreateNewSubspace,
@ -30,6 +29,7 @@ import {
showSpacePreferences,
showSpaceSettings,
} from "../../../utils/space";
import { leaveSpace } from "../../../utils/leave-behaviour";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";

View file

@ -19,7 +19,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
interface IProps {
@ -32,7 +31,6 @@ interface IProps {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
export default class AskInviteAnywayDialog extends React.Component<IProps> {
private onInviteClicked = (): void => {
this.props.onInviteAnyways();

View file

@ -25,7 +25,6 @@ import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Heading from '../typography/Heading';
import { IDialogProps } from "./IDialogProps";
import { PosthogScreenTracker, ScreenName } from "../../../PosthogTrackers";
@ -81,7 +80,6 @@ interface IProps extends IDialogProps {
* Includes a div for the title, and a keypress handler which cancels the
* dialog on escape.
*/
@replaceableComponent("views.dialogs.BaseDialog")
export default class BaseDialog extends React.Component<IProps> {
private matrixClient: MatrixClient;

View file

@ -22,7 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserSettingsDialog";
import { UserTab } from "./UserTab";
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
// XXX: Keep this around for re-use in future Betas

View file

@ -24,7 +24,6 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import sendBugReport, { downloadBugReport } from '../../../rageshake/submit-rageshake';
import AccessibleButton from "../elements/AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import QuestionDialog from "./QuestionDialog";
import BaseDialog from "./BaseDialog";
import Field from '../elements/Field';
@ -50,7 +49,6 @@ interface IState {
downloadProgress: string;
}
@replaceableComponent("views.dialogs.BugReportDialog")
export default class BugReportDialog extends React.Component<IProps, IState> {
private unmounted: boolean;

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import ConfirmRedactDialog from './ConfirmRedactDialog';
import ErrorDialog from './ErrorDialog';
import BaseDialog from "./BaseDialog";
@ -45,7 +44,6 @@ interface IState {
*
* To avoid this, we keep the dialog open as long as /redact is in progress.
*/
@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
export default class ConfirmAndWaitRedactDialog extends React.PureComponent<IProps, IState> {
constructor(props) {
super(props);

View file

@ -20,7 +20,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import ErrorDialog from './ErrorDialog';
import TextInputDialog from "./TextInputDialog";
@ -31,7 +30,6 @@ interface IProps {
/*
* A dialog for confirming a redaction.
*/
@replaceableComponent("views.dialogs.ConfirmRedactDialog")
export default class ConfirmRedactDialog extends React.Component<IProps> {
render() {
return (

View file

@ -19,7 +19,6 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import MemberAvatar from '../avatars/MemberAvatar';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@ -55,7 +54,6 @@ interface IState {
* to make it obvious what is going to happen.
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
export default class ConfirmUserActionDialog extends React.Component<IProps, IState> {
static defaultProps = {
danger: false,

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@ -25,7 +24,6 @@ interface IProps {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {
private onConfirm = (): void => {
this.props.onFinished(true);

View file

@ -24,8 +24,7 @@ import SdkConfig from '../../../SdkConfig';
import withValidation, { IFieldState } from '../elements/Validation';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IOpts } from "../../../createRoom";
import Field from "../elements/Field";
import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
@ -35,6 +34,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { privateShouldBeEncrypted } from "../../../utils/rooms";
interface IProps {
type?: RoomType;
@ -58,7 +58,6 @@ interface IState {
canChangeEncryption: boolean;
}
@replaceableComponent("views.dialogs.CreateRoomDialog")
export default class CreateRoomDialog extends React.Component<IProps, IState> {
private readonly supportsRestricted: boolean;
private nameField = createRef<Field>();
@ -104,7 +103,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
createOpts.preset = Preset.PublicChat;
opts.guestAccess = false;
const { alias } = this.state;
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
createOpts.room_alias_name = alias.substring(1, alias.indexOf(":"));
} else {
// If we cannot change encryption we pass `true` for safety, the server should automatically do this for us.
opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true;

View file

@ -21,13 +21,13 @@ import { logger } from "matrix-js-sdk/src/logger";
import Analytics from '../../../Analytics';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth";
import { DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import StyledCheckbox from "../elements/StyledCheckbox";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
interface IProps {
onFinished: (success: boolean) => void;
@ -46,7 +46,6 @@ interface IState {
continueKind: string;
}
@replaceableComponent("views.dialogs.DeactivateAccountDialog")
export default class DeactivateAccountDialog extends React.Component<IProps, IState> {
constructor(props) {
super(props);
@ -124,7 +123,7 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
defaultDispatcher.fire(Action.TriggerLogout);
this.props.onFinished(true);
}).catch(e => {
logger.error(e);

View file

@ -28,7 +28,6 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
interface IProps {
@ -44,7 +43,6 @@ interface IState {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.ErrorDialog")
export default class ErrorDialog extends React.Component<IProps, IState> {
public static defaultProps = {
focus: true,

View file

@ -43,10 +43,10 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
import { roomContextDetailsText } from "../../../Rooms";
import { Action } from "../../../dispatcher/actions";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { ButtonEvent } from "../elements/AccessibleButton";
import { roomContextDetailsText } from "../../../utils/i18n-helpers";
const AVATAR_SIZE = 30;

View file

@ -32,7 +32,6 @@ import {
IPostmessageResponseData,
PostmessageAction,
} from "./HostSignupDialogTypes";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IConfigOptions } from "../../../IConfigOptions";
import { SnakedObject } from "../../../utils/SnakedObject";
@ -46,7 +45,6 @@ interface IState {
minimized: boolean;
}
@replaceableComponent("views.dialogs.HostSignupDialog")
export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
private readonly config: SnakedObject<IConfigOptions["host_signup"]>;

View file

@ -21,7 +21,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import VerificationComplete from "../verification/VerificationComplete";
import VerificationCancelled from "../verification/VerificationCancelled";
@ -54,7 +53,6 @@ interface IState {
sas: IGeneratedSas;
}
@replaceableComponent("views.dialogs.IncomingSasDialog")
export default class IncomingSasDialog extends React.Component<IProps, IState> {
private showSasEvent: ISasEvent;

View file

@ -19,8 +19,9 @@ import React, { ReactNode, KeyboardEvent } from 'react';
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {
title?: string;
@ -44,9 +45,6 @@ export default class InfoDialog extends React.Component<IProps> {
};
render() {
// FIXME: Using a regular import will break the app
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog
className="mx_InfoDialog"

Some files were not shown because too many files have changed in this diff Show more