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:
commit
ae6eaa5acc
19 changed files with 223 additions and 97 deletions
|
@ -72,7 +72,7 @@ limitations under the License.
|
|||
|
||||
.mx_AccessibleButton_kind_danger_outline {
|
||||
color: $button-danger-bg-color;
|
||||
background-color: $button-secondary-bg-color;
|
||||
background-color: transparent;
|
||||
border: 1px solid $button-danger-bg-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,8 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
|||
|
||||
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
|
||||
* 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 };
|
||||
},
|
||||
'img': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||
let src = attribs.src;
|
||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||
// 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
|
||||
// images" preference is disabled. Future work might expose some UI to reveal them
|
||||
// like standalone image events have.
|
||||
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
|
||||
if (!src || !SettingsStore.getValue("showImages")) {
|
||||
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 height = Number(attribs.height) || 600;
|
||||
attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height);
|
||||
attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height);
|
||||
return { tagName, attribs };
|
||||
},
|
||||
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||
|
|
14
src/Rooms.ts
14
src/Rooms.ts
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import AliasCustomisations from './customisations/Alias';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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 {
|
||||
|
|
|
@ -447,7 +447,8 @@ function textForPowerEvent(event): () => string | null {
|
|||
!event.getContent() || !event.getContent().users) {
|
||||
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
|
||||
const users = [];
|
||||
Object.keys(event.getContent().users).forEach(
|
||||
|
@ -463,9 +464,16 @@ function textForPowerEvent(event): () => string | null {
|
|||
const diffs = [];
|
||||
users.forEach((userId) => {
|
||||
// Previous power level
|
||||
const from = event.getPrevContent().users[userId];
|
||||
let from = event.getPrevContent().users[userId];
|
||||
if (!Number.isInteger(from)) {
|
||||
from = previousUserDefault;
|
||||
}
|
||||
// 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) {
|
||||
diffs.push({ userId, from, to });
|
||||
}
|
||||
|
@ -479,8 +487,8 @@ function textForPowerEvent(event): () => string | null {
|
|||
powerLevelDiffText: diffs.map(diff =>
|
||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||
userId: diff.userId,
|
||||
fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault),
|
||||
toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault),
|
||||
fromPowerLevel: Roles.textualPowerLevel(diff.from, previousUserDefault),
|
||||
toPowerLevel: Roles.textualPowerLevel(diff.to, currentUserDefault),
|
||||
}),
|
||||
).join(", "),
|
||||
});
|
||||
|
|
|
@ -46,6 +46,7 @@ import DirectorySearchBox from "../views/elements/DirectorySearchBox";
|
|||
import ScrollPanel from "./ScrollPanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { getDisplayAliasForAliasSet } from "../../Rooms";
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
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
|
||||
// but works with the objects we get from the public room list
|
||||
function getDisplayAliasForRoom(room: IPublicRoomsChunkRoom) {
|
||||
return room.canonical_alias || room.aliases?.[0] || "";
|
||||
return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases);
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import { useStateToggle } from "../../hooks/useStateToggle";
|
|||
import { getChildOrder } from "../../stores/SpaceStore";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { linkifyElement } from "../../HtmlUtils";
|
||||
import { getDisplayAliasForAliasSet } from "../../Rooms";
|
||||
|
||||
interface IHierarchyProps {
|
||||
space: Room;
|
||||
|
@ -637,5 +638,5 @@ export default SpaceRoomDirectory;
|
|||
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
||||
// but works with the objects we get from the public room list
|
||||
function getDisplayAliasForRoom(room: ISpaceSummaryRoom) {
|
||||
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
|
||||
return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases);
|
||||
}
|
||||
|
|
|
@ -238,6 +238,7 @@ export default class AppTile extends React.Component {
|
|||
case 'm.sticker':
|
||||
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
||||
dis.dispatch({ action: 'stickerpicker_close' });
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
}
|
||||
|
|
|
@ -244,7 +244,11 @@ export default class TextualBody extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
|
|
|
@ -15,12 +15,13 @@ 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";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||
import DirectoryCustomisations from '../../../customisations/Directory';
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -67,10 +68,15 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
|
|||
render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const enabled = (
|
||||
DirectoryCustomisations.requireCanonicalAliasAccessToPublish?.() === false ||
|
||||
this.props.canSetCanonicalAlias
|
||||
);
|
||||
|
||||
return (
|
||||
<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
onChange={this.onRoomPublishChange}
|
||||
disabled={!this.props.canSetCanonicalAlias}
|
||||
disabled={!enabled}
|
||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||
domain: client.getDomain(),
|
||||
})}
|
||||
|
|
|
@ -14,43 +14,57 @@ See the License for the specific language governing permissions and
|
|||
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 { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||
import LinkPreviewWidget from "./LinkPreviewWidget";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
|
||||
const INITIAL_NUM_PREVIEWS = 2;
|
||||
|
||||
interface IProps {
|
||||
links: string[]; // the URLs to be previewed
|
||||
mxEvent: MatrixEvent; // the Event associated with the preview
|
||||
onCancelClick?(): void; // called when the preview's cancel ('hide') button is clicked
|
||||
onHeightChanged?(): void; // called when the preview's contents has loaded
|
||||
onCancelClick(): void; // called when the preview's cancel ('hide') button is clicked
|
||||
onHeightChanged(): void; // called when the preview's contents has loaded
|
||||
}
|
||||
|
||||
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
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(() => {
|
||||
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;
|
||||
if (links.length > INITIAL_NUM_PREVIEWS) {
|
||||
let toggleButton: JSX.Element;
|
||||
if (previews.length > INITIAL_NUM_PREVIEWS) {
|
||||
toggleButton = <AccessibleButton onClick={toggleExpanded}>
|
||||
{ expanded
|
||||
? _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>;
|
||||
}
|
||||
|
||||
return <div className="mx_LinkPreviewGroup">
|
||||
{ shownLinks.map((link, i) => (
|
||||
<LinkPreviewWidget key={link} link={link} mxEvent={mxEvent} onHeightChanged={onHeightChanged}>
|
||||
{ showPreviews.map(([link, preview], i) => (
|
||||
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
|
||||
{ i === 0 ? (
|
||||
<AccessibleButton
|
||||
className="mx_LinkPreviewGroup_hide"
|
||||
|
|
|
@ -21,7 +21,6 @@ import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
|
|||
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import Modal from "../../../Modal";
|
||||
import * as ImageUtils from "../../../ImageUtils";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -29,37 +28,15 @@ import { mediaFromMxc } from "../../../customisations/Media";
|
|||
import ImageView from '../elements/ImageView';
|
||||
|
||||
interface IProps {
|
||||
link: string; // the URL being previewed
|
||||
link: string;
|
||||
preview: IPreviewUrlResponse;
|
||||
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")
|
||||
export default class LinkPreviewWidget extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||
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() {
|
||||
if (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 => {
|
||||
const p = this.state.preview;
|
||||
const p = this.props.preview;
|
||||
if (ev.button != 0 || ev.metaKey) return;
|
||||
ev.preventDefault();
|
||||
|
||||
|
@ -99,7 +72,7 @@ export default class LinkPreviewWidget extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const p = this.state.preview;
|
||||
const p = this.props.preview;
|
||||
if (!p || Object.keys(p).length === 0) {
|
||||
return <div />;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
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 { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { getDisplayAliasForAliasSet } from '../../../Rooms';
|
||||
|
||||
export function getDisplayAliasForRoom(room) {
|
||||
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
||||
return getDisplayAliasForAliasSet(room.canonicalAlias, room.aliases);
|
||||
}
|
||||
|
||||
export const roomShape = PropTypes.shape({
|
||||
|
|
|
@ -280,6 +280,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
const mutedUsers = [];
|
||||
|
||||
Object.keys(userLevels).forEach((user) => {
|
||||
if (!Number.isInteger(userLevels[user])) { return; }
|
||||
const canChange = userLevels[user] < currentUserLevel && canChangeLevels;
|
||||
if (userLevels[user] > defaultUserLevel) { // privileged
|
||||
privilegedUsers.push(
|
||||
|
|
31
src/customisations/Alias.ts
Normal file
31
src/customisations/Alias.ts
Normal 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;
|
31
src/customisations/Directory.ts
Normal file
31
src/customisations/Directory.ts
Normal 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;
|
|
@ -695,6 +695,7 @@
|
|||
"Error leaving room": "Error leaving room",
|
||||
"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.",
|
||||
"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 %(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",
|
||||
|
|
|
@ -22,7 +22,6 @@ import { RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS } from "./RightPanelStoreP
|
|||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { Action } from '../dispatcher/actions';
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import RoomViewStore from './RoomViewStore';
|
||||
|
||||
interface RightPanelStoreState {
|
||||
// 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> {
|
||||
private static instance: RightPanelStore;
|
||||
private state: RightPanelStoreState;
|
||||
private lastRoomId: string;
|
||||
|
||||
constructor() {
|
||||
super(dis);
|
||||
|
@ -147,8 +147,10 @@ export default class RightPanelStore extends Store<ActionPayload> {
|
|||
__onDispatch(payload: ActionPayload) {
|
||||
switch (payload.action) {
|
||||
case 'view_room':
|
||||
if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink
|
||||
// fallthrough
|
||||
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
|
||||
if (MEMBER_INFO_PHASES.includes(this.state.lastRoomPhase)) {
|
||||
|
|
|
@ -39,6 +39,9 @@ const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UN
|
|||
|
||||
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
|
||||
*/
|
||||
|
@ -130,9 +133,14 @@ export default class MultiInviter {
|
|||
if (!room) throw new Error("Room not found");
|
||||
|
||||
const member = room.getMember(addr);
|
||||
if (member && ['join', 'invite'].includes(member.membership)) {
|
||||
throw new new MatrixError({
|
||||
errcode: "RIOT.ALREADY_IN_ROOM",
|
||||
if (member.membership === "join") {
|
||||
throw new MatrixError({
|
||||
errcode: USER_ALREADY_JOINED,
|
||||
error: "Member already joined",
|
||||
});
|
||||
} else if (member.membership === "invite") {
|
||||
throw new MatrixError({
|
||||
errcode: USER_ALREADY_INVITED,
|
||||
error: "Member already invited",
|
||||
});
|
||||
}
|
||||
|
@ -180,30 +188,47 @@ export default class MultiInviter {
|
|||
|
||||
let errorText;
|
||||
let fatal = false;
|
||||
if (err.errcode === 'M_FORBIDDEN') {
|
||||
fatal = true;
|
||||
errorText = _t('You do not have permission to invite people to this room.');
|
||||
} else if (err.errcode === "RIOT.ALREADY_IN_ROOM") {
|
||||
errorText = _t("User %(userId)s is already in the room", { userId: address });
|
||||
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
|
||||
// we're being throttled so wait a bit & try again
|
||||
setTimeout(() => {
|
||||
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
}, 5000);
|
||||
return;
|
||||
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) {
|
||||
errorText = _t("User %(user_id)s does not exist", { user_id: address });
|
||||
} else if (err.errcode === 'M_PROFILE_UNDISCLOSED') {
|
||||
errorText = _t("User %(user_id)s may or may not exist", { user_id: address });
|
||||
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !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);
|
||||
} else if (err.errcode === "M_BAD_STATE") {
|
||||
errorText = _t("The user must be unbanned before they can be invited.");
|
||||
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") {
|
||||
errorText = _t("The user's homeserver does not support the version of the room.");
|
||||
} else {
|
||||
switch (err.errcode) {
|
||||
case "M_FORBIDDEN":
|
||||
errorText = _t('You do not have permission to invite people to this room.');
|
||||
fatal = true;
|
||||
break;
|
||||
case USER_ALREADY_INVITED:
|
||||
errorText = _t("User %(userId)s is already invited to the room", { userId: address });
|
||||
break;
|
||||
case USER_ALREADY_JOINED:
|
||||
errorText = _t("User %(userId)s is already in the room", { userId: address });
|
||||
break;
|
||||
case "M_LIMIT_EXCEEDED":
|
||||
// we're being throttled so wait a bit & try again
|
||||
setTimeout(() => {
|
||||
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
}, 5000);
|
||||
return;
|
||||
case "M_NOT_FOUND":
|
||||
case "M_USER_NOT_FOUND":
|
||||
errorText = _t("User %(user_id)s does not exist", { user_id: address });
|
||||
break;
|
||||
case "M_PROFILE_UNDISCLOSED":
|
||||
errorText = _t("User %(user_id)s may or may not exist", { user_id: address });
|
||||
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');
|
||||
}
|
||||
|
||||
|
|
|
@ -22,8 +22,10 @@ import sdk from "../../../skinned-sdk";
|
|||
import { mkEvent, mkStubRoom } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
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() });
|
||||
|
||||
|
@ -305,10 +307,9 @@ describe("<TextualBody />", () => {
|
|||
const wrapper = mount(<TextualBody mxEvent={ev} showUrlPreview={true} onHeightChanged={() => {}} />);
|
||||
expect(wrapper.text()).toBe(ev.getContent().body);
|
||||
|
||||
let widgets = wrapper.find("LinkPreviewWidget");
|
||||
// at this point we should have exactly one widget
|
||||
expect(widgets.length).toBe(1);
|
||||
expect(widgets.at(0).prop("link")).toBe("https://matrix.org/");
|
||||
let widgets = wrapper.find("LinkPreviewGroup");
|
||||
// at this point we should have exactly one link
|
||||
expect(widgets.at(0).prop("links")).toEqual(["https://matrix.org/"]);
|
||||
|
||||
// simulate an event edit and check the transition from the old URL preview to the new one
|
||||
const ev2 = mkEvent({
|
||||
|
@ -333,11 +334,9 @@ describe("<TextualBody />", () => {
|
|||
|
||||
// XXX: this is to give TextualBody enough time for state to settle
|
||||
wrapper.setState({}, () => {
|
||||
widgets = wrapper.find("LinkPreviewWidget");
|
||||
// at this point we should have exactly two widgets (not the matrix.org one anymore)
|
||||
expect(widgets.length).toBe(2);
|
||||
expect(widgets.at(0).prop("link")).toBe("https://vector.im/");
|
||||
expect(widgets.at(1).prop("link")).toBe("https://riot.im/");
|
||||
widgets = wrapper.find("LinkPreviewGroup");
|
||||
// at this point we should have exactly two links (not the matrix.org one anymore)
|
||||
expect(widgets.at(0).prop("links")).toEqual(["https://vector.im/", "https://riot.im/"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue