Merge remote-tracking branch 'origin/develop' into feat/add-formating-buttons-to-wysiwyg

This commit is contained in:
Florian Duros 2022-10-13 12:42:21 +02:00
commit f85f53248b
No known key found for this signature in database
GPG key ID: 9700AA5870258A0B
128 changed files with 4303 additions and 1024 deletions

View file

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

View 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>) }
</>;
};

View file

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

View file

@ -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) {

View file

@ -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");
}

View file

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

View file

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

View file

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

View file

@ -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,
});
}}
>

View file

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

View file

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

View file

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

View file

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

View file

@ -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 /> }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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