Merge branch 'develop' into t3chguy/spaces4.9

This commit is contained in:
Michael Telatynski 2021-03-16 11:14:38 +00:00 committed by GitHub
commit 688407abe6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
403 changed files with 4997 additions and 1886 deletions

View file

@ -22,6 +22,7 @@ import classNames from "classnames";
import {Key} from "../../Keyboard";
import {Writeable} from "../../@types/common";
import {replaceableComponent} from "../../utils/replaceableComponent";
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@ -91,6 +92,7 @@ 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 class ContextMenu extends React.PureComponent<IProps, IState> {
private initialFocus: HTMLElement;
@ -467,6 +469,7 @@ export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<
return [isOpen, button, open, close, setIsOpen];
};
@replaceableComponent("structures.LegacyContextMenu")
export default class LegacyContextMenu extends ContextMenu {
render() {
return this.renderMenu(false);

View file

@ -21,7 +21,9 @@ import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import classNames from 'classnames';
import * as FormattingUtils from '../../utils/FormattingUtils';
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.CustomRoomTagPanel")
class CustomRoomTagPanel extends React.Component {
constructor(props) {
super(props);

View file

@ -16,8 +16,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import request from 'browser-request';

View file

@ -26,10 +26,12 @@ import { _t } from '../../languageHandler';
import BaseCard from "../views/right_panel/BaseCard";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice";
import {replaceableComponent} from "../../utils/replaceableComponent";
/*
* Component which shows the filtered file using a TimelinePanel
*/
@replaceableComponent("structures.FilePanel")
class FilePanel extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,

View file

@ -16,7 +16,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.GenericErrorPage")
export default class GenericErrorPage extends React.PureComponent {
static propTypes = {
title: PropTypes.object.isRequired, // jsx for title

View file

@ -30,7 +30,9 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
import SettingsStore from "../../settings/SettingsStore";
import UserTagTile from "../views/elements/UserTagTile";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.GroupFilterPanel")
class GroupFilterPanel extends React.Component {
static contextType = MatrixClientContext;

View file

@ -39,6 +39,8 @@ import {Group} from "matrix-js-sdk";
import {allSettled, sleep} from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar";
import {mediaFromMxc} from "../../customisations/Media";
import {replaceableComponent} from "../../utils/replaceableComponent";
const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1>
@ -367,8 +369,7 @@ class FeaturedUser extends React.Component {
const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
const httpUrl = MatrixClientPeg.get()
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
const httpUrl = mediaFromMxc(this.props.summaryInfo.avatar_url).getSquareThumbnailHttp(64);
const deleteButton = this.props.editing ?
<img
@ -391,6 +392,7 @@ class FeaturedUser extends React.Component {
const GROUP_JOINPOLICY_OPEN = "open";
const GROUP_JOINPOLICY_INVITE = "invite";
@replaceableComponent("structures.GroupView")
export default class GroupView extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
@ -979,10 +981,9 @@ export default class GroupView extends React.Component {
<Spinner />
</div>;
}
const httpInviterAvatar = this.state.inviterProfile ?
this._matrixClient.mxcUrlToHttp(
this.state.inviterProfile.avatarUrl, 36, 36,
) : null;
const httpInviterAvatar = this.state.inviterProfile
? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36)
: null;
const inviter = group.inviter || {};
let inviterName = inviter.userId;

View file

@ -22,11 +22,13 @@ import {
import { _t } from "../../languageHandler";
import { HostSignupStore } from "../../stores/HostSignupStore";
import SdkConfig from "../../SdkConfig";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps {}
interface IState {}
@replaceableComponent("structures.HostSignupAction")
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
private openDialog = async () => {
await HostSignupStore.instance.setHostSignupActive(true);

View file

@ -17,7 +17,9 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.IndicatorScrollbar")
export default class IndicatorScrollbar extends React.Component {
static propTypes = {
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator

View file

@ -22,9 +22,11 @@ import PropTypes from 'prop-types';
import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
import * as sdk from '../../index';
import {replaceableComponent} from "../../utils/replaceableComponent";
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
@replaceableComponent("structures.InteractiveAuthComponent")
export default class InteractiveAuthComponent extends React.Component {
static propTypes = {
// matrix client to use for UI auth requests

View file

@ -36,9 +36,10 @@ import {Key} from "../../Keyboard";
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
import {replaceableComponent} from "../../utils/replaceableComponent";
import {mediaFromMxc} from "../../customisations/Media";
interface IProps {
isMinimized: boolean;
@ -59,6 +60,7 @@ const cssClasses = [
"mx_RoomSublist_showNButton",
];
@replaceableComponent("structures.LeftPanel")
export default class LeftPanel extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private groupFilterPanelWatcherRef: string;
@ -118,7 +120,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
if (settingBgMxc) {
avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize);
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
}
const avatarUrlProp = `url(${avatarUrl})`;

View file

@ -57,6 +57,7 @@ import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@ -129,6 +130,7 @@ 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

@ -17,7 +17,9 @@ limitations under the License.
import React from 'react';
import { Resizable } from 're-resizable';
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.MainSplit")
export default class MainSplit extends React.Component {
_onResizeStart = () => {
this.props.resizeNotifier.startResizing();

View file

@ -84,6 +84,7 @@ import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import SpaceStore from "../../stores/SpaceStore";
import SpaceRoomDirectory from "./SpaceRoomDirectory";
import {replaceableComponent} from "../../utils/replaceableComponent";
/** constants for MatrixChat.state.view */
export enum Views {
@ -208,6 +209,7 @@ interface IState {
roomJustCreatedOpts?: IOpts;
}
@replaceableComponent("structures.MatrixChat")
export default class MatrixChat extends React.PureComponent<IProps, IState> {
static displayName = "MatrixChat";
@ -580,6 +582,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
break;
case 'logout':
dis.dispatch({action: "hangup_all"});
Lifecycle.logout();
break;
case 'require_registration':

View file

@ -34,6 +34,7 @@ import {textForEvent} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
import {replaceableComponent} from "../../utils/replaceableComponent";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message'];
@ -66,6 +67,7 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType()
/* (almost) stateless UI component which builds the event tiles in the room timeline.
*/
@replaceableComponent("structures.MessagePanel")
export default class MessagePanel extends React.Component {
static propTypes = {
// true to give the component a 'display: none' style.
@ -498,6 +500,9 @@ export default class MessagePanel extends React.Component {
let prevEvent = null; // the last event we showed
// Note: the EventTile might still render a "sent/sending receipt" independent of
// this information. When not providing read receipt information, the tile is likely
// to assume that sent receipts are to be shown more often.
this._readReceiptsByEvent = {};
if (this.props.showReadReceipts) {
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
@ -534,10 +539,17 @@ export default class MessagePanel extends React.Component {
const nextEvent = i < this.props.events.length - 1
? this.props.events[i + 1]
: null;
// The next event with tile is used to to determine the 'last successful' flag
// when rendering the tile. The shouldShowEvent function is pretty quick at what
// it does, so this should have no significant cost even when a room is used for
// not-chat purposes.
const nextTile = this.props.events.slice(i + 1).find(e => this._shouldShowEvent(e));
// make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent));
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextTile));
prevEvent = mxEv;
}
@ -553,7 +565,7 @@ export default class MessagePanel extends React.Component {
return ret;
}
_getTilesForEvent(prevEvent, mxEv, last, nextEvent) {
_getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) {
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
@ -595,6 +607,30 @@ export default class MessagePanel extends React.Component {
const readReceipts = this._readReceiptsByEvent[eventId];
let isLastSuccessful = false;
const isSentState = s => !s || s === 'sent';
const isSent = isSentState(mxEv.getAssociatedStatus());
const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent);
if (!hasNextEvent && isSent) {
isLastSuccessful = true;
} else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
isLastSuccessful = true;
}
// This is a bit nuanced, but if our next event is hidden but a future event is not
// hidden then we're not the last successful.
if (
nextEventWithTile &&
nextEventWithTile !== nextEvent &&
isSentState(nextEventWithTile.getAssociatedStatus())
) {
isLastSuccessful = false;
}
// We only want to consider "last successful" if the event is sent by us, otherwise of course
// it's successful: we received it.
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
// use txnId as key if available so that we don't remount during sending
ret.push(
<li
@ -620,6 +656,7 @@ export default class MessagePanel extends React.Component {
permalinkCreator={this.props.permalinkCreator}
last={last}
lastInSection={willWantDateSeparator}
lastSuccessful={isLastSuccessful}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}

View file

@ -24,7 +24,9 @@ import dis from '../../dispatcher/dispatcher';
import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.MyGroups")
export default class MyGroups extends React.Component {
static contextType = MatrixClientContext;

View file

@ -18,6 +18,7 @@ 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 {
}
@ -26,6 +27,7 @@ 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

@ -23,10 +23,12 @@ import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
import BaseCard from "../views/right_panel/BaseCard";
import {replaceableComponent} from "../../utils/replaceableComponent";
/*
* Component which shows the global notification list using a TimelinePanel
*/
@replaceableComponent("structures.NotificationPanel")
class NotificationPanel extends React.Component {
static propTypes = {
onClose: PropTypes.func.isRequired,

View file

@ -34,7 +34,9 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.RightPanel")
export default class RightPanel extends React.Component {
static get propTypes() {
return {

View file

@ -27,13 +27,14 @@ import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore";
import CountlyAnalytics from "../../CountlyAnalytics";
import {replaceableComponent} from "../../utils/replaceableComponent";
import {mediaFromMxc} from "../../customisations/Media";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800;
@ -42,6 +43,7 @@ function track(action) {
Analytics.trackEvent('RoomDirectory', action);
}
@replaceableComponent("structures.RoomDirectory")
export default class RoomDirectory extends React.Component {
static propTypes = {
initialText: PropTypes.string,
@ -519,10 +521,9 @@ export default class RoomDirectory extends React.Component {
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyAndSanitizeHtml(topic);
const avatarUrl = getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
room.avatar_url, 32, 32, "crop",
);
let avatarUrl = null;
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
return [
<div key={ `${room.room_id}_avatar` }
onClick={(ev) => this.onRoomClicked(room, ev)}

View file

@ -25,6 +25,7 @@ import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps {
isMinimized: boolean;
@ -37,6 +38,7 @@ interface IState {
focused: boolean;
}
@replaceableComponent("structures.RoomSearch")
export default class RoomSearch extends React.PureComponent<IProps, IState> {
private dispatcherRef: string;
private inputRef: React.RefObject<HTMLInputElement> = createRef();

View file

@ -23,6 +23,7 @@ import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions";
import {replaceableComponent} from "../../utils/replaceableComponent";
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
@ -35,6 +36,7 @@ function getUnsentMessages(room) {
});
}
@replaceableComponent("structures.RoomStatusBar")
export default class RoomStatusBar extends React.Component {
static propTypes = {
// the room this statusbar is representing.

View file

@ -82,6 +82,7 @@ import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutS
import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
import {replaceableComponent} from "../../utils/replaceableComponent";
const DEBUG = false;
let debuglog = function(msg: string) {};
@ -195,6 +196,7 @@ export interface IState {
dragCounter: number;
}
@replaceableComponent("structures.RoomView")
export default class RoomView extends React.Component<IProps, IState> {
private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription;
@ -711,9 +713,9 @@ export default class RoomView extends React.Component<IProps, IState> {
[payload.file], this.state.room.roomId, this.context);
break;
case 'notifier_enabled':
case 'upload_started':
case 'upload_finished':
case 'upload_canceled':
case Action.UploadStarted:
case Action.UploadFinished:
case Action.UploadCanceled:
this.forceUpdate();
break;
case 'call_state': {
@ -1911,7 +1913,7 @@ export default class RoomView extends React.Component<IProps, IState> {
);
}
if (this.state.room?.isSpaceRoom()) {
if (SettingsStore.getValue("feature_spaces") && this.state.room?.isSpaceRoom()) {
return <SpaceRoomView
space={this.state.room}
justCreatedOpts={this.props.justCreatedOpts}

View file

@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
const DEBUG_SCROLL = false;
@ -83,6 +84,7 @@ if (DEBUG_SCROLL) {
* offset as normal.
*/
@replaceableComponent("structures.ScrollPanel")
export default class ScrollPanel extends React.Component {
static propTypes = {
/* stickyBottom: if set to true, then once the user hits the bottom of

View file

@ -22,7 +22,9 @@ import dis from '../../dispatcher/dispatcher';
import {throttle} from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames';
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.SearchBox")
export default class SearchBox extends React.Component {
static propTypes = {
onSearch: PropTypes.func,

View file

@ -34,6 +34,7 @@ import {EnhancedMap} from "../../utils/maps";
import StyledCheckbox from "../views/elements/StyledCheckbox";
import AutoHideScrollbar from "./AutoHideScrollbar";
import BaseAvatar from "../views/avatars/BaseAvatar";
import {mediaFromMxc} from "../../customisations/Media";
interface IProps {
space: Room;
@ -64,6 +65,7 @@ export interface ISpaceSummaryEvent {
state_key: string;
content: {
order?: string;
suggested?: boolean;
auto_join?: boolean;
via?: string;
};
@ -91,7 +93,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
const evContent = event?.getContent();
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get();
@ -102,12 +104,12 @@ const SubSpace: React.FC<ISubspaceProps> = ({
let actions;
if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setAutoJoin = () => {
_setAutoJoin(v => {
const setSuggested = () => {
_setSuggested(v => {
queueAction({
event,
removed,
autoJoin: !v,
suggested: !v,
});
return !v;
});
@ -118,7 +120,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
queueAction({
event,
removed: !v,
autoJoin,
suggested,
});
return !v;
});
@ -131,7 +133,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
} else {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
<StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>;
}
} else {
@ -157,12 +159,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
let url: string;
if (space.avatar_url) {
url = MatrixClientPeg.get().mxcUrlToHttp(
space.avatar_url,
Math.floor(24 * window.devicePixelRatio),
Math.floor(24 * window.devicePixelRatio),
"crop",
);
url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio));
}
return <div className="mx_SpaceRoomDirectory_subspace">
@ -182,8 +179,8 @@ const SubSpace: React.FC<ISubspaceProps> = ({
interface IAction {
event: MatrixEvent;
suggested: boolean;
removed: boolean;
autoJoin: boolean;
}
interface IRoomTileProps {
@ -199,7 +196,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
const evContent = event?.getContent();
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get();
@ -209,12 +206,12 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
let actions;
if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setAutoJoin = () => {
_setAutoJoin(v => {
const setSuggested = () => {
_setSuggested(v => {
queueAction({
event,
removed,
autoJoin: !v,
suggested: !v,
});
return !v;
});
@ -225,7 +222,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
queueAction({
event,
removed: !v,
autoJoin,
suggested,
});
return !v;
});
@ -238,7 +235,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
} else {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
<StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>;
}
} else {
@ -264,12 +261,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
let url: string;
if (room.avatar_url) {
url = cli.mxcUrlToHttp(
room.avatar_url,
Math.floor(32 * window.devicePixelRatio),
Math.floor(32 * window.devicePixelRatio),
"crop",
);
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(32 * window.devicePixelRatio));
}
const content = <React.Fragment>
@ -445,10 +437,10 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
const onSaveButtonClicked = () => {
// TODO setBusy
pendingActions.current.forEach(({event, autoJoin, removed}) => {
pendingActions.current.forEach(({event, suggested, removed}) => {
const content = {
...event.getContent(),
auto_join: autoJoin,
suggested,
};
if (removed) {
@ -463,7 +455,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
if (isEditing) {
adminButton = <React.Fragment>
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
<span>{ _t("All users join by default") }</span>
<span>{ _t("Promoted to users") }</span>
</React.Fragment>;
} else {
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;

View file

@ -94,26 +94,95 @@ const useMyRoomMembership = (room: Room) => {
return membership;
};
const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space);
const joinRule = space.getJoinRule();
const userId = cli.getUserId();
let inviterSection;
let joinButtons;
if (myMembership === "invite") {
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
<FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} />
<AccessibleButton kind="link" onClick={onRejectButtonClicked}>
{_t("Decline")}
</AccessibleButton>
</div>;
} else if (myMembership !== "join" && joinRule === "public") {
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
<FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
</div>;
const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender();
const inviter = inviteSender && space.getMember(inviteSender);
if (inviteSender) {
inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
<MemberAvatar member={inviter} width={32} height={32} />
<div>
<div className="mx_SpaceRoomView_preview_inviter_name">
{ _t("<inviter/> invites you", {}, {
inviter: () => <b>{ inviter.name || inviteSender }</b>,
}) }
</div>
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
{ inviteSender }
</div> : null }
</div>
</div>;
}
joinButtons = <>
<FormButton label={_t("Reject")} kind="secondary" onClick={onRejectButtonClicked} />
<FormButton label={_t("Accept")} onClick={onJoinButtonClicked} />
</>;
} else {
joinButtons = <FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
}
let visibilitySection;
if (space.getJoinRule() === "public") {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_public">
{ _t("Public space") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_private">
{ _t("Private space") }
</span>;
}
return <div className="mx_SpaceRoomView_preview">
{ inviterSection }
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<h1 className="mx_SpaceRoomView_preview_name">
<RoomName room={space} />
</h1>
<div className="mx_SpaceRoomView_preview_info">
{ visibilitySection }
<RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
className="mx_SpaceRoomView_preview_memberCount"
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount>
</div>
<RoomTopic room={space}>
{(topic, ref) =>
<div className="mx_SpaceRoomView_preview_topic" ref={ref}>
{ topic }
</div>
}
</RoomTopic>
<div className="mx_SpaceRoomView_preview_joinButtons">
{ joinButtons }
</div>
</div>;
};
const SpaceLanding = ({ space }) => {
const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space);
const userId = cli.getUserId();
let inviteButton;
if (myMembership === "join" && space.canInvite(userId)) {
inviteButton = (
@ -227,26 +296,7 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
) : null}
</RoomMemberCount>
</div> };
if (myMembership === "invite") {
const inviteSender = space.getMember(userId)?.events.member?.getSender();
const inviter = inviteSender && space.getMember(inviteSender);
if (inviteSender) {
return _t("<inviter/> invited you to <name/>", {}, {
name: tags.name,
inviter: () => inviter
? <span className="mx_SpaceRoomView_landing_inviter">
<MemberAvatar member={inviter} width={26} height={26} viewUserOnClick={true} />
{ inviter.name }
</span>
: <span className="mx_SpaceRoomView_landing_inviter">
{ inviteSender }
</span>,
}) as JSX.Element;
} else {
return _t("You have been invited to <name/>", {}, tags) as JSX.Element;
}
} else if (shouldShowSpaceSettings(cli, space)) {
if (shouldShowSpaceSettings(cli, space)) {
if (space.getJoinRule() === "public") {
return _t("Your public space <name/>", {}, tags) as JSX.Element;
} else {
@ -260,7 +310,6 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
<div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} />
</div>
{ joinButtons }
<div className="mx_SpaceRoomView_landing_adminButtons">
{ inviteButton }
{ addRoomButtons }
@ -548,16 +597,19 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
private renderBody() {
switch (this.state.phase) {
case Phase.Landing:
return <SpaceLanding
space={this.props.space}
onJoinButtonClicked={this.props.onJoinButtonClicked}
onRejectButtonClicked={this.props.onRejectButtonClicked}
/>;
if (this.props.space.getMyMembership() === "join") {
return <SpaceLanding space={this.props.space} />;
} else {
return <SpacePreview
space={this.props.space}
onJoinButtonClicked={this.props.onJoinButtonClicked}
onRejectButtonClicked={this.props.onRejectButtonClicked}
/>;
}
case Phase.PublicCreateRooms:
return <SpaceSetupFirstRooms
space={this.props.space}
title={_t("What discussions do you want to have?")}
title={_t("What are some things you want to discuss?")}
description={_t("We'll create rooms for each topic.")}
onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>;

View file

@ -20,6 +20,7 @@ import * as React from "react";
import {_t} from '../../languageHandler';
import * as sdk from "../../index";
import AutoHideScrollbar from './AutoHideScrollbar';
import {replaceableComponent} from "../../utils/replaceableComponent";
/**
* Represents a tab for the TabbedView.
@ -45,6 +46,7 @@ interface IState {
activeTabIndex: number;
}
@replaceableComponent("structures.TabbedView")
export default class TabbedView extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -37,6 +37,7 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
import {haveTileForEvent} from "../views/rooms/EventTile";
import {UIFeature} from "../../settings/UIFeature";
import {objectHasDiff} from "../../utils/objects";
import {replaceableComponent} from "../../utils/replaceableComponent";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@ -55,6 +56,7 @@ if (DEBUG) {
*
* Also responsible for handling and sending read receipts.
*/
@replaceableComponent("structures.TimelinePanel")
class TimelinePanel extends React.Component {
static propTypes = {
// The js-sdk EventTimelineSet object for the timeline sequence we are

View file

@ -17,12 +17,14 @@ limitations under the License.
import * as React from "react";
import ToastStore, {IToast} from "../../stores/ToastStore";
import classNames from "classnames";
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);

View file

@ -1,109 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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 PropTypes from 'prop-types';
import ContentMessages from '../../ContentMessages';
import dis from "../../dispatcher/dispatcher";
import filesize from "filesize";
import { _t } from '../../languageHandler';
export default class UploadBar extends React.Component {
static propTypes = {
room: PropTypes.object,
};
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
dis.unregister(this.dispatcherRef);
}
onAction = payload => {
switch (payload.action) {
case 'upload_progress':
case 'upload_finished':
case 'upload_canceled':
case 'upload_failed':
if (this.mounted) this.forceUpdate();
break;
}
};
render() {
const uploads = ContentMessages.sharedInstance().getCurrentUploads();
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
// check in RoomView
//
// uploads = [{
// roomId: this.props.room.roomId,
// loaded: 123493,
// total: 347534,
// fileName: "testing_fooble.jpg",
// }];
if (uploads.length == 0) {
return <div />;
}
let upload;
for (let i = 0; i < uploads.length; ++i) {
if (uploads[i].roomId == this.props.room.roomId) {
upload = uploads[i];
break;
}
}
if (!upload) {
return <div />;
}
const innerProgressStyle = {
width: ((upload.loaded / (upload.total || 1)) * 100) + '%',
};
let uploadedSize = filesize(upload.loaded);
const totalSize = filesize(upload.total);
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
uploadedSize = uploadedSize.replace(/ .*/, '');
}
// MUST use var name 'count' for pluralization to kick in
const uploadText = _t(
"Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)},
);
return (
<div className="mx_UploadBar">
<div className="mx_UploadBar_uploadProgressOuter">
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
</div>
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src={require("../../../res/img/fileicon.png")} width="17" height="22" />
<img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src={require("../../../res/img/cancel.svg")} width="18" height="18"
onClick={function() { ContentMessages.sharedInstance().cancelUpload(upload.promise); }}
/>
<div className="mx_UploadBar_uploadBytes">
{ uploadedSize } / { totalSize }
</div>
<div className="mx_UploadBar_uploadFilename">{ uploadText }</div>
</div>
);
}
}

View file

@ -0,0 +1,102 @@
/*
Copyright 2015, 2016, 2019, 2021 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 { Room } from "matrix-js-sdk/src/models/room";
import ContentMessages from '../../ContentMessages';
import dis from "../../dispatcher/dispatcher";
import filesize from "filesize";
import { _t } from '../../languageHandler';
import { ActionPayload } from "../../dispatcher/payloads";
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";
interface IProps {
room: Room;
}
interface IState {
currentUpload?: IUpload;
uploadsHere: IUpload[];
}
@replaceableComponent("structures.UploadBar")
export default class UploadBar extends React.Component<IProps, IState> {
private dispatcherRef: string;
private mounted: boolean;
constructor(props) {
super(props);
this.state = {uploadsHere: []};
}
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
dis.unregister(this.dispatcherRef);
}
private onAction = (payload: ActionPayload) => {
switch (payload.action) {
case Action.UploadStarted:
case Action.UploadProgress:
case Action.UploadFinished:
case Action.UploadCanceled:
case Action.UploadFailed: {
if (!this.mounted) return;
const uploads = ContentMessages.sharedInstance().getCurrentUploads();
const uploadsHere = uploads.filter(u => u.roomId === this.props.room.roomId);
this.setState({currentUpload: uploadsHere[0], uploadsHere});
break;
}
}
};
private onCancelClick = (ev) => {
ev.preventDefault();
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise);
};
render() {
if (!this.state.currentUpload) {
return null;
}
// MUST use var name 'count' for pluralization to kick in
const uploadText = _t(
"Uploading %(filename)s and %(count)s others", {
filename: this.state.currentUpload.fileName,
count: this.state.uploadsHere.length - 1,
},
);
const uploadSize = filesize(this.state.currentUpload.total);
return (
<div className="mx_UploadBar">
<div className="mx_UploadBar_filename">{uploadText} ({uploadSize})</div>
<AccessibleButton onClick={this.onCancelClick} className='mx_UploadBar_cancel' />
<ProgressBar value={this.state.currentUpload.loaded} max={this.state.currentUpload.total} />
</div>
);
}
}

View file

@ -56,6 +56,7 @@ import HostSignupAction from "./HostSignupAction";
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import RoomName from "../views/elements/RoomName";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps {
isMinimized: boolean;
@ -69,6 +70,7 @@ interface IState {
selectedSpace?: Room;
}
@replaceableComponent("structures.UserMenu")
export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef: string;
private themeWatcherRef: string;

View file

@ -23,7 +23,9 @@ import * as sdk from "../../index";
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
import HomePage from "./HomePage";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.UserView")
export default class UserView extends React.Component {
static get propTypes() {
return {

View file

@ -16,34 +16,176 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import SyntaxHighlight from '../views/elements/SyntaxHighlight';
import {_t} from "../../languageHandler";
import React from "react";
import PropTypes from "prop-types";
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
import { _t } from "../../languageHandler";
import * as sdk from "../../index";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
import { canEditContent } from "../../utils/EventUtils";
import { MatrixClientPeg } from '../../MatrixClientPeg';
import { replaceableComponent } from "../../utils/replaceableComponent";
@replaceableComponent("structures.ViewSource")
export default class ViewSource extends React.Component {
static propTypes = {
content: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
roomId: PropTypes.string.isRequired,
eventId: PropTypes.string.isRequired,
mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t('View Source')}>
<div className="mx_ViewSource_label_left">Room ID: { this.props.roomId }</div>
<div className="mx_ViewSource_label_right">Event ID: { this.props.eventId }</div>
<div className="mx_ViewSource_label_bottom" />
constructor(props) {
super(props);
<div className="mx_Dialog_content">
<SyntaxHighlight className="json">
{ JSON.stringify(this.props.content, null, 2) }
</SyntaxHighlight>
this.state = {
isEditing: false,
};
}
onBack() {
// TODO: refresh the "Event ID:" modal header
this.setState({ isEditing: false });
}
onEdit() {
this.setState({ isEditing: true });
}
// returns the dialog body for viewing the event source
viewSourceContent() {
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
const isEncrypted = mxEvent.isEncrypted();
const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private
const originalEventSource = mxEvent.event;
if (isEncrypted) {
return (
<>
<details open className="mx_ViewSource_details">
<summary>
<span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
</summary>
<SyntaxHighlight className="json">{JSON.stringify(decryptedEventSource, null, 2)}</SyntaxHighlight>
</details>
<details className="mx_ViewSource_details">
<summary>
<span className="mx_ViewSource_heading">{_t("Original event source")}</span>
</summary>
<SyntaxHighlight className="json">{JSON.stringify(originalEventSource, null, 2)}</SyntaxHighlight>
</details>
</>
);
} else {
return (
<>
<div className="mx_ViewSource_heading">{_t("Original event source")}</div>
<SyntaxHighlight className="json">{JSON.stringify(originalEventSource, null, 2)}</SyntaxHighlight>
</>
);
}
}
// returns the id of the initial message, not the id of the previous edit
getBaseEventId() {
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
const isEncrypted = mxEvent.isEncrypted();
const baseMxEvent = this.props.mxEvent;
if (isEncrypted) {
// `relates_to` field is inside the encrypted event
return mxEvent.event.content["m.relates_to"]?.event_id ?? baseMxEvent.getId();
} else {
return mxEvent.getContent()["m.relates_to"]?.event_id ?? baseMxEvent.getId();
}
}
// returns the SendCustomEvent component prefilled with the correct details
editSourceContent() {
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
const isStateEvent = mxEvent.isState();
const roomId = mxEvent.getRoomId();
const originalContent = mxEvent.getContent();
if (isStateEvent) {
return (
<MatrixClientContext.Consumer>
{(cli) => (
<SendCustomEvent
room={cli.getRoom(roomId)}
forceStateEvent={true}
onBack={() => this.onBack()}
inputs={{
eventType: mxEvent.getType(),
evContent: JSON.stringify(originalContent, null, "\t"),
stateKey: mxEvent.getStateKey(),
}}
/>
)}
</MatrixClientContext.Consumer>
);
} else {
// prefill an edit-message event
// keep only the `body` and `msgtype` fields of originalContent
const bodyToStartFrom = originalContent["m.new_content"]?.body ?? originalContent.body; // prefill the last edit body, to start editing from there
const newContent = {
"body": ` * ${bodyToStartFrom}`,
"msgtype": originalContent.msgtype,
"m.new_content": {
body: bodyToStartFrom,
msgtype: originalContent.msgtype,
},
"m.relates_to": {
rel_type: "m.replace",
event_id: this.getBaseEventId(),
},
};
return (
<MatrixClientContext.Consumer>
{(cli) => (
<SendCustomEvent
room={cli.getRoom(roomId)}
forceStateEvent={false}
forceGeneralEvent={true}
onBack={() => this.onBack()}
inputs={{
eventType: mxEvent.getType(),
evContent: JSON.stringify(newContent, null, "\t"),
}}
/>
)}
</MatrixClientContext.Consumer>
);
}
}
canSendStateEvent(mxEvent) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(mxEvent.getRoomId());
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
const isEditing = this.state.isEditing;
const roomId = mxEvent.getRoomId();
const eventId = mxEvent.getId();
const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent);
return (
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
<div>
<div className="mx_ViewSource_label_left">Room ID: {roomId}</div>
<div className="mx_ViewSource_label_left">Event ID: {eventId}</div>
<div className="mx_ViewSource_separator" />
{isEditing ? this.editSourceContent() : this.viewSourceContent()}
</div>
{!isEditing && canEdit && (
<div className="mx_Dialog_buttons">
<button onClick={() => this.onEdit()}>{_t("Edit")}</button>
</div>
)}
</BaseDialog>
);
}

View file

@ -20,13 +20,16 @@ import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import {
SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
} from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("structures.auth.CompleteSecurity")
export default class CompleteSecurity extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
@ -58,7 +61,9 @@ export default class CompleteSecurity extends React.Component {
let icon;
let title;
if (phase === PHASE_INTRO) {
if (phase === PHASE_LOADING) {
return null;
} else if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
} else if (phase === PHASE_DONE) {

View file

@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
import AuthPage from '../../views/auth/AuthPage';
import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("structures.auth.E2eSetup")
export default class E2eSetup extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,

View file

@ -27,6 +27,7 @@ import classNames from 'classnames';
import AuthPage from "../../views/auth/AuthPage";
import CountlyAnalytics from "../../../CountlyAnalytics";
import ServerPicker from "../../views/elements/ServerPicker";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// Phases
// Show the forgot password inputs
@ -38,6 +39,7 @@ const PHASE_EMAIL_SENT = 3;
// User has clicked the link in email and completed reset
const PHASE_DONE = 4;
@replaceableComponent("structures.auth.ForgotPassword")
export default class ForgotPassword extends React.Component {
static propTypes = {
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,

View file

@ -35,6 +35,7 @@ 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";
// These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n.
@ -99,6 +100,7 @@ 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,6 +30,7 @@ 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";
interface IProps {
serverConfig: ValidatedServerConfig;
@ -109,6 +110,7 @@ interface IState {
ssoFlow?: ISSOFlow;
}
@replaceableComponent("structures.auth.Registration")
export default class Registration extends React.Component<IProps, IState> {
loginLogic: Login;

View file

@ -17,17 +17,20 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import * as sdk from '../../../index';
import {
SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
PHASE_FINISHED,
} from '../../../stores/SetupEncryptionStore';
import {replaceableComponent} from "../../../utils/replaceableComponent";
function keyHasPassphrase(keyInfo) {
return (
@ -37,6 +40,7 @@ function keyHasPassphrase(keyInfo) {
);
}
@replaceableComponent("structures.auth.SetupEncryptionBody")
export default class SetupEncryptionBody extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
@ -81,6 +85,22 @@ export default class SetupEncryptionBody extends React.Component {
store.usePassPhrase();
}
_onVerifyClick = () => {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const requestPromise = cli.requestVerification(userId);
this.props.onFinished(true);
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequestPromise: requestPromise,
member: cli.getUser(userId),
onFinished: async () => {
const request = await requestPromise;
request.cancel();
},
});
}
onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skip();
@ -132,32 +152,22 @@ export default class SetupEncryptionBody extends React.Component {
</AccessibleButton>;
}
const brand = SdkConfig.get().brand;
let verifyButton;
if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
{ _t("Verify with another session") }
</AccessibleButton>;
}
return (
<div>
<p>{_t(
"Confirm your identity by verifying this login from one of your other sessions, " +
"granting it access to encrypted messages.",
"Verify this login to access your encrypted messages and " +
"prove to others that this login is really you.",
)}</p>
<p>{_t(
"This requires the latest %(brand)s on your other devices:",
{ brand },
)}</p>
<div className="mx_CompleteSecurity_clients">
<div className="mx_CompleteSecurity_clients_desktop">
<div>{_t("%(brand)s Web", { brand })}</div>
<div>{_t("%(brand)s Desktop", { brand })}</div>
</div>
<div className="mx_CompleteSecurity_clients_mobile">
<div>{_t("%(brand)s iOS", { brand })}</div>
<div>{_t("%(brand)s Android", { brand })}</div>
</div>
<p>{_t("or another cross-signing capable Matrix client")}</p>
</div>
<div className="mx_CompleteSecurity_actionRow">
{verifyButton}
{useRecoveryKeyButton}
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
{_t("Skip")}
@ -215,7 +225,7 @@ export default class SetupEncryptionBody extends React.Component {
</div>
</div>
);
} else if (phase === PHASE_BUSY) {
} else if (phase === PHASE_BUSY || phase === PHASE_LOADING) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <Spinner />;
} else {

View file

@ -26,6 +26,7 @@ import {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";
const LOGIN_VIEW = {
LOADING: 1,
@ -41,6 +42,7 @@ const FLOWS_TO_VIEWS = {
"m.login.sso": LOGIN_VIEW.SSO,
};
@replaceableComponent("structures.auth.SoftLogout")
export default class SoftLogout extends React.Component {
static propTypes = {
// Query parameters from MatrixChat

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthBody")
export default class AuthBody extends React.PureComponent {
render() {
return <div className="mx_AuthBody">

View file

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

View file

@ -18,7 +18,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthHeader")
export default class AuthHeader extends React.Component {
static propTypes = {
disableLanguageSelector: PropTypes.bool,

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthHeaderLogo")
export default class AuthHeaderLogo extends React.PureComponent {
render() {
return <div className="mx_AuthHeaderLogo">

View file

@ -18,12 +18,14 @@ import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
const DIV_ID = 'mx_recaptcha';
/**
* A pure UI component which displays a captcha form.
*/
@replaceableComponent("views.auth.CaptchaForm")
export default class CaptchaForm extends React.Component {
static propTypes = {
sitePublicKey: PropTypes.string,

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.CompleteSecurityBody")
export default class CompleteSecurityBody extends React.PureComponent {
render() {
return <div className="mx_CompleteSecurityBody">

View file

@ -22,6 +22,7 @@ import * as sdk from '../../../index';
import {COUNTRIES, getEmojiFlag} from '../../../phonenumber';
import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
const COUNTRIES_BY_ISO2 = {};
for (const c of COUNTRIES) {
@ -40,6 +41,7 @@ function countryMatchesSearchQuery(query, country) {
return false;
}
@replaceableComponent("views.auth.CountryDropdown")
export default class CountryDropdown extends React.Component {
constructor(props) {
super(props);

View file

@ -26,6 +26,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@ -75,6 +76,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics";
export const DEFAULT_PHASE = 0;
@replaceableComponent("views.auth.PasswordAuthEntry")
export class PasswordAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.password";
@ -173,6 +175,7 @@ export class PasswordAuthEntry extends React.Component {
}
}
@replaceableComponent("views.auth.RecaptchaAuthEntry")
export class RecaptchaAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.recaptcha";
@ -235,6 +238,7 @@ export class RecaptchaAuthEntry extends React.Component {
}
}
@replaceableComponent("views.auth.TermsAuthEntry")
export class TermsAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.terms";
@ -385,6 +389,7 @@ export class TermsAuthEntry extends React.Component {
}
}
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
export class EmailIdentityAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.email.identity";
@ -432,6 +437,7 @@ export class EmailIdentityAuthEntry extends React.Component {
}
}
@replaceableComponent("views.auth.MsisdnAuthEntry")
export class MsisdnAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.msisdn";
@ -578,6 +584,7 @@ export class MsisdnAuthEntry extends React.Component {
}
}
@replaceableComponent("views.auth.SSOAuthEntry")
export class SSOAuthEntry extends React.Component {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
@ -708,6 +715,7 @@ export class SSOAuthEntry extends React.Component {
}
}
@replaceableComponent("views.auth.FallbackAuthEntry")
export class FallbackAuthEntry extends React.Component {
static propTypes = {
matrixClient: PropTypes.object.isRequired,

View file

@ -22,6 +22,7 @@ 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;
@ -40,6 +41,7 @@ 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

@ -26,6 +26,7 @@ import withValidation from "../elements/Validation";
import * as Email from "../../../email";
import Field from "../elements/Field";
import CountryDropdown from "./CountryDropdown";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -66,6 +67,7 @@ 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

@ -30,6 +30,7 @@ import PassphraseField from "./PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics";
import Field from '../elements/Field';
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
import {replaceableComponent} from "../../../utils/replaceableComponent";
enum RegistrationField {
Email = "field_email",
@ -80,6 +81,7 @@ 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: console.error,

View file

@ -24,10 +24,12 @@ import {_td} from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// translatable strings for Welcome pages
_td("Sign in with SSO");
@replaceableComponent("views.auth.Welcome")
export default class Welcome extends React.PureComponent {
constructor(props) {
super(props);

View file

@ -25,6 +25,7 @@ import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units";
import {ResizeMethod} from "../../../Avatar";
interface IProps {
name: string; // The name (first initial used as default)
@ -35,7 +36,7 @@ interface IProps {
width?: number;
height?: number;
// XXX: resizeMethod not actually used.
resizeMethod?: string;
resizeMethod?: ResizeMethod;
defaultToInitialLetter?: boolean; // true to add default url
onClick?: React.MouseEventHandler;
inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;

View file

@ -30,6 +30,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {_t} from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
room: Room;
@ -68,6 +69,7 @@ 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

@ -1,5 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2021 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.
@ -15,8 +15,10 @@ limitations under the License.
*/
import React from 'react';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import BaseAvatar from './BaseAvatar';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import {ResizeMethod} from "../../../Avatar";
export interface IProps {
groupId?: string;
@ -24,10 +26,11 @@ export interface IProps {
groupAvatarUrl?: string;
width?: number;
height?: number;
resizeMethod?: string;
resizeMethod?: ResizeMethod;
onClick?: React.MouseEventHandler;
}
@replaceableComponent("views.avatars.GroupAvatar")
export default class GroupAvatar extends React.Component<IProps> {
public static defaultProps = {
width: 36,
@ -36,8 +39,8 @@ export default class GroupAvatar extends React.Component<IProps> {
};
getGroupAvatarUrl() {
return MatrixClientPeg.get().mxcUrlToHttp(
this.props.groupAvatarUrl,
if (!this.props.groupAvatarUrl) return null;
return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp(
this.props.width,
this.props.height,
this.props.resizeMethod,

View file

@ -20,15 +20,17 @@ import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import BaseAvatar from "./BaseAvatar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import {ResizeMethod} from "../../../Avatar";
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
member: RoomMember;
fallbackUserId?: string;
width: number;
height: number;
resizeMethod?: string;
resizeMethod?: ResizeMethod;
// The onClick to give the avatar
onClick?: React.MouseEventHandler;
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
@ -42,6 +44,7 @@ interface IState {
imageUrl?: string;
}
@replaceableComponent("views.avatars.MemberAvatar")
export default class MemberAvatar extends React.Component<IProps, IState> {
public static defaultProps = {
width: 40,
@ -61,18 +64,19 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
}
private static getState(props: IProps): IState {
if (props.member && props.member.name) {
return {
name: props.member.name,
title: props.title || props.member.userId,
imageUrl: props.member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
if (props.member?.name) {
let imageUrl = null;
if (props.member.getMxcAvatarUrl()) {
imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
false,
),
);
}
return {
name: props.member.name,
title: props.title || props.member.userId,
imageUrl: imageUrl,
};
} else if (props.fallbackUserId) {
return {

View file

@ -23,7 +23,9 @@ import classNames from 'classnames';
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
export default class MemberStatusMessageAvatar extends React.Component {
static propTypes = {
member: PropTypes.object.isRequired,

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React, {ComponentProps} from 'react';
import Room from 'matrix-js-sdk/src/models/room';
import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo';
import BaseAvatar from './BaseAvatar';
import ImageView from '../elements/ImageView';
@ -23,6 +22,8 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar';
import {ResizeMethod} from "../../../Avatar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
// Room may be left unset here, but if it is,
@ -42,6 +43,7 @@ interface IState {
urls: string[];
}
@replaceableComponent("views.avatars.RoomAvatar")
export default class RoomAvatar extends React.Component<IProps, IState> {
public static defaultProps = {
width: 36,
@ -88,16 +90,16 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
};
private static getImageUrls(props: IProps): string[] {
return [
getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
// Default props don't play nicely with getDerivedStateFromProps
//props.oobData !== undefined ? props.oobData.avatarUrl : {},
props.oobData.avatarUrl,
let oobAvatar = null;
if (props.oobData.avatarUrl) {
oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp(
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
), // highest priority
);
}
return [
oobAvatar, // highest priority
RoomAvatar.getRoomAvatarUrl(props),
].filter(function(url) {
return (url !== null && url !== "");

View file

@ -14,21 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {ComponentProps, useContext} from 'react';
import React, {ComponentProps} from 'react';
import classNames from 'classnames';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {IApp} from "../../../stores/WidgetStore";
import BaseAvatar, {BaseAvatarType} from "./BaseAvatar";
import {mediaFromMxc} from "../../../customisations/Media";
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls"> {
app: IApp;
}
const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 20, ...props }) => {
const cli = useContext(MatrixClientContext);
let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")];
// heuristics for some better icons until Widgets support their own icons
if (app.type.includes("jitsi")) {
@ -47,7 +44,7 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 2
name={app.id}
className={classNames("mx_WidgetAvatar", className)}
// MSC2765
url={app.avatar_url ? getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop") : undefined}
url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : undefined}
urls={iconUrls}
width={width}
height={height}

View file

@ -22,11 +22,13 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import CallHandler from '../../../CallHandler';
import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog';
import Modal from '../../../Modal';
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

@ -19,6 +19,7 @@ import { _t } from '../../../languageHandler';
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import Dialpad from '../voip/DialPad';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps {
call: MatrixCall;
@ -28,6 +29,7 @@ interface IState {
value: string;
}
@replaceableComponent("views.context_menus.DialpadContextMenu")
export default class DialpadContextMenu extends React.Component<IProps, IState> {
constructor(props) {
super(props);

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {replaceableComponent} from "../../../utils/replaceableComponent";
/*
* This component can be used to display generic HTML content in a contextual
@ -23,6 +24,7 @@ import PropTypes from 'prop-types';
*/
@replaceableComponent("views.context_menus.GenericElementContextMenu")
export default class GenericElementContextMenu extends React.Component {
static propTypes = {
element: PropTypes.element.isRequired,

View file

@ -16,7 +16,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.GenericTextContextMenu")
export default class GenericTextContextMenu extends React.Component {
static propTypes = {
message: PropTypes.string.isRequired,

View file

@ -23,7 +23,9 @@ import Modal from '../../../Modal';
import {Group} from 'matrix-js-sdk';
import GroupStore from "../../../stores/GroupStore";
import {MenuItem} from "../../structures/ContextMenu";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.GroupInviteTileContextMenu")
export default class GroupInviteTileContextMenu extends React.Component {
static propTypes = {
group: PropTypes.instanceOf(Group).isRequired,

View file

@ -32,11 +32,13 @@ import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
import {MenuItem} from "../../structures/ContextMenu";
import {EventType} from "matrix-js-sdk/src/@types/event";
import {replaceableComponent} from "../../../utils/replaceableComponent";
function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
}
@replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component {
static propTypes = {
/* the MatrixEvent associated with the context menu */
@ -124,24 +126,9 @@ export default class MessageContextMenu extends React.Component {
};
onViewSourceClick = () => {
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
roomId: ev.getRoomId(),
eventId: ev.getId(),
content: ev.event,
}, 'mx_Dialog_viewsource');
this.closeMenu();
};
onViewClearSourceClick = () => {
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
roomId: ev.getRoomId(),
eventId: ev.getId(),
// FIXME: _clearEvent is private
content: ev._clearEvent,
mxEvent: this.props.mxEvent,
}, 'mx_Dialog_viewsource');
this.closeMenu();
};
@ -309,7 +296,6 @@ export default class MessageContextMenu extends React.Component {
let cancelButton;
let forwardButton;
let pinButton;
let viewClearSourceButton;
let unhidePreviewButton;
let externalURLButton;
let quoteButton;
@ -389,14 +375,6 @@ export default class MessageContextMenu extends React.Component {
</MenuItem>
);
if (mxEvent.getType() !== mxEvent.getWireType()) {
viewClearSourceButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
{ _t('View Decrypted Source') }
</MenuItem>
);
}
if (this.props.eventTileOps) {
if (this.props.eventTileOps.isWidgetHidden()) {
unhidePreviewButton = (
@ -481,7 +459,6 @@ export default class MessageContextMenu extends React.Component {
{ forwardButton }
{ pinButton }
{ viewSourceButton }
{ viewClearSourceButton }
{ unhidePreviewButton }
{ permalinkButton }
{ quoteButton }

View file

@ -20,7 +20,9 @@ import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
export default class StatusMessageContextMenu extends React.Component {
static propTypes = {
// js-sdk User object. Not required because it might not exist.

View file

@ -22,7 +22,9 @@ import dis from '../../../dispatcher/dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions';
import {MenuItem} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.TagTileContextMenu")
export default class TagTileContextMenu extends React.Component {
static propTypes = {
tag: PropTypes.string.isRequired,

View file

@ -28,9 +28,11 @@ import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import QuestionDialog from "../dialogs/QuestionDialog";
import ErrorDialog from "../dialogs/ErrorDialog";
import {WidgetType} from "../../../widgets/WidgetType";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
app: IApp;
@ -54,6 +56,27 @@ const WidgetContextMenu: React.FC<IProps> = ({
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(roomId);
let streamAudioStreamButton;
if (getConfigLivestreamUrl() && WidgetType.JITSI.matches(app.type)) {
const onStreamAudioClick = async () => {
try {
await startJitsiAudioLivestream(widgetMessaging, roomId);
} catch (err) {
console.error("Failed to start livestream", err);
// XXX: won't i18n well, but looks like widget api only support 'message'?
const message = err.message || _t("Unable to start audio streaming.");
Modal.createTrackedDialog('WidgetContext Menu', 'Livestream failed', ErrorDialog, {
title: _t('Failed to start livestream'),
description: message,
});
}
onFinished();
};
streamAudioStreamButton = <IconizedContextMenuOption
onClick={onStreamAudioClick} label={_t("Start audio stream")}
/>;
}
let unpinButton;
if (showUnpin) {
const onUnpinClick = () => {
@ -163,6 +186,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
return <IconizedContextMenu {...props} chevronFace={ChevronFace.None} onFinished={onFinished}>
<IconizedContextMenuOptionList>
{ streamAudioStreamButton }
{ editButton }
{ revokeButton }
{ deleteButton }
@ -175,4 +199,3 @@ const WidgetContextMenu: React.FC<IProps> = ({
};
export default WidgetContextMenu;

View file

@ -33,6 +33,7 @@ import { abbreviateUrl } from '../../../utils/UrlUtils';
import {sleep} from "../../../utils/promise";
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
import {replaceableComponent} from "../../../utils/replaceableComponent";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -43,7 +44,7 @@ const addressTypeName = {
'email': _td("email address"),
};
@replaceableComponent("views.dialogs.AddressPickerDialog")
export default class AddressPickerDialog extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,

View file

@ -20,7 +20,9 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
export default class AskInviteAnywayDialog extends React.Component {
static propTypes = {
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]

View file

@ -26,6 +26,7 @@ import AccessibleButton from '../elements/AccessibleButton';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
/*
* Basic container for modal dialogs.
@ -33,6 +34,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
* 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 {
static propTypes = {
// onFinished callback to call when Escape is pressed

View file

@ -25,7 +25,9 @@ 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";
@replaceableComponent("views.dialogs.BugReportDialog")
export default class BugReportDialog extends React.Component {
constructor(props) {
super(props);

View file

@ -26,11 +26,12 @@ import SdkConfig from "../../../SdkConfig";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import InviteDialog from "./InviteDialog";
import BaseAvatar from "../avatars/BaseAvatar";
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
interface IProps extends IDialogProps {
roomId: string;
@ -52,6 +53,7 @@ interface IState {
busy: boolean;
}
@replaceableComponent("views.dialogs.CommunityPrototypeInviteDialog")
export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
@ -140,12 +142,14 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
private renderPerson(person: IPerson, key: any) {
const avatarSize = 36;
let avatarUrl = null;
if (person.user.getMxcAvatarUrl()) {
avatarUrl = mediaFromMxc(person.user.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize);
}
return (
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
<BaseAvatar
url={getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), person.user.getMxcAvatarUrl(),
avatarSize, avatarSize, "crop")}
url={avatarUrl}
name={person.user.name}
idName={person.user.userId}
width={avatarSize}

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
/*
* A dialog for confirming a redaction.
@ -30,6 +31,7 @@ import { _t } from '../../../languageHandler';
*
* 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 {
constructor(props) {
super(props);

View file

@ -17,10 +17,12 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
/*
* A dialog for confirming a redaction.
*/
@replaceableComponent("views.dialogs.ConfirmRedactDialog")
export default class ConfirmRedactDialog extends React.Component {
render() {
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');

View file

@ -20,6 +20,8 @@ import { MatrixClient } from 'matrix-js-sdk';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
/*
* A dialog for confirming an operation on another user.
@ -29,6 +31,7 @@ import { GroupMemberType } from '../../../groups';
* 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 {
static propTypes = {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
@ -106,8 +109,9 @@ export default class ConfirmUserActionDialog extends React.Component {
name = this.props.member.name;
userId = this.props.member.userId;
} else {
const httpAvatarUrl = this.props.groupMember.avatarUrl ?
this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null;
const httpAvatarUrl = this.props.groupMember.avatarUrl
? mediaFromMxc(this.props.groupMember.avatarUrl).getSquareThumbnailHttp(48)
: null;
name = this.props.groupMember.displayname || this.props.groupMember.userId;
userId = this.props.groupMember.userId;
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;

View file

@ -18,7 +18,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
export default class ConfirmWipeDeviceDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,

View file

@ -25,6 +25,7 @@ import InfoTooltip from "../elements/InfoTooltip";
import dis from "../../../dispatcher/dispatcher";
import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
import GroupStore from "../../../stores/GroupStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends IDialogProps {
}
@ -38,6 +39,7 @@ interface IState {
avatarPreview: string;
}
@replaceableComponent("views.dialogs.CreateCommunityPrototypeDialog")
export default class CreateCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();

View file

@ -20,7 +20,9 @@ import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.CreateGroupDialog")
export default class CreateGroupDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,

View file

@ -27,7 +27,9 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard";
import {privateShouldBeEncrypted} from "../../../createRoom";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.CreateRoomDialog")
export default class CreateRoomDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,

View file

@ -26,7 +26,9 @@ 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";
@replaceableComponent("views.dialogs.DeactivateAccountDialog")
export default class DeactivateAccountDialog extends React.Component {
constructor(props) {
super(props);

View file

@ -38,6 +38,7 @@ import {SETTINGS} from "../../../settings/Settings";
import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent";
class GenericEditor extends React.PureComponent {
// static propTypes = {onBack: PropTypes.func.isRequired};
@ -73,13 +74,14 @@ class GenericEditor extends React.PureComponent {
}
}
class SendCustomEvent extends GenericEditor {
export class SendCustomEvent extends GenericEditor {
static getLabel() { return _t('Send Custom Event'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
forceStateEvent: PropTypes.bool,
forceGeneralEvent: PropTypes.bool,
inputs: PropTypes.object,
};
@ -140,6 +142,8 @@ class SendCustomEvent extends GenericEditor {
</div>;
}
const showTglFlip = !this.state.message && !this.props.forceStateEvent && !this.props.forceGeneralEvent;
return <div>
<div className="mx_DevTools_content">
<div className="mx_DevTools_eventTypeStateKeyGroup">
@ -155,7 +159,7 @@ class SendCustomEvent extends GenericEditor {
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
{ !this.state.message && !this.props.forceStateEvent && <div style={{float: "right"}}>
{ showTglFlip && <div style={{float: "right"}}>
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
</div> }
@ -1089,6 +1093,7 @@ const Entries = [
SettingsExplorer,
];
@replaceableComponent("views.dialogs.DevtoolsDialog")
export default class DevtoolsDialog extends React.PureComponent {
static propTypes = {
roomId: PropTypes.string.isRequired,

View file

@ -23,6 +23,8 @@ import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import FlairStore from "../../../stores/FlairStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
interface IProps extends IDialogProps {
communityId: string;
@ -38,6 +40,7 @@ interface IState {
}
// XXX: This is a lot of duplication from the create dialog, just in a different shape
@replaceableComponent("views.dialogs.EditCommunityPrototypeDialog")
export default class EditCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
@ -116,7 +119,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
let preview = <img src={this.state.avatarPreview} className="mx_EditCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
if (this.state.currentAvatarUrl) {
const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl);
const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp;
preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
} else {
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />

View file

@ -29,7 +29,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.ErrorDialog")
export default class ErrorDialog extends React.Component {
static propTypes = {
title: PropTypes.string,

View file

@ -31,6 +31,7 @@ import {
IPostmessageResponseData,
PostmessageAction,
} from "./HostSignupDialogTypes";
import {replaceableComponent} from "../../../utils/replaceableComponent";
const HOST_SIGNUP_KEY = "host_signup";
@ -42,6 +43,7 @@ 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: IHostSignupConfig;

View file

@ -19,6 +19,8 @@ import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
const PHASE_START = 0;
const PHASE_SHOW_SAS = 1;
@ -26,6 +28,7 @@ const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
const PHASE_VERIFIED = 3;
const PHASE_CANCELLED = 4;
@replaceableComponent("views.dialogs.IncomingSasDialog")
export default class IncomingSasDialog extends React.Component {
static propTypes = {
verifier: PropTypes.object.isRequired,
@ -121,22 +124,21 @@ export default class IncomingSasDialog extends React.Component {
const Spinner = sdk.getComponent("views.elements.Spinner");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const isSelf = this.props.verifier.userId == MatrixClientPeg.get().getUserId();
const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId();
let profile;
if (this.state.opponentProfile) {
const oppProfile = this.state.opponentProfile;
if (oppProfile) {
const url = oppProfile.avatar_url
? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio))
: null;
profile = <div className="mx_IncomingSasDialog_opponentProfile">
<BaseAvatar name={this.state.opponentProfile.displayname}
<BaseAvatar name={oppProfile.displayname}
idName={this.props.verifier.userId}
url={MatrixClientPeg.get().mxcUrlToHttp(
this.state.opponentProfile.avatar_url,
Math.floor(48 * window.devicePixelRatio),
Math.floor(48 * window.devicePixelRatio),
'crop',
)}
url={url}
width={48} height={48} resizeMethod='crop'
/>
<h2>{this.state.opponentProfile.displayname}</h2>
<h2>{oppProfile.displayname}</h2>
</div>;
} else if (this.state.opponentProfileError) {
profile = <div>

View file

@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component {
className: PropTypes.string,
title: PropTypes.string,
description: PropTypes.node,
button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool),
button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
onFinished: PropTypes.func,
hasCloseButton: PropTypes.bool,
onKeyDown: PropTypes.func,

View file

@ -20,7 +20,9 @@ import {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher';
import {Action} from "../../../dispatcher/actions";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.IntegrationsDisabledDialog")
export default class IntegrationsDisabledDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,

View file

@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.IntegrationsImpossibleDialog")
export default class IntegrationsImpossibleDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,

View file

@ -25,7 +25,9 @@ import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.InteractiveAuthDialog")
export default class InteractiveAuthDialog extends React.Component {
static propTypes = {
// matrix client to use for UI auth requests

View file

@ -22,7 +22,6 @@ import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Pe
import DMRoomMap from "../../../utils/DMRoomMap";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import SdkConfig from "../../../SdkConfig";
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import * as Email from "../../../email";
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
import {abbreviateUrl} from "../../../utils/UrlUtils";
@ -42,13 +41,14 @@ import {UIFeature} from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {Room} from "matrix-js-sdk/src/models/room";
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
export const KIND_SPACE_INVITE = "space_invite";
export const KIND_CALL_TRANSFER = "call_transfer";
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
@ -160,9 +160,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
width={avatarSize} height={avatarSize} />
: <BaseAvatar
className='mx_InviteDialog_userTile_avatar'
url={getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
avatarSize, avatarSize, "crop")}
url={this.props.member.getMxcAvatarUrl()
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
: null}
name={this.props.member.name}
idName={this.props.member.userId}
width={avatarSize}
@ -262,9 +262,9 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
width={avatarSize} height={avatarSize} />
: <BaseAvatar
url={getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
avatarSize, avatarSize, "crop")}
url={this.props.member.getMxcAvatarUrl()
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
: null}
name={this.props.member.name}
idName={this.props.member.userId}
width={avatarSize}
@ -310,7 +310,7 @@ interface IInviteDialogProps {
// not provided.
kind: string,
// The room ID this dialog is for. Only required for KIND_INVITE and KIND_SPACE_INVITE.
// The room ID this dialog is for. Only required for KIND_INVITE.
roomId: string,
// The call to transfer. Only required for KIND_CALL_TRANSFER.
@ -337,6 +337,7 @@ interface IInviteDialogState {
errorText: string,
}
@replaceableComponent("views.dialogs.InviteDialog")
export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> {
static defaultProps = {
kind: KIND_DM,
@ -349,8 +350,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
constructor(props) {
super(props);
if ((props.kind === KIND_INVITE || props.kind === KIND_SPACE_INVITE) && !props.roomId) {
throw new Error("When using KIND_INVITE or KIND_SPACE_INVITE a roomId is required for an InviteDialog");
if ((props.kind === KIND_INVITE) && !props.roomId) {
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
}
@ -1027,7 +1028,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
sectionSubname = _t("May include members not in %(communityName)s", {communityName});
}
if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
if (this.props.kind === KIND_INVITE) {
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
}
@ -1248,25 +1249,31 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}
buttonText = _t("Go");
goButtonFn = this._startDm;
} else if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
title = this.props.kind === KIND_INVITE ? _t("Invite to this room") : _t("Invite to this space");
} else if (this.props.kind === KIND_INVITE) {
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
const isSpace = room?.isSpaceRoom();
title = isSpace
? _t("Invite to %(spaceName)s", {
spaceName: room.name || _t("Unnamed Space"),
})
: _t("Invite to this room");
let helpTextUntranslated;
if (this.props.kind === KIND_INVITE) {
if (isSpace) {
if (identityServersEnabled) {
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
"(like <userId/>) or <a>share this room</a>.");
"(like <userId/>) or <a>share this space</a>.");
} else {
helpTextUntranslated = _td("Invite someone using their name, username " +
"(like <userId/>) or <a>share this room</a>.");
"(like <userId/>) or <a>share this space</a>.");
}
} else { // KIND_SPACE_INVITE
} else {
if (identityServersEnabled) {
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
"(like <userId/>) or <a>share this space</a>.");
"(like <userId/>) or <a>share this room</a>.");
} else {
helpTextUntranslated = _td("Invite someone using their name, username " +
"(like <userId/>) or <a>share this space</a>.");
"(like <userId/>) or <a>share this room</a>.");
}
}

View file

@ -22,7 +22,9 @@ import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.LogoutDialog")
export default class LogoutDialog extends React.Component {
defaultProps = {
onFinished: function() {},

View file

@ -24,7 +24,9 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog")
export default class ManualDeviceKeyVerificationDialog extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,

View file

@ -21,7 +21,9 @@ import { _t } from '../../../languageHandler';
import * as sdk from "../../../index";
import {wantsDateSeparator} from '../../../DateUtils';
import SettingsStore from '../../../settings/SettingsStore';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.MessageEditHistoryDialog")
export default class MessageEditHistoryDialog extends React.PureComponent {
static propTypes = {
mxEvent: PropTypes.object.isRequired,

View file

@ -38,6 +38,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
import { arrayFastClone } from "../../../utils/arrays";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
widgetDefinition: IModalWidgetOpenRequestData;
@ -53,6 +54,7 @@ interface IState {
const MAX_BUTTONS = 3;
@replaceableComponent("views.dialogs.ModalWidgetDialog")
export default class ModalWidgetDialog extends React.PureComponent<IProps, IState> {
private readonly widget: Widget;
private readonly possibleButtons: ModalButtonID[];

View file

@ -66,6 +66,10 @@ export default class NewSessionReviewDialog extends React.PureComponent {
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequestPromise: requestPromise,
member: cli.getUser(userId),
onFinished: async () => {
const request = await requestPromise;
request.cancel();
},
});
}

View file

@ -22,10 +22,12 @@ import {MatrixEvent} from "matrix-js-sdk";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SdkConfig from '../../../SdkConfig';
import Markdown from '../../../Markdown';
import {replaceableComponent} from "../../../utils/replaceableComponent";
/*
* A dialog for reporting an event.
*/
@replaceableComponent("views.dialogs.ReportEventDialog")
export default class ReportEventDialog extends PureComponent {
static propTypes = {
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,

View file

@ -30,6 +30,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
import {replaceableComponent} from "../../../utils/replaceableComponent";
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
@ -38,6 +39,7 @@ export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
@replaceableComponent("views.dialogs.RoomSettingsDialog")
export default class RoomSettingsDialog extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
@ -116,7 +118,7 @@ export default class RoomSettingsDialog extends React.Component {
return (
<BaseDialog className='mx_RoomSettingsDialog' hasCancel={true}
onFinished={this.props.onFinished} title={_t("Room Settings - %(roomName)s", {roomName})}>
<div className='ms_SettingsDialog_content'>
<div className='mx_SettingsDialog_content'>
<TabbedView tabs={this._getTabs()} />
</div>
</BaseDialog>

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