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

Commit f3dbf29

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

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 {
@@ -379,6 +385,7 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
379385
}
380386

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

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

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

445457
this.dispatcherRef = dis.register(this.onAction);
@@ -644,6 +656,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
644656
)
645657
: false,
646658
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
659+
askToJoin: this.context.roomViewStore.askToJoin(),
660+
knocked: this.context.roomViewStore.knocked(),
647661
};
648662

649663
if (
@@ -886,6 +900,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
886900
this.setState({
887901
room: room,
888902
peekLoading: false,
903+
canAskToJoin: this.askToJoinEnabled && room.getJoinRule() === JoinRule.Knock,
889904
});
890905
this.onRoomLoaded(room);
891906
})
@@ -914,7 +929,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
914929
} else if (room) {
915930
// Stop peeking because we have joined this room previously
916931
this.context.client?.stopPeeking();
917-
this.setState({ isPeeking: false });
932+
this.setState({
933+
isPeeking: false,
934+
canAskToJoin: this.askToJoinEnabled && room.getJoinRule() === JoinRule.Knock,
935+
});
918936
}
919937
}
920938
}
@@ -1588,6 +1606,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
15881606
roomId,
15891607
opts: { inviteSignUrl: signUrl },
15901608
metricsTrigger: this.state.room?.getMyMembership() === "invite" ? "Invite" : "RoomPreview",
1609+
canAskToJoin: this.state.canAskToJoin,
15911610
});
15921611
}
15931612

@@ -1992,6 +2011,29 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
19922011
);
19932012
}
19942013

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

@@ -2057,6 +2099,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
20572099
oobData={this.props.oobData}
20582100
signUrl={this.props.threepidInvite?.signUrl}
20592101
roomId={this.state.roomId}
2102+
askToJoin={this.state.askToJoin}
2103+
knocked={this.state.knocked}
2104+
onSubmitAskToJoin={this.onSubmitAskToJoin}
2105+
onCancelAskToJoin={this.onCancelAskToJoin}
20602106
/>
20612107
</ErrorBoundary>
20622108
</div>
@@ -2131,6 +2177,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
21312177
}
21322178
}
21332179

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

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)