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

Commit

Permalink
Allow knocking rooms
Browse files Browse the repository at this point in the history
Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>
  • Loading branch information
Charly Nguyen committed Aug 4, 2023

Verified

This commit was signed with the committer’s verified signature.
yegor256 Yegor Bugayenko
1 parent e6af09e commit b88e646
Showing 18 changed files with 689 additions and 7 deletions.
10 changes: 10 additions & 0 deletions res/css/views/rooms/_RoomPreviewBar.pcss
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@ limitations under the License.
display: flex;
flex-direction: row;
align-items: center;
margin: 0;
}
}

@@ -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
@@ -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;
askToJoin: 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,
askToJoin: 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,
askToJoin: this.context.roomViewStore.askToJoin(),
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}
askToJoin={this.state.askToJoin}
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}
askToJoin={myMembership === "leave" || this.state.askToJoin}
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.

76 changes: 74 additions & 2 deletions src/components/views/rooms/RoomPreviewBar.tsx
Original file line number Diff line number Diff line change
@@ -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",
AskToJoin = "AskToJoin",
Knocked = "Knocked",
}

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

askToJoin?: 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> {
@@ -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.askToJoin) {
return MessageCase.AskToJoin;
}

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.AskToJoin: {
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.AskToJoin,
})}
>
{actions}
</div>
<div className="mx_RoomPreviewBar_footer">{footer}</div>
</div>
);
3 changes: 3 additions & 0 deletions src/contexts/RoomContext.ts
Original file line number Diff line number Diff line change
@@ -71,6 +71,9 @@ const RoomContext = createContext<
narrow: false,
activeCall: null,
msc3946ProcessDynamicPredecessor: false,
canAskToJoin: false,
askToJoin: false,
knocked: false,
});
RoomContext.displayName = "RoomContext";
export default RoomContext;
15 changes: 15 additions & 0 deletions src/dispatcher/actions.ts
Original file line number Diff line number Diff line change
@@ -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 ask to join a room.
*/
AskToJoin = "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
@@ -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
@@ -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

0 comments on commit b88e646

Please sign in to comment.