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
|
@ -37,7 +37,7 @@ import { MatrixError } from "matrix-js-sdk/src/http-api";
|
|||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
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 { 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 { WaitingForThirdPartyRoomView } from "./WaitingForThirdPartyRoomView";
|
||||
import { isNotUndefined } from "../../Typeguards";
|
||||
import { CancelAskToJoinPayload } from "../../dispatcher/payloads/CancelAskToJoinPayload";
|
||||
import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload";
|
||||
|
||||
const DEBUG = false;
|
||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||
|
@ -238,6 +240,10 @@ export interface IRoomState {
|
|||
liveTimeline?: EventTimeline;
|
||||
narrow: boolean;
|
||||
msc3946ProcessDynamicPredecessor: boolean;
|
||||
|
||||
canAskToJoin: boolean;
|
||||
promptAskToJoin: boolean;
|
||||
knocked: boolean;
|
||||
}
|
||||
|
||||
interface LocalRoomViewProps {
|
||||
|
@ -384,6 +390,7 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
|
|||
}
|
||||
|
||||
export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
private readonly askToJoinEnabled: boolean;
|
||||
private readonly dispatcherRef: 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>) {
|
||||
super(props, context);
|
||||
|
||||
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
|
||||
|
||||
if (!context.client) {
|
||||
throw new Error("Unable to create RoomView without MatrixClient");
|
||||
}
|
||||
|
@ -445,6 +454,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
liveTimeline: undefined,
|
||||
narrow: false,
|
||||
msc3946ProcessDynamicPredecessor: SettingsStore.getValue("feature_dynamic_room_predecessors"),
|
||||
canAskToJoin: this.askToJoinEnabled,
|
||||
promptAskToJoin: false,
|
||||
knocked: false,
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -649,6 +661,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
)
|
||||
: false,
|
||||
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
|
||||
promptAskToJoin: this.context.roomViewStore.promptAskToJoin(),
|
||||
knocked: this.context.roomViewStore.knocked(),
|
||||
};
|
||||
|
||||
if (
|
||||
|
@ -891,6 +905,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.setState({
|
||||
room: room,
|
||||
peekLoading: false,
|
||||
canAskToJoin: this.askToJoinEnabled && room.getJoinRule() === JoinRule.Knock,
|
||||
});
|
||||
this.onRoomLoaded(room);
|
||||
})
|
||||
|
@ -919,7 +934,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
} else if (room) {
|
||||
// Stop peeking because we have joined this room previously
|
||||
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,
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
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 {
|
||||
if (!this.context.client) return null;
|
||||
|
||||
|
@ -2062,6 +2115,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
oobData={this.props.oobData}
|
||||
signUrl={this.props.threepidInvite?.signUrl}
|
||||
roomId={this.state.roomId}
|
||||
promptAskToJoin={this.state.promptAskToJoin}
|
||||
knocked={this.state.knocked}
|
||||
onSubmitAskToJoin={this.onSubmitAskToJoin}
|
||||
onCancelAskToJoin={this.onCancelAskToJoin}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</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.
|
||||
// Display the "normal" room view.
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
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 { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
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 { UIFeature } from "../../../settings/UIFeature";
|
||||
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";
|
||||
|
||||
|
@ -53,6 +55,8 @@ enum MessageCase {
|
|||
ViewingRoom = "ViewingRoom",
|
||||
RoomNotFound = "RoomNotFound",
|
||||
OtherError = "OtherError",
|
||||
PromptAskToJoin = "PromptAskToJoin",
|
||||
Knocked = "Knocked",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
|
@ -95,6 +99,11 @@ interface IProps {
|
|||
onRejectClick?(): void;
|
||||
onRejectAndIgnoreClick?(): void;
|
||||
onForgetClick?(): void;
|
||||
|
||||
promptAskToJoin?: boolean;
|
||||
knocked?: boolean;
|
||||
onSubmitAskToJoin?(reason?: string): void;
|
||||
onCancelAskToJoin?(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -102,6 +111,7 @@ interface IState {
|
|||
accountEmails?: string[];
|
||||
invitedEmailMxid?: string;
|
||||
threePidFetchError?: MatrixError;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
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;
|
||||
} else if (this.props.loading || this.state.busy) {
|
||||
return MessageCase.Loading;
|
||||
} else if (this.props.knocked) {
|
||||
return MessageCase.Knocked;
|
||||
} else if (this.props.promptAskToJoin) {
|
||||
return MessageCase.PromptAskToJoin;
|
||||
}
|
||||
|
||||
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() });
|
||||
};
|
||||
|
||||
private onChangeReason = (event: ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
this.setState({ reason: event.target.value });
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const roomName = this.props.room?.name ?? this.props.roomAlias ?? "";
|
||||
|
@ -581,6 +599,54 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
];
|
||||
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;
|
||||
|
@ -650,7 +716,13 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
{subTitleElements}
|
||||
</div>
|
||||
{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>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue