Skip to content
This repository has been 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 <[email protected]>
  • Loading branch information
Charly Nguyen committed Aug 2, 2023
1 parent e6bf67a commit f3dbf29
Show file tree
Hide file tree
Showing 18 changed files with 656 additions and 6 deletions.
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_full {
width: 100%;
}
66 changes: 64 additions & 2 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,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 @@ -119,6 +119,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 @@ -232,6 +234,10 @@ export interface IRoomState {
liveTimeline?: EventTimeline;
narrow: boolean;
msc3946ProcessDynamicPredecessor: boolean;

canAskToJoin: boolean;
askToJoin: boolean;
knocked: boolean;
}

interface LocalRoomViewProps {
Expand Down Expand Up @@ -379,6 +385,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 @@ -396,6 +403,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");
}
Expand Down Expand Up @@ -440,6 +449,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);
Expand Down Expand Up @@ -644,6 +656,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 (
Expand Down Expand Up @@ -886,6 +900,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 @@ -914,7 +929,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 @@ -1588,6 +1606,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 @@ -1992,6 +2011,29 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
);
}

private onSubmitAskToJoin = (reason?: string): void => {
const roomId = this.getRoomId();

if (isNotUndefined(roomId)) {
dis.dispatch<SubmitAskToJoinPayload>({
action: Action.SubmitAskToJoin,
roomId,
opts: { reason },
});
}
};

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 @@ -2057,6 +2099,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>
Expand Down Expand Up @@ -2131,6 +2177,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
}

if (this.state.canAskToJoin) {
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.

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 } from "matrix-js-sdk/src/models/room";
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
Expand All @@ -36,6 +36,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 @@ -54,6 +56,8 @@ enum MessageCase {
ViewingRoom = "ViewingRoom",
RoomNotFound = "RoomNotFound",
OtherError = "OtherError",
AskToJoin = "AskToJoin",
Knocked = "Knocked",
}

interface IProps {
Expand Down Expand Up @@ -96,13 +100,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> {
Expand Down Expand Up @@ -187,6 +197,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) {
Expand Down Expand Up @@ -282,6 +296,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 @@ -582,6 +600,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_full"
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 @@ -651,7 +717,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_full: messageCase === MessageCase.AskToJoin,
})}
>
{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,
askToJoin: 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 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 The Matrix.org Foundation C.I.C.
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

0 comments on commit f3dbf29

Please sign in to comment.