Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/ts/c1

 Conflicts:
	src/components/structures/RoomDirectory.tsx
	src/components/views/room_settings/RoomPublishSetting.tsx
This commit is contained in:
Michael Telatynski 2021-07-12 18:58:18 +01:00
commit ae6eaa5acc
19 changed files with 223 additions and 97 deletions

View file

@ -72,7 +72,7 @@ limitations under the License.
.mx_AccessibleButton_kind_danger_outline { .mx_AccessibleButton_kind_danger_outline {
color: $button-danger-bg-color; color: $button-danger-bg-color;
background-color: $button-secondary-bg-color; background-color: transparent;
border: 1px solid $button-danger-bg-color; border: 1px solid $button-danger-bg-color;
} }

View file

@ -60,6 +60,8 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
/* /*
* Return true if the given string contains emoji * Return true if the given string contains emoji
* Uses a much, much simpler regex than emojibase's so will give false * Uses a much, much simpler regex than emojibase's so will give false
@ -176,18 +178,31 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
return { tagName, attribs }; return { tagName, attribs };
}, },
'img': function(tagName: string, attribs: sanitizeHtml.Attributes) { 'img': function(tagName: string, attribs: sanitizeHtml.Attributes) {
let src = attribs.src;
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and // because transformTags is used _before_ we filter by allowedSchemesByTag and
// we don't want to allow images with `https?` `src`s. // we don't want to allow images with `https?` `src`s.
// We also drop inline images (as if they were not present at all) when the "show // We also drop inline images (as if they were not present at all) when the "show
// images" preference is disabled. Future work might expose some UI to reveal them // images" preference is disabled. Future work might expose some UI to reveal them
// like standalone image events have. // like standalone image events have.
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) { if (!src || !SettingsStore.getValue("showImages")) {
return { tagName, attribs: {} }; return { tagName, attribs: {} };
} }
if (!src.startsWith("mxc://")) {
const match = MEDIA_API_MXC_REGEX.exec(src);
if (match) {
src = `mxc://${match[1]}/${match[2]}`;
}
}
if (!src.startsWith("mxc://")) {
return { tagName, attribs: {} };
}
const width = Number(attribs.width) || 800; const width = Number(attribs.width) || 800;
const height = Number(attribs.height) || 600; const height = Number(attribs.height) || 600;
attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height); attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height);
return { tagName, attribs }; return { tagName, attribs };
}, },
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) { 'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {

View file

@ -17,6 +17,7 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
import AliasCustomisations from './customisations/Alias';
/** /**
* Given a room object, return the alias we should use for it, * Given a room object, return the alias we should use for it,
@ -28,7 +29,18 @@ import { MatrixClientPeg } from './MatrixClientPeg';
* @returns {string} A display alias for the given room * @returns {string} A display alias for the given room
*/ */
export function getDisplayAliasForRoom(room: Room): string { export function getDisplayAliasForRoom(room: Room): string {
return room.getCanonicalAlias() || room.getAltAliases()[0]; return getDisplayAliasForAliasSet(
room.getCanonicalAlias(), room.getAltAliases(),
);
}
// The various display alias getters should all feed through this one path so
// there's a single place to change the logic.
export function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: string[]): string {
if (AliasCustomisations.getDisplayAliasForAliasSet) {
return AliasCustomisations.getDisplayAliasForAliasSet(canonicalAlias, altAliases);
}
return canonicalAlias || altAliases?.[0];
} }
export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean { export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean {

View file

@ -447,7 +447,8 @@ function textForPowerEvent(event): () => string | null {
!event.getContent() || !event.getContent().users) { !event.getContent() || !event.getContent().users) {
return null; return null;
} }
const userDefault = event.getContent().users_default || 0; const previousUserDefault = event.getPrevContent().users_default || 0;
const currentUserDefault = event.getContent().users_default || 0;
// Construct set of userIds // Construct set of userIds
const users = []; const users = [];
Object.keys(event.getContent().users).forEach( Object.keys(event.getContent().users).forEach(
@ -463,9 +464,16 @@ function textForPowerEvent(event): () => string | null {
const diffs = []; const diffs = [];
users.forEach((userId) => { users.forEach((userId) => {
// Previous power level // Previous power level
const from = event.getPrevContent().users[userId]; let from = event.getPrevContent().users[userId];
if (!Number.isInteger(from)) {
from = previousUserDefault;
}
// Current power level // Current power level
const to = event.getContent().users[userId]; let to = event.getContent().users[userId];
if (!Number.isInteger(to)) {
to = currentUserDefault;
}
if (from === previousUserDefault && to === currentUserDefault) { return; }
if (to !== from) { if (to !== from) {
diffs.push({ userId, from, to }); diffs.push({ userId, from, to });
} }
@ -479,8 +487,8 @@ function textForPowerEvent(event): () => string | null {
powerLevelDiffText: diffs.map(diff => powerLevelDiffText: diffs.map(diff =>
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId: diff.userId, userId: diff.userId,
fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault), fromPowerLevel: Roles.textualPowerLevel(diff.from, previousUserDefault),
toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault), toPowerLevel: Roles.textualPowerLevel(diff.to, currentUserDefault),
}), }),
).join(", "), ).join(", "),
}); });

View file

@ -46,6 +46,7 @@ import DirectorySearchBox from "../views/elements/DirectorySearchBox";
import ScrollPanel from "./ScrollPanel"; import ScrollPanel from "./ScrollPanel";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import { ActionPayload } from "../../dispatcher/payloads"; import { ActionPayload } from "../../dispatcher/payloads";
import { getDisplayAliasForAliasSet } from "../../Rooms";
const MAX_NAME_LENGTH = 80; const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800; const MAX_TOPIC_LENGTH = 800;
@ -833,5 +834,5 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom // Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
// but works with the objects we get from the public room list // but works with the objects we get from the public room list
function getDisplayAliasForRoom(room: IPublicRoomsChunkRoom) { function getDisplayAliasForRoom(room: IPublicRoomsChunkRoom) {
return room.canonical_alias || room.aliases?.[0] || ""; return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases);
} }

View file

@ -43,6 +43,7 @@ import { useStateToggle } from "../../hooks/useStateToggle";
import { getChildOrder } from "../../stores/SpaceStore"; import { getChildOrder } from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { linkifyElement } from "../../HtmlUtils"; import { linkifyElement } from "../../HtmlUtils";
import { getDisplayAliasForAliasSet } from "../../Rooms";
interface IHierarchyProps { interface IHierarchyProps {
space: Room; space: Room;
@ -637,5 +638,5 @@ export default SpaceRoomDirectory;
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom // Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
// but works with the objects we get from the public room list // but works with the objects we get from the public room list
function getDisplayAliasForRoom(room: ISpaceSummaryRoom) { function getDisplayAliasForRoom(room: ISpaceSummaryRoom) {
return room.canonical_alias || (room.aliases ? room.aliases[0] : ""); return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases);
} }

View file

@ -238,6 +238,7 @@ export default class AppTile extends React.Component {
case 'm.sticker': case 'm.sticker':
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) { if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
dis.dispatch({ action: 'post_sticker_message', data: payload.data }); dis.dispatch({ action: 'post_sticker_message', data: payload.data });
dis.dispatch({ action: 'stickerpicker_close' });
} else { } else {
console.warn('Ignoring sticker message. Invalid capability'); console.warn('Ignoring sticker message. Invalid capability');
} }

View file

@ -244,7 +244,11 @@ export default class TextualBody extends React.Component<IProps, IState> {
} }
private highlightCode(code: HTMLElement): void { private highlightCode(code: HTMLElement): void {
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) { // Auto-detect language only if enabled and only for codeblocks
if (
SettingsStore.getValue("enableSyntaxHighlightLanguageDetection") &&
code.parentElement instanceof HTMLPreElement
) {
highlight.highlightBlock(code); highlight.highlightBlock(code);
} else { } else {
// Only syntax highlight if there's a class starting with language- // Only syntax highlight if there's a class starting with language-

View file

@ -15,12 +15,13 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { Visibility } from "matrix-js-sdk/src/@types/partials";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Visibility } from "matrix-js-sdk/src/@types/partials"; import DirectoryCustomisations from '../../../customisations/Directory';
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -67,10 +68,15 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
render() { render() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const enabled = (
DirectoryCustomisations.requireCanonicalAliasAccessToPublish?.() === false ||
this.props.canSetCanonicalAlias
);
return ( return (
<LabelledToggleSwitch value={this.state.isRoomPublished} <LabelledToggleSwitch value={this.state.isRoomPublished}
onChange={this.onRoomPublishChange} onChange={this.onRoomPublishChange}
disabled={!this.props.canSetCanonicalAlias} disabled={!enabled}
label={_t("Publish this room to the public in %(domain)s's room directory?", { label={_t("Publish this room to the public in %(domain)s's room directory?", {
domain: client.getDomain(), domain: client.getDomain(),
})} })}

View file

@ -14,43 +14,57 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useEffect } from "react"; import React, { useContext, useEffect } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
import { useStateToggle } from "../../../hooks/useStateToggle"; import { useStateToggle } from "../../../hooks/useStateToggle";
import LinkPreviewWidget from "./LinkPreviewWidget"; import LinkPreviewWidget from "./LinkPreviewWidget";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
const INITIAL_NUM_PREVIEWS = 2; const INITIAL_NUM_PREVIEWS = 2;
interface IProps { interface IProps {
links: string[]; // the URLs to be previewed links: string[]; // the URLs to be previewed
mxEvent: MatrixEvent; // the Event associated with the preview mxEvent: MatrixEvent; // the Event associated with the preview
onCancelClick?(): void; // called when the preview's cancel ('hide') button is clicked onCancelClick(): void; // called when the preview's cancel ('hide') button is clicked
onHeightChanged?(): void; // called when the preview's contents has loaded onHeightChanged(): void; // called when the preview's contents has loaded
} }
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => { const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => {
const cli = useContext(MatrixClientContext);
const [expanded, toggleExpanded] = useStateToggle(); const [expanded, toggleExpanded] = useStateToggle();
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 => {
console.error("Failed to get URL preview: " + error);
});
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
}, [links, ts], []);
useEffect(() => { useEffect(() => {
onHeightChanged(); onHeightChanged();
}, [onHeightChanged, expanded]); }, [onHeightChanged, expanded, previews]);
const shownLinks = expanded ? links : links.slice(0, INITIAL_NUM_PREVIEWS); const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS);
let toggleButton; let toggleButton: JSX.Element;
if (links.length > INITIAL_NUM_PREVIEWS) { if (previews.length > INITIAL_NUM_PREVIEWS) {
toggleButton = <AccessibleButton onClick={toggleExpanded}> toggleButton = <AccessibleButton onClick={toggleExpanded}>
{ expanded { expanded
? _t("Collapse") ? _t("Collapse")
: _t("Show %(count)s other previews", { count: links.length - shownLinks.length }) } : _t("Show %(count)s other previews", { count: previews.length - showPreviews.length }) }
</AccessibleButton>; </AccessibleButton>;
} }
return <div className="mx_LinkPreviewGroup"> return <div className="mx_LinkPreviewGroup">
{ shownLinks.map((link, i) => ( { showPreviews.map(([link, preview], i) => (
<LinkPreviewWidget key={link} link={link} mxEvent={mxEvent} onHeightChanged={onHeightChanged}> <LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
{ i === 0 ? ( { i === 0 ? (
<AccessibleButton <AccessibleButton
className="mx_LinkPreviewGroup_hide" className="mx_LinkPreviewGroup_hide"

View file

@ -21,7 +21,6 @@ import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
import { linkifyElement } from '../../../HtmlUtils'; import { linkifyElement } from '../../../HtmlUtils';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import * as ImageUtils from "../../../ImageUtils"; import * as ImageUtils from "../../../ImageUtils";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -29,37 +28,15 @@ import { mediaFromMxc } from "../../../customisations/Media";
import ImageView from '../elements/ImageView'; import ImageView from '../elements/ImageView';
interface IProps { interface IProps {
link: string; // the URL being previewed link: string;
preview: IPreviewUrlResponse;
mxEvent: MatrixEvent; // the Event associated with the preview mxEvent: MatrixEvent; // the Event associated with the preview
onHeightChanged(): void; // called when the preview's contents has loaded
}
interface IState {
preview?: IPreviewUrlResponse;
} }
@replaceableComponent("views.rooms.LinkPreviewWidget") @replaceableComponent("views.rooms.LinkPreviewWidget")
export default class LinkPreviewWidget extends React.Component<IProps, IState> { export default class LinkPreviewWidget extends React.Component<IProps> {
private unmounted = false;
private readonly description = createRef<HTMLDivElement>(); private readonly description = createRef<HTMLDivElement>();
constructor(props) {
super(props);
this.state = {
preview: null,
};
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((preview) => {
if (this.unmounted) {
return;
}
this.setState({ preview }, this.props.onHeightChanged);
}, (error) => {
console.error("Failed to get URL preview: " + error);
});
}
componentDidMount() { componentDidMount() {
if (this.description.current) { if (this.description.current) {
linkifyElement(this.description.current); linkifyElement(this.description.current);
@ -72,12 +49,8 @@ export default class LinkPreviewWidget extends React.Component<IProps, IState> {
} }
} }
componentWillUnmount() {
this.unmounted = true;
}
private onImageClick = ev => { private onImageClick = ev => {
const p = this.state.preview; const p = this.props.preview;
if (ev.button != 0 || ev.metaKey) return; if (ev.button != 0 || ev.metaKey) return;
ev.preventDefault(); ev.preventDefault();
@ -99,7 +72,7 @@ export default class LinkPreviewWidget extends React.Component<IProps, IState> {
}; };
render() { render() {
const p = this.state.preview; const p = this.props.preview;
if (!p || Object.keys(p).length === 0) { if (!p || Object.keys(p).length === 0) {
return <div />; return <div />;
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2017 New Vector Ltd. Copyright 2017-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -21,9 +21,10 @@ import { linkifyElement } from '../../../HtmlUtils';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
import { getDisplayAliasForAliasSet } from '../../../Rooms';
export function getDisplayAliasForRoom(room) { export function getDisplayAliasForRoom(room) {
return room.canonicalAlias || (room.aliases ? room.aliases[0] : ""); return getDisplayAliasForAliasSet(room.canonicalAlias, room.aliases);
} }
export const roomShape = PropTypes.shape({ export const roomShape = PropTypes.shape({

View file

@ -280,6 +280,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
const mutedUsers = []; const mutedUsers = [];
Object.keys(userLevels).forEach((user) => { Object.keys(userLevels).forEach((user) => {
if (!Number.isInteger(userLevels[user])) { return; }
const canChange = userLevels[user] < currentUserLevel && canChangeLevels; const canChange = userLevels[user] < currentUserLevel && canChangeLevels;
if (userLevels[user] > defaultUserLevel) { // privileged if (userLevels[user] > defaultUserLevel) { // privileged
privilegedUsers.push( privilegedUsers.push(

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.
*/
function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: string[]): string {
// E.g. prefer one of the aliases over another
return null;
}
// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IAliasCustomisations {
getDisplayAliasForAliasSet?: typeof getDisplayAliasForAliasSet;
}
// A real customisation module will define and export one or more of the
// customisation points that make up `IAliasCustomisations`.
export default {} as IAliasCustomisations;

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.
*/
function requireCanonicalAliasAccessToPublish(): boolean {
// Some environments may not care about this requirement and could return false
return true;
}
// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IDirectoryCustomisations {
requireCanonicalAliasAccessToPublish?: typeof requireCanonicalAliasAccessToPublish;
}
// A real customisation module will define and export one or more of the
// customisation points that make up `IDirectoryCustomisations`.
export default {} as IDirectoryCustomisations;

View file

@ -695,6 +695,7 @@
"Error leaving room": "Error leaving room", "Error leaving room": "Error leaving room",
"Unrecognised address": "Unrecognised address", "Unrecognised address": "Unrecognised address",
"You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.",
"User %(userId)s is already invited to the room": "User %(userId)s is already invited to the room",
"User %(userId)s is already in the room": "User %(userId)s is already in the room", "User %(userId)s is already in the room": "User %(userId)s is already in the room",
"User %(user_id)s does not exist": "User %(user_id)s does not exist", "User %(user_id)s does not exist": "User %(user_id)s does not exist",
"User %(user_id)s may or may not exist": "User %(user_id)s may or may not exist", "User %(user_id)s may or may not exist": "User %(user_id)s may or may not exist",

View file

@ -22,7 +22,6 @@ import { RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS } from "./RightPanelStoreP
import { ActionPayload } from "../dispatcher/payloads"; import { ActionPayload } from "../dispatcher/payloads";
import { Action } from '../dispatcher/actions'; import { Action } from '../dispatcher/actions';
import { SettingLevel } from "../settings/SettingLevel"; import { SettingLevel } from "../settings/SettingLevel";
import RoomViewStore from './RoomViewStore';
interface RightPanelStoreState { interface RightPanelStoreState {
// Whether or not to show the right panel at all. We split out rooms and groups // Whether or not to show the right panel at all. We split out rooms and groups
@ -68,6 +67,7 @@ const MEMBER_INFO_PHASES = [
export default class RightPanelStore extends Store<ActionPayload> { export default class RightPanelStore extends Store<ActionPayload> {
private static instance: RightPanelStore; private static instance: RightPanelStore;
private state: RightPanelStoreState; private state: RightPanelStoreState;
private lastRoomId: string;
constructor() { constructor() {
super(dis); super(dis);
@ -147,8 +147,10 @@ export default class RightPanelStore extends Store<ActionPayload> {
__onDispatch(payload: ActionPayload) { __onDispatch(payload: ActionPayload) {
switch (payload.action) { switch (payload.action) {
case 'view_room': case 'view_room':
if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink
// fallthrough
case 'view_group': case 'view_group':
if (payload.room_id === RoomViewStore.getRoomId()) break; // skip this transition, probably a permalink this.lastRoomId = payload.room_id;
// Reset to the member list if we're viewing member info // Reset to the member list if we're viewing member info
if (MEMBER_INFO_PHASES.includes(this.state.lastRoomPhase)) { if (MEMBER_INFO_PHASES.includes(this.state.lastRoomPhase)) {

View file

@ -39,6 +39,9 @@ const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UN
export type CompletionStates = Record<string, InviteState>; export type CompletionStates = Record<string, InviteState>;
const USER_ALREADY_JOINED = "IO.ELEMENT.ALREADY_JOINED";
const USER_ALREADY_INVITED = "IO.ELEMENT.ALREADY_INVITED";
/** /**
* Invites multiple addresses to a room or group, handling rate limiting from the server * Invites multiple addresses to a room or group, handling rate limiting from the server
*/ */
@ -130,9 +133,14 @@ export default class MultiInviter {
if (!room) throw new Error("Room not found"); if (!room) throw new Error("Room not found");
const member = room.getMember(addr); const member = room.getMember(addr);
if (member && ['join', 'invite'].includes(member.membership)) { if (member.membership === "join") {
throw new new MatrixError({ throw new MatrixError({
errcode: "RIOT.ALREADY_IN_ROOM", errcode: USER_ALREADY_JOINED,
error: "Member already joined",
});
} else if (member.membership === "invite") {
throw new MatrixError({
errcode: USER_ALREADY_INVITED,
error: "Member already invited", error: "Member already invited",
}); });
} }
@ -180,30 +188,47 @@ export default class MultiInviter {
let errorText; let errorText;
let fatal = false; let fatal = false;
if (err.errcode === 'M_FORBIDDEN') { switch (err.errcode) {
fatal = true; case "M_FORBIDDEN":
errorText = _t('You do not have permission to invite people to this room.'); errorText = _t('You do not have permission to invite people to this room.');
} else if (err.errcode === "RIOT.ALREADY_IN_ROOM") { fatal = true;
errorText = _t("User %(userId)s is already in the room", { userId: address }); break;
} else if (err.errcode === 'M_LIMIT_EXCEEDED') { case USER_ALREADY_INVITED:
// we're being throttled so wait a bit & try again errorText = _t("User %(userId)s is already invited to the room", { userId: address });
setTimeout(() => { break;
this.doInvite(address, ignoreProfile).then(resolve, reject); case USER_ALREADY_JOINED:
}, 5000); errorText = _t("User %(userId)s is already in the room", { userId: address });
return; break;
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) { case "M_LIMIT_EXCEEDED":
errorText = _t("User %(user_id)s does not exist", { user_id: address }); // we're being throttled so wait a bit & try again
} else if (err.errcode === 'M_PROFILE_UNDISCLOSED') { setTimeout(() => {
errorText = _t("User %(user_id)s may or may not exist", { user_id: address }); this.doInvite(address, ignoreProfile).then(resolve, reject);
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) { }, 5000);
// Invite without the profile check return;
console.warn(`User ${address} does not have a profile - inviting anyways automatically`); case "M_NOT_FOUND":
this.doInvite(address, true).then(resolve, reject); case "M_USER_NOT_FOUND":
} else if (err.errcode === "M_BAD_STATE") { errorText = _t("User %(user_id)s does not exist", { user_id: address });
errorText = _t("The user must be unbanned before they can be invited."); break;
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") { case "M_PROFILE_UNDISCLOSED":
errorText = _t("The user's homeserver does not support the version of the room."); errorText = _t("User %(user_id)s may or may not exist", { user_id: address });
} else { break;
case "M_PROFILE_NOT_FOUND":
if (!ignoreProfile) {
// Invite without the profile check
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
this.doInvite(address, true).then(resolve, reject);
return;
}
break;
case "M_BAD_STATE":
errorText = _t("The user must be unbanned before they can be invited.");
break;
case "M_UNSUPPORTED_ROOM_VERSION":
errorText = _t("The user's homeserver does not support the version of the room.");
break;
}
if (!errorText) {
errorText = _t('Unknown server error'); errorText = _t('Unknown server error');
} }

View file

@ -22,8 +22,10 @@ import sdk from "../../../skinned-sdk";
import { mkEvent, mkStubRoom } from "../../../test-utils"; import { mkEvent, mkStubRoom } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import * as languageHandler from "../../../../src/languageHandler"; import * as languageHandler from "../../../../src/languageHandler";
import * as TestUtils from "../../../test-utils";
const TextualBody = sdk.getComponent("views.messages.TextualBody"); const _TextualBody = sdk.getComponent("views.messages.TextualBody");
const TextualBody = TestUtils.wrapInMatrixClientContext(_TextualBody);
configure({ adapter: new Adapter() }); configure({ adapter: new Adapter() });
@ -305,10 +307,9 @@ describe("<TextualBody />", () => {
const wrapper = mount(<TextualBody mxEvent={ev} showUrlPreview={true} onHeightChanged={() => {}} />); const wrapper = mount(<TextualBody mxEvent={ev} showUrlPreview={true} onHeightChanged={() => {}} />);
expect(wrapper.text()).toBe(ev.getContent().body); expect(wrapper.text()).toBe(ev.getContent().body);
let widgets = wrapper.find("LinkPreviewWidget"); let widgets = wrapper.find("LinkPreviewGroup");
// at this point we should have exactly one widget // at this point we should have exactly one link
expect(widgets.length).toBe(1); expect(widgets.at(0).prop("links")).toEqual(["https://matrix.org/"]);
expect(widgets.at(0).prop("link")).toBe("https://matrix.org/");
// simulate an event edit and check the transition from the old URL preview to the new one // simulate an event edit and check the transition from the old URL preview to the new one
const ev2 = mkEvent({ const ev2 = mkEvent({
@ -333,11 +334,9 @@ describe("<TextualBody />", () => {
// XXX: this is to give TextualBody enough time for state to settle // XXX: this is to give TextualBody enough time for state to settle
wrapper.setState({}, () => { wrapper.setState({}, () => {
widgets = wrapper.find("LinkPreviewWidget"); widgets = wrapper.find("LinkPreviewGroup");
// at this point we should have exactly two widgets (not the matrix.org one anymore) // at this point we should have exactly two links (not the matrix.org one anymore)
expect(widgets.length).toBe(2); expect(widgets.at(0).prop("links")).toEqual(["https://vector.im/", "https://riot.im/"]);
expect(widgets.at(0).prop("link")).toBe("https://vector.im/");
expect(widgets.at(1).prop("link")).toBe("https://riot.im/");
}); });
}); });
}); });