Allow knocking rooms (#11353)
Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>
This commit is contained in:
parent
e6af09e424
commit
5152aad059
18 changed files with 689 additions and 7 deletions
|
@ -30,6 +30,7 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,3 +149,12 @@ a.mx_RoomPreviewBar_inviter {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomPreviewBar_icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomPreviewBar_fullWidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||||
import { HistoryVisibility } from "matrix-js-sdk/src/@types/partials";
|
import { HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
||||||
import { IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set";
|
import { IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set";
|
||||||
|
|
||||||
|
@ -125,6 +125,8 @@ import WidgetUtils from "../../utils/WidgetUtils";
|
||||||
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
|
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
|
||||||
import { WaitingForThirdPartyRoomView } from "./WaitingForThirdPartyRoomView";
|
import { WaitingForThirdPartyRoomView } from "./WaitingForThirdPartyRoomView";
|
||||||
import { isNotUndefined } from "../../Typeguards";
|
import { isNotUndefined } from "../../Typeguards";
|
||||||
|
import { CancelAskToJoinPayload } from "../../dispatcher/payloads/CancelAskToJoinPayload";
|
||||||
|
import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||||
|
@ -238,6 +240,10 @@ export interface IRoomState {
|
||||||
liveTimeline?: EventTimeline;
|
liveTimeline?: EventTimeline;
|
||||||
narrow: boolean;
|
narrow: boolean;
|
||||||
msc3946ProcessDynamicPredecessor: boolean;
|
msc3946ProcessDynamicPredecessor: boolean;
|
||||||
|
|
||||||
|
canAskToJoin: boolean;
|
||||||
|
promptAskToJoin: boolean;
|
||||||
|
knocked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LocalRoomViewProps {
|
interface LocalRoomViewProps {
|
||||||
|
@ -384,6 +390,7 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
|
private readonly askToJoinEnabled: boolean;
|
||||||
private readonly dispatcherRef: string;
|
private readonly dispatcherRef: string;
|
||||||
private settingWatchers: string[];
|
private settingWatchers: string[];
|
||||||
|
|
||||||
|
@ -401,6 +408,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
|
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
|
||||||
|
|
||||||
if (!context.client) {
|
if (!context.client) {
|
||||||
throw new Error("Unable to create RoomView without MatrixClient");
|
throw new Error("Unable to create RoomView without MatrixClient");
|
||||||
}
|
}
|
||||||
|
@ -445,6 +454,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
liveTimeline: undefined,
|
liveTimeline: undefined,
|
||||||
narrow: false,
|
narrow: false,
|
||||||
msc3946ProcessDynamicPredecessor: SettingsStore.getValue("feature_dynamic_room_predecessors"),
|
msc3946ProcessDynamicPredecessor: SettingsStore.getValue("feature_dynamic_room_predecessors"),
|
||||||
|
canAskToJoin: this.askToJoinEnabled,
|
||||||
|
promptAskToJoin: false,
|
||||||
|
knocked: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
@ -649,6 +661,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
)
|
)
|
||||||
: false,
|
: false,
|
||||||
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
|
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
|
||||||
|
promptAskToJoin: this.context.roomViewStore.promptAskToJoin(),
|
||||||
|
knocked: this.context.roomViewStore.knocked(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -891,6 +905,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
this.setState({
|
this.setState({
|
||||||
room: room,
|
room: room,
|
||||||
peekLoading: false,
|
peekLoading: false,
|
||||||
|
canAskToJoin: this.askToJoinEnabled && room.getJoinRule() === JoinRule.Knock,
|
||||||
});
|
});
|
||||||
this.onRoomLoaded(room);
|
this.onRoomLoaded(room);
|
||||||
})
|
})
|
||||||
|
@ -919,7 +934,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
} else if (room) {
|
} else if (room) {
|
||||||
// Stop peeking because we have joined this room previously
|
// Stop peeking because we have joined this room previously
|
||||||
this.context.client?.stopPeeking();
|
this.context.client?.stopPeeking();
|
||||||
this.setState({ isPeeking: false });
|
this.setState({
|
||||||
|
isPeeking: false,
|
||||||
|
canAskToJoin: this.askToJoinEnabled && room.getJoinRule() === JoinRule.Knock,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1593,6 +1611,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
roomId,
|
roomId,
|
||||||
opts: { inviteSignUrl: signUrl },
|
opts: { inviteSignUrl: signUrl },
|
||||||
metricsTrigger: this.state.room?.getMyMembership() === "invite" ? "Invite" : "RoomPreview",
|
metricsTrigger: this.state.room?.getMyMembership() === "invite" ? "Invite" : "RoomPreview",
|
||||||
|
canAskToJoin: this.state.canAskToJoin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1997,6 +2016,40 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the submission of a request to join a room.
|
||||||
|
*
|
||||||
|
* @param {string} reason - An optional reason for the request to join.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
private onSubmitAskToJoin = (reason?: string): void => {
|
||||||
|
const roomId = this.getRoomId();
|
||||||
|
|
||||||
|
if (isNotUndefined(roomId)) {
|
||||||
|
dis.dispatch<SubmitAskToJoinPayload>({
|
||||||
|
action: Action.SubmitAskToJoin,
|
||||||
|
roomId,
|
||||||
|
opts: { reason },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the cancellation of a request to join a room.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
private onCancelAskToJoin = (): void => {
|
||||||
|
const roomId = this.getRoomId();
|
||||||
|
|
||||||
|
if (isNotUndefined(roomId)) {
|
||||||
|
dis.dispatch<CancelAskToJoinPayload>({
|
||||||
|
action: Action.CancelAskToJoin,
|
||||||
|
roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
if (!this.context.client) return null;
|
if (!this.context.client) return null;
|
||||||
|
|
||||||
|
@ -2062,6 +2115,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
oobData={this.props.oobData}
|
oobData={this.props.oobData}
|
||||||
signUrl={this.props.threepidInvite?.signUrl}
|
signUrl={this.props.threepidInvite?.signUrl}
|
||||||
roomId={this.state.roomId}
|
roomId={this.state.roomId}
|
||||||
|
promptAskToJoin={this.state.promptAskToJoin}
|
||||||
|
knocked={this.state.knocked}
|
||||||
|
onSubmitAskToJoin={this.onSubmitAskToJoin}
|
||||||
|
onCancelAskToJoin={this.onCancelAskToJoin}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2136,6 +2193,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.canAskToJoin && ["knock", "leave"].includes(myMembership)) {
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomView">
|
||||||
|
<ErrorBoundary>
|
||||||
|
<RoomPreviewBar
|
||||||
|
room={this.state.room}
|
||||||
|
promptAskToJoin={myMembership === "leave" || this.state.promptAskToJoin}
|
||||||
|
knocked={myMembership === "knock" || this.state.knocked}
|
||||||
|
onSubmitAskToJoin={this.onSubmitAskToJoin}
|
||||||
|
onCancelAskToJoin={this.onCancelAskToJoin}
|
||||||
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// We have successfully loaded this room, and are not previewing.
|
// We have successfully loaded this room, and are not previewing.
|
||||||
// Display the "normal" room view.
|
// Display the "normal" room view.
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { ChangeEvent, ReactNode } from "react";
|
||||||
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
@ -35,6 +35,8 @@ import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
||||||
|
import { Icon as AskToJoinIcon } from "../../../../res/img/element-icons/ask-to-join.svg";
|
||||||
|
import Field from "../elements/Field";
|
||||||
|
|
||||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||||
|
|
||||||
|
@ -53,6 +55,8 @@ enum MessageCase {
|
||||||
ViewingRoom = "ViewingRoom",
|
ViewingRoom = "ViewingRoom",
|
||||||
RoomNotFound = "RoomNotFound",
|
RoomNotFound = "RoomNotFound",
|
||||||
OtherError = "OtherError",
|
OtherError = "OtherError",
|
||||||
|
PromptAskToJoin = "PromptAskToJoin",
|
||||||
|
Knocked = "Knocked",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -95,6 +99,11 @@ interface IProps {
|
||||||
onRejectClick?(): void;
|
onRejectClick?(): void;
|
||||||
onRejectAndIgnoreClick?(): void;
|
onRejectAndIgnoreClick?(): void;
|
||||||
onForgetClick?(): void;
|
onForgetClick?(): void;
|
||||||
|
|
||||||
|
promptAskToJoin?: boolean;
|
||||||
|
knocked?: boolean;
|
||||||
|
onSubmitAskToJoin?(reason?: string): void;
|
||||||
|
onCancelAskToJoin?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -102,6 +111,7 @@ interface IState {
|
||||||
accountEmails?: string[];
|
accountEmails?: string[];
|
||||||
invitedEmailMxid?: string;
|
invitedEmailMxid?: string;
|
||||||
threePidFetchError?: MatrixError;
|
threePidFetchError?: MatrixError;
|
||||||
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
|
@ -186,6 +196,10 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
return MessageCase.Rejecting;
|
return MessageCase.Rejecting;
|
||||||
} else if (this.props.loading || this.state.busy) {
|
} else if (this.props.loading || this.state.busy) {
|
||||||
return MessageCase.Loading;
|
return MessageCase.Loading;
|
||||||
|
} else if (this.props.knocked) {
|
||||||
|
return MessageCase.Knocked;
|
||||||
|
} else if (this.props.promptAskToJoin) {
|
||||||
|
return MessageCase.PromptAskToJoin;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.inviterName) {
|
if (this.props.inviterName) {
|
||||||
|
@ -281,6 +295,10 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
dis.dispatch({ action: "start_registration", screenAfterLogin: this.makeScreenAfterLogin() });
|
dis.dispatch({ action: "start_registration", screenAfterLogin: this.makeScreenAfterLogin() });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onChangeReason = (event: ChangeEvent<HTMLTextAreaElement>): void => {
|
||||||
|
this.setState({ reason: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
const roomName = this.props.room?.name ?? this.props.roomAlias ?? "";
|
const roomName = this.props.room?.name ?? this.props.roomAlias ?? "";
|
||||||
|
@ -581,6 +599,54 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case MessageCase.PromptAskToJoin: {
|
||||||
|
if (roomName) {
|
||||||
|
title = _t("Ask to join %(roomName)s?", { roomName });
|
||||||
|
} else {
|
||||||
|
title = _t("Ask to join?");
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
|
||||||
|
subTitle = [
|
||||||
|
avatar,
|
||||||
|
_t(
|
||||||
|
"You need to be granted access to this room in order to view or participate in the conversation. You can send a request to join below.",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
reasonElement = (
|
||||||
|
<Field
|
||||||
|
autoFocus
|
||||||
|
className="mx_RoomPreviewBar_fullWidth"
|
||||||
|
element="textarea"
|
||||||
|
onChange={this.onChangeReason}
|
||||||
|
placeholder={_t("Message (optional)")}
|
||||||
|
type="text"
|
||||||
|
value={this.state.reason ?? ""}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
primaryActionHandler = () =>
|
||||||
|
this.props.onSubmitAskToJoin && this.props.onSubmitAskToJoin(this.state.reason);
|
||||||
|
primaryActionLabel = _t("Request access");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MessageCase.Knocked: {
|
||||||
|
title = _t("Request to join sent");
|
||||||
|
|
||||||
|
subTitle = [
|
||||||
|
<>
|
||||||
|
<AskToJoinIcon className="mx_Icon mx_Icon_16 mx_RoomPreviewBar_icon" />
|
||||||
|
{_t("Your request to join is pending.")}
|
||||||
|
</>,
|
||||||
|
];
|
||||||
|
|
||||||
|
secondaryActionHandler = this.props.onCancelAskToJoin;
|
||||||
|
secondaryActionLabel = _t("Cancel request");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let subTitleElements;
|
let subTitleElements;
|
||||||
|
@ -650,7 +716,13 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
{subTitleElements}
|
{subTitleElements}
|
||||||
</div>
|
</div>
|
||||||
{reasonElement}
|
{reasonElement}
|
||||||
<div className="mx_RoomPreviewBar_actions">{actions}</div>
|
<div
|
||||||
|
className={classNames("mx_RoomPreviewBar_actions", {
|
||||||
|
mx_RoomPreviewBar_fullWidth: messageCase === MessageCase.PromptAskToJoin,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
<div className="mx_RoomPreviewBar_footer">{footer}</div>
|
<div className="mx_RoomPreviewBar_footer">{footer}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -71,6 +71,9 @@ const RoomContext = createContext<
|
||||||
narrow: false,
|
narrow: false,
|
||||||
activeCall: null,
|
activeCall: null,
|
||||||
msc3946ProcessDynamicPredecessor: false,
|
msc3946ProcessDynamicPredecessor: false,
|
||||||
|
canAskToJoin: false,
|
||||||
|
promptAskToJoin: false,
|
||||||
|
knocked: false,
|
||||||
});
|
});
|
||||||
RoomContext.displayName = "RoomContext";
|
RoomContext.displayName = "RoomContext";
|
||||||
export default RoomContext;
|
export default RoomContext;
|
||||||
|
|
|
@ -351,4 +351,19 @@ export enum Action {
|
||||||
* Fired when we want to view a thread, either a new one or an existing one
|
* Fired when we want to view a thread, either a new one or an existing one
|
||||||
*/
|
*/
|
||||||
ShowThread = "show_thread",
|
ShowThread = "show_thread",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when requesting to prompt for ask to join a room.
|
||||||
|
*/
|
||||||
|
PromptAskToJoin = "prompt_ask_to_join",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when requesting to submit an ask to join a room. Use with a SubmitAskToJoinPayload.
|
||||||
|
*/
|
||||||
|
SubmitAskToJoin = "submit_ask_to_join",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when requesting to cancel an ask to join a room. Use with a CancelAskToJoinPayload.
|
||||||
|
*/
|
||||||
|
CancelAskToJoin = "cancel_ask_to_join",
|
||||||
}
|
}
|
||||||
|
|
24
src/dispatcher/payloads/CancelAskToJoinPayload.ts
Normal file
24
src/dispatcher/payloads/CancelAskToJoinPayload.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 Nordeck IT + Consulting GmbH
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Action } from "../actions";
|
||||||
|
import { ActionPayload } from "../payloads";
|
||||||
|
|
||||||
|
export interface CancelAskToJoinPayload extends Pick<ActionPayload, "action"> {
|
||||||
|
action: Action.CancelAskToJoin;
|
||||||
|
|
||||||
|
roomId: string;
|
||||||
|
}
|
|
@ -24,4 +24,6 @@ export interface JoinRoomErrorPayload extends Pick<ActionPayload, "action"> {
|
||||||
|
|
||||||
roomId: string;
|
roomId: string;
|
||||||
err: MatrixError;
|
err: MatrixError;
|
||||||
|
|
||||||
|
canAskToJoin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,5 +29,7 @@ export interface JoinRoomPayload extends Pick<ActionPayload, "action"> {
|
||||||
|
|
||||||
// additional parameters for the purpose of metrics & instrumentation
|
// additional parameters for the purpose of metrics & instrumentation
|
||||||
metricsTrigger: JoinedRoomEvent["trigger"];
|
metricsTrigger: JoinedRoomEvent["trigger"];
|
||||||
|
|
||||||
|
canAskToJoin?: boolean;
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
27
src/dispatcher/payloads/SubmitAskToJoinPayload.ts
Normal file
27
src/dispatcher/payloads/SubmitAskToJoinPayload.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 Nordeck IT + Consulting GmbH
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { KnockRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||||
|
|
||||||
|
import { Action } from "../actions";
|
||||||
|
import { ActionPayload } from "../payloads";
|
||||||
|
|
||||||
|
export interface SubmitAskToJoinPayload extends Pick<ActionPayload, "action"> {
|
||||||
|
action: Action.SubmitAskToJoin;
|
||||||
|
|
||||||
|
roomId: string;
|
||||||
|
opts?: KnockRoomOpts;
|
||||||
|
}
|
|
@ -893,6 +893,8 @@
|
||||||
"You attempted to join using a room ID without providing a list of servers to join through. Room IDs are internal identifiers and cannot be used to join a room without additional information.": "You attempted to join using a room ID without providing a list of servers to join through. Room IDs are internal identifiers and cannot be used to join a room without additional information.",
|
"You attempted to join using a room ID without providing a list of servers to join through. Room IDs are internal identifiers and cannot be used to join a room without additional information.": "You attempted to join using a room ID without providing a list of servers to join through. Room IDs are internal identifiers and cannot be used to join a room without additional information.",
|
||||||
"If you know a room address, try joining through that instead.": "If you know a room address, try joining through that instead.",
|
"If you know a room address, try joining through that instead.": "If you know a room address, try joining through that instead.",
|
||||||
"Failed to join": "Failed to join",
|
"Failed to join": "Failed to join",
|
||||||
|
"You need an invite to access this room.": "You need an invite to access this room.",
|
||||||
|
"Failed to cancel": "Failed to cancel",
|
||||||
"Connection lost": "Connection lost",
|
"Connection lost": "Connection lost",
|
||||||
"You were disconnected from the call. (Error: %(message)s)": "You were disconnected from the call. (Error: %(message)s)",
|
"You were disconnected from the call. (Error: %(message)s)": "You were disconnected from the call. (Error: %(message)s)",
|
||||||
"All rooms": "All rooms",
|
"All rooms": "All rooms",
|
||||||
|
@ -2124,6 +2126,14 @@
|
||||||
"This room or space is not accessible at this time.": "This room or space is not accessible at this time.",
|
"This room or space is not accessible at this time.": "This room or space is not accessible at this time.",
|
||||||
"Try again later, or ask a room or space admin to check if you have access.": "Try again later, or ask a room or space admin to check if you have access.",
|
"Try again later, or ask a room or space admin to check if you have access.": "Try again later, or ask a room or space admin to check if you have access.",
|
||||||
"%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.",
|
"%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.",
|
||||||
|
"Ask to join %(roomName)s?": "Ask to join %(roomName)s?",
|
||||||
|
"Ask to join?": "Ask to join?",
|
||||||
|
"You need to be granted access to this room in order to view or participate in the conversation. You can send a request to join below.": "You need to be granted access to this room in order to view or participate in the conversation. You can send a request to join below.",
|
||||||
|
"Message (optional)": "Message (optional)",
|
||||||
|
"Request access": "Request access",
|
||||||
|
"Request to join sent": "Request to join sent",
|
||||||
|
"Your request to join is pending.": "Your request to join is pending.",
|
||||||
|
"Cancel request": "Cancel request",
|
||||||
"Leave": "Leave",
|
"Leave": "Leave",
|
||||||
"<inviter/> invites you": "<inviter/> invites you",
|
"<inviter/> invites you": "<inviter/> invites you",
|
||||||
"To view %(roomName)s, you need an invite": "To view %(roomName)s, you need an invite",
|
"To view %(roomName)s, you need an invite": "To view %(roomName)s, you need an invite",
|
||||||
|
|
|
@ -61,6 +61,8 @@ import { IRoomStateEventsActionPayload } from "../actions/MatrixActionCreators";
|
||||||
import { showCantStartACallDialog } from "../voice-broadcast/utils/showCantStartACallDialog";
|
import { showCantStartACallDialog } from "../voice-broadcast/utils/showCantStartACallDialog";
|
||||||
import { pauseNonLiveBroadcastFromOtherRoom } from "../voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom";
|
import { pauseNonLiveBroadcastFromOtherRoom } from "../voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom";
|
||||||
import { ActionPayload } from "../dispatcher/payloads";
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
|
import { CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJoinPayload";
|
||||||
|
import { SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload";
|
||||||
|
|
||||||
const NUM_JOIN_RETRY = 5;
|
const NUM_JOIN_RETRY = 5;
|
||||||
|
|
||||||
|
@ -118,6 +120,9 @@ interface State {
|
||||||
* Whether we're viewing a call or call lobby in this room
|
* Whether we're viewing a call or call lobby in this room
|
||||||
*/
|
*/
|
||||||
viewingCall: boolean;
|
viewingCall: boolean;
|
||||||
|
|
||||||
|
promptAskToJoin: boolean;
|
||||||
|
knocked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_STATE: State = {
|
const INITIAL_STATE: State = {
|
||||||
|
@ -138,6 +143,8 @@ const INITIAL_STATE: State = {
|
||||||
viaServers: [],
|
viaServers: [],
|
||||||
wasContextSwitch: false,
|
wasContextSwitch: false,
|
||||||
viewingCall: false,
|
viewingCall: false,
|
||||||
|
promptAskToJoin: false,
|
||||||
|
knocked: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Listener = (isActive: boolean) => void;
|
type Listener = (isActive: boolean) => void;
|
||||||
|
@ -356,6 +363,18 @@ export class RoomViewStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Action.PromptAskToJoin: {
|
||||||
|
this.setState({ promptAskToJoin: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Action.SubmitAskToJoin: {
|
||||||
|
this.submitAskToJoin(payload as SubmitAskToJoinPayload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Action.CancelAskToJoin: {
|
||||||
|
this.cancelAskToJoin(payload as CancelAskToJoinPayload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,7 +582,12 @@ export class RoomViewStore extends EventEmitter {
|
||||||
action: Action.JoinRoomError,
|
action: Action.JoinRoomError,
|
||||||
roomId,
|
roomId,
|
||||||
err,
|
err,
|
||||||
|
canAskToJoin: payload.canAskToJoin,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (payload.canAskToJoin) {
|
||||||
|
this.dis?.dispatch({ action: Action.PromptAskToJoin });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -632,7 +656,7 @@ export class RoomViewStore extends EventEmitter {
|
||||||
joining: false,
|
joining: false,
|
||||||
joinError: payload.err,
|
joinError: payload.err,
|
||||||
});
|
});
|
||||||
if (payload.err) {
|
if (payload.err && !payload.canAskToJoin) {
|
||||||
this.showJoinRoomError(payload.err, payload.roomId);
|
this.showJoinRoomError(payload.err, payload.roomId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -746,4 +770,57 @@ export class RoomViewStore extends EventEmitter {
|
||||||
public isViewingCall(): boolean {
|
public isViewingCall(): boolean {
|
||||||
return this.state.viewingCall;
|
return this.state.viewingCall;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current state of the 'promptForAskToJoin' property.
|
||||||
|
*
|
||||||
|
* @returns {boolean} The value of the 'promptForAskToJoin' property.
|
||||||
|
*/
|
||||||
|
public promptAskToJoin(): boolean {
|
||||||
|
return this.state.promptAskToJoin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current state of the 'knocked' property.
|
||||||
|
*
|
||||||
|
* @returns {boolean} The value of the 'knocked' property.
|
||||||
|
*/
|
||||||
|
public knocked(): boolean {
|
||||||
|
return this.state.knocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits a request to join a room by sending a knock request.
|
||||||
|
*
|
||||||
|
* @param {SubmitAskToJoinPayload} payload - The payload containing information to submit the request.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
private submitAskToJoin(payload: SubmitAskToJoinPayload): void {
|
||||||
|
MatrixClientPeg.safeGet()
|
||||||
|
.knockRoom(payload.roomId, { viaServers: this.state.viaServers, ...payload.opts })
|
||||||
|
.then(() => this.setState({ promptAskToJoin: false, knocked: true }))
|
||||||
|
.catch((err: MatrixError) => {
|
||||||
|
this.setState({ promptAskToJoin: false });
|
||||||
|
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: _t("Failed to join"),
|
||||||
|
description: err.httpStatus === 403 ? _t("You need an invite to access this room.") : err.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels a request to join a room by sending a leave request.
|
||||||
|
*
|
||||||
|
* @param {CancelAskToJoinPayload} payload - The payload containing information to cancel the request.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
private cancelAskToJoin(payload: CancelAskToJoinPayload): void {
|
||||||
|
MatrixClientPeg.safeGet()
|
||||||
|
.leave(payload.roomId)
|
||||||
|
.then(() => this.setState({ knocked: false }))
|
||||||
|
.catch((err: MatrixError) =>
|
||||||
|
Modal.createDialog(ErrorDialog, { title: _t("Failed to cancel"), description: err.message }),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React, { createRef, RefObject } from "react";
|
import React, { createRef, RefObject } from "react";
|
||||||
import { mocked, MockedObject } from "jest-mock";
|
import { mocked, MockedObject } from "jest-mock";
|
||||||
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { Room, RoomEvent, EventType, MatrixError, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
import { Room, RoomEvent, EventType, JoinRule, MatrixError, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib";
|
import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib";
|
||||||
import { fireEvent, render, screen, RenderResult } from "@testing-library/react";
|
import { fireEvent, render, screen, RenderResult } from "@testing-library/react";
|
||||||
|
@ -34,10 +34,12 @@ import {
|
||||||
mkRoomMemberJoinEvent,
|
mkRoomMemberJoinEvent,
|
||||||
mkThirdPartyInviteEvent,
|
mkThirdPartyInviteEvent,
|
||||||
emitPromise,
|
emitPromise,
|
||||||
|
createTestClient,
|
||||||
|
untilDispatch,
|
||||||
} from "../../test-utils";
|
} from "../../test-utils";
|
||||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||||
import { Action } from "../../../src/dispatcher/actions";
|
import { Action } from "../../../src/dispatcher/actions";
|
||||||
import { defaultDispatcher } from "../../../src/dispatcher/dispatcher";
|
import dis, { defaultDispatcher } from "../../../src/dispatcher/dispatcher";
|
||||||
import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
|
||||||
import { RoomView as _RoomView } from "../../../src/components/structures/RoomView";
|
import { RoomView as _RoomView } from "../../../src/components/structures/RoomView";
|
||||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||||
|
@ -543,4 +545,41 @@ describe("RoomView", () => {
|
||||||
expect(screen.queryByLabelText("Forget room")).not.toBeInTheDocument();
|
expect(screen.queryByLabelText("Forget room")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("knock rooms", () => {
|
||||||
|
const client = createTestClient();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
|
||||||
|
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||||
|
jest.spyOn(dis, "dispatch");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows to request to join", async () => {
|
||||||
|
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
|
||||||
|
jest.spyOn(client, "knockRoom").mockResolvedValue({ room_id: room.roomId });
|
||||||
|
|
||||||
|
await mountRoomView();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Request access" }));
|
||||||
|
await untilDispatch(Action.SubmitAskToJoin, dis);
|
||||||
|
|
||||||
|
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: "submit_ask_to_join",
|
||||||
|
roomId: room.roomId,
|
||||||
|
opts: { reason: undefined },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows to cancel a join request", async () => {
|
||||||
|
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
|
||||||
|
jest.spyOn(client, "leave").mockResolvedValue({});
|
||||||
|
jest.spyOn(room, "getMyMembership").mockReturnValue("knock");
|
||||||
|
|
||||||
|
await mountRoomView();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Cancel request" }));
|
||||||
|
await untilDispatch(Action.CancelAskToJoin, dis);
|
||||||
|
|
||||||
|
expect(dis.dispatch).toHaveBeenCalledWith({ action: "cancel_ask_to_join", roomId: room.roomId });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -425,4 +425,60 @@ describe("<RoomPreviewBar />", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("message case AskToJoin", () => {
|
||||||
|
it("renders the corresponding message", () => {
|
||||||
|
const component = getComponent({ promptAskToJoin: true });
|
||||||
|
expect(getMessage(component)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the corresponding message with a generic title", () => {
|
||||||
|
const component = render(<RoomPreviewBar promptAskToJoin />);
|
||||||
|
expect(getMessage(component)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the corresponding actions", () => {
|
||||||
|
const component = getComponent({ promptAskToJoin: true });
|
||||||
|
expect(getActions(component)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers the primary action callback", () => {
|
||||||
|
const onSubmitAskToJoin = jest.fn();
|
||||||
|
const component = getComponent({ promptAskToJoin: true, onSubmitAskToJoin });
|
||||||
|
|
||||||
|
fireEvent.click(getPrimaryActionButton(component)!);
|
||||||
|
expect(onSubmitAskToJoin).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers the primary action callback with a reason", () => {
|
||||||
|
const onSubmitAskToJoin = jest.fn();
|
||||||
|
const reason = "some reason";
|
||||||
|
const component = getComponent({ promptAskToJoin: true, onSubmitAskToJoin });
|
||||||
|
|
||||||
|
fireEvent.change(component.container.querySelector("textarea")!, { target: { value: reason } });
|
||||||
|
fireEvent.click(getPrimaryActionButton(component)!);
|
||||||
|
|
||||||
|
expect(onSubmitAskToJoin).toHaveBeenCalledWith(reason);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("message case Knocked", () => {
|
||||||
|
it("renders the corresponding message", () => {
|
||||||
|
const component = getComponent({ knocked: true });
|
||||||
|
expect(getMessage(component)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the corresponding actions", () => {
|
||||||
|
const component = getComponent({ knocked: true, onCancelAskToJoin: () => {} });
|
||||||
|
expect(getActions(component)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers the secondary action callback", () => {
|
||||||
|
const onCancelAskToJoin = jest.fn();
|
||||||
|
const component = getComponent({ knocked: true, onCancelAskToJoin });
|
||||||
|
|
||||||
|
fireEvent.click(getSecondaryActionButton(component)!);
|
||||||
|
expect(onCancelAskToJoin).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -83,6 +83,9 @@ describe("<SendMessageComposer/>", () => {
|
||||||
narrow: false,
|
narrow: false,
|
||||||
activeCall: null,
|
activeCall: null,
|
||||||
msc3946ProcessDynamicPredecessor: false,
|
msc3946ProcessDynamicPredecessor: false,
|
||||||
|
canAskToJoin: false,
|
||||||
|
promptAskToJoin: false,
|
||||||
|
knocked: false,
|
||||||
};
|
};
|
||||||
describe("createMessageContent", () => {
|
describe("createMessageContent", () => {
|
||||||
const permalinkCreator = jest.fn() as any;
|
const permalinkCreator = jest.fn() as any;
|
||||||
|
|
|
@ -1,5 +1,121 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<RoomPreviewBar /> message case AskToJoin renders the corresponding actions 1`] = `
|
||||||
|
<div
|
||||||
|
class="mx_RoomPreviewBar_actions mx_RoomPreviewBar_fullWidth"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Request access
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RoomPreviewBar /> message case AskToJoin renders the corresponding message 1`] = `
|
||||||
|
<div
|
||||||
|
class="mx_RoomPreviewBar_message"
|
||||||
|
>
|
||||||
|
<h3>
|
||||||
|
Ask to join RoomPreviewBar-test-room?
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
|
||||||
|
>
|
||||||
|
R
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
loading="lazy"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 36px; height: 36px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You need to be granted access to this room in order to view or participate in the conversation. You can send a request to join below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RoomPreviewBar /> message case AskToJoin renders the corresponding message with a generic title 1`] = `
|
||||||
|
<div
|
||||||
|
class="mx_RoomPreviewBar_message"
|
||||||
|
>
|
||||||
|
<h3>
|
||||||
|
Ask to join?
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
loading="lazy"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 36px; height: 36px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You need to be granted access to this room in order to view or participate in the conversation. You can send a request to join below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RoomPreviewBar /> message case Knocked renders the corresponding actions 1`] = `
|
||||||
|
<div
|
||||||
|
class="mx_RoomPreviewBar_actions"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel request
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RoomPreviewBar /> message case Knocked renders the corresponding message 1`] = `
|
||||||
|
<div
|
||||||
|
class="mx_RoomPreviewBar_message"
|
||||||
|
>
|
||||||
|
<h3>
|
||||||
|
Request to join sent
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
<div
|
||||||
|
class="mx_Icon mx_Icon_16 mx_RoomPreviewBar_icon"
|
||||||
|
/>
|
||||||
|
Your request to join is pending.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`<RoomPreviewBar /> renders banned message 1`] = `
|
exports[`<RoomPreviewBar /> renders banned message 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_RoomPreviewBar_message"
|
class="mx_RoomPreviewBar_message"
|
||||||
|
|
|
@ -39,6 +39,10 @@ import {
|
||||||
} from "../../src/voice-broadcast";
|
} from "../../src/voice-broadcast";
|
||||||
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
|
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
|
||||||
import Modal from "../../src/Modal";
|
import Modal from "../../src/Modal";
|
||||||
|
import ErrorDialog from "../../src/components/views/dialogs/ErrorDialog";
|
||||||
|
import { CancelAskToJoinPayload } from "../../src/dispatcher/payloads/CancelAskToJoinPayload";
|
||||||
|
import { JoinRoomErrorPayload } from "../../src/dispatcher/payloads/JoinRoomErrorPayload";
|
||||||
|
import { SubmitAskToJoinPayload } from "../../src/dispatcher/payloads/SubmitAskToJoinPayload";
|
||||||
|
|
||||||
jest.mock("../../src/Modal");
|
jest.mock("../../src/Modal");
|
||||||
|
|
||||||
|
@ -97,6 +101,8 @@ describe("RoomViewStore", function () {
|
||||||
supportsThreads: jest.fn(),
|
supportsThreads: jest.fn(),
|
||||||
isInitialSyncComplete: jest.fn().mockResolvedValue(false),
|
isInitialSyncComplete: jest.fn().mockResolvedValue(false),
|
||||||
relations: jest.fn(),
|
relations: jest.fn(),
|
||||||
|
knockRoom: jest.fn(),
|
||||||
|
leave: jest.fn(),
|
||||||
});
|
});
|
||||||
const room = new Room(roomId, mockClient, userId);
|
const room = new Room(roomId, mockClient, userId);
|
||||||
const room2 = new Room(roomId2, mockClient, userId);
|
const room2 = new Room(roomId2, mockClient, userId);
|
||||||
|
@ -111,6 +117,21 @@ describe("RoomViewStore", function () {
|
||||||
await untilDispatch(Action.ViewRoom, dis);
|
await untilDispatch(Action.ViewRoom, dis);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dispatchPromptAskToJoin = async () => {
|
||||||
|
dis.dispatch({ action: Action.PromptAskToJoin });
|
||||||
|
await untilDispatch(Action.PromptAskToJoin, dis);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatchSubmitAskToJoin = async (roomId: string, reason?: string) => {
|
||||||
|
dis.dispatch<SubmitAskToJoinPayload>({ action: Action.SubmitAskToJoin, roomId, opts: { reason } });
|
||||||
|
await untilDispatch(Action.SubmitAskToJoin, dis);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatchCancelAskToJoin = async (roomId: string) => {
|
||||||
|
dis.dispatch<CancelAskToJoinPayload>({ action: Action.CancelAskToJoin, roomId });
|
||||||
|
await untilDispatch(Action.CancelAskToJoin, dis);
|
||||||
|
};
|
||||||
|
|
||||||
let roomViewStore: RoomViewStore;
|
let roomViewStore: RoomViewStore;
|
||||||
let slidingSyncManager: SlidingSyncManager;
|
let slidingSyncManager: SlidingSyncManager;
|
||||||
let dis: MatrixDispatcher;
|
let dis: MatrixDispatcher;
|
||||||
|
@ -436,4 +457,131 @@ describe("RoomViewStore", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Action.JoinRoom", () => {
|
||||||
|
it("dispatches Action.JoinRoomError and Action.AskToJoin when the join fails", async () => {
|
||||||
|
const err = new MatrixError();
|
||||||
|
|
||||||
|
jest.spyOn(dis, "dispatch");
|
||||||
|
jest.spyOn(mockClient, "joinRoom").mockRejectedValueOnce(err);
|
||||||
|
|
||||||
|
dis.dispatch({ action: Action.JoinRoom, canAskToJoin: true });
|
||||||
|
await untilDispatch(Action.PromptAskToJoin, dis);
|
||||||
|
|
||||||
|
expect(mocked(dis.dispatch).mock.calls[0][0]).toEqual({ action: "join_room", canAskToJoin: true });
|
||||||
|
expect(mocked(dis.dispatch).mock.calls[1][0]).toEqual({
|
||||||
|
action: "join_room_error",
|
||||||
|
roomId: null,
|
||||||
|
err,
|
||||||
|
canAskToJoin: true,
|
||||||
|
});
|
||||||
|
expect(mocked(dis.dispatch).mock.calls[2][0]).toEqual({ action: "prompt_ask_to_join" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Action.JoinRoomError", () => {
|
||||||
|
const err = new MatrixError();
|
||||||
|
beforeEach(() => jest.spyOn(roomViewStore, "showJoinRoomError"));
|
||||||
|
|
||||||
|
it("calls showJoinRoomError()", async () => {
|
||||||
|
dis.dispatch<JoinRoomErrorPayload>({ action: Action.JoinRoomError, roomId, err });
|
||||||
|
await untilDispatch(Action.JoinRoomError, dis);
|
||||||
|
expect(roomViewStore.showJoinRoomError).toHaveBeenCalledWith(err, roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call showJoinRoomError() when canAskToJoin is true", async () => {
|
||||||
|
dis.dispatch<JoinRoomErrorPayload>({ action: Action.JoinRoomError, roomId, err, canAskToJoin: true });
|
||||||
|
await untilDispatch(Action.JoinRoomError, dis);
|
||||||
|
expect(roomViewStore.showJoinRoomError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("askToJoin()", () => {
|
||||||
|
it("returns false", () => {
|
||||||
|
expect(roomViewStore.promptAskToJoin()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true", async () => {
|
||||||
|
await dispatchPromptAskToJoin();
|
||||||
|
expect(roomViewStore.promptAskToJoin()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("knocked()", () => {
|
||||||
|
it("returns false", () => {
|
||||||
|
expect(roomViewStore.knocked()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true", async () => {
|
||||||
|
jest.spyOn(mockClient, "knockRoom").mockResolvedValue({ room_id: roomId });
|
||||||
|
await dispatchSubmitAskToJoin(roomId);
|
||||||
|
expect(roomViewStore.knocked()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Action.SubmitAskToJoin", () => {
|
||||||
|
const reason = "some reason";
|
||||||
|
beforeEach(async () => await dispatchPromptAskToJoin());
|
||||||
|
|
||||||
|
it("calls knockRoom(), sets askToJoin state to false and knocked state to true", async () => {
|
||||||
|
jest.spyOn(mockClient, "knockRoom").mockResolvedValue({ room_id: roomId });
|
||||||
|
await dispatchSubmitAskToJoin(roomId, reason);
|
||||||
|
|
||||||
|
expect(mockClient.knockRoom).toHaveBeenCalledWith(roomId, { reason, viaServers: [] });
|
||||||
|
expect(roomViewStore.promptAskToJoin()).toBe(false);
|
||||||
|
expect(roomViewStore.knocked()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls knockRoom(), sets askToJoin to false, keeps knocked state false and shows an error dialog", async () => {
|
||||||
|
const error = new MatrixError(undefined, 403);
|
||||||
|
jest.spyOn(mockClient, "knockRoom").mockRejectedValue(error);
|
||||||
|
await dispatchSubmitAskToJoin(roomId, reason);
|
||||||
|
|
||||||
|
expect(mockClient.knockRoom).toHaveBeenCalledWith(roomId, { reason, viaServers: [] });
|
||||||
|
expect(roomViewStore.promptAskToJoin()).toBe(false);
|
||||||
|
expect(roomViewStore.knocked()).toBe(false);
|
||||||
|
expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, {
|
||||||
|
description: "You need an invite to access this room.",
|
||||||
|
title: "Failed to join",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an error dialog with a generic error message", async () => {
|
||||||
|
const error = new MatrixError();
|
||||||
|
jest.spyOn(mockClient, "knockRoom").mockRejectedValue(error);
|
||||||
|
await dispatchSubmitAskToJoin(roomId);
|
||||||
|
expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, {
|
||||||
|
description: error.message,
|
||||||
|
title: "Failed to join",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Action.CancelAskToJoin", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.spyOn(mockClient, "knockRoom").mockResolvedValue({ room_id: roomId });
|
||||||
|
await dispatchSubmitAskToJoin(roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls leave() and sets knocked state to false", async () => {
|
||||||
|
jest.spyOn(mockClient, "leave").mockResolvedValue({});
|
||||||
|
await dispatchCancelAskToJoin(roomId);
|
||||||
|
|
||||||
|
expect(mockClient.leave).toHaveBeenCalledWith(roomId);
|
||||||
|
expect(roomViewStore.knocked()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls leave(), keeps knocked state true and shows an error dialog", async () => {
|
||||||
|
const error = new MatrixError();
|
||||||
|
jest.spyOn(mockClient, "leave").mockRejectedValue(error);
|
||||||
|
await dispatchCancelAskToJoin(roomId);
|
||||||
|
|
||||||
|
expect(mockClient.leave).toHaveBeenCalledWith(roomId);
|
||||||
|
expect(roomViewStore.knocked()).toBe(true);
|
||||||
|
expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, {
|
||||||
|
description: error.message,
|
||||||
|
title: "Failed to cancel",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -88,6 +88,9 @@ export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoom
|
||||||
narrow: false,
|
narrow: false,
|
||||||
activeCall: null,
|
activeCall: null,
|
||||||
msc3946ProcessDynamicPredecessor: false,
|
msc3946ProcessDynamicPredecessor: false,
|
||||||
|
canAskToJoin: false,
|
||||||
|
promptAskToJoin: false,
|
||||||
|
knocked: false,
|
||||||
|
|
||||||
...override,
|
...override,
|
||||||
};
|
};
|
||||||
|
|
|
@ -242,6 +242,8 @@ export function createTestClient(): MatrixClient {
|
||||||
getSyncStateData: jest.fn(),
|
getSyncStateData: jest.fn(),
|
||||||
getDehydratedDevice: jest.fn(),
|
getDehydratedDevice: jest.fn(),
|
||||||
exportRoomKeys: jest.fn(),
|
exportRoomKeys: jest.fn(),
|
||||||
|
knockRoom: jest.fn(),
|
||||||
|
leave: jest.fn(),
|
||||||
} as unknown as MatrixClient;
|
} as unknown as MatrixClient;
|
||||||
|
|
||||||
client.reEmitter = new ReEmitter(client);
|
client.reEmitter = new ReEmitter(client);
|
||||||
|
|
Loading…
Reference in a new issue