mirror of https://github.com/vector-im/riot-web
Merge branch 'matrix-org:develop' into issue-19180
commit
0fe133c14f
res/css/views
src
components
structures
views
directory
right_panel
room_settings
settings/account
spaces
i18n/strings
settings
stores
utils
test
|
@ -58,10 +58,6 @@ limitations under the License.
|
||||||
background-color: $authpage-body-bg-color;
|
background-color: $authpage-body-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Field label {
|
|
||||||
color: $authpage-primary-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Field_labelAlwaysTopLeft label,
|
.mx_Field_labelAlwaysTopLeft label,
|
||||||
.mx_Field select + label /* Always show a select's label on top to not collide with the value */,
|
.mx_Field select + label /* Always show a select's label on top to not collide with the value */,
|
||||||
.mx_Field input:focus + label,
|
.mx_Field input:focus + label,
|
||||||
|
|
|
@ -75,7 +75,7 @@ limitations under the License.
|
||||||
@mixin ProgressBarBorderRadius 8px;
|
@mixin ProgressBarBorderRadius 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpace_progressText {
|
.mx_AddExistingToSpaceDialog_progressText {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
|
|
|
@ -74,6 +74,7 @@ limitations under the License.
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
line-height: $font-15px;
|
line-height: $font-15px;
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
|
margin-top: -13px; // match height of buttons to prevent height changing
|
||||||
|
|
||||||
.mx_ProgressBar {
|
.mx_ProgressBar {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
|
|
@ -100,7 +100,6 @@ limitations under the License.
|
||||||
color 0.25s ease-out 0.1s,
|
color 0.25s ease-out 0.1s,
|
||||||
transform 0.25s ease-out 0.1s,
|
transform 0.25s ease-out 0.1s,
|
||||||
background-color 0.25s ease-out 0.1s;
|
background-color 0.25s ease-out 0.1s;
|
||||||
color: $primary-content;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
|
|
@ -22,6 +22,12 @@ limitations under the License.
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(.mx_RoomSublist_minimized) {
|
||||||
|
.mx_RoomSublist_headerContainer {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomSublist_headerContainer {
|
.mx_RoomSublist_headerContainer {
|
||||||
// Create a flexbox to make alignment easy
|
// Create a flexbox to make alignment easy
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -41,9 +47,7 @@ limitations under the License.
|
||||||
// The combined height must be set in the LeftPanel component for sticky headers
|
// The combined height must be set in the LeftPanel component for sticky headers
|
||||||
// to work correctly.
|
// to work correctly.
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
// Allow the container to collapse on itself if its children
|
height: 24px;
|
||||||
// are not in the normal document flow
|
|
||||||
max-height: 24px;
|
|
||||||
color: $roomlist-header-color;
|
color: $roomlist-header-color;
|
||||||
|
|
||||||
.mx_RoomSublist_stickable {
|
.mx_RoomSublist_stickable {
|
||||||
|
@ -172,14 +176,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In the general case, we reserve space for each sublist header to prevent
|
|
||||||
// scroll jumps when they become sticky. However, that leaves a gap when
|
|
||||||
// scrolled to the top above the first sublist (whose header can only ever
|
|
||||||
// stick to top), so we make sure to exclude the first visible sublist.
|
|
||||||
&:not(.mx_RoomSublist_hidden) ~ .mx_RoomSublist .mx_RoomSublist_headerContainer {
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSublist_resizeBox {
|
.mx_RoomSublist_resizeBox {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|
|
@ -142,15 +142,11 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
|
||||||
// space rooms cannot be DMs so skip the rest
|
// space rooms cannot be DMs so skip the rest
|
||||||
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return null;
|
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return null;
|
||||||
|
|
||||||
let otherMember = null;
|
// If the room is not a DM don't fallback to a member avatar
|
||||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) return null;
|
||||||
if (otherUserId) {
|
|
||||||
otherMember = room.getMember(otherUserId);
|
// If there are only two members in the DM use the avatar of the other member
|
||||||
} else {
|
const otherMember = room.getAvatarFallbackMember();
|
||||||
// if the room is not marked as a 1:1, but only has max 2 members
|
|
||||||
// then still try to show any avatar (pref. other member)
|
|
||||||
otherMember = room.getAvatarFallbackMember();
|
|
||||||
}
|
|
||||||
if (otherMember?.getMxcAvatarUrl()) {
|
if (otherMember?.getMxcAvatarUrl()) {
|
||||||
return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,10 +42,15 @@ export interface IInviteResult {
|
||||||
*
|
*
|
||||||
* @param {string} roomId The ID of the room to invite to
|
* @param {string} roomId The ID of the room to invite to
|
||||||
* @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
* @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
||||||
|
* @param {function} progressCallback optional callback, fired after each invite.
|
||||||
* @returns {Promise} Promise
|
* @returns {Promise} Promise
|
||||||
*/
|
*/
|
||||||
export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> {
|
export function inviteMultipleToRoom(
|
||||||
const inviter = new MultiInviter(roomId);
|
roomId: string,
|
||||||
|
addresses: string[],
|
||||||
|
progressCallback?: () => void,
|
||||||
|
): Promise<IInviteResult> {
|
||||||
|
const inviter = new MultiInviter(roomId, progressCallback);
|
||||||
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,8 +109,8 @@ export function isValid3pidInvite(event: MatrixEvent): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> {
|
export function inviteUsersToRoom(roomId: string, userIds: string[], progressCallback?: () => void): Promise<void> {
|
||||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
return inviteMultipleToRoom(roomId, userIds, progressCallback).then((result) => {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
showAnyInviteErrors(result.states, room, result.inviter);
|
showAnyInviteErrors(result.states, room, result.inviter);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
|
|
@ -452,7 +452,9 @@ function setBotOptions(event: MessageEvent<any>, roomId: string, userId: string)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBotPower(event: MessageEvent<any>, roomId: string, userId: string, level: number): void {
|
async function setBotPower(
|
||||||
|
event: MessageEvent<any>, roomId: string, userId: string, level: number, ignoreIfGreater?: boolean,
|
||||||
|
): Promise<void> {
|
||||||
if (!(Number.isInteger(level) && level >= 0)) {
|
if (!(Number.isInteger(level) && level >= 0)) {
|
||||||
sendError(event, _t('Power level must be positive integer.'));
|
sendError(event, _t('Power level must be positive integer.'));
|
||||||
return;
|
return;
|
||||||
|
@ -465,22 +467,34 @@ function setBotPower(event: MessageEvent<any>, roomId: string, userId: string, l
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
|
try {
|
||||||
const powerEvent = new MatrixEvent(
|
const powerLevels = await client.getStateEvent(roomId, "m.room.power_levels", "");
|
||||||
|
|
||||||
|
// If the PL is equal to or greater than the requested PL, ignore.
|
||||||
|
if (ignoreIfGreater === true) {
|
||||||
|
// As per https://matrix.org/docs/spec/client_server/r0.6.0#m-room-power-levels
|
||||||
|
const currentPl = (
|
||||||
|
powerLevels.content.users && powerLevels.content.users[userId]
|
||||||
|
) || powerLevels.content.users_default || 0;
|
||||||
|
|
||||||
|
if (currentPl >= level) {
|
||||||
|
return sendResponse(event, {
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await client.setPowerLevel(roomId, userId, level, new MatrixEvent(
|
||||||
{
|
{
|
||||||
type: "m.room.power_levels",
|
type: "m.room.power_levels",
|
||||||
content: powerLevels,
|
content: powerLevels,
|
||||||
},
|
},
|
||||||
);
|
));
|
||||||
|
return sendResponse(event, {
|
||||||
client.setPowerLevel(roomId, userId, level, powerEvent).then(() => {
|
success: true,
|
||||||
sendResponse(event, {
|
|
||||||
success: true,
|
|
||||||
});
|
|
||||||
}, (err) => {
|
|
||||||
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
|
|
||||||
});
|
});
|
||||||
});
|
} catch (err) {
|
||||||
|
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMembershipState(event: MessageEvent<any>, roomId: string, userId: string): void {
|
function getMembershipState(event: MessageEvent<any>, roomId: string, userId: string): void {
|
||||||
|
@ -678,7 +692,7 @@ const onMessage = function(event: MessageEvent<any>): void {
|
||||||
setBotOptions(event, roomId, userId);
|
setBotOptions(event, roomId, userId);
|
||||||
break;
|
break;
|
||||||
case Action.SetBotPower:
|
case Action.SetBotPower:
|
||||||
setBotPower(event, roomId, userId, event.data.level);
|
setBotPower(event, roomId, userId, event.data.level, event.data.ignoreIfGreater);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
|
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import FocusLock from "react-focus-lock";
|
||||||
|
|
||||||
import { Key } from "../../Keyboard";
|
import { Key } from "../../Keyboard";
|
||||||
import { Writeable } from "../../@types/common";
|
import { Writeable } from "../../@types/common";
|
||||||
|
@ -43,8 +44,6 @@ function getOrCreateContainer(): HTMLDivElement {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
|
||||||
|
|
||||||
export interface IPosition {
|
export interface IPosition {
|
||||||
top?: number;
|
top?: number;
|
||||||
bottom?: number;
|
bottom?: number;
|
||||||
|
@ -84,6 +83,10 @@ export interface IProps extends IPosition {
|
||||||
// it will be mounted to a container at the root of the DOM.
|
// it will be mounted to a container at the root of the DOM.
|
||||||
mountAsChild?: boolean;
|
mountAsChild?: boolean;
|
||||||
|
|
||||||
|
// If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered
|
||||||
|
// within an existing FocusLock e.g inside a modal.
|
||||||
|
focusLock?: boolean;
|
||||||
|
|
||||||
// Function to be called on menu close
|
// Function to be called on menu close
|
||||||
onFinished();
|
onFinished();
|
||||||
// on resize callback
|
// on resize callback
|
||||||
|
@ -99,7 +102,7 @@ interface IState {
|
||||||
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||||
@replaceableComponent("structures.ContextMenu")
|
@replaceableComponent("structures.ContextMenu")
|
||||||
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
private initialFocus: HTMLElement;
|
private readonly initialFocus: HTMLElement;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
hasBackground: true,
|
hasBackground: true,
|
||||||
|
@ -108,6 +111,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
contextMenuElem: null,
|
contextMenuElem: null,
|
||||||
};
|
};
|
||||||
|
@ -121,14 +125,13 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
this.initialFocus.focus();
|
this.initialFocus.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private collectContextMenuRect = (element) => {
|
private collectContextMenuRect = (element: HTMLDivElement) => {
|
||||||
// We don't need to clean up when unmounting, so ignore
|
// We don't need to clean up when unmounting, so ignore
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
let first = element.querySelector('[role^="menuitem"]');
|
const first = element.querySelector<HTMLElement>('[role^="menuitem"]')
|
||||||
if (!first) {
|
|| element.querySelector<HTMLElement>('[tab-index]');
|
||||||
first = element.querySelector('[tab-index]');
|
|
||||||
}
|
|
||||||
if (first) {
|
if (first) {
|
||||||
first.focus();
|
first.focus();
|
||||||
}
|
}
|
||||||
|
@ -205,7 +208,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
descending = true;
|
descending = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
} while (element && !element.getAttribute("role")?.startsWith("menuitem"));
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
(element as HTMLElement).focus();
|
(element as HTMLElement).focus();
|
||||||
|
@ -383,6 +386,17 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let body = <>
|
||||||
|
{ chevron }
|
||||||
|
{ props.children }
|
||||||
|
</>;
|
||||||
|
|
||||||
|
if (props.focusLock) {
|
||||||
|
body = <FocusLock>
|
||||||
|
{ body }
|
||||||
|
</FocusLock>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||||
|
@ -397,8 +411,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
ref={this.collectContextMenuRect}
|
ref={this.collectContextMenuRect}
|
||||||
role={this.props.managed ? "menu" : undefined}
|
role={this.props.managed ? "menu" : undefined}
|
||||||
>
|
>
|
||||||
{ chevron }
|
{ body }
|
||||||
{ props.children }
|
|
||||||
</div>
|
</div>
|
||||||
{ background }
|
{ background }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,17 +15,17 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
|
Dispatch,
|
||||||
|
KeyboardEvent,
|
||||||
|
KeyboardEventHandler,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
KeyboardEvent,
|
|
||||||
KeyboardEventHandler,
|
|
||||||
useContext,
|
|
||||||
SetStateAction,
|
|
||||||
Dispatch,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
|
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
|
||||||
|
@ -33,7 +33,8 @@ import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
|
import { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { sortBy } from "lodash";
|
import { sortBy, uniqBy } from "lodash";
|
||||||
|
import { GuestAccess, HistoryVisibility } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
@ -333,6 +334,30 @@ interface IHierarchyLevelProps {
|
||||||
onToggleClick?(parentId: string, childId: string): void;
|
onToggleClick?(parentId: string, childId: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom): IHierarchyRoom => {
|
||||||
|
const history = cli.getRoomUpgradeHistory(room.room_id, true);
|
||||||
|
const cliRoom = history[history.length - 1];
|
||||||
|
if (cliRoom) {
|
||||||
|
return {
|
||||||
|
...room,
|
||||||
|
room_id: cliRoom.roomId,
|
||||||
|
room_type: cliRoom.getType(),
|
||||||
|
name: cliRoom.name,
|
||||||
|
topic: cliRoom.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent().topic,
|
||||||
|
avatar_url: cliRoom.getMxcAvatarUrl(),
|
||||||
|
canonical_alias: cliRoom.getCanonicalAlias(),
|
||||||
|
aliases: cliRoom.getAltAliases(),
|
||||||
|
world_readable: cliRoom.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")?.getContent()
|
||||||
|
.history_visibility === HistoryVisibility.WorldReadable,
|
||||||
|
guest_can_join: cliRoom.currentState.getStateEvents(EventType.RoomGuestAccess, "")?.getContent()
|
||||||
|
.guest_access === GuestAccess.CanJoin,
|
||||||
|
num_joined_members: cliRoom.getJoinedMemberCount(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return room;
|
||||||
|
};
|
||||||
|
|
||||||
export const HierarchyLevel = ({
|
export const HierarchyLevel = ({
|
||||||
root,
|
root,
|
||||||
roomSet,
|
roomSet,
|
||||||
|
@ -353,7 +378,7 @@ export const HierarchyLevel = ({
|
||||||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: IHierarchyRelation) => {
|
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: IHierarchyRelation) => {
|
||||||
const room = hierarchy.roomMap.get(ev.state_key);
|
const room = hierarchy.roomMap.get(ev.state_key);
|
||||||
if (room && roomSet.has(room)) {
|
if (room && roomSet.has(room)) {
|
||||||
result[room.room_type === RoomType.Space ? 0 : 1].push(room);
|
result[room.room_type === RoomType.Space ? 0 : 1].push(toLocalRoom(cli, room));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [[] as IHierarchyRoom[], [] as IHierarchyRoom[]]);
|
}, [[] as IHierarchyRoom[], [] as IHierarchyRoom[]]);
|
||||||
|
@ -361,7 +386,7 @@ export const HierarchyLevel = ({
|
||||||
const newParents = new Set(parents).add(root.room_id);
|
const newParents = new Set(parents).add(root.room_id);
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{
|
{
|
||||||
childRooms.map(room => (
|
uniqBy(childRooms, "room_id").map(room => (
|
||||||
<Tile
|
<Tile
|
||||||
key={room.room_id}
|
key={room.room_id}
|
||||||
room={room}
|
room={room}
|
||||||
|
@ -410,50 +435,39 @@ export const HierarchyLevel = ({
|
||||||
|
|
||||||
const INITIAL_PAGE_SIZE = 20;
|
const INITIAL_PAGE_SIZE = 20;
|
||||||
|
|
||||||
export const useSpaceSummary = (space: Room): {
|
export const useRoomHierarchy = (space: Room): {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
rooms: IHierarchyRoom[];
|
rooms: IHierarchyRoom[];
|
||||||
hierarchy: RoomHierarchy;
|
hierarchy: RoomHierarchy;
|
||||||
loadMore(pageSize?: number): Promise <void>;
|
loadMore(pageSize?: number): Promise <void>;
|
||||||
} => {
|
} => {
|
||||||
const [rooms, setRooms] = useState<IHierarchyRoom[]>([]);
|
const [rooms, setRooms] = useState<IHierarchyRoom[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [hierarchy, setHierarchy] = useState<RoomHierarchy>();
|
const [hierarchy, setHierarchy] = useState<RoomHierarchy>();
|
||||||
|
|
||||||
const resetHierarchy = useCallback(() => {
|
const resetHierarchy = useCallback(() => {
|
||||||
const hierarchy = new RoomHierarchy(space, INITIAL_PAGE_SIZE);
|
const hierarchy = new RoomHierarchy(space, INITIAL_PAGE_SIZE);
|
||||||
setHierarchy(hierarchy);
|
|
||||||
|
|
||||||
let discard = false;
|
|
||||||
hierarchy.load().then(() => {
|
hierarchy.load().then(() => {
|
||||||
if (discard) return;
|
if (space !== hierarchy.root) return; // discard stale results
|
||||||
setRooms(hierarchy.rooms);
|
setRooms(hierarchy.rooms);
|
||||||
setLoading(false);
|
|
||||||
});
|
});
|
||||||
|
setHierarchy(hierarchy);
|
||||||
return () => {
|
|
||||||
discard = true;
|
|
||||||
};
|
|
||||||
}, [space]);
|
}, [space]);
|
||||||
useEffect(resetHierarchy, [resetHierarchy]);
|
useEffect(resetHierarchy, [resetHierarchy]);
|
||||||
|
|
||||||
useDispatcher(defaultDispatcher, (payload => {
|
useDispatcher(defaultDispatcher, (payload => {
|
||||||
if (payload.action === Action.UpdateSpaceHierarchy) {
|
if (payload.action === Action.UpdateSpaceHierarchy) {
|
||||||
setLoading(true);
|
|
||||||
setRooms([]); // TODO
|
setRooms([]); // TODO
|
||||||
resetHierarchy();
|
resetHierarchy();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const loadMore = useCallback(async (pageSize?: number) => {
|
const loadMore = useCallback(async (pageSize?: number) => {
|
||||||
if (loading || !hierarchy.canLoadMore || hierarchy.noSupport) return;
|
if (hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport) return;
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
await hierarchy.load(pageSize);
|
await hierarchy.load(pageSize);
|
||||||
setRooms(hierarchy.rooms);
|
setRooms(hierarchy.rooms);
|
||||||
setLoading(false);
|
}, [hierarchy]);
|
||||||
}, [loading, hierarchy]);
|
|
||||||
|
|
||||||
|
const loading = hierarchy?.loading ?? true;
|
||||||
return { loading, rooms, hierarchy, loadMore };
|
return { loading, rooms, hierarchy, loadMore };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -587,7 +601,7 @@ const SpaceHierarchy = ({
|
||||||
|
|
||||||
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
|
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
|
||||||
|
|
||||||
const { loading, rooms, hierarchy, loadMore } = useSpaceSummary(space);
|
const { loading, rooms, hierarchy, loadMore } = useRoomHierarchy(space);
|
||||||
|
|
||||||
const filteredRoomSet = useMemo<Set<IHierarchyRoom>>(() => {
|
const filteredRoomSet = useMemo<Set<IHierarchyRoom>>(() => {
|
||||||
if (!rooms?.length) return new Set();
|
if (!rooms?.length) return new Set();
|
||||||
|
|
|
@ -39,6 +39,8 @@ import dis from "../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { UserTab } from "./UserSettingsDialog";
|
import { UserTab } from "./UserSettingsDialog";
|
||||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||||
|
import { inviteUsersToRoom } from "../../../RoomInvite";
|
||||||
|
import ProgressBar from "../elements/ProgressBar";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
@ -90,10 +92,22 @@ export interface IGroupSummary {
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
enum Progress {
|
||||||
|
NotStarted,
|
||||||
|
ValidatingInputs,
|
||||||
|
FetchingData,
|
||||||
|
CreatingSpace,
|
||||||
|
InvitingUsers,
|
||||||
|
// anything beyond here is inviting user n - 4
|
||||||
|
}
|
||||||
|
|
||||||
const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string>(null);
|
const [error, setError] = useState<string>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
|
const [progress, setProgress] = useState(Progress.NotStarted);
|
||||||
|
const [numInvites, setNumInvites] = useState(0);
|
||||||
|
const busy = progress > 0;
|
||||||
|
|
||||||
const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
|
@ -122,30 +136,34 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setBusy(true);
|
setProgress(Progress.ValidatingInputs);
|
||||||
|
|
||||||
// require & validate the space name field
|
// require & validate the space name field
|
||||||
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||||
setBusy(false);
|
setProgress(0);
|
||||||
spaceNameField.current.focus();
|
spaceNameField.current.focus();
|
||||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// validate the space name alias field but do not require it
|
// validate the space name alias field but do not require it
|
||||||
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
||||||
setBusy(false);
|
setProgress(0);
|
||||||
spaceAliasField.current.focus();
|
spaceAliasField.current.focus();
|
||||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setProgress(Progress.FetchingData);
|
||||||
|
|
||||||
const [rooms, members, invitedMembers] = await Promise.all([
|
const [rooms, members, invitedMembers] = await Promise.all([
|
||||||
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
||||||
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||||
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
setNumInvites(members.length + invitedMembers.length);
|
||||||
|
|
||||||
const viaMap = new Map<string, string[]>();
|
const viaMap = new Map<string, string[]>();
|
||||||
for (const { roomId, canonicalAlias } of rooms) {
|
for (const { roomId, canonicalAlias } of rooms) {
|
||||||
const room = cli.getRoom(roomId);
|
const room = cli.getRoom(roomId);
|
||||||
|
@ -167,6 +185,8 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setProgress(Progress.CreatingSpace);
|
||||||
|
|
||||||
const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
||||||
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
||||||
creation_content: {
|
creation_content: {
|
||||||
|
@ -179,11 +199,16 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
via: viaMap.get(roomId) || [],
|
via: viaMap.get(roomId) || [],
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
invite: [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId()),
|
// we do not specify the inviters here because Synapse applies a limit and this may cause it to trip
|
||||||
}, {
|
}, {
|
||||||
andView: false,
|
andView: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setProgress(Progress.InvitingUsers);
|
||||||
|
|
||||||
|
const userIds = [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId());
|
||||||
|
await inviteUsersToRoom(roomId, userIds, () => setProgress(p => p + 1));
|
||||||
|
|
||||||
// eagerly remove it from the community panel
|
// eagerly remove it from the community panel
|
||||||
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
||||||
|
|
||||||
|
@ -250,7 +275,7 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
setError(e);
|
setError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBusy(false);
|
setProgress(Progress.NotStarted);
|
||||||
};
|
};
|
||||||
|
|
||||||
let footer;
|
let footer;
|
||||||
|
@ -267,13 +292,41 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
{ _t("Retry") }
|
{ _t("Retry") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>;
|
</>;
|
||||||
|
} else if (busy) {
|
||||||
|
let description: string;
|
||||||
|
switch (progress) {
|
||||||
|
case Progress.ValidatingInputs:
|
||||||
|
case Progress.FetchingData:
|
||||||
|
description = _t("Fetching data...");
|
||||||
|
break;
|
||||||
|
case Progress.CreatingSpace:
|
||||||
|
description = _t("Creating Space...");
|
||||||
|
break;
|
||||||
|
case Progress.InvitingUsers:
|
||||||
|
default:
|
||||||
|
description = _t("Adding rooms... (%(progress)s out of %(count)s)", {
|
||||||
|
count: numInvites,
|
||||||
|
progress,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer = <span>
|
||||||
|
<ProgressBar
|
||||||
|
value={progress > Progress.FetchingData ? progress : 0}
|
||||||
|
max={numInvites + Progress.InvitingUsers}
|
||||||
|
/>
|
||||||
|
<div className="mx_CreateSpaceFromCommunityDialog_progressText">
|
||||||
|
{ description }
|
||||||
|
</div>
|
||||||
|
</span>;
|
||||||
} else {
|
} else {
|
||||||
footer = <>
|
footer = <>
|
||||||
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished()}>
|
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSpaceClick}>
|
<AccessibleButton kind="primary" onClick={onCreateSpaceClick}>
|
||||||
{ busy ? _t("Creating...") : _t("Create Space") }
|
{ _t("Create Space") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,18 +44,31 @@ interface IProps {
|
||||||
initialTabId?: string;
|
initialTabId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
roomName: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
||||||
export default class RoomSettingsDialog extends React.Component<IProps> {
|
export default class RoomSettingsDialog extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { roomName: '' };
|
||||||
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||||
|
this.onRoomName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
if (this.dispatcherRef) {
|
if (this.dispatcherRef) {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload): void => {
|
private onAction = (payload): void => {
|
||||||
|
@ -66,6 +79,12 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onRoomName = (): void => {
|
||||||
|
this.setState({
|
||||||
|
roomName: MatrixClientPeg.get().getRoom(this.props.roomId).name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private getTabs(): Tab[] {
|
private getTabs(): Tab[] {
|
||||||
const tabs: Tab[] = [];
|
const tabs: Tab[] = [];
|
||||||
|
|
||||||
|
@ -122,7 +141,7 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name;
|
const roomName = this.state.roomName;
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className='mx_RoomSettingsDialog'
|
className='mx_RoomSettingsDialog'
|
||||||
|
|
|
@ -268,7 +268,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonRect = handle.current.getBoundingClientRect();
|
const buttonRect = handle.current.getBoundingClientRect();
|
||||||
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu}>
|
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu} focusLock>
|
||||||
<div className="mx_NetworkDropdown_menu">
|
<div className="mx_NetworkDropdown_menu">
|
||||||
{ options }
|
{ options }
|
||||||
<MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
|
<MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
|
||||||
|
|
|
@ -817,7 +817,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||||
const isMe = me.userId === member.userId;
|
const isMe = me.userId === member.userId;
|
||||||
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
|
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
|
||||||
|
|
||||||
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
|
if (!isMe && canAffectUser && me.powerLevel >= kickPowerLevel) {
|
||||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||||
}
|
}
|
||||||
if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
|
if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
|
||||||
|
@ -825,10 +825,10 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (canAffectUser && me.powerLevel >= banPowerLevel) {
|
if (!isMe && canAffectUser && me.powerLevel >= banPowerLevel) {
|
||||||
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||||
}
|
}
|
||||||
if (canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
|
if (!isMe && canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
|
||||||
muteButton = (
|
muteButton = (
|
||||||
<MuteToggleButton
|
<MuteToggleButton
|
||||||
member={member}
|
member={member}
|
||||||
|
|
|
@ -35,7 +35,7 @@ interface IState {
|
||||||
avatarFile: File;
|
avatarFile: File;
|
||||||
originalTopic: string;
|
originalTopic: string;
|
||||||
topic: string;
|
topic: string;
|
||||||
enableProfileSave: boolean;
|
profileFieldsTouched: Record<string, boolean>;
|
||||||
canSetName: boolean;
|
canSetName: boolean;
|
||||||
canSetTopic: boolean;
|
canSetTopic: boolean;
|
||||||
canSetAvatar: boolean;
|
canSetAvatar: boolean;
|
||||||
|
@ -71,7 +71,7 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
avatarFile: null,
|
avatarFile: null,
|
||||||
originalTopic: topic,
|
originalTopic: topic,
|
||||||
topic: topic,
|
topic: topic,
|
||||||
enableProfileSave: false,
|
profileFieldsTouched: {},
|
||||||
canSetName: room.currentState.maySendStateEvent('m.room.name', client.getUserId()),
|
canSetName: room.currentState.maySendStateEvent('m.room.name', client.getUserId()),
|
||||||
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
||||||
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
|
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
|
||||||
|
@ -88,17 +88,24 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
avatarFile: null,
|
avatarFile: null,
|
||||||
enableProfileSave: true,
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private isSaveEnabled = () => {
|
||||||
|
return Boolean(Object.values(this.state.profileFieldsTouched).length);
|
||||||
|
};
|
||||||
|
|
||||||
private cancelProfileChanges = async (e: React.MouseEvent): Promise<void> => {
|
private cancelProfileChanges = async (e: React.MouseEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.state.enableProfileSave) return;
|
if (!this.isSaveEnabled()) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
enableProfileSave: false,
|
profileFieldsTouched: {},
|
||||||
displayName: this.state.originalDisplayName,
|
displayName: this.state.originalDisplayName,
|
||||||
topic: this.state.originalTopic,
|
topic: this.state.originalTopic,
|
||||||
avatarUrl: this.state.originalAvatarUrl,
|
avatarUrl: this.state.originalAvatarUrl,
|
||||||
|
@ -110,8 +117,8 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.state.enableProfileSave) return;
|
if (!this.isSaveEnabled()) return;
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({ profileFieldsTouched: {} });
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
@ -156,18 +163,38 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ displayName: e.target.value });
|
this.setState({ displayName: e.target.value });
|
||||||
if (this.state.originalDisplayName === e.target.value) {
|
if (this.state.originalDisplayName === e.target.value) {
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({
|
||||||
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
name: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ enableProfileSave: true });
|
this.setState({
|
||||||
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTopicChanged = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
private onTopicChanged = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||||
this.setState({ topic: e.target.value });
|
this.setState({ topic: e.target.value });
|
||||||
if (this.state.originalTopic === e.target.value) {
|
if (this.state.originalTopic === e.target.value) {
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({
|
||||||
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
topic: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ enableProfileSave: true });
|
this.setState({
|
||||||
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
topic: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -176,7 +203,10 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: this.state.originalAvatarUrl,
|
avatarUrl: this.state.originalAvatarUrl,
|
||||||
avatarFile: null,
|
avatarFile: null,
|
||||||
enableProfileSave: false,
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
avatar: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -187,7 +217,10 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: String(ev.target.result),
|
avatarUrl: String(ev.target.result),
|
||||||
avatarFile: file,
|
avatarFile: file,
|
||||||
enableProfileSave: true,
|
profileFieldsTouched: {
|
||||||
|
...this.state.profileFieldsTouched,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
|
@ -205,14 +238,14 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.cancelProfileChanges}
|
onClick={this.cancelProfileChanges}
|
||||||
kind="link"
|
kind="link"
|
||||||
disabled={!this.state.enableProfileSave}
|
disabled={!this.isSaveEnabled()}
|
||||||
>
|
>
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.saveProfile}
|
onClick={this.saveProfile}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={!this.state.enableProfileSave}
|
disabled={!this.isSaveEnabled()}
|
||||||
>
|
>
|
||||||
{ _t("Save") }
|
{ _t("Save") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
|
|
@ -268,7 +268,7 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.onContinueClick}
|
onClick={this.onContinueClick}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={this.state.continueDisabled}
|
disabled={this.state.continueDisabled || this.state.newPhoneNumberCode.length === 0}
|
||||||
>
|
>
|
||||||
{ _t("Continue") }
|
{ _t("Continue") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
|
|
@ -17,9 +17,8 @@ limitations under the License.
|
||||||
import React, { ComponentProps, RefObject, SyntheticEvent, KeyboardEvent, useContext, useRef, useState } from "react";
|
import React, { ComponentProps, RefObject, SyntheticEvent, KeyboardEvent, useContext, useRef, useState } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import FocusLock from "react-focus-lock";
|
|
||||||
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
|
||||||
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||||
|
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
@ -361,9 +360,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
wrapperClassName="mx_SpaceCreateMenu_wrapper"
|
wrapperClassName="mx_SpaceCreateMenu_wrapper"
|
||||||
managed={false}
|
managed={false}
|
||||||
>
|
>
|
||||||
<FocusLock returnFocus={true}>
|
{ body }
|
||||||
{ body }
|
|
||||||
</FocusLock>
|
|
||||||
</ContextMenu>;
|
</ContextMenu>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ export const EMOJI: IEmoji[] = EMOJIBASE.map((emojiData: Omit<IEmoji, "shortcode
|
||||||
// If there's ever a gap in shortcode coverage, we fudge it by
|
// If there's ever a gap in shortcode coverage, we fudge it by
|
||||||
// filling it in with the emoji's CLDR annotation
|
// filling it in with the emoji's CLDR annotation
|
||||||
const shortcodeData = SHORTCODES[emojiData.hexcode] ??
|
const shortcodeData = SHORTCODES[emojiData.hexcode] ??
|
||||||
[emojiData.annotation.toLowerCase().replace(/ /g, "_")];
|
[emojiData.annotation.toLowerCase().replace(/\W+/g, "_")];
|
||||||
|
|
||||||
const emoji: IEmoji = {
|
const emoji: IEmoji = {
|
||||||
...emojiData,
|
...emojiData,
|
||||||
|
|
|
@ -2280,6 +2280,8 @@
|
||||||
"<SpaceName/> has been made and everyone who was a part of the community has been invited to it.": "<SpaceName/> has been made and everyone who was a part of the community has been invited to it.",
|
"<SpaceName/> has been made and everyone who was a part of the community has been invited to it.": "<SpaceName/> has been made and everyone who was a part of the community has been invited to it.",
|
||||||
"To create a Space from another community, just pick the community in Preferences.": "To create a Space from another community, just pick the community in Preferences.",
|
"To create a Space from another community, just pick the community in Preferences.": "To create a Space from another community, just pick the community in Preferences.",
|
||||||
"Failed to migrate community": "Failed to migrate community",
|
"Failed to migrate community": "Failed to migrate community",
|
||||||
|
"Fetching data...": "Fetching data...",
|
||||||
|
"Creating Space...": "Creating Space...",
|
||||||
"Create Space from community": "Create Space from community",
|
"Create Space from community": "Create Space from community",
|
||||||
"A link to the Space will be put in your community description.": "A link to the Space will be put in your community description.",
|
"A link to the Space will be put in your community description.": "A link to the Space will be put in your community description.",
|
||||||
"All rooms will be added and all community members will be invited.": "All rooms will be added and all community members will be invited.",
|
"All rooms will be added and all community members will be invited.": "All rooms will be added and all community members will be invited.",
|
||||||
|
|
|
@ -162,9 +162,10 @@ export default class SettingsStore {
|
||||||
|
|
||||||
const watcherId = `${new Date().getTime()}_${SettingsStore.watcherCount++}_${settingName}_${roomId}`;
|
const watcherId = `${new Date().getTime()}_${SettingsStore.watcherCount++}_${settingName}_${roomId}`;
|
||||||
|
|
||||||
const localizedCallback = (changedInRoomId, atLevel, newValAtLevel) => {
|
const localizedCallback = (changedInRoomId: string | null, atLevel: SettingLevel, newValAtLevel: any) => {
|
||||||
const newValue = SettingsStore.getValue(originalSettingName);
|
const newValue = SettingsStore.getValue(originalSettingName);
|
||||||
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
|
const newValueAtLevel = SettingsStore.getValueAt(atLevel, originalSettingName) ?? newValAtLevel;
|
||||||
|
callbackFn(originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
SettingsStore.watchers.set(watcherId, localizedCallback);
|
SettingsStore.watchers.set(watcherId, localizedCallback);
|
||||||
|
|
|
@ -283,7 +283,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs();
|
const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs();
|
||||||
return getChildOrder(ev.getContent().order, createTs, roomId);
|
return getChildOrder(ev.getContent().order, createTs, roomId);
|
||||||
}).map(ev => {
|
}).map(ev => {
|
||||||
return this.matrixClient.getRoom(ev.getStateKey());
|
const history = this.matrixClient.getRoomUpgradeHistory(ev.getStateKey(), true);
|
||||||
|
return history[history.length - 1];
|
||||||
}).filter(room => {
|
}).filter(room => {
|
||||||
return room?.getMyMembership() === "join" || room?.getMyMembership() === "invite";
|
return room?.getMyMembership() === "join" || room?.getMyMembership() === "invite";
|
||||||
}) || [];
|
}) || [];
|
||||||
|
@ -511,8 +512,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
hiddenChildren.get(spaceId)?.forEach(roomId => {
|
hiddenChildren.get(spaceId)?.forEach(roomId => {
|
||||||
roomIds.add(roomId);
|
roomIds.add(roomId);
|
||||||
});
|
});
|
||||||
this.spaceFilteredRooms.set(spaceId, roomIds);
|
|
||||||
return roomIds;
|
// Expand room IDs to all known versions of the given rooms
|
||||||
|
const expandedRoomIds = new Set(Array.from(roomIds).flatMap(roomId => {
|
||||||
|
return this.matrixClient.getRoomUpgradeHistory(roomId, true).map(r => r.roomId);
|
||||||
|
}));
|
||||||
|
this.spaceFilteredRooms.set(spaceId, expandedRoomIds);
|
||||||
|
return expandedRoomIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
fn(s.roomId, new Set());
|
fn(s.roomId, new Set());
|
||||||
|
@ -793,7 +799,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// 1 is Home, 2-9 are the spaces after Home
|
// 1 is Home, 2-9 are the spaces after Home
|
||||||
if (payload.num === 1) {
|
if (payload.num === 1) {
|
||||||
this.setActiveSpace(null);
|
this.setActiveSpace(null);
|
||||||
} else if (this.spacePanelSpaces.length >= payload.num) {
|
} else if (payload.num > 0 && this.spacePanelSpaces.length > payload.num - 2) {
|
||||||
this.setActiveSpace(this.spacePanelSpaces[payload.num - 2]);
|
this.setActiveSpace(this.spacePanelSpaces[payload.num - 2]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -62,8 +62,9 @@ export default class MultiInviter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} targetId The ID of the room or group to invite to
|
* @param {string} targetId The ID of the room or group to invite to
|
||||||
|
* @param {function} progressCallback optional callback, fired after each invite.
|
||||||
*/
|
*/
|
||||||
constructor(targetId: string) {
|
constructor(targetId: string, private readonly progressCallback?: () => void) {
|
||||||
if (targetId[0] === '+') {
|
if (targetId[0] === '+') {
|
||||||
this.roomId = null;
|
this.roomId = null;
|
||||||
this.groupId = targetId;
|
this.groupId = targetId;
|
||||||
|
@ -181,6 +182,7 @@ export default class MultiInviter {
|
||||||
delete this.errors[address];
|
delete this.errors[address];
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
|
this.progressCallback?.();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
if (this.canceled) {
|
if (this.canceled) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -0,0 +1,251 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import "../../../skinned-sdk";
|
||||||
|
|
||||||
|
import * as TestUtils from '../../../test-utils';
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||||
|
|
||||||
|
import DMRoomMap from '../../../../src/utils/DMRoomMap';
|
||||||
|
import RoomHeader from '../../../../src/components/views/rooms/RoomHeader';
|
||||||
|
|
||||||
|
import { Room, PendingEventOrdering, MatrixEvent, MatrixClient } from 'matrix-js-sdk';
|
||||||
|
import { SearchScope } from '../../../../src/components/views/rooms/SearchBar';
|
||||||
|
import { E2EStatus } from '../../../../src/utils/ShieldUtils';
|
||||||
|
import { PlaceCallType } from '../../../../src/CallHandler';
|
||||||
|
import { mkEvent } from '../../../test-utils';
|
||||||
|
|
||||||
|
describe('RoomHeader', () => {
|
||||||
|
it('shows the room avatar in a room with only ourselves', () => {
|
||||||
|
// When we render a non-DM room with 1 person in it
|
||||||
|
const room = createRoom({ name: "X Room", isDm: false, userIds: [] });
|
||||||
|
const rendered = render(room);
|
||||||
|
|
||||||
|
// Then the room's avatar is the initial of its name
|
||||||
|
const initial = findSpan(rendered, ".mx_BaseAvatar_initial");
|
||||||
|
expect(initial.innerHTML).toEqual("X");
|
||||||
|
|
||||||
|
// And there is no image avatar (because it's not set on this room)
|
||||||
|
const image = findImg(rendered, ".mx_BaseAvatar_image");
|
||||||
|
expect(image.src).toEqual("data:image/png;base64,00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the room avatar in a room with 2 people', () => {
|
||||||
|
// When we render a non-DM room with 2 people in it
|
||||||
|
const room = createRoom(
|
||||||
|
{ name: "Y Room", isDm: false, userIds: ["other"] });
|
||||||
|
const rendered = render(room);
|
||||||
|
|
||||||
|
// Then the room's avatar is the initial of its name
|
||||||
|
const initial = findSpan(rendered, ".mx_BaseAvatar_initial");
|
||||||
|
expect(initial.innerHTML).toEqual("Y");
|
||||||
|
|
||||||
|
// And there is no image avatar (because it's not set on this room)
|
||||||
|
const image = findImg(rendered, ".mx_BaseAvatar_image");
|
||||||
|
expect(image.src).toEqual("data:image/png;base64,00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the room avatar in a room with >2 people', () => {
|
||||||
|
// When we render a non-DM room with 3 people in it
|
||||||
|
const room = createRoom(
|
||||||
|
{ name: "Z Room", isDm: false, userIds: ["other1", "other2"] });
|
||||||
|
const rendered = render(room);
|
||||||
|
|
||||||
|
// Then the room's avatar is the initial of its name
|
||||||
|
const initial = findSpan(rendered, ".mx_BaseAvatar_initial");
|
||||||
|
expect(initial.innerHTML).toEqual("Z");
|
||||||
|
|
||||||
|
// And there is no image avatar (because it's not set on this room)
|
||||||
|
const image = findImg(rendered, ".mx_BaseAvatar_image");
|
||||||
|
expect(image.src).toEqual("data:image/png;base64,00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the room avatar in a DM with only ourselves', () => {
|
||||||
|
// When we render a non-DM room with 1 person in it
|
||||||
|
const room = createRoom({ name: "Z Room", isDm: true, userIds: [] });
|
||||||
|
const rendered = render(room);
|
||||||
|
|
||||||
|
// Then the room's avatar is the initial of its name
|
||||||
|
const initial = findSpan(rendered, ".mx_BaseAvatar_initial");
|
||||||
|
expect(initial.innerHTML).toEqual("Z");
|
||||||
|
|
||||||
|
// And there is no image avatar (because it's not set on this room)
|
||||||
|
const image = findImg(rendered, ".mx_BaseAvatar_image");
|
||||||
|
expect(image.src).toEqual("data:image/png;base64,00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the user avatar in a DM with 2 people', () => {
|
||||||
|
// Note: this is the interesting case - this is the ONLY
|
||||||
|
// time we should use the user's avatar.
|
||||||
|
|
||||||
|
// When we render a DM room with only 2 people in it
|
||||||
|
const room = createRoom({ name: "Y Room", isDm: true, userIds: ["other"] });
|
||||||
|
const rendered = render(room);
|
||||||
|
|
||||||
|
// Then we use the other user's avatar as our room's image avatar
|
||||||
|
const image = findImg(rendered, ".mx_BaseAvatar_image");
|
||||||
|
expect(image.src).toEqual(
|
||||||
|
"http://this.is.a.url/example.org/other");
|
||||||
|
|
||||||
|
// And there is no initial avatar
|
||||||
|
expect(
|
||||||
|
rendered.querySelectorAll(".mx_BaseAvatar_initial"),
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the room avatar in a DM with >2 people', () => {
|
||||||
|
// When we render a DM room with 3 people in it
|
||||||
|
const room = createRoom({
|
||||||
|
name: "Z Room", isDm: true, userIds: ["other1", "other2"] });
|
||||||
|
const rendered = render(room);
|
||||||
|
|
||||||
|
// Then the room's avatar is the initial of its name
|
||||||
|
const initial = findSpan(rendered, ".mx_BaseAvatar_initial");
|
||||||
|
expect(initial.innerHTML).toEqual("Z");
|
||||||
|
|
||||||
|
// And there is no image avatar (because it's not set on this room)
|
||||||
|
const image = findImg(rendered, ".mx_BaseAvatar_image");
|
||||||
|
expect(image.src).toEqual("data:image/png;base64,00");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IRoomCreationInfo {
|
||||||
|
name: string;
|
||||||
|
isDm: boolean;
|
||||||
|
userIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRoom(info: IRoomCreationInfo) {
|
||||||
|
TestUtils.stubClient();
|
||||||
|
const client: MatrixClient = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
const roomId = '!1234567890:domain';
|
||||||
|
const userId = client.getUserId();
|
||||||
|
if (info.isDm) {
|
||||||
|
client.getAccountData = (eventType) => {
|
||||||
|
expect(eventType).toEqual("m.direct");
|
||||||
|
return mkDirectEvent(roomId, userId, info.userIds);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
DMRoomMap.makeShared().start();
|
||||||
|
|
||||||
|
const room = new Room(roomId, client, userId, {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherJoinEvents = [];
|
||||||
|
for (const otherUserId of info.userIds) {
|
||||||
|
otherJoinEvents.push(mkJoinEvent(roomId, otherUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
room.currentState.setStateEvents([
|
||||||
|
mkCreationEvent(roomId, userId),
|
||||||
|
mkNameEvent(roomId, userId, info.name),
|
||||||
|
mkJoinEvent(roomId, userId),
|
||||||
|
...otherJoinEvents,
|
||||||
|
]);
|
||||||
|
room.recalculate();
|
||||||
|
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(room: Room): HTMLDivElement {
|
||||||
|
const parentDiv = document.createElement('div');
|
||||||
|
document.body.appendChild(parentDiv);
|
||||||
|
ReactDOM.render(
|
||||||
|
(
|
||||||
|
<RoomHeader
|
||||||
|
room={room}
|
||||||
|
inRoom={true}
|
||||||
|
onSettingsClick={() => {}}
|
||||||
|
onSearchClick={() => {}}
|
||||||
|
onForgetClick={() => {}}
|
||||||
|
onCallPlaced={(_type: PlaceCallType) => {}}
|
||||||
|
onAppsClick={() => {}}
|
||||||
|
e2eStatus={E2EStatus.Normal}
|
||||||
|
appsShown={true}
|
||||||
|
searchInfo={{
|
||||||
|
searchTerm: "",
|
||||||
|
searchScope: SearchScope.Room,
|
||||||
|
searchCount: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
parentDiv,
|
||||||
|
);
|
||||||
|
return parentDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkCreationEvent(roomId: string, userId: string): MatrixEvent {
|
||||||
|
return mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.create",
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
content: {
|
||||||
|
creator: userId,
|
||||||
|
room_version: "5",
|
||||||
|
predecessor: {
|
||||||
|
room_id: "!prevroom",
|
||||||
|
event_id: "$someevent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkNameEvent(
|
||||||
|
roomId: string, userId: string, name: string,
|
||||||
|
): MatrixEvent {
|
||||||
|
return mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.name",
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
content: { name },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkJoinEvent(roomId: string, userId: string) {
|
||||||
|
const ret = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.member",
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
content: {
|
||||||
|
"membership": "join",
|
||||||
|
"avatar_url": "mxc://example.org/" + userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ret.event.state_key = userId;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkDirectEvent(
|
||||||
|
roomId: string, userId: string, otherUsers: string[],
|
||||||
|
): MatrixEvent {
|
||||||
|
const content = {};
|
||||||
|
for (const otherUserId of otherUsers) {
|
||||||
|
content[otherUserId] = [roomId];
|
||||||
|
}
|
||||||
|
return mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.direct",
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSpan(parent: HTMLElement, selector: string): HTMLSpanElement {
|
||||||
|
const els = parent.querySelectorAll(selector);
|
||||||
|
expect(els.length).toEqual(1);
|
||||||
|
return els[0] as HTMLSpanElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findImg(parent: HTMLElement, selector: string): HTMLImageElement {
|
||||||
|
const els = parent.querySelectorAll(selector);
|
||||||
|
expect(els.length).toEqual(1);
|
||||||
|
return els[0] as HTMLImageElement;
|
||||||
|
}
|
|
@ -77,6 +77,7 @@ describe("SpaceStore", () => {
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
|
client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
|
||||||
|
client.getRoomUpgradeHistory.mockImplementation(roomId => [rooms.find(room => room.roomId === roomId)]);
|
||||||
await testUtils.setupAsyncStoreWithClient(store, client);
|
await testUtils.setupAsyncStoreWithClient(store, client);
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,6 +47,8 @@ export function createTestClient() {
|
||||||
getIdentityServerUrl: jest.fn(),
|
getIdentityServerUrl: jest.fn(),
|
||||||
getDomain: jest.fn().mockReturnValue("matrix.rog"),
|
getDomain: jest.fn().mockReturnValue("matrix.rog"),
|
||||||
getUserId: jest.fn().mockReturnValue("@userId:matrix.rog"),
|
getUserId: jest.fn().mockReturnValue("@userId:matrix.rog"),
|
||||||
|
getUser: jest.fn().mockReturnValue({ on: jest.fn() }),
|
||||||
|
credentials: { userId: "@userId:matrix.rog" },
|
||||||
|
|
||||||
getPushActionsForEvent: jest.fn(),
|
getPushActionsForEvent: jest.fn(),
|
||||||
getRoom: jest.fn().mockImplementation(mkStubRoom),
|
getRoom: jest.fn().mockImplementation(mkStubRoom),
|
||||||
|
@ -76,7 +78,7 @@ export function createTestClient() {
|
||||||
content: {},
|
content: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
mxcUrlToHttp: (mxc) => 'http://this.is.a.url/',
|
mxcUrlToHttp: (mxc) => `http://this.is.a.url/${mxc.substring(6)}`,
|
||||||
setAccountData: jest.fn(),
|
setAccountData: jest.fn(),
|
||||||
setRoomAccountData: jest.fn(),
|
setRoomAccountData: jest.fn(),
|
||||||
sendTyping: jest.fn().mockResolvedValue({}),
|
sendTyping: jest.fn().mockResolvedValue({}),
|
||||||
|
@ -93,12 +95,15 @@ export function createTestClient() {
|
||||||
sessionStore: {
|
sessionStore: {
|
||||||
store: {
|
store: {
|
||||||
getItem: jest.fn(),
|
getItem: jest.fn(),
|
||||||
|
setItem: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pushRules: {},
|
pushRules: {},
|
||||||
decryptEventIfNeeded: () => Promise.resolve(),
|
decryptEventIfNeeded: () => Promise.resolve(),
|
||||||
isUserIgnored: jest.fn().mockReturnValue(false),
|
isUserIgnored: jest.fn().mockReturnValue(false),
|
||||||
getCapabilities: jest.fn().mockResolvedValue({}),
|
getCapabilities: jest.fn().mockResolvedValue({}),
|
||||||
|
supportsExperimentalThreads: () => false,
|
||||||
|
getRoomUpgradeHistory: jest.fn().mockReturnValue([]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,9 +135,11 @@ export function mkEvent(opts) {
|
||||||
};
|
};
|
||||||
if (opts.skey) {
|
if (opts.skey) {
|
||||||
event.state_key = opts.skey;
|
event.state_key = opts.skey;
|
||||||
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
} else if ([
|
||||||
"m.room.power_levels", "m.room.topic", "m.room.history_visibility", "m.room.encryption",
|
"m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
||||||
"com.example.state"].indexOf(opts.type) !== -1) {
|
"m.room.power_levels", "m.room.topic", "m.room.history_visibility",
|
||||||
|
"m.room.encryption", "m.room.member", "com.example.state",
|
||||||
|
].indexOf(opts.type) !== -1) {
|
||||||
event.state_key = "";
|
event.state_key = "";
|
||||||
}
|
}
|
||||||
return opts.event ? new MatrixEvent(event) : event;
|
return opts.event ? new MatrixEvent(event) : event;
|
||||||
|
|
Loading…
Reference in New Issue