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

Commit 3a5f2c8

Browse files
author
Charly Nguyen
committed
Allow knocking rooms
Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>
1 parent d94808a commit 3a5f2c8

File tree

18 files changed

+656
-6
lines changed

18 files changed

+656
-6
lines changed

res/css/views/rooms/_RoomPreviewBar.pcss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ limitations under the License.
3030
display: flex;
3131
flex-direction: row;
3232
align-items: center;
33+
margin: 0;
3334
}
3435
}
3536

@@ -148,3 +149,12 @@ a.mx_RoomPreviewBar_inviter {
148149
text-decoration: underline;
149150
cursor: pointer;
150151
}
152+
153+
.mx_RoomPreviewBar_icon {
154+
margin-right: 8px;
155+
vertical-align: text-top;
156+
}
157+
158+
.mx_RoomPreviewBar_full {
159+
width: 100%;
160+
}

src/components/structures/RoomView.tsx

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { MatrixError } from "matrix-js-sdk/src/http-api";
3131
import { ClientEvent } from "matrix-js-sdk/src/client";
3232
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
3333
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
34-
import { HistoryVisibility } from "matrix-js-sdk/src/@types/partials";
34+
import { HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
3535
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
3636
import { IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set";
3737

@@ -119,6 +119,8 @@ import WidgetUtils from "../../utils/WidgetUtils";
119119
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
120120
import { WaitingForThirdPartyRoomView } from "./WaitingForThirdPartyRoomView";
121121
import { isNotUndefined } from "../../Typeguards";
122+
import { CancelAskToJoinPayload } from "../../dispatcher/payloads/CancelAskToJoinPayload";
123+
import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload";
122124

123125
const DEBUG = false;
124126
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
@@ -232,6 +234,10 @@ export interface IRoomState {
232234
liveTimeline?: EventTimeline;
233235
narrow: boolean;
234236
msc3946ProcessDynamicPredecessor: boolean;
237+
238+
canAskToJoin: boolean;
239+
askToJoin: boolean;
240+
knocked: boolean;
235241
}
236242

237243
interface LocalRoomViewProps {
@@ -378,6 +384,7 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
378384
}
379385

380386
export class RoomView extends React.Component<IRoomProps, IRoomState> {
387+
private readonly askToJoinEnabled: boolean;
381388
private readonly dispatcherRef: string;
382389
private settingWatchers: string[];
383390

@@ -395,6 +402,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
395402
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
396403
super(props, context);
397404

405+
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
406+
398407
if (!context.client) {
399408
throw new Error("Unable to create RoomView without MatrixClient");
400409
}
@@ -439,6 +448,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
439448
liveTimeline: undefined,
440449
narrow: false,
441450
msc3946ProcessDynamicPredecessor: SettingsStore.getValue("feature_dynamic_room_predecessors"),
451+
canAskToJoin: this.askToJoinEnabled,
452+
askToJoin: false,
453+
knocked: false,
442454
};
443455

444456
this.dispatcherRef = dis.register(this.onAction);
@@ -643,6 +655,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
643655
)
644656
: false,
645657
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
658+
askToJoin: this.context.roomViewStore.askToJoin(),
659+
knocked: this.context.roomViewStore.knocked(),
646660
};
647661

648662
if (
@@ -885,6 +899,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
885899
this.setState({
886900
room: room,
887901
peekLoading: false,
902+
canAskToJoin: this.askToJoinEnabled && room.getJoinRule() === JoinRule.Knock,
888903
});
889904
this.onRoomLoaded(room);
890905
})
@@ -913,7 +928,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
913928
} else if (room) {
914929
// Stop peeking because we have joined this room previously
915930
this.context.client?.stopPeeking();
916-
this.setState({ isPeeking: false });
931+
this.setState({
932+
isPeeking: false,
933+
canAskToJoin: this.askToJoinEnabled && room.getJoinRule() === JoinRule.Knock,
934+
});
917935
}
918936
}
919937
}
@@ -1587,6 +1605,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
15871605
roomId,
15881606
opts: { inviteSignUrl: signUrl },
15891607
metricsTrigger: this.state.room?.getMyMembership() === "invite" ? "Invite" : "RoomPreview",
1608+
canAskToJoin: this.state.canAskToJoin,
15901609
});
15911610
}
15921611

@@ -1991,6 +2010,29 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
19912010
);
19922011
}
19932012

2013+
private onSubmitAskToJoin = (reason?: string): void => {
2014+
const roomId = this.getRoomId();
2015+
2016+
if (isNotUndefined(roomId)) {
2017+
dis.dispatch<SubmitAskToJoinPayload>({
2018+
action: Action.SubmitAskToJoin,
2019+
roomId,
2020+
opts: { reason },
2021+
});
2022+
}
2023+
};
2024+
2025+
private onCancelAskToJoin = (): void => {
2026+
const roomId = this.getRoomId();
2027+
2028+
if (isNotUndefined(roomId)) {
2029+
dis.dispatch<CancelAskToJoinPayload>({
2030+
action: Action.CancelAskToJoin,
2031+
roomId,
2032+
});
2033+
}
2034+
};
2035+
19942036
public render(): ReactNode {
19952037
if (!this.context.client) return null;
19962038

@@ -2056,6 +2098,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
20562098
oobData={this.props.oobData}
20572099
signUrl={this.props.threepidInvite?.signUrl}
20582100
roomId={this.state.roomId}
2101+
askToJoin={this.state.askToJoin}
2102+
knocked={this.state.knocked}
2103+
onSubmitAskToJoin={this.onSubmitAskToJoin}
2104+
onCancelAskToJoin={this.onCancelAskToJoin}
20592105
/>
20602106
</ErrorBoundary>
20612107
</div>
@@ -2130,6 +2176,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
21302176
}
21312177
}
21322178

2179+
if (this.state.canAskToJoin && ["knock", "leave"].includes(myMembership)) {
2180+
return (
2181+
<div className="mx_RoomView">
2182+
<ErrorBoundary>
2183+
<RoomPreviewBar
2184+
room={this.state.room}
2185+
askToJoin={myMembership === "leave" || this.state.askToJoin}
2186+
knocked={myMembership === "knock" || this.state.knocked}
2187+
onSubmitAskToJoin={this.onSubmitAskToJoin}
2188+
onCancelAskToJoin={this.onCancelAskToJoin}
2189+
/>
2190+
</ErrorBoundary>
2191+
</div>
2192+
);
2193+
}
2194+
21332195
// We have successfully loaded this room, and are not previewing.
21342196
// Display the "normal" room view.
21352197

src/components/views/rooms/RoomPreviewBar.tsx

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { ReactNode } from "react";
17+
import React, { ChangeEvent, ReactNode } from "react";
1818
import { Room } from "matrix-js-sdk/src/models/room";
1919
import { MatrixError } from "matrix-js-sdk/src/http-api";
2020
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
@@ -36,6 +36,8 @@ import RoomAvatar from "../avatars/RoomAvatar";
3636
import SettingsStore from "../../../settings/SettingsStore";
3737
import { UIFeature } from "../../../settings/UIFeature";
3838
import { ModuleRunner } from "../../../modules/ModuleRunner";
39+
import { Icon as AskToJoinIcon } from "../../../../res/img/element-icons/ask-to-join.svg";
40+
import Field from "../elements/Field";
3941

4042
const MemberEventHtmlReasonField = "io.element.html_reason";
4143

@@ -54,6 +56,8 @@ enum MessageCase {
5456
ViewingRoom = "ViewingRoom",
5557
RoomNotFound = "RoomNotFound",
5658
OtherError = "OtherError",
59+
AskToJoin = "AskToJoin",
60+
Knocked = "Knocked",
5761
}
5862

5963
interface IProps {
@@ -96,13 +100,19 @@ interface IProps {
96100
onRejectClick?(): void;
97101
onRejectAndIgnoreClick?(): void;
98102
onForgetClick?(): void;
103+
104+
askToJoin?: boolean;
105+
knocked?: boolean;
106+
onSubmitAskToJoin?(reason?: string): void;
107+
onCancelAskToJoin?(): void;
99108
}
100109

101110
interface IState {
102111
busy: boolean;
103112
accountEmails?: string[];
104113
invitedEmailMxid?: string;
105114
threePidFetchError?: MatrixError;
115+
reason?: string;
106116
}
107117

108118
export default class RoomPreviewBar extends React.Component<IProps, IState> {
@@ -187,6 +197,10 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
187197
return MessageCase.Rejecting;
188198
} else if (this.props.loading || this.state.busy) {
189199
return MessageCase.Loading;
200+
} else if (this.props.knocked) {
201+
return MessageCase.Knocked;
202+
} else if (this.props.askToJoin) {
203+
return MessageCase.AskToJoin;
190204
}
191205

192206
if (this.props.inviterName) {
@@ -282,6 +296,10 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
282296
dis.dispatch({ action: "start_registration", screenAfterLogin: this.makeScreenAfterLogin() });
283297
};
284298

299+
private onChangeReason = (event: ChangeEvent<HTMLTextAreaElement>): void => {
300+
this.setState({ reason: event.target.value });
301+
};
302+
285303
public render(): React.ReactNode {
286304
const brand = SdkConfig.get().brand;
287305
const roomName = this.props.room?.name ?? this.props.roomAlias ?? "";
@@ -582,6 +600,54 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
582600
];
583601
break;
584602
}
603+
case MessageCase.AskToJoin: {
604+
if (roomName) {
605+
title = _t("Ask to join %(roomName)s?", { roomName });
606+
} else {
607+
title = _t("Ask to join?");
608+
}
609+
610+
const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
611+
subTitle = [
612+
avatar,
613+
_t(
614+
"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.",
615+
),
616+
];
617+
618+
reasonElement = (
619+
<Field
620+
autoFocus
621+
className="mx_RoomPreviewBar_full"
622+
element="textarea"
623+
onChange={this.onChangeReason}
624+
placeholder={_t("Message (optional)")}
625+
type="text"
626+
value={this.state.reason ?? ""}
627+
/>
628+
);
629+
630+
primaryActionHandler = () =>
631+
this.props.onSubmitAskToJoin && this.props.onSubmitAskToJoin(this.state.reason);
632+
primaryActionLabel = _t("Request access");
633+
634+
break;
635+
}
636+
case MessageCase.Knocked: {
637+
title = _t("Request to join sent");
638+
639+
subTitle = [
640+
<>
641+
<AskToJoinIcon className="mx_Icon mx_Icon_16 mx_RoomPreviewBar_icon" />
642+
{_t("Your request to join is pending.")}
643+
</>,
644+
];
645+
646+
secondaryActionHandler = this.props.onCancelAskToJoin;
647+
secondaryActionLabel = _t("Cancel request");
648+
649+
break;
650+
}
585651
}
586652

587653
let subTitleElements;
@@ -651,7 +717,13 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
651717
{subTitleElements}
652718
</div>
653719
{reasonElement}
654-
<div className="mx_RoomPreviewBar_actions">{actions}</div>
720+
<div
721+
className={classNames("mx_RoomPreviewBar_actions", {
722+
mx_RoomPreviewBar_full: messageCase === MessageCase.AskToJoin,
723+
})}
724+
>
725+
{actions}
726+
</div>
655727
<div className="mx_RoomPreviewBar_footer">{footer}</div>
656728
</div>
657729
);

src/contexts/RoomContext.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ const RoomContext = createContext<
7171
narrow: false,
7272
activeCall: null,
7373
msc3946ProcessDynamicPredecessor: false,
74+
canAskToJoin: false,
75+
askToJoin: false,
76+
knocked: false,
7477
});
7578
RoomContext.displayName = "RoomContext";
7679
export default RoomContext;

src/dispatcher/actions.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,4 +351,19 @@ export enum Action {
351351
* Fired when we want to view a thread, either a new one or an existing one
352352
*/
353353
ShowThread = "show_thread",
354+
355+
/**
356+
* Fired when requesting to ask to join a room.
357+
*/
358+
AskToJoin = "ask_to_join",
359+
360+
/**
361+
* Fired when requesting to submit an ask to join a room. Use with a SubmitAskToJoinPayload.
362+
*/
363+
SubmitAskToJoin = "submit_ask_to_join",
364+
365+
/**
366+
* Fired when requesting to cancel an ask to join a room. Use with a CancelAskToJoinPayload.
367+
*/
368+
CancelAskToJoin = "cancel_ask_to_join",
354369
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { Action } from "../actions";
18+
import { ActionPayload } from "../payloads";
19+
20+
export interface CancelAskToJoinPayload extends Pick<ActionPayload, "action"> {
21+
action: Action.CancelAskToJoin;
22+
23+
roomId: string;
24+
}

src/dispatcher/payloads/JoinRoomErrorPayload.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ export interface JoinRoomErrorPayload extends Pick<ActionPayload, "action"> {
2424

2525
roomId: string;
2626
err: MatrixError;
27+
28+
canAskToJoin?: boolean;
2729
}

src/dispatcher/payloads/JoinRoomPayload.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,7 @@ export interface JoinRoomPayload extends Pick<ActionPayload, "action"> {
2929

3030
// additional parameters for the purpose of metrics & instrumentation
3131
metricsTrigger: JoinedRoomEvent["trigger"];
32+
33+
canAskToJoin?: boolean;
3234
}
3335
/* eslint-enable camelcase */

0 commit comments

Comments
 (0)