Merge remote-tracking branch 'origin/develop' into feat/add-formating-buttons-to-wysiwyg
This commit is contained in:
commit
f85f53248b
128 changed files with 4303 additions and 1024 deletions
|
@ -39,6 +39,7 @@ interface IOptionListProps {
|
|||
|
||||
interface IOptionProps extends React.ComponentProps<typeof MenuItem> {
|
||||
iconClassName?: string;
|
||||
isDestructive?: boolean;
|
||||
}
|
||||
|
||||
interface ICheckboxProps extends React.ComponentProps<typeof MenuItemCheckbox> {
|
||||
|
@ -112,12 +113,14 @@ export const IconizedContextMenuOption: React.FC<IOptionProps> = ({
|
|||
className,
|
||||
iconClassName,
|
||||
children,
|
||||
isDestructive,
|
||||
...props
|
||||
}) => {
|
||||
return <MenuItem
|
||||
{...props}
|
||||
className={classNames(className, {
|
||||
mx_IconizedContextMenu_item: true,
|
||||
mx_IconizedContextMenu_itemDestructive: isDestructive,
|
||||
})}
|
||||
label={label}
|
||||
>
|
||||
|
|
66
src/components/views/context_menus/KebabContextMenu.tsx
Normal file
66
src/components/views/context_menus/KebabContextMenu.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Icon as ContextMenuIcon } from '../../../../res/img/element-icons/context-menu.svg';
|
||||
import { ChevronFace, ContextMenuButton, useContextMenu } from '../../structures/ContextMenu';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import IconizedContextMenu, { IconizedContextMenuOptionList } from './IconizedContextMenu';
|
||||
|
||||
const contextMenuBelow = (elementRect: DOMRect) => {
|
||||
// align the context menu's icons with the icon which opened the context menu
|
||||
const left = elementRect.left + window.scrollX + elementRect.width;
|
||||
const top = elementRect.bottom + window.scrollY;
|
||||
const chevronFace = ChevronFace.None;
|
||||
return { left, top, chevronFace };
|
||||
};
|
||||
|
||||
interface KebabContextMenuProps extends Partial<React.ComponentProps<typeof AccessibleButton>> {
|
||||
options: React.ReactNode[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const KebabContextMenu: React.FC<KebabContextMenuProps> = ({
|
||||
options,
|
||||
title,
|
||||
...props
|
||||
}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
return <>
|
||||
<ContextMenuButton
|
||||
{...props}
|
||||
onClick={openMenu}
|
||||
title={title}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={button}
|
||||
>
|
||||
<ContextMenuIcon className='mx_KebabContextMenu_icon' />
|
||||
</ContextMenuButton>
|
||||
{ menuDisplayed && (<IconizedContextMenu
|
||||
onFinished={closeMenu}
|
||||
compact
|
||||
rightAligned
|
||||
closeOnInteraction
|
||||
{...contextMenuBelow(button.current.getBoundingClientRect())}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ options }
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>) }
|
||||
</>;
|
||||
};
|
|
@ -16,7 +16,6 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import request from 'browser-request';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
|
@ -37,22 +36,33 @@ export default class ChangelogDialog extends React.Component<IProps> {
|
|||
this.state = {};
|
||||
}
|
||||
|
||||
private async fetchChanges(repo: string, oldVersion: string, newVersion: string): Promise<void> {
|
||||
const url = `https://riot.im/github/repos/${repo}/compare/${oldVersion}...${newVersion}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
this.setState({ [repo]: res.statusText });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await res.json();
|
||||
this.setState({ [repo]: body.commits });
|
||||
} catch (err) {
|
||||
this.setState({ [repo]: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const version = this.props.newVersion.split('-');
|
||||
const version2 = this.props.version.split('-');
|
||||
if (version == null || version2 == null) return;
|
||||
// parse versions of form: [vectorversion]-react-[react-sdk-version]-js-[js-sdk-version]
|
||||
for (let i=0; i<REPOS.length; i++) {
|
||||
for (let i = 0; i < REPOS.length; i++) {
|
||||
const oldVersion = version2[2*i];
|
||||
const newVersion = version[2*i];
|
||||
const url = `https://riot.im/github/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`;
|
||||
request(url, (err, response, body) => {
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
this.setState({ [REPOS[i]]: response.statusText });
|
||||
return;
|
||||
}
|
||||
this.setState({ [REPOS[i]]: JSON.parse(body).commits });
|
||||
});
|
||||
this.fetchChanges(REPOS[i], oldVersion, newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -654,12 +654,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
|||
const token = await authClient.getAccessToken();
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
||||
'email',
|
||||
term,
|
||||
undefined, // callback
|
||||
token,
|
||||
);
|
||||
const lookup = await MatrixClientPeg.get().lookupThreePid('email', term, token);
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
if (!lookup || !lookup.mxid) {
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/matrix';
|
||||
import { MatrixClient, Method } from 'matrix-js-sdk/src/matrix';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -33,17 +33,10 @@ import { SettingLevel } from "../../../settings/SettingLevel";
|
|||
* @throws if the proxy server is unreachable or not configured to the given homeserver
|
||||
*/
|
||||
async function syncHealthCheck(cli: MatrixClient): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), 10 * 1000); // 10s
|
||||
const url = cli.http.getUrl("/sync", {}, "/_matrix/client/unstable/org.matrix.msc3575");
|
||||
const res = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
method: "POST",
|
||||
await cli.http.authedRequest(Method.Post, "/sync", undefined, undefined, {
|
||||
localTimeoutMs: 10 * 1000, // 10s
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc3575",
|
||||
});
|
||||
clearTimeout(id);
|
||||
if (res.status != 200) {
|
||||
throw new Error(`syncHealthCheck: server returned HTTP ${res.status}`);
|
||||
}
|
||||
logger.info("server natively support sliding sync OK");
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
searchQuery: string;
|
||||
langs: string[];
|
||||
langs: Awaited<ReturnType<typeof languageHandler.getAllLanguagesFromJson>>;
|
||||
}
|
||||
|
||||
export default class LanguageDropdown extends React.Component<IProps, IState> {
|
||||
|
@ -60,7 +60,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
|
|||
});
|
||||
this.setState({ langs });
|
||||
}).catch(() => {
|
||||
this.setState({ langs: ['en'] });
|
||||
this.setState({ langs: [{ value: 'en', label: "English" }] });
|
||||
});
|
||||
|
||||
if (!this.props.value) {
|
||||
|
@ -83,7 +83,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
|
|||
return <Spinner />;
|
||||
}
|
||||
|
||||
let displayedLanguages;
|
||||
let displayedLanguages: Awaited<ReturnType<typeof languageHandler.getAllLanguagesFromJson>>;
|
||||
if (this.state.searchQuery) {
|
||||
displayedLanguages = this.state.langs.filter((lang) => {
|
||||
return languageMatchesSearchQuery(this.state.searchQuery, lang);
|
||||
|
|
|
@ -74,7 +74,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({
|
|||
if (!ev.target.files?.length) return;
|
||||
setBusy(true);
|
||||
const file = ev.target.files[0];
|
||||
const uri = await cli.uploadContent(file);
|
||||
const { content_uri: uri } = await cli.uploadContent(file);
|
||||
await setAvatarUrl(uri);
|
||||
setBusy(false);
|
||||
}}
|
||||
|
|
|
@ -43,6 +43,8 @@ import MjolnirBody from "./MjolnirBody";
|
|||
import MBeaconBody from "./MBeaconBody";
|
||||
import { IEventTileOps } from "../rooms/EventTile";
|
||||
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from '../../../voice-broadcast';
|
||||
import { Features } from '../../../settings/Settings';
|
||||
import { SettingLevel } from '../../../settings/SettingLevel';
|
||||
|
||||
// onMessageAllowed is handled internally
|
||||
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
|
||||
|
@ -60,6 +62,10 @@ export interface IOperableEventTile {
|
|||
getEventTileOps(): IEventTileOps;
|
||||
}
|
||||
|
||||
interface State {
|
||||
voiceBroadcastEnabled: boolean;
|
||||
}
|
||||
|
||||
const baseBodyTypes = new Map<string, typeof React.Component>([
|
||||
[MsgType.Text, TextualBody],
|
||||
[MsgType.Notice, TextualBody],
|
||||
|
@ -78,7 +84,7 @@ const baseEvTypes = new Map<string, React.ComponentType<Partial<IBodyProps>>>([
|
|||
[VoiceBroadcastInfoEventType, VoiceBroadcastBody],
|
||||
]);
|
||||
|
||||
export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
|
||||
export default class MessageEvent extends React.Component<IProps, State> implements IMediaBody, IOperableEventTile {
|
||||
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
|
||||
private mediaHelper: MediaEventHelper;
|
||||
private bodyTypes = new Map<string, typeof React.Component>(baseBodyTypes.entries());
|
||||
|
@ -86,6 +92,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||
|
||||
public static contextType = MatrixClientContext;
|
||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
||||
private voiceBroadcastSettingWatcherRef: string;
|
||||
|
||||
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
|
@ -95,15 +102,29 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||
}
|
||||
|
||||
this.updateComponentMaps();
|
||||
|
||||
this.state = {
|
||||
// only check voice broadcast settings for a voice broadcast event
|
||||
voiceBroadcastEnabled: this.props.mxEvent.getType() === VoiceBroadcastInfoEventType
|
||||
&& SettingsStore.getValue(Features.VoiceBroadcast),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.props.mxEvent.addListener(MatrixEventEvent.Decrypted, this.onDecrypted);
|
||||
|
||||
if (this.props.mxEvent.getType() === VoiceBroadcastInfoEventType) {
|
||||
this.watchVoiceBroadcastFeatureSetting();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted);
|
||||
this.mediaHelper?.destroy();
|
||||
|
||||
if (this.voiceBroadcastSettingWatcherRef) {
|
||||
SettingsStore.unwatchSetting(this.voiceBroadcastSettingWatcherRef);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>) {
|
||||
|
@ -147,6 +168,16 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private watchVoiceBroadcastFeatureSetting(): void {
|
||||
this.voiceBroadcastSettingWatcherRef = SettingsStore.watchSetting(
|
||||
Features.VoiceBroadcast,
|
||||
null,
|
||||
(settingName: string, roomId: string, atLevel: SettingLevel, newValAtLevel, newValue: boolean) => {
|
||||
this.setState({ voiceBroadcastEnabled: newValue });
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const type = this.props.mxEvent.getType();
|
||||
|
@ -174,7 +205,11 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||
BodyType = MLocationBody;
|
||||
}
|
||||
|
||||
if (type === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) {
|
||||
if (
|
||||
this.state.voiceBroadcastEnabled
|
||||
&& type === VoiceBroadcastInfoEventType
|
||||
&& content?.state === VoiceBroadcastInfoState.Started
|
||||
) {
|
||||
BodyType = VoiceBroadcastBody;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ import EncryptionPanel from "./EncryptionPanel";
|
|||
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../dialogs/UserTab";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||
import BaseCard from "./BaseCard";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
|
@ -1331,8 +1330,7 @@ const BasicUserInfo: React.FC<{
|
|||
className="mx_UserInfo_field"
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
action: Action.ViewUserDeviceSettings,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -134,7 +134,7 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
if (this.state.avatarFile) {
|
||||
const uri = await client.uploadContent(this.state.avatarFile);
|
||||
const { content_uri: uri } = await client.uploadContent(this.state.avatarFile);
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', { url: uri }, '');
|
||||
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
||||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
|
|
|
@ -148,7 +148,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
const result = await MatrixClientPeg.get().lookupThreePid(
|
||||
'email',
|
||||
this.props.invitedEmail,
|
||||
undefined /* callback */,
|
||||
identityAccessToken,
|
||||
);
|
||||
this.setState({ invitedEmailMxid: result.mxid });
|
||||
|
|
|
@ -115,13 +115,13 @@ export default class ChangeAvatar extends React.Component<IProps, IState> {
|
|||
this.setState({
|
||||
phase: Phases.Uploading,
|
||||
});
|
||||
const httpPromise = MatrixClientPeg.get().uploadContent(file).then((url) => {
|
||||
const httpPromise = MatrixClientPeg.get().uploadContent(file).then(({ content_uri: url }) => {
|
||||
newUrl = url;
|
||||
if (this.props.room) {
|
||||
return MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'm.room.avatar',
|
||||
{ url: url },
|
||||
{ url },
|
||||
'',
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -111,7 +111,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
|
|||
logger.log(
|
||||
`Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` +
|
||||
` (${this.state.avatarFile.size}) bytes`);
|
||||
const uri = await client.uploadContent(this.state.avatarFile);
|
||||
const { content_uri: uri } = await client.uploadContent(this.state.avatarFile);
|
||||
await client.setAvatarUrl(uri);
|
||||
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
||||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
|
|
|
@ -14,17 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications';
|
||||
import React, { useState } from 'react';
|
||||
import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications';
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import Spinner from '../../elements/Spinner';
|
||||
import SettingsSubsection from '../shared/SettingsSubsection';
|
||||
import { SettingsSubsectionHeading } from '../shared/SettingsSubsectionHeading';
|
||||
import DeviceDetails from './DeviceDetails';
|
||||
import DeviceExpandDetailsButton from './DeviceExpandDetailsButton';
|
||||
import DeviceTile from './DeviceTile';
|
||||
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
|
||||
import { ExtendedDevice } from './types';
|
||||
import { KebabContextMenu } from '../../context_menus/KebabContextMenu';
|
||||
import { IconizedContextMenuOption } from '../../context_menus/IconizedContextMenu';
|
||||
|
||||
interface Props {
|
||||
device?: ExtendedDevice;
|
||||
|
@ -34,9 +37,48 @@ interface Props {
|
|||
setPushNotifications?: (deviceId: string, enabled: boolean) => Promise<void> | undefined;
|
||||
onVerifyCurrentDevice: () => void;
|
||||
onSignOutCurrentDevice: () => void;
|
||||
signOutAllOtherSessions?: () => void;
|
||||
saveDeviceName: (deviceName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
type CurrentDeviceSectionHeadingProps =
|
||||
Pick<Props, 'onSignOutCurrentDevice' | 'signOutAllOtherSessions'>
|
||||
& { disabled?: boolean };
|
||||
|
||||
const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> = ({
|
||||
onSignOutCurrentDevice,
|
||||
signOutAllOtherSessions,
|
||||
disabled,
|
||||
}) => {
|
||||
const menuOptions = [
|
||||
<IconizedContextMenuOption
|
||||
key="sign-out"
|
||||
label={_t('Sign out')}
|
||||
onClick={onSignOutCurrentDevice}
|
||||
isDestructive
|
||||
/>,
|
||||
...(signOutAllOtherSessions
|
||||
? [
|
||||
<IconizedContextMenuOption
|
||||
key="sign-out-all-others"
|
||||
label={_t('Sign out all other sessions')}
|
||||
onClick={signOutAllOtherSessions}
|
||||
isDestructive
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
),
|
||||
];
|
||||
return <SettingsSubsectionHeading heading={_t('Current session')}>
|
||||
<KebabContextMenu
|
||||
disabled={disabled}
|
||||
title={_t('Options')}
|
||||
options={menuOptions}
|
||||
data-testid='current-session-menu'
|
||||
/>
|
||||
</SettingsSubsectionHeading>;
|
||||
};
|
||||
|
||||
const CurrentDeviceSection: React.FC<Props> = ({
|
||||
device,
|
||||
isLoading,
|
||||
|
@ -45,13 +87,18 @@ const CurrentDeviceSection: React.FC<Props> = ({
|
|||
setPushNotifications,
|
||||
onVerifyCurrentDevice,
|
||||
onSignOutCurrentDevice,
|
||||
signOutAllOtherSessions,
|
||||
saveDeviceName,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return <SettingsSubsection
|
||||
heading={_t('Current session')}
|
||||
data-testid='current-session-section'
|
||||
heading={<CurrentDeviceSectionHeading
|
||||
onSignOutCurrentDevice={onSignOutCurrentDevice}
|
||||
signOutAllOtherSessions={signOutAllOtherSessions}
|
||||
disabled={isLoading || !device || isSigningOut}
|
||||
/>}
|
||||
>
|
||||
{ /* only show big spinner on first load */ }
|
||||
{ isLoading && !device && <Spinner /> }
|
||||
|
|
|
@ -16,17 +16,22 @@ limitations under the License.
|
|||
|
||||
import React, { HTMLAttributes } from "react";
|
||||
|
||||
import Heading from "../../typography/Heading";
|
||||
import { SettingsSubsectionHeading } from "./SettingsSubsectionHeading";
|
||||
|
||||
export interface SettingsSubsectionProps extends HTMLAttributes<HTMLDivElement> {
|
||||
heading: string;
|
||||
heading: string | React.ReactNode;
|
||||
description?: string | React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({ heading, description, children, ...rest }) => (
|
||||
<div {...rest} className="mx_SettingsSubsection">
|
||||
<Heading className="mx_SettingsSubsection_heading" size='h3'>{ heading }</Heading>
|
||||
{ typeof heading === 'string'
|
||||
? <SettingsSubsectionHeading heading={heading} />
|
||||
: <>
|
||||
{ heading }
|
||||
</>
|
||||
}
|
||||
{ !!description && <div className="mx_SettingsSubsection_description">{ description }</div> }
|
||||
<div className="mx_SettingsSubsection_content">
|
||||
{ children }
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { HTMLAttributes } from "react";
|
||||
|
||||
import Heading from "../../typography/Heading";
|
||||
|
||||
export interface SettingsSubsectionHeadingProps extends HTMLAttributes<HTMLDivElement> {
|
||||
heading: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SettingsSubsectionHeading: React.FC<SettingsSubsectionHeadingProps> = ({ heading, children, ...rest }) => (
|
||||
<div {...rest} className="mx_SettingsSubsectionHeading">
|
||||
<Heading className="mx_SettingsSubsectionHeading_heading" size='h3'>{ heading }</Heading>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
|
@ -80,7 +80,10 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
|||
|
||||
let betaSection;
|
||||
if (betas.length) {
|
||||
betaSection = <div className="mx_SettingsTab_section">
|
||||
betaSection = <div
|
||||
data-testid="labs-beta-section"
|
||||
className="mx_SettingsTab_section"
|
||||
>
|
||||
{ betas.map(f => <BetaCard key={f} featureId={f} />) }
|
||||
</div>;
|
||||
}
|
||||
|
@ -137,7 +140,11 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
|||
|
||||
labsSections = <>
|
||||
{ sortBy(Array.from(groups.entries()), "0").map(([group, flags]) => (
|
||||
<div className="mx_SettingsTab_section" key={group}>
|
||||
<div
|
||||
className="mx_SettingsTab_section"
|
||||
key={group}
|
||||
data-testid={`labs-group-${group}`}
|
||||
>
|
||||
<span className="mx_SettingsTab_subheading">{ _t(labGroupNames[group]) }</span>
|
||||
{ flags }
|
||||
</div>
|
||||
|
|
|
@ -346,19 +346,29 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
||||
{ warning }
|
||||
const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager");
|
||||
const devicesSection = useNewSessionManager
|
||||
? null
|
||||
: <>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Where you're signed in") }</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<div
|
||||
className="mx_SettingsTab_section"
|
||||
data-testid="devices-section"
|
||||
>
|
||||
<span className="mx_SettingsTab_subsectionText">
|
||||
{ _t(
|
||||
"Manage your signed-in devices below. " +
|
||||
"A device's name is visible to people you communicate with.",
|
||||
"A device's name is visible to people you communicate with.",
|
||||
) }
|
||||
</span>
|
||||
<DevicesPanel />
|
||||
</div>
|
||||
</>;
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
||||
{ warning }
|
||||
{ devicesSection }
|
||||
<div className="mx_SettingsTab_heading">{ _t("Encryption") }</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
{ secureBackup }
|
||||
|
|
|
@ -171,6 +171,10 @@ const SessionManagerTab: React.FC = () => {
|
|||
setSelectedDeviceIds([]);
|
||||
}, [filter, setSelectedDeviceIds]);
|
||||
|
||||
const signOutAllOtherSessions = shouldShowOtherSessions ? () => {
|
||||
onSignOutOtherDevices(Object.keys(otherDevices));
|
||||
}: undefined;
|
||||
|
||||
return <SettingsTab heading={_t('Sessions')}>
|
||||
<SecurityRecommendations
|
||||
devices={devices}
|
||||
|
@ -186,6 +190,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
saveDeviceName={(deviceName) => saveDeviceName(currentDeviceId, deviceName)}
|
||||
onVerifyCurrentDevice={onVerifyCurrentDevice}
|
||||
onSignOutCurrentDevice={onSignOutCurrentDevice}
|
||||
signOutAllOtherSessions={signOutAllOtherSessions}
|
||||
/>
|
||||
{
|
||||
shouldShowOtherSessions &&
|
||||
|
|
|
@ -154,9 +154,10 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli?.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
const room = MatrixClientPeg.get().getRoom(this.state.viewedRoomId);
|
||||
const room = cli?.getRoom(this.state.viewedRoomId);
|
||||
if (room) {
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue