1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-11-07 10:46:24 +03:00

Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner
2021-07-15 14:54:06 +02:00
109 changed files with 1345 additions and 727 deletions

View File

@@ -54,7 +54,7 @@ export default class InteractiveAuthComponent extends React.Component {
// * emailSid {string} If email auth was performed, the sid of
// the auth session.
// * clientSecret {string} The client secret used in auth
// sessions with the ID server.
// sessions with the identity server.
onAuthFinished: PropTypes.func.isRequired,
// Inputs provided by the user to the auth process

View File

@@ -63,6 +63,7 @@ import ToastContainer from './ToastContainer';
import MyGroups from "./MyGroups";
import UserView from "./UserView";
import GroupView from "./GroupView";
import SpaceStore from "../../stores/SpaceStore";
// 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.
@@ -631,7 +632,7 @@ class LoggedInView extends React.Component<IProps, IState> {
>
<ToastContainer />
<div ref={this._resizeContainer} className={bodyClasses}>
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
{ SpaceStore.spacesEnabled ? <SpacePanel /> : null }
<LeftPanel
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}

View File

@@ -251,7 +251,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private pageChanging: boolean;
private tokenLogin?: boolean;
private accountPassword?: string;
private accountPasswordTimer?: NodeJS.Timeout;
private accountPasswordTimer?: number;
private focusComposer: boolean;
private subTitleStatus: string;
private prevWindowWidth: number;
@@ -561,7 +561,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
switch (payload.action) {
case 'MatrixActions.accountData':
// XXX: This is a collection of several hacks to solve a minor problem. We want to
// update our local state when the ID server changes, but don't want to put that in
// update our local state when the identity server changes, but don't want to put that in
// the js-sdk as we'd be then dictating how all consumers need to behave. However,
// this component is already bloated and we probably don't want this tiny logic in
// here, but there's no better place in the react-sdk for it. Additionally, we're
@@ -1099,7 +1099,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private leaveRoomWarnings(roomId: string) {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
const isSpace = SpaceStore.spacesEnabled && roomToLeave?.isSpaceRoom();
// Show a warning if there are additional complications.
const warnings = [];
@@ -1137,7 +1137,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this.leaveRoomWarnings(roomId);
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
const isSpace = SpaceStore.spacesEnabled && roomToLeave?.isSpaceRoom();
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
title: isSpace ? _t("Leave space") : _t("Leave room"),
description: (
@@ -1687,7 +1687,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const type = screen === "start_sso" ? "sso" : "cas";
PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin());
} else if (screen === 'groups') {
if (SettingsStore.getValue("feature_spaces")) {
if (SpaceStore.spacesEnabled) {
dis.dispatch({ action: "view_home_page" });
return;
}
@@ -1774,7 +1774,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
subAction: params.action,
});
} else if (screen.indexOf('group/') === 0) {
if (SettingsStore.getValue("feature_spaces")) {
if (SpaceStore.spacesEnabled) {
dis.dispatch({ action: "view_home_page" });
return;
}

View File

@@ -48,6 +48,7 @@ import NotificationPanel from "./NotificationPanel";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
import { throttle } from 'lodash';
import SpaceStore from "../../stores/SpaceStore";
interface IProps {
room?: Room; // if showing panels for a given room, this is set
@@ -107,7 +108,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
return RightPanelPhases.GroupMemberList;
}
return rps.groupPanelPhase;
} else if (SettingsStore.getValue("feature_spaces") && this.props.room?.isSpaceRoom()
} else if (SpaceStore.spacesEnabled && this.props.room?.isSpaceRoom()
&& !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)
) {
return RightPanelPhases.SpaceMemberList;

View File

@@ -16,6 +16,9 @@ limitations under the License.
*/
import React from "react";
import { IFieldType, IInstance, IProtocol, IPublicRoomsChunkRoom } from "matrix-js-sdk/src/client";
import { Visibility } from "matrix-js-sdk/src/@types/partials";
import { IRoomDirectoryOptions } from "matrix-js-sdk/src/@types/requests";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
@@ -25,7 +28,7 @@ import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
import { ALL_ROOMS, IFieldType, IInstance, IProtocol, Protocols } from "../views/directory/NetworkDropdown";
import NetworkDropdown, { ALL_ROOMS, Protocols } from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import GroupStore from "../../stores/GroupStore";
@@ -40,7 +43,6 @@ import ErrorDialog from "../views/dialogs/ErrorDialog";
import QuestionDialog from "../views/dialogs/QuestionDialog";
import BaseDialog from "../views/dialogs/BaseDialog";
import DirectorySearchBox from "../views/elements/DirectorySearchBox";
import NetworkDropdown from "../views/directory/NetworkDropdown";
import ScrollPanel from "./ScrollPanel";
import Spinner from "../views/elements/Spinner";
import { ActionPayload } from "../../dispatcher/payloads";
@@ -61,7 +63,7 @@ interface IProps extends IDialogProps {
}
interface IState {
publicRooms: IRoom[];
publicRooms: IPublicRoomsChunkRoom[];
loading: boolean;
protocolsLoading: boolean;
error?: string;
@@ -72,35 +74,12 @@ interface IState {
communityName?: string;
}
/* eslint-disable camelcase */
interface IRoom {
room_id: string;
name?: string;
avatar_url?: string;
topic?: string;
canonical_alias?: string;
aliases?: string[];
world_readable: boolean;
guest_can_join: boolean;
num_joined_members: number;
}
interface IPublicRoomsRequest {
limit?: number;
since?: string;
server?: string;
filter?: object;
include_all_networks?: boolean;
third_party_instance_id?: string;
}
/* eslint-enable camelcase */
@replaceableComponent("structures.RoomDirectory")
export default class RoomDirectory extends React.Component<IProps, IState> {
private readonly startTime: number;
private unmounted = false;
private nextBatch: string = null;
private filterTimeout: NodeJS.Timeout;
private filterTimeout: number;
private protocols: Protocols;
constructor(props) {
@@ -253,7 +232,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// remember the next batch token when we sent the request
// too. If it's changed, appending to the list will corrupt it.
const nextBatch = this.nextBatch;
const opts: IPublicRoomsRequest = { limit: 20 };
const opts: IRoomDirectoryOptions = { limit: 20 };
if (roomServer != MatrixClientPeg.getHomeserverName()) {
opts.server = roomServer;
}
@@ -326,7 +305,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
* HS admins to do this through the RoomSettings interface, but
* this needs SPEC-417.
*/
private removeFromDirectory(room: IRoom) {
private removeFromDirectory(room: IPublicRoomsChunkRoom) {
const alias = getDisplayAliasForRoom(room);
const name = room.name || alias || _t('Unnamed room');
@@ -346,7 +325,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
const modal = Modal.createDialog(Spinner);
let step = _t('remove %(name)s from the directory.', { name: name });
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, Visibility.Private).then(() => {
if (!alias) return;
step = _t('delete the address.');
return MatrixClientPeg.get().deleteAlias(alias);
@@ -368,7 +347,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
});
}
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: ButtonEvent) => {
// If room was shift-clicked, remove it from the room directory
if (ev.shiftKey && !this.state.selectedCommunityId) {
ev.preventDefault();
@@ -481,17 +460,17 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
}
};
private onPreviewClick = (ev: ButtonEvent, room: IRoom) => {
private onPreviewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
this.showRoom(room, null, false, true);
ev.stopPropagation();
};
private onViewClick = (ev: ButtonEvent, room: IRoom) => {
private onViewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
this.showRoom(room);
ev.stopPropagation();
};
private onJoinClick = (ev: ButtonEvent, room: IRoom) => {
private onJoinClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
this.showRoom(room, null, true);
ev.stopPropagation();
};
@@ -509,7 +488,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
this.showRoom(null, alias, autoJoin);
}
private showRoom(room: IRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
private showRoom(room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
this.onFinished();
const payload: ActionPayload = {
action: 'view_room',
@@ -558,7 +537,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
dis.dispatch(payload);
}
private createRoomCells(room: IRoom) {
private createRoomCells(room: IPublicRoomsChunkRoom) {
const client = MatrixClientPeg.get();
const clientRoom = client.getRoom(room.room_id);
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
@@ -854,6 +833,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
// but works with the objects we get from the public room list
function getDisplayAliasForRoom(room: IRoom) {
function getDisplayAliasForRoom(room: IPublicRoomsChunkRoom) {
return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases);
}

View File

@@ -25,8 +25,8 @@ import React, { createRef } from 'react';
import classNames from 'classnames';
import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
import { EventSubscription } from "fbemitter";
import { ISearchResults } from 'matrix-js-sdk/src/@types/search';
import shouldHideEvent from '../../shouldHideEvent';
import { _t } from '../../languageHandler';
@@ -89,6 +89,7 @@ import RoomStatusBar from "./RoomStatusBar";
import MessageComposer from '../views/rooms/MessageComposer';
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
import SpaceStore from "../../stores/SpaceStore";
const DEBUG = false;
let debuglog = function(msg: string) {};
@@ -133,12 +134,7 @@ export interface IState {
searching: boolean;
searchTerm?: string;
searchScope?: SearchScope;
searchResults?: XOR<{}, {
count: number;
highlights: string[];
results: SearchResult[];
next_batch: string; // eslint-disable-line camelcase
}>;
searchResults?: XOR<{}, ISearchResults>;
searchHighlights?: string[];
searchInProgress?: boolean;
callState?: CallState;
@@ -921,6 +917,7 @@ export default class RoomView extends React.Component<IProps, IState> {
// called when state.room is first initialised (either at initial load,
// after a successful peek, or after we join the room).
private onRoomLoaded = (room: Room) => {
if (this.unmounted) return;
// Attach a widget store listener only when we get a room
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.onWidgetLayoutChange(); // provoke an update
@@ -935,9 +932,9 @@ export default class RoomView extends React.Component<IProps, IState> {
};
private async calculateRecommendedVersion(room: Room) {
this.setState({
upgradeRecommendation: await room.getRecommendedVersion(),
});
const upgradeRecommendation = await room.getRecommendedVersion();
if (this.unmounted) return;
this.setState({ upgradeRecommendation });
}
private async loadMembersIfJoined(room: Room) {
@@ -1027,23 +1024,19 @@ export default class RoomView extends React.Component<IProps, IState> {
};
private async updateE2EStatus(room: Room) {
if (!this.context.isRoomEncrypted(room.roomId)) {
return;
}
if (!this.context.isCryptoEnabled()) {
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
this.setState({
e2eStatus: E2EStatus.Warning,
});
return;
if (!this.context.isRoomEncrypted(room.roomId)) return;
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
let e2eStatus = E2EStatus.Warning;
if (this.context.isCryptoEnabled()) {
/* At this point, the user has encryption on and cross-signing on */
e2eStatus = await shieldStatusForRoom(this.context, room);
}
/* At this point, the user has encryption on and cross-signing on */
this.setState({
e2eStatus: await shieldStatusForRoom(this.context, room),
});
if (this.unmounted) return;
this.setState({ e2eStatus });
}
private onAccountData = (event: MatrixEvent) => {
@@ -1137,7 +1130,7 @@ export default class RoomView extends React.Component<IProps, IState> {
if (this.state.searchResults.next_batch) {
debuglog("requesting more search results");
const searchPromise = searchPagination(this.state.searchResults);
const searchPromise = searchPagination(this.state.searchResults as ISearchResults);
return this.handleSearchResult(searchPromise);
} else {
debuglog("no more search results");
@@ -1753,10 +1746,8 @@ export default class RoomView extends React.Component<IProps, IState> {
}
const myMembership = this.state.room.getMyMembership();
if (myMembership === "invite"
// SpaceRoomView handles invites itself
&& (!SettingsStore.getValue("feature_spaces") || !this.state.room.isSpaceRoom())
) {
// SpaceRoomView handles invites itself
if (myMembership === "invite" && (!SpaceStore.spacesEnabled || !this.state.room.isSpaceRoom())) {
if (this.state.joining || this.state.rejecting) {
return (
<ErrorBoundary>
@@ -1887,7 +1878,7 @@ export default class RoomView extends React.Component<IProps, IState> {
room={this.state.room}
/>
);
if (!this.state.canPeek && (!SettingsStore.getValue("feature_spaces") || !this.state.room?.isSpaceRoom())) {
if (!this.state.canPeek && (!SpaceStore.spacesEnabled || !this.state.room?.isSpaceRoom())) {
return (
<div className="mx_RoomView">
{ previewBar }

View File

@@ -187,7 +187,7 @@ export default class ScrollPanel extends React.Component<IProps> {
private fillRequestWhileRunning: boolean;
private scrollState: IScrollState;
private preventShrinkingState: IPreventShrinkingState;
private unfillDebouncer: NodeJS.Timeout;
private unfillDebouncer: number;
private bottomGrowth: number;
private pages: number;
private heightUpdateInProgress: boolean;

View File

@@ -18,6 +18,7 @@ import React, { ReactNode, useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
import classNames from "classnames";
import { sortBy } from "lodash";
@@ -52,36 +53,6 @@ interface IHierarchyProps {
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
}
/* eslint-disable camelcase */
export interface ISpaceSummaryRoom {
canonical_alias?: string;
aliases: string[];
avatar_url?: string;
guest_can_join: boolean;
name?: string;
num_joined_members: number;
room_id: string;
topic?: string;
world_readable: boolean;
num_refs: number;
room_type: string;
}
export interface ISpaceSummaryEvent {
room_id: string;
event_id: string;
origin_server_ts: number;
type: string;
state_key: string;
content: {
order?: string;
suggested?: boolean;
auto_join?: boolean;
via?: string[];
};
}
/* eslint-enable camelcase */
interface ITileProps {
room: ISpaceSummaryRoom;
suggested?: boolean;

View File

@@ -62,7 +62,6 @@ import IconizedContextMenu, {
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { BetaPill } from "../views/beta/BetaCard";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import SettingsStore from "../../settings/SettingsStore";
import Modal from "../../Modal";
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
import SdkConfig from "../../SdkConfig";
@@ -178,7 +177,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
const [busy, setBusy] = useState(false);
const spacesEnabled = SettingsStore.getValue("feature_spaces");
const spacesEnabled = SpaceStore.spacesEnabled;
const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
&& space.getJoinRule() !== JoinRule.Public;
@@ -854,7 +853,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
private renderBody() {
switch (this.state.phase) {
case Phase.Landing:
if (this.state.myMembership === "join" && SettingsStore.getValue("feature_spaces")) {
if (this.state.myMembership === "join" && SpaceStore.spacesEnabled) {
return <SpaceLanding space={this.props.space} />;
} else {
return <SpacePreview

View File

@@ -20,6 +20,7 @@ import * as React from "react";
import { _t } from '../../languageHandler';
import AutoHideScrollbar from './AutoHideScrollbar';
import { replaceableComponent } from "../../utils/replaceableComponent";
import classNames from "classnames";
import AccessibleButton from "../views/elements/AccessibleButton";
/**
@@ -37,9 +38,16 @@ export class Tab {
}
}
export enum TabLocation {
LEFT = 'left',
TOP = 'top',
}
interface IProps {
tabs: Tab[];
initialTabId?: string;
tabLocation: TabLocation;
onChange?: (tabId: string) => void;
}
interface IState {
@@ -62,6 +70,10 @@ export default class TabbedView extends React.Component<IProps, IState> {
};
}
static defaultProps = {
tabLocation: TabLocation.LEFT,
};
private _getActiveTabIndex() {
if (!this.state || !this.state.activeTabIndex) return 0;
return this.state.activeTabIndex;
@@ -75,6 +87,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
private _setActiveTab(tab: Tab) {
const idx = this.props.tabs.indexOf(tab);
if (idx !== -1) {
if (this.props.onChange) this.props.onChange(tab.id);
this.setState({ activeTabIndex: idx });
} else {
console.error("Could not find tab " + tab.label + " in tabs");
@@ -119,8 +132,14 @@ export default class TabbedView extends React.Component<IProps, IState> {
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
const tabbedViewClasses = classNames({
'mx_TabbedView': true,
'mx_TabbedView_tabsOnLeft': this.props.tabLocation == TabLocation.LEFT,
'mx_TabbedView_tabsOnTop': this.props.tabLocation == TabLocation.TOP,
});
return (
<div className="mx_TabbedView">
<div className={tabbedViewClasses}>
<div className="mx_TabbedView_tabLabels">
{labels}
</div>

View File

@@ -1051,6 +1051,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
{ windowLimit: this.props.timelineCap });
const onLoaded = () => {
if (this.unmounted) return;
// clear the timeline min-height when
// (re)loading the timeline
if (this.messagePanel.current) {
@@ -1092,6 +1094,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
};
const onError = (error) => {
if (this.unmounted) return;
this.setState({ timelineLoading: false });
console.error(
`Error loading timeline panel at ${eventId}: ${error}`,

View File

@@ -90,7 +90,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
};
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
if (SettingsStore.getValue("feature_spaces")) {
if (SpaceStore.spacesEnabled) {
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
@@ -115,7 +115,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
this.tagStoreRef.remove();
if (SettingsStore.getValue("feature_spaces")) {
if (SpaceStore.spacesEnabled) {
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
MatrixClientPeg.get().removeListener("Room", this.onRoom);

View File

@@ -41,7 +41,7 @@ import CaptchaForm from "./CaptchaForm";
* one HS whilst beign a guest on another).
* loginType: the login type of the auth stage being attempted
* authSessionId: session id from the server
* clientSecret: The client secret in use for ID server auth sessions
* clientSecret: The client secret in use for identity server auth sessions
* stageParams: params from the server for the stage being attempted
* errorText: error message from a previous attempt to authenticate
* submitAuthDict: a function which will be called with the new auth dict
@@ -54,8 +54,8 @@ import CaptchaForm from "./CaptchaForm";
* Defined keys for stages are:
* m.login.email.identity:
* * emailSid: string representing the sid of the active
* verification session from the ID server, or
* null if no session is active.
* verification session from the identity server,
* or null if no session is active.
* fail: a function which should be called with an error object if an
* error occurred during the auth stage. This will cause the auth
* session to be failed and the process to go back to the start.

View File

@@ -105,7 +105,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
</div>
<img src={image} alt="" />
</div>
{ extraSettings && <div className="mx_BetaCard_relatedSettings">
{ extraSettings && value && <div className="mx_BetaCard_relatedSettings">
{ extraSettings.map(key => (
<SettingsFlag key={key} name={key} level={SettingLevel.DEVICE} />
)) }

View File

@@ -53,7 +53,7 @@ export default class CallContextMenu extends React.Component<IProps> {
onTransferClick = () => {
Modal.createTrackedDialog(
'Transfer Call', '', InviteDialog, { kind: KIND_CALL_TRANSFER, call: this.props.call },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
/*className=*/"mx_InviteDialog_transferWrapper", /*isPriority=*/false, /*isStatic=*/true,
);
this.props.onFinished();
};

View File

@@ -15,11 +15,11 @@ limitations under the License.
*/
import React from 'react';
import { _t } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton";
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import Field from "../elements/Field";
import Dialpad from '../voip/DialPad';
import DialPad from '../voip/DialPad';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps {
@@ -45,24 +45,29 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
this.setState({ value: this.state.value + digit });
};
onCancelClick = () => {
this.props.onFinished();
};
onChange = (ev) => {
this.setState({ value: ev.target.value });
};
render() {
return <ContextMenu {...this.props}>
<div className="mx_DialPadContextMenu_header">
<div className="mx_DialPadContextMenuWrapper">
<div>
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
<AccessibleButton className="mx_DialPadContextMenu_cancel" onClick={this.onCancelClick} />
</div>
<div className="mx_DialPadContextMenu_header">
<Field className="mx_DialPadContextMenu_dialled"
value={this.state.value} autoFocus={true}
onChange={this.onChange}
/>
</div>
<div className="mx_DialPadContextMenu_dialPad">
<DialPad onDigitPress={this.onDigitPress} hasDial={false} />
</div>
<Field className="mx_DialPadContextMenu_dialled"
value={this.state.value} autoFocus={true}
onChange={this.onChange}
/>
</div>
<div className="mx_DialPadContextMenu_horizSep" />
<div className="mx_DialPadContextMenu_dialPad">
<Dialpad onDigitPress={this.onDigitPress} hasDialAndDelete={false} />
</div>
</ContextMenu>;
}

View File

@@ -43,6 +43,7 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
import SpaceStore from "../../../stores/SpaceStore";
const AVATAR_SIZE = 30;
@@ -180,7 +181,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
const spacesEnabled = useFeatureEnabled("feature_spaces");
const spacesEnabled = SpaceStore.spacesEnabled;
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
const previewLayout = useSettingValue<Layout>("layout");

View File

@@ -46,7 +46,7 @@ export default class IntegrationsImpossibleDialog extends React.Component {
<div className='mx_IntegrationsImpossibleDialog_content'>
<p>
{_t(
"Your %(brand)s doesn't allow you to use an Integration Manager to do this. " +
"Your %(brand)s doesn't allow you to use an integration manager to do this. " +
"Please contact an admin.",
{ brand },
)}

View File

@@ -32,7 +32,6 @@ import Modal from "../../../Modal";
import { humanizeTime } from "../../../utils/humanize";
import createRoom, {
canEncryptToAllUsers,
ensureDMExists,
findDMForUser,
privateShouldBeEncrypted,
} from "../../../createRoom";
@@ -64,9 +63,15 @@ import { copyPlaintext, selectText } from "../../../utils/strings";
import * as ContextMenu from "../../structures/ContextMenu";
import { toRightOf } from "../../structures/ContextMenu";
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
import { TransferCallPayload } from '../../../dispatcher/payloads/TransferCallPayload';
import Field from '../elements/Field';
import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView';
import Dialpad from '../voip/DialPad';
import QuestionDialog from "./QuestionDialog";
import Spinner from "../elements/Spinner";
import BaseDialog from "./BaseDialog";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import SpaceStore from "../../../stores/SpaceStore";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@@ -79,11 +84,19 @@ interface IRecentUser {
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
// NB. This dialog needs the 'mx_InviteDialog_transferWrapper' wrapper class to have the correct
// padding on the bottom (because all modals have 24px padding on all sides), so this needs to
// be passed when creating the modal
export const KIND_CALL_TRANSFER = "call_transfer";
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
enum TabId {
UserDirectory = 'users',
DialPad = 'dialpad',
}
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
// for 3PIDs/email addresses.
@@ -109,11 +122,11 @@ export abstract class Member {
class DirectoryMember extends Member {
private readonly _userId: string;
private readonly displayName: string;
private readonly avatarUrl: string;
private readonly displayName?: string;
private readonly avatarUrl?: string;
// eslint-disable-next-line camelcase
constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
constructor(userDirResult: { user_id: string, display_name?: string, avatar_url?: string }) {
super();
this._userId = userDirResult.user_id;
this.displayName = userDirResult.display_name;
@@ -356,6 +369,8 @@ interface IInviteDialogState {
canUseIdentityServer: boolean;
tryingIdentityServer: boolean;
consultFirst: boolean;
dialPadValue: string;
currentTabId: TabId;
// These two flags are used for the 'Go' button to communicate what is going on.
busy: boolean;
@@ -370,7 +385,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
};
private closeCopiedTooltip: () => void;
private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
private debounceTimer: number = null; // actually number because we're in the browser
private editorRef = createRef<HTMLInputElement>();
private unmounted = false;
@@ -407,6 +422,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
tryingIdentityServer: false,
consultFirst: false,
dialPadValue: '',
currentTabId: TabId.UserDirectory,
// These two flags are used for the 'Go' button to communicate what is going on.
busy: false,
@@ -768,44 +785,32 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
};
private transferCall = async () => {
this.convertFilter();
const targets = this.convertFilter();
const targetIds = targets.map(t => t.userId);
if (targetIds.length > 1) {
this.setState({
errorText: _t("A call can only be transferred to a single user."),
});
}
if (this.state.consultFirst) {
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), targetIds[0]);
dis.dispatch({
action: 'place_call',
type: this.props.call.type,
room_id: dmRoomId,
transferee: this.props.call,
});
dis.dispatch({
action: 'view_room',
room_id: dmRoomId,
should_peek: false,
joining: false,
});
this.props.onFinished();
} else {
this.setState({ busy: true });
try {
await this.props.call.transfer(targetIds[0]);
this.setState({ busy: false });
this.props.onFinished();
} catch (e) {
if (this.state.currentTabId == TabId.UserDirectory) {
this.convertFilter();
const targets = this.convertFilter();
const targetIds = targets.map(t => t.userId);
if (targetIds.length > 1) {
this.setState({
busy: false,
errorText: _t("Failed to transfer call"),
errorText: _t("A call can only be transferred to a single user."),
});
return;
}
dis.dispatch({
action: Action.TransferCallToMatrixID,
call: this.props.call,
destination: targetIds[0],
consultFirst: this.state.consultFirst,
} as TransferCallPayload);
} else {
dis.dispatch({
action: Action.TransferCallToPhoneNumber,
call: this.props.call,
destination: this.state.dialPadValue,
consultFirst: this.state.consultFirst,
} as TransferCallPayload);
}
this.props.onFinished();
};
private onKeyDown = (e) => {
@@ -827,6 +832,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}
};
private onCancel = () => {
this.props.onFinished([]);
};
private updateSuggestions = async (term) => {
MatrixClientPeg.get().searchUserDirectory({ term }).then(async r => {
if (term !== this.state.filterText) {
@@ -962,11 +971,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
private toggleMember = (member: Member) => {
if (!this.state.busy) {
let filterText = this.state.filterText;
const targets = this.state.targets.map(t => t); // cheap clone for mutation
let targets = this.state.targets.map(t => t); // cheap clone for mutation
const idx = targets.indexOf(member);
if (idx >= 0) {
targets.splice(idx, 1);
} else {
if (this.props.kind === KIND_CALL_TRANSFER && targets.length > 0) {
targets = [];
}
targets.push(member);
filterText = ""; // clear the filter when the user accepts a suggestion
}
@@ -1189,6 +1201,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}
private renderEditor() {
const hasPlaceholder = (
this.props.kind == KIND_CALL_TRANSFER &&
this.state.targets.length === 0 &&
this.state.filterText.length === 0
);
const targets = this.state.targets.map(t => (
<DMUserTile member={t} onRemove={!this.state.busy && this.removeMember} key={t.userId} />
));
@@ -1201,8 +1218,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
ref={this.editorRef}
onPaste={this.onPaste}
autoFocus={true}
disabled={this.state.busy}
disabled={this.state.busy || (this.props.kind == KIND_CALL_TRANSFER && this.state.targets.length > 0)}
autoComplete="off"
placeholder={hasPlaceholder ? _t("Search") : null}
/>
);
return (
@@ -1249,6 +1267,28 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}
}
private onDialFormSubmit = ev => {
ev.preventDefault();
this.transferCall();
};
private onDialChange = ev => {
this.setState({ dialPadValue: ev.currentTarget.value });
};
private onDigitPress = digit => {
this.setState({ dialPadValue: this.state.dialPadValue + digit });
};
private onDeletePress = () => {
if (this.state.dialPadValue.length === 0) return;
this.setState({ dialPadValue: this.state.dialPadValue.slice(0, -1) });
};
private onTabChange = (tabId: TabId) => {
this.setState({ currentTabId: tabId });
};
private async onLinkClick(e) {
e.preventDefault();
selectText(e.target);
@@ -1278,12 +1318,16 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
let helpText;
let buttonText;
let goButtonFn;
let consultConnectSection;
let extraSection;
let footer;
let keySharingWarning = <span />;
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
const hasSelection = this.state.targets.length > 0
|| (this.state.filterText && this.state.filterText.includes('@'));
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
if (this.props.kind === KIND_DM) {
@@ -1364,7 +1408,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
</div>;
} else if (this.props.kind === KIND_INVITE) {
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
const isSpace = SpaceStore.spacesEnabled && room?.isSpaceRoom();
title = isSpace
? _t("Invite to %(spaceName)s", {
spaceName: room.name || _t("Unnamed Space"),
@@ -1421,23 +1465,116 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}
} else if (this.props.kind === KIND_CALL_TRANSFER) {
title = _t("Transfer");
buttonText = _t("Transfer");
goButtonFn = this.transferCall;
footer = <div>
consultConnectSection = <div className="mx_InviteDialog_transferConsultConnect">
<label>
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
{_t("Consult first")}
</label>
<AccessibleButton
kind="secondary"
onClick={this.onCancel}
className='mx_InviteDialog_transferConsultConnect_pushRight'
>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton
kind="primary"
onClick={this.transferCall}
className='mx_InviteDialog_transferButton'
disabled={!hasSelection && this.state.dialPadValue === ''}
>
{_t("Transfer")}
</AccessibleButton>
</div>;
} else {
console.error("Unknown kind of InviteDialog: " + this.props.kind);
}
const hasSelection = this.state.targets.length > 0
|| (this.state.filterText && this.state.filterText.includes('@'));
const goButton = this.props.kind == KIND_CALL_TRANSFER ? null : <AccessibleButton
kind="primary"
onClick={goButtonFn}
className='mx_InviteDialog_goButton'
disabled={this.state.busy || !hasSelection}
>
{buttonText}
</AccessibleButton>;
const usersSection = <React.Fragment>
<p className='mx_InviteDialog_helpText'>{helpText}</p>
<div className='mx_InviteDialog_addressBar'>
{this.renderEditor()}
<div className='mx_InviteDialog_buttonAndSpinner'>
{goButton}
{spinner}
</div>
</div>
{keySharingWarning}
{this.renderIdentityServerWarning()}
<div className='error'>{this.state.errorText}</div>
<div className='mx_InviteDialog_userSections'>
{this.renderSection('recents')}
{this.renderSection('suggestions')}
{extraSection}
</div>
{footer}
</React.Fragment>;
let dialogContent;
if (this.props.kind === KIND_CALL_TRANSFER) {
const tabs = [];
tabs.push(new Tab(
TabId.UserDirectory, _td("User Directory"), 'mx_InviteDialog_userDirectoryIcon', usersSection,
));
const backspaceButton = (
<DialPadBackspaceButton onBackspacePress={this.onDeletePress} />
);
// Only show the backspace button if the field has content
let dialPadField;
if (this.state.dialPadValue.length !== 0) {
dialPadField = <Field className="mx_InviteDialog_dialPadField" id="dialpad_number"
value={this.state.dialPadValue}
autoFocus={true}
onChange={this.onDialChange}
postfixComponent={backspaceButton}
/>;
} else {
dialPadField = <Field className="mx_InviteDialog_dialPadField" id="dialpad_number"
value={this.state.dialPadValue}
autoFocus={true}
onChange={this.onDialChange}
/>;
}
const dialPadSection = <div className="mx_InviteDialog_dialPad">
<form onSubmit={this.onDialFormSubmit}>
{dialPadField}
</form>
<Dialpad hasDial={false}
onDigitPress={this.onDigitPress} onDeletePress={this.onDeletePress}
/>
</div>;
tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection));
dialogContent = <React.Fragment>
<TabbedView tabs={tabs} initialTabId={this.state.currentTabId}
tabLocation={TabLocation.TOP} onChange={this.onTabChange}
/>
{consultConnectSection}
</React.Fragment>;
} else {
dialogContent = <React.Fragment>
{usersSection}
{consultConnectSection}
</React.Fragment>;
}
return (
<BaseDialog
className={classNames("mx_InviteDialog", {
className={classNames({
mx_InviteDialog_transfer: this.props.kind === KIND_CALL_TRANSFER,
mx_InviteDialog_other: this.props.kind !== KIND_CALL_TRANSFER,
mx_InviteDialog_hasFooter: !!footer,
})}
hasCancel={true}
@@ -1445,30 +1582,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
title={title}
>
<div className='mx_InviteDialog_content'>
<p className='mx_InviteDialog_helpText'>{helpText}</p>
<div className='mx_InviteDialog_addressBar'>
{this.renderEditor()}
<div className='mx_InviteDialog_buttonAndSpinner'>
<AccessibleButton
kind="primary"
onClick={goButtonFn}
className='mx_InviteDialog_goButton'
disabled={this.state.busy || !hasSelection}
>
{buttonText}
</AccessibleButton>
{spinner}
</div>
</div>
{keySharingWarning}
{this.renderIdentityServerWarning()}
<div className='error'>{this.state.errorText}</div>
<div className='mx_InviteDialog_userSections'>
{this.renderSection('recents')}
{this.renderSection('suggestions')}
{extraSection}
</div>
{footer}
{dialogContent}
</div>
</BaseDialog>
);

View File

@@ -35,7 +35,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu.js";
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
const socials = [
{

View File

@@ -90,9 +90,9 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
switch (serviceType) {
case SERVICE_TYPES.IS:
return <div>{_t("Identity Server")}<br />({host})</div>;
return <div>{_t("Identity server")}<br />({host})</div>;
case SERVICE_TYPES.IM:
return <div>{_t("Integration Manager")}<br />({host})</div>;
return <div>{_t("Integration manager")}<br />({host})</div>;
}
}

View File

@@ -21,7 +21,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import BaseDialog from "./BaseDialog";
import EncryptionPanel from "../right_panel/EncryptionPanel";
import { User } from 'matrix-js-sdk';
import { User } from 'matrix-js-sdk/src/models/user';
interface IProps {
verificationRequest: VerificationRequest;

View File

@@ -17,6 +17,7 @@ limitations under the License.
import React, { useEffect, useState } from "react";
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { IProtocol } from "matrix-js-sdk/src/client";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { instanceForInstanceId } from '../../../utils/DirectoryUtils';
@@ -83,30 +84,6 @@ const validServer = withValidation<undefined, { error?: MatrixError }>({
],
});
/* eslint-disable camelcase */
export interface IFieldType {
regexp: string;
placeholder: string;
}
export interface IInstance {
desc: string;
icon?: string;
fields: object;
network_id: string;
// XXX: this is undocumented but we rely on it.
instance_id: string;
}
export interface IProtocol {
user_fields: string[];
location_fields: string[];
icon: string;
field_types: Record<string, IFieldType>;
instances: IInstance[];
}
/* eslint-enable camelcase */
export type Protocols = Record<string, IProtocol>;
interface IProps {

View File

@@ -114,7 +114,7 @@ export default class AppPermission extends React.Component {
// Due to i18n limitations, we can't dedupe the code for variables in these two messages.
const warning = this.state.isWrapped
? _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.",
? _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.",
{ widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip })
: _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
{ widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip });

View File

@@ -0,0 +1,31 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from "react";
import AccessibleButton from "./AccessibleButton";
interface IProps {
// Callback for when the button is pressed
onBackspacePress: () => void;
}
export default class DialPadBackspaceButton extends React.PureComponent<IProps> {
render() {
return <div className="mx_DialPadBackspaceButtonWrapper">
<AccessibleButton className="mx_DialPadBackspaceButton" onClick={this.props.onBackspacePress} />
</div>;
}
}

View File

@@ -32,7 +32,7 @@ interface IProps {
hasAvatar: boolean;
noAvatarLabel?: string;
hasAvatarLabel?: string;
setAvatarUrl(url: string): Promise<void>;
setAvatarUrl(url: string): Promise<unknown>;
}
const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => {

View File

@@ -15,12 +15,14 @@
*/
import React from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import Flair from '../elements/Flair';
import FlairStore from '../../../stores/FlairStore';
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
interface IProps {
mxEvent: MatrixEvent;
@@ -36,7 +38,7 @@ interface IState {
@replaceableComponent("views.messages.SenderProfile")
export default class SenderProfile extends React.Component<IProps, IState> {
static contextType = MatrixClientContext;
private unmounted: boolean;
private unmounted = false;
constructor(props: IProps) {
super(props);
@@ -49,8 +51,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
}
componentDidMount() {
this.unmounted = false;
this._updateRelatedGroups();
this.updateRelatedGroups();
if (this.state.userGroups.length === 0) {
this.getPublicisedGroups();
@@ -64,35 +65,29 @@ export default class SenderProfile extends React.Component<IProps, IState> {
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
}
async getPublicisedGroups() {
if (!this.unmounted) {
const userGroups = await FlairStore.getPublicisedGroupsCached(
this.context, this.props.mxEvent.getSender(),
);
this.setState({ userGroups });
}
private async getPublicisedGroups() {
const userGroups = await FlairStore.getPublicisedGroupsCached(this.context, this.props.mxEvent.getSender());
if (this.unmounted) return;
this.setState({ userGroups });
}
onRoomStateEvents = event => {
if (event.getType() === 'm.room.related_groups' &&
event.getRoomId() === this.props.mxEvent.getRoomId()
) {
this._updateRelatedGroups();
private onRoomStateEvents = (event: MatrixEvent) => {
if (event.getType() === 'm.room.related_groups' && event.getRoomId() === this.props.mxEvent.getRoomId()) {
this.updateRelatedGroups();
}
};
_updateRelatedGroups() {
if (this.unmounted) return;
private updateRelatedGroups() {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
if (!room) return;
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
this.setState({
relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [],
relatedGroups: relatedGroupsEvent?.getContent().groups || [],
});
}
_getDisplayedGroups(userGroups, relatedGroups) {
private getDisplayedGroups(userGroups?: string[], relatedGroups?: string[]) {
let displayedGroups = userGroups || [];
if (relatedGroups && relatedGroups.length > 0) {
displayedGroups = relatedGroups.filter((groupId) => {
@@ -113,7 +108,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || "";
const mxid = mxEvent.sender?.userId || mxEvent.getSender() || "";
if (msgtype === 'm.emote') {
if (msgtype === MsgType.Emote) {
return null; // emote message must include the name so don't duplicate it
}
@@ -128,7 +123,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
let flair;
if (this.props.enableFlair) {
const displayedGroups = this._getDisplayedGroups(
const displayedGroups = this.getDisplayedGroups(
this.state.userGroups, this.state.relatedGroups,
);

View File

@@ -69,6 +69,7 @@ import RoomName from "../elements/RoomName";
import { mediaFromMxc } from "../../../customisations/Media";
import UIStore from "../../../stores/UIStore";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import SpaceStore from "../../../stores/SpaceStore";
export interface IDevice {
deviceId: string;
@@ -728,7 +729,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({ member, room, powerLevels,
// if muting self, warn as it may be irreversible
if (target === cli.getUserId()) {
try {
if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return;
if (!(await warnSelfDemote(SpaceStore.spacesEnabled && room?.isSpaceRoom()))) return;
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
return;
@@ -817,7 +818,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
}
if (me.powerLevel >= redactPowerLevel && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) {
if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
redactButton = (
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
);
@@ -1096,7 +1097,7 @@ const PowerLevelEditor: React.FC<{
} else if (myUserId === target) {
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
try {
if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return;
if (!(await warnSelfDemote(SpaceStore.spacesEnabled && room?.isSpaceRoom()))) return;
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
}
@@ -1326,10 +1327,10 @@ const BasicUserInfo: React.FC<{
if (!isRoomEncrypted) {
if (!cryptoEnabled) {
text = _t("This client does not support end-to-end encryption.");
} else if (room && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) {
} else if (room && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
text = _t("Messages in this room are not end-to-end encrypted.");
}
} else if (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom()) {
} else if (!SpaceStore.spacesEnabled || !room.isSpaceRoom()) {
text = _t("Messages in this room are end-to-end encrypted.");
}
@@ -1405,7 +1406,7 @@ const BasicUserInfo: React.FC<{
canInvite={roomPermissions.canInvite}
isIgnored={isIgnored}
member={member as RoomMember}
isSpace={SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()}
isSpace={SpaceStore.spacesEnabled && room?.isSpaceRoom()}
/>
{ adminToolsContainer }
@@ -1568,7 +1569,7 @@ const UserInfo: React.FC<IProps> = ({
previousPhase = RightPanelPhases.RoomMemberInfo;
refireParams = { member: member };
} else if (room) {
previousPhase = previousPhase = SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()
previousPhase = previousPhase = SpaceStore.spacesEnabled && room.isSpaceRoom()
? RightPanelPhases.SpaceMemberList
: RightPanelPhases.RoomMemberList;
}
@@ -1617,7 +1618,7 @@ const UserInfo: React.FC<IProps> = ({
}
let scopeHeader;
if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) {
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={room} height={32} width={32} />
<RoomName room={room} />

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React from "react";
import { Visibility } from "matrix-js-sdk/src/@types/partials";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { _t } from "../../../languageHandler";
@@ -50,7 +51,7 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
client.setRoomDirectoryVisibility(
this.props.roomId,
newValue ? 'public' : 'private',
newValue ? Visibility.Public : Visibility.Private,
).catch(() => {
// Roll back the local echo on the change
this.setState({ isRoomPublished: valueBefore });

View File

@@ -55,7 +55,7 @@ interface IState {
export default class Autocomplete extends React.PureComponent<IProps, IState> {
autocompleter: Autocompleter;
queryRequested: string;
debounceCompletionsRequest: NodeJS.Timeout;
debounceCompletionsRequest: number;
private containerRef = createRef<HTMLDivElement>();
constructor(props) {

View File

@@ -1123,10 +1123,10 @@ export default class EventTile extends React.Component<IProps, IState> {
"data-scroll-tokens": scrollToken,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
}, [
ircTimestamp,
sender,
ircPadlock,
}, <>
{ ircTimestamp }
{ sender }
{ ircPadlock }
<div className="mx_EventTile_line" key="mx_EventTile_line">
{ groupTimestamp }
{ groupPadlock }
@@ -1144,11 +1144,10 @@ export default class EventTile extends React.Component<IProps, IState> {
{ keyRequestInfo }
{ reactionsRow }
{ actionBar }
</div>,
msgOption,
avatar,
])
</div>
{ msgOption }
{ avatar }
</>)
);
}
}

View File

@@ -40,10 +40,12 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
const ts = mxEvent.getTs();
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(link => {
return cli.getUrlPreview(link, ts).then(preview => [link, preview], error => {
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
try {
return [link, await cli.getUrlPreview(link, ts)];
} catch (error) {
console.error("Failed to get URL preview: " + error);
});
}
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
}, [links, ts], []);

View File

@@ -43,6 +43,7 @@ import EntityTile from "./EntityTile";
import MemberTile from "./MemberTile";
import BaseAvatar from '../avatars/BaseAvatar';
import { throttle } from 'lodash';
import SpaceStore from "../../../stores/SpaceStore";
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
@@ -509,7 +510,7 @@ export default class MemberList extends React.Component<IProps, IState> {
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat && chat.roomId === this.props.roomId) {
inviteButtonText = _t("Invite to this community");
} else if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) {
} else if (SpaceStore.spacesEnabled && room.isSpaceRoom()) {
inviteButtonText = _t("Invite to this space");
}
@@ -549,7 +550,7 @@ export default class MemberList extends React.Component<IProps, IState> {
let previousPhase = RightPanelPhases.RoomSummary;
// We have no previousPhase for when viewing a MemberList from a Space
let scopeHeader;
if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) {
previousPhase = undefined;
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={room} height={32} width={32} />

View File

@@ -417,7 +417,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
}
private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
if (SettingsStore.getValue("feature_spaces")) return [];
if (SpaceStore.spacesEnabled) return [];
// TODO: Put community invites in a more sensible place (not in the room list)
// See https://github.com/vector-im/element-web/issues/14456
return MatrixClientPeg.get().getGroups().filter(g => {

View File

@@ -224,7 +224,7 @@ export default class Stickerpicker extends React.PureComponent {
}
_getStickerpickerContent() {
// Handle Integration Manager errors
// Handle integration manager errors
if (this.state._imError) {
return this._errorStickerpickerContent();
}

View File

@@ -25,9 +25,9 @@ import { isValid3pidInvite } from "../../../RoomInvite";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
import ErrorDialog from '../dialogs/ErrorDialog';
import AccessibleButton from '../elements/AccessibleButton';
import SpaceStore from "../../../stores/SpaceStore";
interface IProps {
event: MatrixEvent;
@@ -134,7 +134,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
}
let scopeHeader;
if (SettingsStore.getValue("feature_spaces") && this.room.isSpaceRoom()) {
if (SpaceStore.spacesEnabled && this.room.isSpaceRoom()) {
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={this.room} height={32} width={32} />
<RoomName room={this.room} />

View File

@@ -44,7 +44,7 @@ const REACHABILITY_TIMEOUT = 10000; // ms
async function checkIdentityServerUrl(u) {
const parsedUrl = url.parse(u);
if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS");
if (parsedUrl.protocol !== 'https:') return _t("Identity server URL must be HTTPS");
// XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the
// js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it
@@ -53,17 +53,17 @@ async function checkIdentityServerUrl(u) {
if (response.ok) {
return null;
} else if (response.status < 200 || response.status >= 300) {
return _t("Not a valid Identity Server (status code %(code)s)", { code: response.status });
return _t("Not a valid identity server (status code %(code)s)", { code: response.status });
} else {
return _t("Could not connect to Identity Server");
return _t("Could not connect to identity server");
}
} catch (e) {
return _t("Could not connect to Identity Server");
return _t("Could not connect to identity server");
}
}
interface IProps {
// Whether or not the ID server is missing terms. This affects the text
// Whether or not the identity server is missing terms. This affects the text
// shown to the user.
missingTerms: boolean;
}
@@ -87,7 +87,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
let defaultIdServer = '';
if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) {
// If no ID server is configured but there's one in the config, prepopulate
// If no identity server is configured but there's one in the config, prepopulate
// the field to help the user.
defaultIdServer = abbreviateUrl(getDefaultIdentityServerUrl());
}
@@ -112,7 +112,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
}
private onAction = (payload: ActionPayload) => {
// We react to changes in the ID server in the event the user is staring at this form
// We react to changes in the identity server in the event the user is staring at this form
// when changing their identity server on another device.
if (payload.action !== "id_server_changed") return;
@@ -356,7 +356,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
let sectionTitle;
let bodyText;
if (idServerUrl) {
sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateUrl(idServerUrl) });
sectionTitle = _t("Identity server (%(server)s)", { server: abbreviateUrl(idServerUrl) });
bodyText = _t(
"You are currently using <server></server> to discover and be discoverable by " +
"existing contacts you know. You can change your identity server below.",
@@ -371,7 +371,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
);
}
} else {
sectionTitle = _t("Identity Server");
sectionTitle = _t("Identity server");
bodyText = _t(
"You are not currently using an identity server. " +
"To discover and be discoverable by existing contacts you know, " +

View File

@@ -65,13 +65,13 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
if (currentManager) {
managerName = `(${currentManager.name})`;
bodyText = _t(
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
"and sticker packs.",
{ serverName: currentManager.name },
{ b: sub => <b>{sub}</b> },
);
} else {
bodyText = _t("Use an Integration Manager to manage bots, widgets, and sticker packs.");
bodyText = _t("Use an integration manager to manage bots, widgets, and sticker packs.");
}
return (
@@ -86,7 +86,7 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
<br />
<br />
{_t(
"Integration Managers receive configuration data, and can modify widgets, " +
"Integration managers receive configuration data, and can modify widgets, " +
"send room invites, and set power levels on your behalf.",
)}
</span>

View File

@@ -75,7 +75,8 @@ interface IState extends IThemeState {
export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
private readonly MESSAGE_PREVIEW_TEXT = _t("Hey you. You're the best!");
private themeTimer: NodeJS.Timeout;
private themeTimer: number;
private unmounted = false;
constructor(props: IProps) {
super(props);
@@ -101,6 +102,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
const client = MatrixClientPeg.get();
const userId = client.getUserId();
const profileInfo = await client.getProfileInfo(userId);
if (this.unmounted) return;
this.setState({
userId,
@@ -109,6 +111,10 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
});
}
componentWillUnmount() {
this.unmounted = true;
}
private calculateThemeState(): IThemeState {
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
// show the right values for things.

View File

@@ -364,7 +364,7 @@ export default class GeneralUserSettingsTab extends React.Component {
onFinished={this.state.requiredPolicyInfo.resolve}
introElement={intro}
/>
{ /* has its own heading as it includes the current ID server */ }
{ /* has its own heading as it includes the current identity server */ }
<SetIdServer missingTerms={true} />
</div>
);
@@ -387,7 +387,7 @@ export default class GeneralUserSettingsTab extends React.Component {
return (
<div className="mx_SettingsTab_section">
{threepidSection}
{ /* has its own heading as it includes the current ID server */ }
{ /* has its own heading as it includes the current identity server */ }
<SetIdServer />
</div>
);

View File

@@ -290,7 +290,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
{_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
{_t("Identity server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
<br />
<details>
<summary>{_t("Access Token")}</summary><br />

View File

@@ -42,7 +42,6 @@ import {
import { Key } from "../../../Keyboard";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import SettingsStore from "../../../settings/SettingsStore";
interface IButtonProps {
space?: Room;
@@ -134,7 +133,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
const [invites, spaces, activeSpace] = useSpaces();
const activeSpaces = activeSpace ? [activeSpace] : [];
const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
const homeNotificationState = SpaceStore.spacesTweakAllRoomsEnabled
? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
return <div className="mx_SpaceTreeLevel">
@@ -142,7 +141,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
className="mx_SpaceButton_home"
onClick={() => SpaceStore.instance.setActiveSpace(null)}
selected={!activeSpace}
tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
tooltip={SpaceStore.spacesTweakAllRoomsEnabled ? _t("All rooms") : _t("Home")}
notificationState={homeNotificationState}
isNarrow={isPanelCollapsed}
/>

View File

@@ -39,7 +39,7 @@ enum SpaceVisibility {
const useLocalEcho = <T extends any>(
currentFactory: () => T,
setterFn: (value: T) => Promise<void>,
setterFn: (value: T) => Promise<unknown>,
errorFn: (error: Error) => void,
): [value: T, handler: (value: T) => void] => {
const [value, setValue] = useState(currentFactory);

View File

@@ -44,7 +44,7 @@ interface IState {
@replaceableComponent("views.toasts.VerificationRequestToast")
export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
private intervalHandle: NodeJS.Timeout;
private intervalHandle: number;
constructor(props) {
super(props);

View File

@@ -19,16 +19,17 @@ import AccessibleButton from "../elements/AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
const BUTTON_LETTERS = ['', 'ABC', 'DEF', 'GHI', 'JKL', 'MNO', 'PQRS', 'TUV', 'WXYZ', '', '+', ''];
enum DialPadButtonKind {
Digit,
Delete,
Dial,
}
interface IButtonProps {
kind: DialPadButtonKind;
digit?: string;
digitSubtext?: string;
onButtonPress: (string) => void;
}
@@ -42,11 +43,10 @@ class DialPadButton extends React.PureComponent<IButtonProps> {
case DialPadButtonKind.Digit:
return <AccessibleButton className="mx_DialPad_button" onClick={this.onClick}>
{this.props.digit}
<div className="mx_DialPad_buttonSubText">
{this.props.digitSubtext}
</div>
</AccessibleButton>;
case DialPadButtonKind.Delete:
return <AccessibleButton className="mx_DialPad_button mx_DialPad_deleteButton"
onClick={this.onClick}
/>;
case DialPadButtonKind.Dial:
return <AccessibleButton className="mx_DialPad_button mx_DialPad_dialButton" onClick={this.onClick} />;
}
@@ -55,7 +55,7 @@ class DialPadButton extends React.PureComponent<IButtonProps> {
interface IProps {
onDigitPress: (string) => void;
hasDialAndDelete: boolean;
hasDial: boolean;
onDeletePress?: (string) => void;
onDialPress?: (string) => void;
}
@@ -65,16 +65,15 @@ export default class Dialpad extends React.PureComponent<IProps> {
render() {
const buttonNodes = [];
for (const button of BUTTONS) {
for (let i = 0; i < BUTTONS.length; i++) {
const button = BUTTONS[i];
const digitSubtext = BUTTON_LETTERS[i];
buttonNodes.push(<DialPadButton key={button} kind={DialPadButtonKind.Digit}
digit={button} onButtonPress={this.props.onDigitPress}
digit={button} digitSubtext={digitSubtext} onButtonPress={this.props.onDigitPress}
/>);
}
if (this.props.hasDialAndDelete) {
buttonNodes.push(<DialPadButton key="del" kind={DialPadButtonKind.Delete}
onButtonPress={this.props.onDeletePress}
/>);
if (this.props.hasDial) {
buttonNodes.push(<DialPadButton key="dial" kind={DialPadButtonKind.Dial}
onButtonPress={this.props.onDialPress}
/>);

View File

@@ -15,7 +15,6 @@ limitations under the License.
*/
import * as React from "react";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field";
import DialPad from './DialPad';
@@ -23,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload";
import { Action } from "../../../dispatcher/actions";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
interface IProps {
onFinished: (boolean) => void;
@@ -74,22 +74,38 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
};
render() {
const backspaceButton = (
<DialPadBackspaceButton onBackspacePress={this.onDeletePress} />
);
// Only show the backspace button if the field has content
let dialPadField;
if (this.state.value.length !== 0) {
dialPadField = <Field className="mx_DialPadModal_field" id="dialpad_number"
value={this.state.value}
autoFocus={true}
onChange={this.onChange}
postfixComponent={backspaceButton}
/>;
} else {
dialPadField = <Field className="mx_DialPadModal_field" id="dialpad_number"
value={this.state.value}
autoFocus={true}
onChange={this.onChange}
/>;
}
return <div className="mx_DialPadModal">
<div>
<AccessibleButton className="mx_DialPadModal_cancel" onClick={this.onCancelClick} />
</div>
<div className="mx_DialPadModal_header">
<div>
<span className="mx_DialPadModal_title">{_t("Dial pad")}</span>
<AccessibleButton className="mx_DialPadModal_cancel" onClick={this.onCancelClick} />
</div>
<form onSubmit={this.onFormSubmit}>
<Field className="mx_DialPadModal_field" id="dialpad_number"
value={this.state.value} autoFocus={true}
onChange={this.onChange}
/>
{dialPadField}
</form>
</div>
<div className="mx_DialPadModal_horizSep" />
<div className="mx_DialPadModal_dialPad">
<DialPad hasDialAndDelete={true}
<DialPad hasDial={true}
onDigitPress={this.onDigitPress}
onDeletePress={this.onDeletePress}
onDialPress={this.onDialPress}