Merge branch 'develop' into edit-view-source

This commit is contained in:
Panagiotis 2021-03-11 17:07:56 +02:00
commit be7fb33a67
349 changed files with 3402 additions and 1184 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,7 @@ import {Group} from "matrix-js-sdk";
import {allSettled, sleep} from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1>
@ -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,

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

@ -40,6 +40,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps {
isMinimized: boolean;
@ -60,6 +61,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;

View file

@ -56,6 +56,7 @@ import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { IOpts } from "../../createRoom";
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.
@ -128,6 +129,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";

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');
@ -598,12 +610,23 @@ export default class MessagePanel extends React.Component {
let isLastSuccessful = false;
const isSentState = s => !s || s === 'sent';
const isSent = isSentState(mxEv.getAssociatedStatus());
if (!nextEvent && isSent) {
const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent);
if (!hasNextEvent && isSent) {
isLastSuccessful = true;
} else if (nextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
} 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();

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

@ -34,6 +34,7 @@ import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore";
import CountlyAnalytics from "../../CountlyAnalytics";
import {replaceableComponent} from "../../utils/replaceableComponent";
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,

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

@ -64,6 +64,7 @@ export interface ISpaceSummaryEvent {
state_key: string;
content: {
order?: string;
suggested?: boolean;
auto_join?: boolean;
via?: string;
};
@ -91,7 +92,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 +103,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 +119,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
queueAction({
event,
removed: !v,
autoJoin,
suggested,
});
return !v;
});
@ -131,7 +132,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 {
@ -182,8 +183,8 @@ const SubSpace: React.FC<ISubspaceProps> = ({
interface IAction {
event: MatrixEvent;
suggested: boolean;
removed: boolean;
autoJoin: boolean;
}
interface IRoomTileProps {
@ -199,7 +200,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 +210,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 +226,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
queueAction({
event,
removed: !v,
autoJoin,
suggested,
});
return !v;
});
@ -238,7 +239,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 {
@ -445,10 +446,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 +464,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

@ -25,6 +25,7 @@ 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;
@ -35,6 +36,7 @@ interface IState {
uploadsHere: IUpload[];
}
@replaceableComponent("structures.UploadBar")
export default class UploadBar extends React.Component<IProps, IState> {
private dispatcherRef: string;
private mounted: boolean;

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

@ -25,7 +25,9 @@ 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 = {
onFinished: PropTypes.func.isRequired,

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