Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Allow knocking rooms #11353

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions res/css/views/rooms/_RoomPreviewBar.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ limitations under the License.
display: flex;
flex-direction: row;
align-items: center;
margin: 0;
}
}

Expand Down Expand Up @@ -148,3 +149,12 @@ a.mx_RoomPreviewBar_inviter {
text-decoration: underline;
cursor: pointer;
}

.mx_RoomPreviewBar_icon {
margin-right: 8px;
vertical-align: text-top;
}

.mx_RoomPreviewBar_fullWidth {
width: 100%;
}
77 changes: 75 additions & 2 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -238,6 +240,10 @@ export interface IRoomState {
liveTimeline?: EventTimeline;
narrow: boolean;
msc3946ProcessDynamicPredecessor: boolean;

canAskToJoin: boolean;
promptAskToJoin: boolean;
knocked: boolean;
}

interface LocalRoomViewProps {
Expand Down Expand Up @@ -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[];

Expand All @@ -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");
germain-gg marked this conversation as resolved.
Show resolved Hide resolved

if (!context.client) {
throw new Error("Unable to create RoomView without MatrixClient");
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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);
})
Expand Down Expand Up @@ -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,
});
}
}
}
Expand Down Expand Up @@ -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,
});
}

Expand Down Expand Up @@ -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 => {
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
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;

Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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.

Expand Down
76 changes: 74 additions & 2 deletions src/components/views/rooms/RoomPreviewBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";

Expand All @@ -53,6 +55,8 @@ enum MessageCase {
ViewingRoom = "ViewingRoom",
RoomNotFound = "RoomNotFound",
OtherError = "OtherError",
PromptAskToJoin = "PromptAskToJoin",
Knocked = "Knocked",
}

interface IProps {
Expand Down Expand Up @@ -95,13 +99,19 @@ interface IProps {
onRejectClick?(): void;
onRejectAndIgnoreClick?(): void;
onForgetClick?(): void;

promptAskToJoin?: boolean;
knocked?: boolean;
onSubmitAskToJoin?(reason?: string): void;
onCancelAskToJoin?(): void;
}

interface IState {
busy: boolean;
accountEmails?: string[];
invitedEmailMxid?: string;
threePidFetchError?: MatrixError;
reason?: string;
}

export default class RoomPreviewBar extends React.Component<IProps, IState> {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 ?? "";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>
);
Expand Down
3 changes: 3 additions & 0 deletions src/contexts/RoomContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const RoomContext = createContext<
narrow: false,
activeCall: null,
msc3946ProcessDynamicPredecessor: false,
canAskToJoin: false,
promptAskToJoin: false,
knocked: false,
});
RoomContext.displayName = "RoomContext";
export default RoomContext;
Expand Down
15 changes: 15 additions & 0 deletions src/dispatcher/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,4 +351,19 @@ export enum Action {
* Fired when we want to view a thread, either a new one or an existing one
*/
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 changes: 24 additions & 0 deletions src/dispatcher/payloads/CancelAskToJoinPayload.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions src/dispatcher/payloads/JoinRoomErrorPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ export interface JoinRoomErrorPayload extends Pick<ActionPayload, "action"> {

roomId: string;
err: MatrixError;

canAskToJoin?: boolean;
}
2 changes: 2 additions & 0 deletions src/dispatcher/payloads/JoinRoomPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ export interface JoinRoomPayload extends Pick<ActionPayload, "action"> {

// additional parameters for the purpose of metrics & instrumentation
metricsTrigger: JoinedRoomEvent["trigger"];

canAskToJoin?: boolean;
}
/* eslint-enable camelcase */
Loading
Loading