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

@ -489,54 +489,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
margin-top: 69px; margin-top: 69px;
} }
.mx_Beta {
color: red;
margin-right: 10px;
position: relative;
top: -3px;
background-color: white;
padding: 0 4px;
border-radius: 3px;
border: 1px solid darkred;
cursor: help;
transition-duration: 200ms;
font-size: smaller;
filter: opacity(0.5);
}
.mx_Beta:hover {
color: white;
border: 1px solid gray;
background-color: darkred;
}
.mx_TintableSvgButton {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
}
.mx_TintableSvgButton object {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.mx_TintableSvgButton span {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
cursor: pointer;
}
// username colors // username colors
// used by SenderProfile & RoomPreviewBar // used by SenderProfile & RoomPreviewBar
.mx_Username_color1 { .mx_Username_color1 {

View file

@ -203,8 +203,9 @@ limitations under the License.
.mx_SpaceRoomDirectory_actions { .mx_SpaceRoomDirectory_actions {
width: 180px; width: 180px;
text-align: right; text-align: right;
height: min-content; margin-left: 28px;
margin: auto 0 auto 28px; display: inline-flex;
align-items: center;
.mx_AccessibleButton { .mx_AccessibleButton {
vertical-align: middle; vertical-align: middle;
@ -223,9 +224,5 @@ limitations under the License.
line-height: $font-15px; line-height: $font-15px;
color: $secondary-fg-color; color: $secondary-fg-color;
} }
.mx_Checkbox {
display: inline-block;
}
} }
} }

View file

@ -20,6 +20,8 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_MainSplit > div:first-child { .mx_MainSplit > div:first-child {
padding: 80px 60px; padding: 80px 60px;
flex-grow: 1; flex-grow: 1;
max-height: 100%;
overflow-y: auto;
h1 { h1 {
margin: 0; margin: 0;
@ -69,9 +71,116 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
.mx_SpaceRoomView_landing { .mx_SpaceRoomView_preview {
overflow-y: auto; padding: 32px 24px !important; // override default padding from above
margin: auto;
max-width: 480px;
box-sizing: border-box;
box-shadow: 2px 15px 30px $dialog-shadow-color;
border: 1px solid $input-border-color;
border-radius: 8px;
.mx_SpaceRoomView_preview_inviter {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: $font-15px;
> div {
margin-left: 8px;
.mx_SpaceRoomView_preview_inviter_name {
line-height: $font-18px;
}
.mx_SpaceRoomView_preview_inviter_mxid {
line-height: $font-24px;
color: $secondary-fg-color;
}
}
}
> .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px;
}
h1.mx_SpaceRoomView_preview_name {
margin: 20px 0 !important; // override default margin from above
}
.mx_SpaceRoomView_preview_info {
color: $tertiary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
margin: 20px 0;
.mx_SpaceRoomView_preview_info_public,
.mx_SpaceRoomView_preview_info_private {
padding-left: 20px;
position: relative;
&::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
top: 0;
left: -2px;
mask-position: center;
mask-repeat: no-repeat;
background-color: $tertiary-fg-color;
}
}
.mx_SpaceRoomView_preview_info_public::before {
mask-size: 12px;
mask-image: url("$(res)/img/globe.svg");
}
.mx_SpaceRoomView_preview_info_private::before {
mask-size: 14px;
mask-image: url("$(res)/img/element-icons/lock.svg");
}
.mx_AccessibleButton_kind_link {
color: inherit;
position: relative;
padding-left: 16px;
&::before {
content: "·"; // visual separator
position: absolute;
left: 6px;
}
}
}
.mx_SpaceRoomView_preview_topic {
font-size: $font-14px;
line-height: $font-22px;
color: $secondary-fg-color;
margin: 20px 0;
max-height: 160px;
overflow-y: auto;
}
.mx_SpaceRoomView_preview_joinButtons {
margin-top: 20px;
.mx_AccessibleButton {
width: 200px;
box-sizing: border-box;
padding: 14px 0;
& + .mx_AccessibleButton {
margin-left: 20px;
}
}
}
}
.mx_SpaceRoomView_landing {
> .mx_BaseAvatar_image, > .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image { > .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px; border-radius: 12px;
@ -128,14 +237,6 @@ $SpaceRoomViewInnerWidth: 428px;
font-size: $font-15px; font-size: $font-15px;
} }
.mx_SpaceRoomView_landing_joinButtons {
margin-top: 24px;
.mx_FormButton {
padding: 8px 22px;
}
}
.mx_SpaceRoomView_landing_adminButtons { .mx_SpaceRoomView_landing_adminButtons {
margin-top: 32px; margin-top: 32px;

View file

@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// XXX: We shouldn't be using TemporaryTile anywhere - delete it. .mx_DecoratedRoomAvatar, .mx_ExtraTile {
.mx_DecoratedRoomAvatar, .mx_TemporaryTile {
position: relative; position: relative;
&.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {

View file

@ -26,7 +26,9 @@ limitations under the License.
padding: 7px 18px; padding: 7px 18px;
text-align: center; text-align: center;
border-radius: 8px; border-radius: 8px;
display: inline-block; display: inline-flex;
align-items: center;
justify-content: center;
font-size: $font-14px; font-size: $font-14px;
} }

View file

@ -33,4 +33,10 @@ limitations under the License.
color: $notice-primary-color; color: $notice-primary-color;
background-color: $notice-primary-bg-color; background-color: $notice-primary-bg-color;
} }
&.mx_AccessibleButton_kind_secondary {
color: $secondary-fg-color;
border: 1px solid $secondary-fg-color;
background-color: unset;
}
} }

View file

@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_UserInfo { .mx_EncryptionInfo_spinner {
.mx_EncryptionInfo_spinner { .mx_Spinner {
.mx_Spinner { margin-top: 25px;
margin-top: 25px; margin-bottom: 15px;
margin-bottom: 15px;
}
text-align: center;
} }
text-align: center;
} }

View file

@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require('fs'); const fs = require('fs');
const { promises: fsp } = fs;
const path = require('path'); const path = require('path');
const glob = require('glob'); const glob = require('glob');
const util = require('util'); const util = require('util');
@ -25,6 +26,8 @@ async function reskindex() {
const header = args.h || args.header; const header = args.h || args.header;
const strm = fs.createWriteStream(componentIndexTmp); const strm = fs.createWriteStream(componentIndexTmp);
// Wait for the open event to ensure the file descriptor is set
await new Promise(resolve => strm.once("open", resolve));
if (header) { if (header) {
strm.write(fs.readFileSync(header)); strm.write(fs.readFileSync(header));
@ -53,14 +56,9 @@ async function reskindex() {
strm.write("export {components};\n"); strm.write("export {components};\n");
// Ensure the file has been fully written to disk before proceeding // Ensure the file has been fully written to disk before proceeding
await util.promisify(fs.fsync)(strm.fd);
await util.promisify(strm.end); await util.promisify(strm.end);
fs.rename(componentIndexTmp, componentIndex, function(err) { await fsp.rename(componentIndexTmp, componentIndex);
if (err) {
console.error("Error moving new index into place: " + err);
} else {
console.log('Reskindex: completed');
}
});
} }
// Expects both arrays of file names to be sorted // Expects both arrays of file names to be sorted
@ -77,9 +75,17 @@ function filesHaveChanged(files, prevFiles) {
return false; return false;
} }
// Wrapper since await at the top level is not well supported yet
function run() {
(async function() {
await reskindex();
console.log("Reskindex completed");
})();
}
// -w indicates watch mode where any FS events will trigger reskindex // -w indicates watch mode where any FS events will trigger reskindex
if (!args.w) { if (!args.w) {
reskindex(); run();
return; return;
} }
@ -87,5 +93,5 @@ let watchDebouncer = null;
chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => { chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
if (path === componentIndex) return; if (path === componentIndex) return;
if (watchDebouncer) clearTimeout(watchDebouncer); if (watchDebouncer) clearTimeout(watchDebouncer);
watchDebouncer = setTimeout(reskindex, 1000); watchDebouncer = setTimeout(run, 1000);
}); });

View file

@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter';
import Modal from './Modal'; import Modal from './Modal';
import * as sdk from './'; import * as sdk from './';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import InviteDialog, {KIND_DM, KIND_INVITE, KIND_SPACE_INVITE} from "./components/views/dialogs/InviteDialog"; import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
@ -50,11 +50,10 @@ export function showStartChatInviteDialog(initialText) {
} }
export function showRoomInviteDialog(roomId) { export function showRoomInviteDialog(roomId) {
const isSpace = MatrixClientPeg.get()?.getRoom(roomId)?.isSpaceRoom();
// This dialog handles the room creation internally - we don't need to worry about it. // This dialog handles the room creation internally - we don't need to worry about it.
Modal.createTrackedDialog( Modal.createTrackedDialog(
"Invite Users", isSpace ? "Space" : "Room", InviteDialog, { "Invite Users", "", InviteDialog, {
kind: isSpace ? KIND_SPACE_INVITE : KIND_INVITE, kind: KIND_INVITE,
roomId, roomId,
}, },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,

View file

@ -23,7 +23,7 @@ class Skinner {
if (!name) throw new Error(`Invalid component name: ${name}`); if (!name) throw new Error(`Invalid component name: ${name}`);
if (this.components === null) { if (this.components === null) {
throw new Error( throw new Error(
"Attempted to get a component before a skin has been loaded."+ `Attempted to get a component (${name}) before a skin has been loaded.`+
" This is probably because either:"+ " This is probably because either:"+
" a) Your app has not called sdk.loadSkin(), or"+ " a) Your app has not called sdk.loadSkin(), or"+
" b) A component has called getComponent at the root level", " b) A component has called getComponent at the root level",

View file

@ -37,7 +37,7 @@ export default class VoipUserMapper {
return results[0].userid; return results[0].userid;
} }
public async getOrCreateVirtualRoomForRoom(roomId: string):Promise<string> { public async getOrCreateVirtualRoomForRoom(roomId: string): Promise<string> {
const userId = DMRoomMap.shared().getUserIdForRoomId(roomId); const userId = DMRoomMap.shared().getUserIdForRoomId(roomId);
if (!userId) return null; if (!userId) return null;
@ -52,7 +52,7 @@ export default class VoipUserMapper {
return virtualRoomId; return virtualRoomId;
} }
public nativeRoomForVirtualRoom(roomId: string):string { public nativeRoomForVirtualRoom(roomId: string): string {
const virtualRoom = MatrixClientPeg.get().getRoom(roomId); const virtualRoom = MatrixClientPeg.get().getRoom(roomId);
if (!virtualRoom) return null; if (!virtualRoom) return null;
const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
@ -60,7 +60,7 @@ export default class VoipUserMapper {
return virtualRoomEvent.getContent()['native_room'] || null; return virtualRoomEvent.getContent()['native_room'] || null;
} }
public isVirtualRoom(room: Room):boolean { public isVirtualRoom(room: Room): boolean {
if (this.nativeRoomForVirtualRoom(room.roomId)) return true; if (this.nativeRoomForVirtualRoom(room.roomId)) return true;
if (this.virtualRoomIdCache.has(room.roomId)) return true; if (this.virtualRoomIdCache.has(room.roomId)) return true;
@ -79,6 +79,8 @@ export default class VoipUserMapper {
} }
public async onNewInvitedRoom(invitedRoom: Room) { public async onNewInvitedRoom(invitedRoom: Room) {
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
const inviterId = invitedRoom.getDMInviter(); const inviterId = invitedRoom.getDMInviter();
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,6 +39,7 @@ import {Group} from "matrix-js-sdk";
import {allSettled, sleep} from "../../utils/promise"; import {allSettled, sleep} from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore"; import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
const LONG_DESC_PLACEHOLDER = _td( const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1> `<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_OPEN = "open";
const GROUP_JOINPOLICY_INVITE = "invite"; const GROUP_JOINPOLICY_INVITE = "invite";
@replaceableComponent("structures.GroupView")
export default class GroupView extends React.Component { export default class GroupView extends React.Component {
static propTypes = { static propTypes = {
groupId: PropTypes.string.isRequired, groupId: PropTypes.string.isRequired,

View file

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

View file

@ -17,7 +17,9 @@ limitations under the License.
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.IndicatorScrollbar")
export default class IndicatorScrollbar extends React.Component { export default class IndicatorScrollbar extends React.Component {
static propTypes = { static propTypes = {
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator // 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 getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
import * as sdk from '../../index'; import * as sdk from '../../index';
import {replaceableComponent} from "../../utils/replaceableComponent";
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session"); export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
@replaceableComponent("structures.InteractiveAuthComponent")
export default class InteractiveAuthComponent extends React.Component { export default class InteractiveAuthComponent extends React.Component {
static propTypes = { static propTypes = {
// matrix client to use for UI auth requests // 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 RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget"; import LeftPanelWidget from "./LeftPanelWidget";
import SpacePanel from "../views/spaces/SpacePanel"; import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -60,6 +61,7 @@ const cssClasses = [
"mx_RoomSublist_showNButton", "mx_RoomSublist_showNButton",
]; ];
@replaceableComponent("structures.LeftPanel")
export default class LeftPanel extends React.Component<IProps, IState> { export default class LeftPanel extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef(); private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private groupFilterPanelWatcherRef: string; private groupFilterPanelWatcherRef: string;

View file

@ -56,6 +56,7 @@ import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse"; import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer'; import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { IOpts } from "../../createRoom"; import { IOpts } from "../../createRoom";
import {replaceableComponent} from "../../utils/replaceableComponent";
// We need to fetch each pinned message individually (if we don't already have it) // 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. // 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. * Components mounted below us can access the matrix client via the react context.
*/ */
@replaceableComponent("structures.LoggedInView")
class LoggedInView extends React.Component<IProps, IState> { class LoggedInView extends React.Component<IProps, IState> {
static displayName = 'LoggedInView'; static displayName = 'LoggedInView';

View file

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

View file

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

View file

@ -34,6 +34,7 @@ import {textForEvent} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap"; import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro"; import NewRoomIntro from "../views/rooms/NewRoomIntro";
import {replaceableComponent} from "../../utils/replaceableComponent";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message']; 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. /* (almost) stateless UI component which builds the event tiles in the room timeline.
*/ */
@replaceableComponent("structures.MessagePanel")
export default class MessagePanel extends React.Component { export default class MessagePanel extends React.Component {
static propTypes = { static propTypes = {
// true to give the component a 'display: none' style. // 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 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 = {}; this._readReceiptsByEvent = {};
if (this.props.showReadReceipts) { if (this.props.showReadReceipts) {
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent(); this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
@ -534,10 +539,17 @@ export default class MessagePanel extends React.Component {
const nextEvent = i < this.props.events.length - 1 const nextEvent = i < this.props.events.length - 1
? this.props.events[i + 1] ? this.props.events[i + 1]
: null; : 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, // make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up // otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate. // 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; prevEvent = mxEv;
} }
@ -553,7 +565,7 @@ export default class MessagePanel extends React.Component {
return ret; return ret;
} }
_getTilesForEvent(prevEvent, mxEv, last, nextEvent) { _getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) {
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary'); const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
const EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator'); const DateSeparator = sdk.getComponent('messages.DateSeparator');
@ -598,12 +610,23 @@ export default class MessagePanel extends React.Component {
let isLastSuccessful = false; let isLastSuccessful = false;
const isSentState = s => !s || s === 'sent'; const isSentState = s => !s || s === 'sent';
const isSent = isSentState(mxEv.getAssociatedStatus()); const isSent = isSentState(mxEv.getAssociatedStatus());
if (!nextEvent && isSent) { const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent);
if (!hasNextEvent && isSent) {
isLastSuccessful = true; isLastSuccessful = true;
} else if (nextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) { } else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
isLastSuccessful = true; 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 // We only want to consider "last successful" if the event is sent by us, otherwise of course
// it's successful: we received it. // it's successful: we received it.
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); 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 AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.MyGroups")
export default class MyGroups extends React.Component { export default class MyGroups extends React.Component {
static contextType = MatrixClientContext; static contextType = MatrixClientContext;

View file

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

View file

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

View file

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

View file

@ -34,6 +34,7 @@ import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import GroupStore from "../../stores/GroupStore"; import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore"; import FlairStore from "../../stores/FlairStore";
import CountlyAnalytics from "../../CountlyAnalytics"; import CountlyAnalytics from "../../CountlyAnalytics";
import {replaceableComponent} from "../../utils/replaceableComponent";
const MAX_NAME_LENGTH = 80; const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800; const MAX_TOPIC_LENGTH = 800;
@ -42,6 +43,7 @@ function track(action) {
Analytics.trackEvent('RoomDirectory', action); Analytics.trackEvent('RoomDirectory', action);
} }
@replaceableComponent("structures.RoomDirectory")
export default class RoomDirectory extends React.Component { export default class RoomDirectory extends React.Component {
static propTypes = { static propTypes = {
initialText: PropTypes.string, initialText: PropTypes.string,

View file

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

View file

@ -23,6 +23,7 @@ import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions"; import {Action} from "../../dispatcher/actions";
import {replaceableComponent} from "../../utils/replaceableComponent";
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
@ -35,6 +36,7 @@ function getUnsentMessages(room) {
}); });
} }
@replaceableComponent("structures.RoomStatusBar")
export default class RoomStatusBar extends React.Component { export default class RoomStatusBar extends React.Component {
static propTypes = { static propTypes = {
// the room this statusbar is representing. // 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 { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView"; import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom"; import { IOpts } from "../../createRoom";
import {replaceableComponent} from "../../utils/replaceableComponent";
const DEBUG = false; const DEBUG = false;
let debuglog = function(msg: string) {}; let debuglog = function(msg: string) {};
@ -195,6 +196,7 @@ export interface IState {
dragCounter: number; dragCounter: number;
} }
@replaceableComponent("structures.RoomView")
export default class RoomView extends React.Component<IProps, IState> { export default class RoomView extends React.Component<IProps, IState> {
private readonly dispatcherRef: string; private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription; 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 return <SpaceRoomView
space={this.state.room} space={this.state.room}
justCreatedOpts={this.props.justCreatedOpts} justCreatedOpts={this.props.justCreatedOpts}

View file

@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import { Key } from '../../Keyboard'; import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer'; import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
const DEBUG_SCROLL = false; const DEBUG_SCROLL = false;
@ -83,6 +84,7 @@ if (DEBUG_SCROLL) {
* offset as normal. * offset as normal.
*/ */
@replaceableComponent("structures.ScrollPanel")
export default class ScrollPanel extends React.Component { export default class ScrollPanel extends React.Component {
static propTypes = { static propTypes = {
/* stickyBottom: if set to true, then once the user hits the bottom of /* 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 {throttle} from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton'; import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames'; import classNames from 'classnames';
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.SearchBox")
export default class SearchBox extends React.Component { export default class SearchBox extends React.Component {
static propTypes = { static propTypes = {
onSearch: PropTypes.func, onSearch: PropTypes.func,

View file

@ -64,6 +64,7 @@ export interface ISpaceSummaryEvent {
state_key: string; state_key: string;
content: { content: {
order?: string; order?: string;
suggested?: boolean;
auto_join?: boolean; auto_join?: boolean;
via?: string; 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 name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
const evContent = event?.getContent(); const evContent = event?.getContent();
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join); const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via); const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -102,12 +103,12 @@ const SubSpace: React.FC<ISubspaceProps> = ({
let actions; let actions;
if (editing && queueAction) { if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setAutoJoin = () => { const setSuggested = () => {
_setAutoJoin(v => { _setSuggested(v => {
queueAction({ queueAction({
event, event,
removed, removed,
autoJoin: !v, suggested: !v,
}); });
return !v; return !v;
}); });
@ -118,7 +119,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
queueAction({ queueAction({
event, event,
removed: !v, removed: !v,
autoJoin, suggested,
}); });
return !v; return !v;
}); });
@ -131,7 +132,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
} else { } else {
actions = <React.Fragment> actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} /> <FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} /> <StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>; </React.Fragment>;
} }
} else { } else {
@ -182,8 +183,8 @@ const SubSpace: React.FC<ISubspaceProps> = ({
interface IAction { interface IAction {
event: MatrixEvent; event: MatrixEvent;
suggested: boolean;
removed: boolean; removed: boolean;
autoJoin: boolean;
} }
interface IRoomTileProps { 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 name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
const evContent = event?.getContent(); const evContent = event?.getContent();
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join); const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via); const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -209,12 +210,12 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
let actions; let actions;
if (editing && queueAction) { if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setAutoJoin = () => { const setSuggested = () => {
_setAutoJoin(v => { _setSuggested(v => {
queueAction({ queueAction({
event, event,
removed, removed,
autoJoin: !v, suggested: !v,
}); });
return !v; return !v;
}); });
@ -225,7 +226,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
queueAction({ queueAction({
event, event,
removed: !v, removed: !v,
autoJoin, suggested,
}); });
return !v; return !v;
}); });
@ -238,7 +239,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
} else { } else {
actions = <React.Fragment> actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} /> <FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} /> <StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>; </React.Fragment>;
} }
} else { } else {
@ -445,10 +446,10 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
const onSaveButtonClicked = () => { const onSaveButtonClicked = () => {
// TODO setBusy // TODO setBusy
pendingActions.current.forEach(({event, autoJoin, removed}) => { pendingActions.current.forEach(({event, suggested, removed}) => {
const content = { const content = {
...event.getContent(), ...event.getContent(),
auto_join: autoJoin, suggested,
}; };
if (removed) { if (removed) {
@ -463,7 +464,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
if (isEditing) { if (isEditing) {
adminButton = <React.Fragment> adminButton = <React.Fragment>
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} /> <FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
<span>{ _t("All users join by default") }</span> <span>{ _t("Promoted to users") }</span>
</React.Fragment>; </React.Fragment>;
} else { } else {
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />; adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;

View file

@ -94,26 +94,95 @@ const useMyRoomMembership = (room: Room) => {
return membership; return membership;
}; };
const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space); const myMembership = useMyRoomMembership(space);
const joinRule = space.getJoinRule();
const userId = cli.getUserId();
let inviterSection;
let joinButtons; let joinButtons;
if (myMembership === "invite") { if (myMembership === "invite") {
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons"> const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender();
<FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} /> const inviter = inviteSender && space.getMember(inviteSender);
<AccessibleButton kind="link" onClick={onRejectButtonClicked}>
{_t("Decline")} if (inviteSender) {
</AccessibleButton> inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
</div>; <MemberAvatar member={inviter} width={32} height={32} />
} else if (myMembership !== "join" && joinRule === "public") { <div>
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons"> <div className="mx_SpaceRoomView_preview_inviter_name">
<FormButton label={_t("Join")} onClick={onJoinButtonClicked} /> { _t("<inviter/> invites you", {}, {
</div>; 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; let inviteButton;
if (myMembership === "join" && space.canInvite(userId)) { if (myMembership === "join" && space.canInvite(userId)) {
inviteButton = ( inviteButton = (
@ -227,26 +296,7 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
) : null} ) : null}
</RoomMemberCount> </RoomMemberCount>
</div> }; </div> };
if (myMembership === "invite") { if (shouldShowSpaceSettings(cli, space)) {
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 (space.getJoinRule() === "public") { if (space.getJoinRule() === "public") {
return _t("Your public space <name/>", {}, tags) as JSX.Element; return _t("Your public space <name/>", {}, tags) as JSX.Element;
} else { } else {
@ -260,7 +310,6 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
<div className="mx_SpaceRoomView_landing_topic"> <div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} /> <RoomTopic room={space} />
</div> </div>
{ joinButtons }
<div className="mx_SpaceRoomView_landing_adminButtons"> <div className="mx_SpaceRoomView_landing_adminButtons">
{ inviteButton } { inviteButton }
{ addRoomButtons } { addRoomButtons }
@ -548,16 +597,19 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
private renderBody() { private renderBody() {
switch (this.state.phase) { switch (this.state.phase) {
case Phase.Landing: case Phase.Landing:
return <SpaceLanding if (this.props.space.getMyMembership() === "join") {
space={this.props.space} return <SpaceLanding space={this.props.space} />;
onJoinButtonClicked={this.props.onJoinButtonClicked} } else {
onRejectButtonClicked={this.props.onRejectButtonClicked} return <SpacePreview
/>; space={this.props.space}
onJoinButtonClicked={this.props.onJoinButtonClicked}
onRejectButtonClicked={this.props.onRejectButtonClicked}
/>;
}
case Phase.PublicCreateRooms: case Phase.PublicCreateRooms:
return <SpaceSetupFirstRooms return <SpaceSetupFirstRooms
space={this.props.space} 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.")} description={_t("We'll create rooms for each topic.")}
onFinished={() => this.setState({ phase: Phase.PublicShare })} onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>; />;

View file

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

View file

@ -37,6 +37,7 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
import {haveTileForEvent} from "../views/rooms/EventTile"; import {haveTileForEvent} from "../views/rooms/EventTile";
import {UIFeature} from "../../settings/UIFeature"; import {UIFeature} from "../../settings/UIFeature";
import {objectHasDiff} from "../../utils/objects"; import {objectHasDiff} from "../../utils/objects";
import {replaceableComponent} from "../../utils/replaceableComponent";
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
@ -55,6 +56,7 @@ if (DEBUG) {
* *
* Also responsible for handling and sending read receipts. * Also responsible for handling and sending read receipts.
*/ */
@replaceableComponent("structures.TimelinePanel")
class TimelinePanel extends React.Component { class TimelinePanel extends React.Component {
static propTypes = { static propTypes = {
// The js-sdk EventTimelineSet object for the timeline sequence we are // 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 * as React from "react";
import ToastStore, {IToast} from "../../stores/ToastStore"; import ToastStore, {IToast} from "../../stores/ToastStore";
import classNames from "classnames"; import classNames from "classnames";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IState { interface IState {
toasts: IToast<any>[]; toasts: IToast<any>[];
countSeen: number; countSeen: number;
} }
@replaceableComponent("structures.ToastContainer")
export default class ToastContainer extends React.Component<{}, IState> { export default class ToastContainer extends React.Component<{}, IState> {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);

View file

@ -25,6 +25,7 @@ import { Action } from "../../dispatcher/actions";
import ProgressBar from "../views/elements/ProgressBar"; import ProgressBar from "../views/elements/ProgressBar";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import { IUpload } from "../../models/IUpload"; import { IUpload } from "../../models/IUpload";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps { interface IProps {
room: Room; room: Room;
@ -35,6 +36,7 @@ interface IState {
uploadsHere: IUpload[]; uploadsHere: IUpload[];
} }
@replaceableComponent("structures.UploadBar")
export default class UploadBar extends React.Component<IProps, IState> { export default class UploadBar extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private mounted: boolean; private mounted: boolean;

View file

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

View file

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

View file

@ -25,7 +25,9 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog"; import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
import { canEditContent } from "../../utils/EventUtils"; import { canEditContent } from "../../utils/EventUtils";
import { MatrixClientPeg } from '../../MatrixClientPeg'; import { MatrixClientPeg } from '../../MatrixClientPeg';
import { replaceableComponent } from "../../utils/replaceableComponent";
@replaceableComponent("structures.ViewSource")
export default class ViewSource extends React.Component { export default class ViewSource extends React.Component {
static propTypes = { static propTypes = {
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,

View file

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

View file

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

View file

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

View file

@ -35,6 +35,7 @@ import InlineSpinner from "../../views/elements/InlineSpinner";
import Spinner from "../../views/elements/Spinner"; import Spinner from "../../views/elements/Spinner";
import SSOButtons from "../../views/elements/SSOButtons"; import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from "../../views/elements/ServerPicker"; 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 // 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. // 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 * 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> { export default class LoginComponent extends React.PureComponent<IProps, IState> {
private unmounted = false; private unmounted = false;
private loginLogic: Login; private loginLogic: Login;

View file

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

View file

@ -17,17 +17,20 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { import {
SetupEncryptionStore, SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO, PHASE_INTRO,
PHASE_BUSY, PHASE_BUSY,
PHASE_DONE, PHASE_DONE,
PHASE_CONFIRM_SKIP, PHASE_CONFIRM_SKIP,
PHASE_FINISHED, PHASE_FINISHED,
} from '../../../stores/SetupEncryptionStore'; } from '../../../stores/SetupEncryptionStore';
import {replaceableComponent} from "../../../utils/replaceableComponent";
function keyHasPassphrase(keyInfo) { function keyHasPassphrase(keyInfo) {
return ( return (
@ -37,6 +40,7 @@ function keyHasPassphrase(keyInfo) {
); );
} }
@replaceableComponent("structures.auth.SetupEncryptionBody")
export default class SetupEncryptionBody extends React.Component { export default class SetupEncryptionBody extends React.Component {
static propTypes = { static propTypes = {
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
@ -81,6 +85,22 @@ export default class SetupEncryptionBody extends React.Component {
store.usePassPhrase(); 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 = () => { onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.skip(); store.skip();
@ -132,32 +152,22 @@ export default class SetupEncryptionBody extends React.Component {
</AccessibleButton>; </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 ( return (
<div> <div>
<p>{_t( <p>{_t(
"Confirm your identity by verifying this login from one of your other sessions, " + "Verify this login to access your encrypted messages and " +
"granting it access to encrypted messages.", "prove to others that this login is really you.",
)}</p> )}</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"> <div className="mx_CompleteSecurity_actionRow">
{verifyButton}
{useRecoveryKeyButton} {useRecoveryKeyButton}
<AccessibleButton kind="danger" onClick={this.onSkipClick}> <AccessibleButton kind="danger" onClick={this.onSkipClick}>
{_t("Skip")} {_t("Skip")}
@ -215,7 +225,7 @@ export default class SetupEncryptionBody extends React.Component {
</div> </div>
</div> </div>
); );
} else if (phase === PHASE_BUSY) { } else if (phase === PHASE_BUSY || phase === PHASE_LOADING) {
const Spinner = sdk.getComponent('views.elements.Spinner'); const Spinner = sdk.getComponent('views.elements.Spinner');
return <Spinner />; return <Spinner />;
} else { } else {

View file

@ -26,6 +26,7 @@ import {sendLoginRequest} from "../../../Login";
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
import SSOButtons from "../../views/elements/SSOButtons"; import SSOButtons from "../../views/elements/SSOButtons";
import {replaceableComponent} from "../../../utils/replaceableComponent";
const LOGIN_VIEW = { const LOGIN_VIEW = {
LOADING: 1, LOADING: 1,
@ -41,6 +42,7 @@ const FLOWS_TO_VIEWS = {
"m.login.sso": LOGIN_VIEW.SSO, "m.login.sso": LOGIN_VIEW.SSO,
}; };
@replaceableComponent("structures.auth.SoftLogout")
export default class SoftLogout extends React.Component { export default class SoftLogout extends React.Component {
static propTypes = { static propTypes = {
// Query parameters from MatrixChat // Query parameters from MatrixChat

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,6 +26,7 @@ import withValidation from "../elements/Validation";
import * as Email from "../../../email"; import * as Email from "../../../email";
import Field from "../elements/Field"; import Field from "../elements/Field";
import CountryDropdown from "./CountryDropdown"; import CountryDropdown from "./CountryDropdown";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// For validating phone numbers without country codes // For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -66,6 +67,7 @@ enum LoginField {
* A pure UI component which displays a username/password form. * A pure UI component which displays a username/password form.
* The email/username/phone fields are fully-controlled, the password field is not. * 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> { export default class PasswordLogin extends React.PureComponent<IProps, IState> {
static defaultProps = { static defaultProps = {
onUsernameChanged: function() {}, onUsernameChanged: function() {},

View file

@ -30,6 +30,7 @@ import PassphraseField from "./PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import Field from '../elements/Field'; import Field from '../elements/Field';
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog'; import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
import {replaceableComponent} from "../../../utils/replaceableComponent";
enum RegistrationField { enum RegistrationField {
Email = "field_email", Email = "field_email",
@ -80,6 +81,7 @@ interface IState {
/* /*
* A pure UI component which displays a registration form. * A pure UI component which displays a registration form.
*/ */
@replaceableComponent("views.auth.RegistrationForm")
export default class RegistrationForm extends React.PureComponent<IProps, IState> { export default class RegistrationForm extends React.PureComponent<IProps, IState> {
static defaultProps = { static defaultProps = {
onValidationChange: console.error, onValidationChange: console.error,

View file

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

View file

@ -30,6 +30,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip"; import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps { interface IProps {
room: Room; room: Room;
@ -68,6 +69,7 @@ function tooltipText(variant: Icon) {
} }
} }
@replaceableComponent("views.avatars.DecoratedRoomAvatar")
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> { export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
private _dmUser: User; private _dmUser: User;
private isUnmounted = false; private isUnmounted = false;

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import BaseAvatar from './BaseAvatar'; import BaseAvatar from './BaseAvatar';
import {replaceableComponent} from "../../../utils/replaceableComponent";
export interface IProps { export interface IProps {
groupId?: string; groupId?: string;
@ -28,6 +29,7 @@ export interface IProps {
onClick?: React.MouseEventHandler; onClick?: React.MouseEventHandler;
} }
@replaceableComponent("views.avatars.GroupAvatar")
export default class GroupAvatar extends React.Component<IProps> { export default class GroupAvatar extends React.Component<IProps> {
public static defaultProps = { public static defaultProps = {
width: 36, width: 36,

View file

@ -22,6 +22,7 @@ import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import BaseAvatar from "./BaseAvatar"; import BaseAvatar from "./BaseAvatar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> { interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
member: RoomMember; member: RoomMember;
@ -42,6 +43,7 @@ interface IState {
imageUrl?: string; imageUrl?: string;
} }
@replaceableComponent("views.avatars.MemberAvatar")
export default class MemberAvatar extends React.Component<IProps, IState> { export default class MemberAvatar extends React.Component<IProps, IState> {
public static defaultProps = { public static defaultProps = {
width: 40, width: 40,

View file

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

View file

@ -23,6 +23,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar'; import * as Avatar from '../../../Avatar';
import {ResizeMethod} from "../../../Avatar"; import {ResizeMethod} from "../../../Avatar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> { interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
// Room may be left unset here, but if it is, // Room may be left unset here, but if it is,
@ -42,6 +43,7 @@ interface IState {
urls: string[]; urls: string[];
} }
@replaceableComponent("views.avatars.RoomAvatar")
export default class RoomAvatar extends React.Component<IProps, IState> { export default class RoomAvatar extends React.Component<IProps, IState> {
public static defaultProps = { public static defaultProps = {
width: 36, width: 36,

View file

@ -22,11 +22,13 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import CallHandler from '../../../CallHandler'; import CallHandler from '../../../CallHandler';
import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog'; import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps { interface IProps extends IContextMenuProps {
call: MatrixCall; call: MatrixCall;
} }
@replaceableComponent("views.context_menus.CallContextMenu")
export default class CallContextMenu extends React.Component<IProps> { export default class CallContextMenu extends React.Component<IProps> {
static propTypes = { static propTypes = {
// js-sdk User object. Not required because it might not exist. // 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 { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import Dialpad from '../voip/DialPad'; import Dialpad from '../voip/DialPad';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps { interface IProps extends IContextMenuProps {
call: MatrixCall; call: MatrixCall;
@ -28,6 +29,7 @@ interface IState {
value: string; value: string;
} }
@replaceableComponent("views.context_menus.DialpadContextMenu")
export default class DialpadContextMenu extends React.Component<IProps, IState> { export default class DialpadContextMenu extends React.Component<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);

View file

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

View file

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

View file

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

View file

@ -32,11 +32,13 @@ import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils'; import { isContentActionable } from '../../../utils/EventUtils';
import {MenuItem} from "../../structures/ContextMenu"; import {MenuItem} from "../../structures/ContextMenu";
import {EventType} from "matrix-js-sdk/src/@types/event"; import {EventType} from "matrix-js-sdk/src/@types/event";
import {replaceableComponent} from "../../../utils/replaceableComponent";
function canCancel(eventStatus) { function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
} }
@replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component { export default class MessageContextMenu extends React.Component {
static propTypes = { static propTypes = {
/* the MatrixEvent associated with the context menu */ /* the MatrixEvent associated with the context menu */

View file

@ -20,7 +20,9 @@ import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
export default class StatusMessageContextMenu extends React.Component { export default class StatusMessageContextMenu extends React.Component {
static propTypes = { static propTypes = {
// js-sdk User object. Not required because it might not exist. // 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 TagOrderActions from '../../../actions/TagOrderActions';
import {MenuItem} from "../../structures/ContextMenu"; import {MenuItem} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.TagTileContextMenu")
export default class TagTileContextMenu extends React.Component { export default class TagTileContextMenu extends React.Component {
static propTypes = { static propTypes = {
tag: PropTypes.string.isRequired, tag: PropTypes.string.isRequired,

View file

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

View file

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

View file

@ -26,6 +26,7 @@ import AccessibleButton from '../elements/AccessibleButton';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
/* /*
* Basic container for modal dialogs. * 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 * Includes a div for the title, and a keypress handler which cancels the
* dialog on escape. * dialog on escape.
*/ */
@replaceableComponent("views.dialogs.BaseDialog")
export default class BaseDialog extends React.Component { export default class BaseDialog extends React.Component {
static propTypes = { static propTypes = {
// onFinished callback to call when Escape is pressed // onFinished callback to call when Escape is pressed

View file

@ -25,7 +25,9 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-rageshake'; import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-rageshake';
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.BugReportDialog")
export default class BugReportDialog extends React.Component { export default class BugReportDialog extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);

View file

@ -31,6 +31,7 @@ import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
import StyledCheckbox from "../elements/StyledCheckbox"; import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog"; import ErrorDialog from "./ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
roomId: string; roomId: string;
@ -52,6 +53,7 @@ interface IState {
busy: boolean; busy: boolean;
} }
@replaceableComponent("views.dialogs.CommunityPrototypeInviteDialog")
export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> { export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);

View file

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

View file

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

View file

@ -20,6 +20,7 @@ import { MatrixClient } from 'matrix-js-sdk';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups'; import { GroupMemberType } from '../../../groups';
import {replaceableComponent} from "../../../utils/replaceableComponent";
/* /*
* A dialog for confirming an operation on another user. * A dialog for confirming an operation on another user.
@ -29,6 +30,7 @@ import { GroupMemberType } from '../../../groups';
* to make it obvious what is going to happen. * to make it obvious what is going to happen.
* Also tweaks the style for 'dangerous' actions (albeit only with colour) * Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/ */
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
export default class ConfirmUserActionDialog extends React.Component { export default class ConfirmUserActionDialog extends React.Component {
static propTypes = { static propTypes = {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember' // matrix-js-sdk (room) member object. Supply either this or 'groupMember'

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,9 @@ import { _t } from '../../../languageHandler';
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth"; import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents"; import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
import StyledCheckbox from "../elements/StyledCheckbox"; import StyledCheckbox from "../elements/StyledCheckbox";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.DeactivateAccountDialog")
export default class DeactivateAccountDialog extends React.Component { export default class DeactivateAccountDialog extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);

View file

@ -38,6 +38,7 @@ import {SETTINGS} from "../../../settings/Settings";
import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore"; import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog"; import ErrorDialog from "./ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent";
class GenericEditor extends React.PureComponent { class GenericEditor extends React.PureComponent {
// static propTypes = {onBack: PropTypes.func.isRequired}; // static propTypes = {onBack: PropTypes.func.isRequired};
@ -1092,6 +1093,7 @@ const Entries = [
SettingsExplorer, SettingsExplorer,
]; ];
@replaceableComponent("views.dialogs.DevtoolsDialog")
export default class DevtoolsDialog extends React.PureComponent { export default class DevtoolsDialog extends React.PureComponent {
static propTypes = { static propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,

View file

@ -23,6 +23,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import FlairStore from "../../../stores/FlairStore"; import FlairStore from "../../../stores/FlairStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
communityId: string; communityId: string;
@ -38,6 +39,7 @@ interface IState {
} }
// XXX: This is a lot of duplication from the create dialog, just in a different shape // 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> { export default class EditCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef(); private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();

View file

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

View file

@ -31,6 +31,7 @@ import {
IPostmessageResponseData, IPostmessageResponseData,
PostmessageAction, PostmessageAction,
} from "./HostSignupDialogTypes"; } from "./HostSignupDialogTypes";
import {replaceableComponent} from "../../../utils/replaceableComponent";
const HOST_SIGNUP_KEY = "host_signup"; const HOST_SIGNUP_KEY = "host_signup";
@ -42,6 +43,7 @@ interface IState {
minimized: boolean; minimized: boolean;
} }
@replaceableComponent("views.dialogs.HostSignupDialog")
export default class HostSignupDialog extends React.PureComponent<IProps, IState> { export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef(); private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
private readonly config: IHostSignupConfig; private readonly config: IHostSignupConfig;

View file

@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
const PHASE_START = 0; const PHASE_START = 0;
const PHASE_SHOW_SAS = 1; const PHASE_SHOW_SAS = 1;
@ -26,6 +27,7 @@ const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
const PHASE_VERIFIED = 3; const PHASE_VERIFIED = 3;
const PHASE_CANCELLED = 4; const PHASE_CANCELLED = 4;
@replaceableComponent("views.dialogs.IncomingSasDialog")
export default class IncomingSasDialog extends React.Component { export default class IncomingSasDialog extends React.Component {
static propTypes = { static propTypes = {
verifier: PropTypes.object.isRequired, verifier: PropTypes.object.isRequired,

View file

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

View file

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

View file

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

View file

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

View file

@ -42,13 +42,13 @@ import {UIFeature} from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import {replaceableComponent} from "../../../utils/replaceableComponent";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here. // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
export const KIND_DM = "dm"; export const KIND_DM = "dm";
export const KIND_INVITE = "invite"; export const KIND_INVITE = "invite";
export const KIND_SPACE_INVITE = "space_invite";
export const KIND_CALL_TRANSFER = "call_transfer"; export const KIND_CALL_TRANSFER = "call_transfer";
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
@ -310,7 +310,7 @@ interface IInviteDialogProps {
// not provided. // not provided.
kind: string, 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, roomId: string,
// The call to transfer. Only required for KIND_CALL_TRANSFER. // The call to transfer. Only required for KIND_CALL_TRANSFER.
@ -337,6 +337,7 @@ interface IInviteDialogState {
errorText: string, errorText: string,
} }
@replaceableComponent("views.dialogs.InviteDialog")
export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> { export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> {
static defaultProps = { static defaultProps = {
kind: KIND_DM, kind: KIND_DM,
@ -349,8 +350,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
constructor(props) { constructor(props) {
super(props); super(props);
if ((props.kind === KIND_INVITE || props.kind === KIND_SPACE_INVITE) && !props.roomId) { if ((props.kind === KIND_INVITE) && !props.roomId) {
throw new Error("When using KIND_INVITE or KIND_SPACE_INVITE a roomId is required for an InviteDialog"); throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) { } else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog"); 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}); 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"); sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
} }
@ -1248,25 +1249,31 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
} }
buttonText = _t("Go"); buttonText = _t("Go");
goButtonFn = this._startDm; goButtonFn = this._startDm;
} else if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) { } else if (this.props.kind === KIND_INVITE) {
title = this.props.kind === KIND_INVITE ? _t("Invite to this room") : _t("Invite to this space"); 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; let helpTextUntranslated;
if (this.props.kind === KIND_INVITE) { if (isSpace) {
if (identityServersEnabled) { if (identityServersEnabled) {
helpTextUntranslated = _td("Invite someone using their name, email address, username " + 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 { } else {
helpTextUntranslated = _td("Invite someone using their name, username " + 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) { if (identityServersEnabled) {
helpTextUntranslated = _td("Invite someone using their name, email address, username " + 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 { } else {
helpTextUntranslated = _td("Invite someone using their name, username " + helpTextUntranslated = _td("Invite someone using their name, username " +
"(like <userId/>) or <a>share this space</a>."); "(like <userId/>) or <a>share this room</a>.");
} }
} }

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