Skip to content

Commit

Permalink
feat(room_access_link): add room access by link
Browse files Browse the repository at this point in the history
  • Loading branch information
marc.sirisak committed Nov 5, 2024
1 parent e2fc0fd commit db2cd18
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 12 deletions.
12 changes: 12 additions & 0 deletions modules/tchap-translations/tchap_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -897,5 +897,17 @@
"encryption|verification|help_link": {
"en": "<a>Learn more and get help</a>",
"fr": "<a>En savoir plus et obtenir de l'aide </a>"
},
"room_settings|security|link_sharing_modal_confirmation": {
"en": "<p>If you activate the access by link, everyone having the link will be able to share it and join the room without invitation.</p> <p>External users will still have to be invited manually to the room.</p>",
"fr": "<p>L'activation de l'accès au salon par lien permettra à n'importe qui en sa possession, de le partager et de rejoindre le salon sans invitation.</p> <p>Les agents externes à la fonction publique devront tout de même être invités manuellement.</p>"
},
"room_settings|security|link_sharing_title": {
"en": "Activate access by link for this room",
"fr": "Activer l'accès au salon par lien"
},
"room_settings|security|link_sharing_caption": {
"en": "The users will be able to join from this link and share it with other users",
"fr": "Les utilisateurs pourront rejoindre ce salon à partir d'un lien puis le partager à d'autres utilisateurs"
}
}
139 changes: 139 additions & 0 deletions src/tchap/components/views/rooms/TchapRoomLinkAccess.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { EventType, GuestAccess, JoinRule, Room } from "matrix-js-sdk/src/matrix";

import React, { useEffect, useState } from 'react';
import LabelledToggleSwitch from 'matrix-react-sdk/src/components/views/elements/LabelledToggleSwitch';
import { _t } from 'matrix-react-sdk/src/languageHandler';
import { MatrixClientPeg } from 'matrix-react-sdk/src/MatrixClientPeg';
import { randomString } from 'matrix-js-sdk/src/randomstring';
import TchapRoomUtils from '../../../util/TchapRoomUtils';
import { makeRoomPermalink } from "matrix-react-sdk/src/utils/permalinks/Permalinks";
import { TchapRoomType } from "../../../@types/tchap";
import { RoomJoinRulesEventContent } from "matrix-js-sdk/lib/types";
import CopyableText from "matrix-react-sdk/src/components/views/elements/CopyableText";

interface ITchapRoomLinkAccessProps {
room: Room,
onBeforeChangeCallback: Function
}

export default function TchapRoomLinkAccess({room, onBeforeChangeCallback}: ITchapRoomLinkAccessProps) : JSX.Element {

const [isLinkSharingActivated, setIsLinkSharingActivated] = useState(false);
const [linkSharingUrl, setLinkSharingUrl] = useState("");
const [disableLinkSharing, setDisableLinkSharing] = useState(false);

// Getting the initial value of the link. We need to check if it was previsouly activated or not
const initialLinkSharingValue = () => {

// We disable link sharing if its a forum or user not admin
if (!TchapRoomUtils.isUserAdmin(room) || TchapRoomUtils.getTchapRoomType(room) === TchapRoomType.Forum) {
setDisableLinkSharing(true);
return;
}

const isActivated = isJoinRulePublic();

if (isActivated) {
const link = makeRoomPermalink(room.client, room.roomId);
setLinkSharingUrl(link);
}
setIsLinkSharingActivated(isActivated)
// updating the parent join rule options
onBeforeChangeCallback(isActivated, true);
}

useEffect(() => {
initialLinkSharingValue();
}, []);

// Create the permalink to share
const _setUpRoomByLink = async () => {
const client = MatrixClientPeg.get()!;
try {
// create an alias if not existing
if (!room.getCanonicalAlias()) {
const aliasName = (room.name?.replace(/[^a-z0-9]/gi, "") ?? "") + randomString(11);
const fullAlias = `#${aliasName}:${client.getDomain()}`;
await client.createAlias(fullAlias, room.roomId)
await client.sendStateEvent(room.roomId, EventType.RoomCanonicalAlias, { alias: fullAlias }, "")
}

// it will take the new alias created previously or the existing one to make a link
const link = makeRoomPermalink(room.client, room.roomId);
setLinkSharingUrl(link);
} catch(err) {
console.error(err);
}
};

// Check if the current join rule is public or not
const isJoinRulePublic = () => {
const currentJoinRule: JoinRule = TchapRoomUtils.getRoomJoinRule(room) ?? JoinRule.Invite // if we dont receive the value we default to invite

return currentJoinRule === JoinRule.Public
}

// Set the new join rule (public or invite)
const _setJoinRules = async (joinRule: JoinRule) => {
try {
await room.client.sendStateEvent(room.roomId, EventType.RoomJoinRules, { join_rule: joinRule } as RoomJoinRulesEventContent, "");
setIsLinkSharingActivated(joinRule === JoinRule.Public);
} catch(err) {
console.error(err);
}
};

const _setGuestAccessRules = async (guestAccess: GuestAccess) => {
try {
await room.client.sendStateEvent(room.roomId, EventType.RoomGuestAccess, {guest_access: guestAccess}, "")
} catch(err) {
console.error(err);
}
};

// Handler to listen on the switch change
const _onLinkSharingSwitchChange = async (checked: boolean) => {
let newJoinRule :JoinRule = checked ? JoinRule.Public : JoinRule.Invite;
setIsLinkSharingActivated(checked);

// if the link sharing is deactivated we also need to update the joinrule parent view to show the other options
if (!checked) {
await _setJoinRules(newJoinRule);
onBeforeChangeCallback(checked)
return;
}

// call parent join rule access, to confirm we want to change to public access and hide the other join options
onBeforeChangeCallback(checked, false, async (actionConfirmed: boolean) => {
// create link if we activate the sharing, otherwise change nothing
if (actionConfirmed) {
if (TchapRoomUtils.getRoomGuessAccessRule(room) === GuestAccess.CanJoin) {
await _setGuestAccessRules(GuestAccess.Forbidden)
}
_setUpRoomByLink();
_setJoinRules(JoinRule.Public)
} else {
// we revert because the action was not confirmed
setIsLinkSharingActivated(!checked);
}
})
};

return (
<div>
<LabelledToggleSwitch value={isLinkSharingActivated}
onChange={ _onLinkSharingSwitchChange }
label={_t("room_settings|security|link_sharing_title")}
caption={_t("room_settings|security|link_sharing_caption")}
disabled={disableLinkSharing}/>

{
isLinkSharingActivated ?
<CopyableText getTextToCopy={() => linkSharingUrl} aria-labelledby="shared_room_link">
{ linkSharingUrl }
</CopyableText>
: null
}
</div>
)
}
73 changes: 64 additions & 9 deletions src/tchap/components/views/settings/TchapJoinRuleSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { ReactNode } from "react";
import React, { ReactNode, useState } from "react";
import { IJoinRuleEventContent, JoinRule, RestrictedAllowType } from "matrix-js-sdk/src/@types/partials";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
Expand Down Expand Up @@ -43,6 +43,8 @@ import QuestionDialog from "matrix-react-sdk/src/components/views/dialogs/Questi

import TchapUIFeature from "../../../util/TchapUIFeature";
import { TchapRoomAccessRule, TchapIAccessRuleEventContent, TchapRoomAccessRulesEventId } from "../../../@types/tchap";
import TchapRoomLinkAccess from "../rooms/TchapRoomLinkAccess";
import TchapRoomUtils from "../../../util/TchapRoomUtils";

interface IProps {
room: Room;
Expand All @@ -56,6 +58,9 @@ interface IProps {
const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeChange, closeSettingsFn }: IProps) => {
const cli = room.client;

// Used to hide join rule option if link is activated
const [isShareLinkActivated, setIsLinkSharingActivated] = useState(false);

const roomSupportsRestricted = doesRoomVersionSupport(room.getVersion(), PreferredRoomVersions.RestrictedRooms);
const preferredRestrictionVersion =
!roomSupportsRestricted && promptUpgrade ? PreferredRoomVersions.RestrictedRooms : undefined;
Expand Down Expand Up @@ -419,15 +424,65 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
setContent(newContent);
};

// This is a callback function used by the child link sharing component
// It will indicate wether or not to hide the joinrule options or not
const activateLinkSharingChange = async (checked: boolean, init: boolean, cb?: Function) => {
setIsLinkSharingActivated(checked);
// if its the initialisation phase we dont need to do anything more other than hide or not the join options
if (init) {
return;
}

// deactivating the share link
if (!checked) {
const currentJoinRule = TchapRoomUtils.getRoomJoinRule(room);
setContent(currentJoinRule ? { join_rule: JoinRule.Invite } : {} as IJoinRuleEventContent);
}

// getting the callback from the link sharing component
if (checked && cb) {
// showing the modal to confirm we want to turn the room to public
const resultConfirmed = await activateLinksharingModal();
setIsLinkSharingActivated(resultConfirmed);
// if we cancel the modal, we need to revert back the switch and values
cb(resultConfirmed);
};
}

const activateLinksharingModal = async (): Promise<boolean> => {
const dialog = Modal.createDialog(QuestionDialog, {
title: _t("room_settings|security|link_sharing_title"),
description: (
<div>
<p>
{_t("room_settings|security|link_sharing_modal_confirmation", null, {
p: (sub) => <p>{sub}</p>,
},)}
</p>
</div>
),
});
const { finished } = dialog;
const [confirm] = await finished;
return !!confirm
}

const renderLinkSharing = () => {
return <TchapRoomLinkAccess room={room} onBeforeChangeCallback={activateLinkSharingChange}></TchapRoomLinkAccess>
}

return (
<StyledRadioGroup
name="joinRule"
value={joinRule}
onChange={onChange}
definitions={definitions}
disabled={disabled}
className="mx_JoinRuleSettings_radioButton"
/>
<>
{!isShareLinkActivated && <StyledRadioGroup
name="joinRule"
value={joinRule}
onChange={onChange}
definitions={definitions}
disabled={disabled}
className="mx_JoinRuleSettings_radioButton"
/>}
{ renderLinkSharing() }
</>
);
};

Expand Down
39 changes: 36 additions & 3 deletions src/tchap/util/TchapRoomUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
* Tchap Room utils.
*/

import { Room } from "matrix-js-sdk/src/matrix";
import { EventTimeline, EventType, Room } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "matrix-react-sdk/src/MatrixClientPeg";

import { TchapRoomAccessRule, TchapRoomAccessRulesEventId, TchapRoomType } from "../@types/tchap";
import { GuestAccess, JoinRule } from "matrix-js-sdk/src/matrix";

export default class TchapRoomUtils {
//inspired by https://github.com/tchapgouv/tchap-android/blob/develop/vector/src/main/java/fr/gouv/tchap/core/utils/RoomUtils.kt#L31
Expand Down Expand Up @@ -38,7 +39,7 @@ export default class TchapRoomUtils {
* @returns string that matches of one TchapRoomAccessRule //todo or null? or empty?
*/
static getTchapRoomAccessRule(room: Room): TchapRoomAccessRule {
return room.currentState.getStateEvents(TchapRoomAccessRulesEventId, "")?.getContent().rule;
return room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(TchapRoomAccessRulesEventId, "")?.getContent().rule;
}

/**
Expand All @@ -47,6 +48,38 @@ export default class TchapRoomUtils {
* @returns true if room is encrypted, false if not
*/
static isRoomEncrypted(roomId: string): boolean {
return MatrixClientPeg.get().isRoomEncrypted(roomId);
return !!MatrixClientPeg.get()?.isRoomEncrypted(roomId);
}

/**
* Get if current is admin of the room
* @param room
* @returns
*/
static isUserAdmin(room: Room) : boolean {
const userId = room.client.getSafeUserId();
return room.getMember(userId)?.powerLevelNorm == 100
}

static getRoomJoinRule(room: Room): JoinRule|undefined {
return room
.getLiveTimeline()
.getState(EventTimeline.FORWARDS)
?.getStateEvents(EventType.RoomJoinRules, "")
?.getContent().join_rule
}

static getRoomGuessAccessRule(room: Room): GuestAccess {
const event : GuestAccess = room
.getLiveTimeline()
.getState(EventTimeline.FORWARDS)
?.getStateEvents(EventType.RoomGuestAccess, "")
?.getContent().guest_access;

if (!event) {
// default to can_join
return GuestAccess.CanJoin;
}
return event;
}
}
Binary file removed tchap-4.7.2-dev-upgrade2-20240916.tar.gz
Binary file not shown.

0 comments on commit db2cd18

Please sign in to comment.