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 {
|
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
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 { 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 {
|
||||||
|
|
|
@ -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(", "),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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-
|
||||||
|
|
|
@ -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(),
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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(
|
||||||
|
|
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",
|
"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",
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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/");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue