Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(room_access_link): add room access by link #1156

Merged
merged 3 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 personnes externes à la fonction publique devront tout de même être invitées 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"
}
}
159 changes: 159 additions & 0 deletions src/tchap/components/views/rooms/TchapRoomLinkAccess.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
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 { 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";
import Modal from "matrix-react-sdk/src/Modal";
import QuestionDialog from "matrix-react-sdk/src/components/views/dialogs/QuestionDialog";

interface ITchapRoomLinkAccessProps {
room: Room,
onUpdateParentView: Function
}

export default function TchapRoomLinkAccess({room, onUpdateParentView}: 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
onUpdateParentView(isActivated, true);
}

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

// Create the permalink to share
const _setUpRoomByLink = async () => {
try {
// create an alias if not existing
if (!room.getCanonicalAlias()) {
const aliasName = (room.name?.replace(/[^a-z0-9]/gi, "") ?? "") + randomString(11);
const fullAlias = `#${aliasName}:${room.client.getDomain()}`;
await room.client.createAlias(fullAlias, room.roomId)
await room.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);
onUpdateParentView(checked)
return;
}

// Show modal for confirmation
const activationIsConfirmed = await activateLinksharingModal();

if (activationIsConfirmed) {
// create link if we activate the sharing, otherwise change nothing
if (TchapRoomUtils.getRoomGuessAccessRule(room) === GuestAccess.CanJoin) {
await _setGuestAccessRules(GuestAccess.Forbidden)
}
await Promise.all([_setUpRoomByLink(), _setJoinRules(JoinRule.Public)]);
onUpdateParentView(checked, false);
} else {
// we revert because the action was not confirmed
setIsLinkSharingActivated(!checked);
}
};


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
}

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}
data-testid="share_link_switch"
/>
{
isLinkSharingActivated ?
<CopyableText getTextToCopy={() => linkSharingUrl} aria-labelledby="shared_room_link">
{ linkSharingUrl }
</CopyableText>
: null
}
</div>
)
}
48 changes: 39 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,40 @@ 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) => {
// hide or display the join rules
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);
}
}

const renderLinkSharing = () => {
return <TchapRoomLinkAccess room={room} onUpdateParentView={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.
Loading
Loading