diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index f3214c4757..4ffab08780 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -44,7 +44,7 @@ import { Action } from "./dispatcher/actions"; import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership"; import SdkConfig from "./SdkConfig"; import SettingsStore from "./settings/SettingsStore"; -import { UIFeature } from "./settings/UIFeature"; +import { UIComponent, UIFeature } from "./settings/UIFeature"; import { CHAT_EFFECTS } from "./effects"; import CallHandler from "./CallHandler"; import { guessAndSetDMRoom } from "./Rooms"; @@ -56,6 +56,7 @@ import InfoDialog from "./components/views/dialogs/InfoDialog"; import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog"; import { logger } from "matrix-js-sdk/src/logger"; +import { shouldShowComponent } from "./customisations/helpers/UIComponents"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -403,6 +404,7 @@ export const Commands = [ command: 'invite', args: ' []', description: _td('Invites user with given id to current room'), + isEnabled: () => shouldShowComponent(UIComponent.InviteUsers), runFn: function(roomId, args) { if (args) { const [address, reason] = args.split(/\s+(.+)/); diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 629c62cde0..54dde2e891 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -81,6 +81,8 @@ import GroupAvatar from "../views/avatars/GroupAvatar"; import { useDispatcher } from "../../hooks/useDispatcher"; import { logger } from "matrix-js-sdk/src/logger"; +import { shouldShowComponent } from "../../customisations/helpers/UIComponents"; +import { UIComponent } from "../../settings/UIFeature"; interface IProps { space: Room; @@ -411,7 +413,7 @@ const SpaceLanding = ({ space }) => { const userId = cli.getUserId(); let inviteButton; - if (myMembership === "join" && space.canInvite(userId)) { + if (myMembership === "join" && space.canInvite(userId) && shouldShowComponent(UIComponent.InviteUsers)) { inviteButton = ( { try { diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index df4f2d21fa..b31b09ab24 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -44,6 +44,8 @@ import MemberTile from "./MemberTile"; import BaseAvatar from '../avatars/BaseAvatar'; import { throttle } from 'lodash'; import SpaceStore from "../../../stores/SpaceStore"; +import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; +import { UIComponent } from "../../../settings/UIFeature"; const getSearchQueryLSKey = (roomId: string) => `mx_MemberList_searchQuarry_${roomId}`; @@ -530,7 +532,7 @@ export default class MemberList extends React.Component { const room = cli.getRoom(this.props.roomId); let inviteButton; - if (room && room.getMyMembership() === 'join') { + if (room?.getMyMembership() === 'join' && shouldShowComponent(UIComponent.InviteUsers)) { let inviteButtonText = _t("Invite to this room"); const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat(); if (chat && chat.roomId === this.props.roomId) { diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 8a96b8a9ba..fbaccfd6b5 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -28,15 +28,17 @@ import AccessibleButton from "../elements/AccessibleButton"; import MiniAvatarUploader, { AVATAR_SIZE } from "../elements/MiniAvatarUploader"; import RoomAvatar from "../avatars/RoomAvatar"; import defaultDispatcher from "../../../dispatcher/dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; import { Action } from "../../../dispatcher/actions"; -import dis from "../../../dispatcher/dispatcher"; import SpaceStore from "../../../stores/SpaceStore"; import { showSpaceInvite } from "../../../utils/space"; import { privateShouldBeEncrypted } from "../../../createRoom"; import EventTileBubble from "../messages/EventTileBubble"; import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; +import { UIComponent } from "../../../settings/UIFeature"; function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean { const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId); @@ -150,7 +152,7 @@ const NewRoomIntro = () => { { _t("Invite to just this room") } } ; - } else if (room.canInvite(cli.getUserId())) { + } else if (room.canInvite(cli.getUserId()) && shouldShowComponent(UIComponent.InviteUsers)) { buttons =
void; @@ -133,32 +135,38 @@ const TAG_AESTHETICS: ITagAestheticsMap = { MatrixClientPeg.get().getUserId()); return - { - e.preventDefault(); - e.stopPropagation(); - onFinished(); - showCreateNewRoom(SpaceStore.instance.activeSpace); - }} - disabled={!canAddRooms} - tooltip={canAddRooms ? undefined - : _t("You do not have permissions to create new rooms in this space")} - /> - { - e.preventDefault(); - e.stopPropagation(); - onFinished(); - showAddExistingRooms(SpaceStore.instance.activeSpace); - }} - disabled={!canAddRooms} - tooltip={canAddRooms ? undefined - : _t("You do not have permissions to add rooms to this space")} - /> + { + shouldShowComponent(UIComponent.CreateRooms) + ? (<> + { + e.preventDefault(); + e.stopPropagation(); + onFinished(); + showCreateNewRoom(SpaceStore.instance.activeSpace); + }} + disabled={!canAddRooms} + tooltip={canAddRooms ? undefined + : _t("You do not have permissions to create new rooms in this space")} + /> + { + e.preventDefault(); + e.stopPropagation(); + onFinished(); + showAddExistingRooms(SpaceStore.instance.activeSpace); + }} + disabled={!canAddRooms} + tooltip={canAddRooms ? undefined + : _t("You do not have permissions to add rooms to this space")} + /> + ) + : null + } { ); let addRoomButton = null; - if (!!this.props.onAddRoom) { + if (!!this.props.onAddRoom && shouldShowComponent(UIComponent.CreateRooms)) { addRoomButton = ( { /> ); } else if (this.props.addRoomContextMenu) { + // We assume that shouldShowComponent() is checked by the context menu itself. addRoomButton = ( {

{ _t("Share invite link") }

{ copiedText }
- { space.canInvite(MatrixClientPeg.get()?.getUserId()) ? { - if (onFinished) onFinished(); - showRoomInviteDialog(space.roomId); - }} - > -

{ _t("Invite people") }

- { _t("Invite with email or username") } -
: null } + { space.canInvite(MatrixClientPeg.get()?.getUserId()) && shouldShowComponent(UIComponent.InviteUsers) + ? { + if (onFinished) onFinished(); + showRoomInviteDialog(space.roomId); + }} + > +

{ _t("Invite people") }

+ { _t("Invite with email or username") } +
+ : null }
; }; diff --git a/src/customisations/ComponentVisibility.ts b/src/customisations/ComponentVisibility.ts new file mode 100644 index 0000000000..3e12805f42 --- /dev/null +++ b/src/customisations/ComponentVisibility.ts @@ -0,0 +1,51 @@ +/* +Copyright 2021 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. +*/ + +// Dev note: this customisation point is heavily inspired by UIFeature flags, though +// with an intention of being used for more complex switching on whether or not a feature +// should be shown. + +// Populate this class with the details of your customisations when copying it. + +import { UIComponent } from "../settings/UIFeature"; + +/** + * Determines whether or not the active MatrixClient user should be able to use + * the given UI component. If shown, the user might still not be able to use the + * component depending on their contextual permissions. For example, invite options + * might be shown to the user but they won't have permission to invite users to + * the current room: the button will appear disabled. + * @param {UIComponent} component The component to check visibility for. + * @returns {boolean} True (default) if the user is able to see the component, false + * otherwise. + */ +function shouldShowComponent(component: UIComponent): boolean { + return true; // default to visible +} + +// This interface summarises all available customisation points and also marks +// them all as optional. This allows customisers to only define and export the +// customisations they need while still maintaining type safety. +export interface IComponentVisibilityCustomisations { + shouldShowComponent?: typeof shouldShowComponent; +} + +// A real customisation module will define and export one or more of the +// customisation points that make up the interface above. +export const ComponentVisibilityCustomisations: IComponentVisibilityCustomisations = { + // while we don't specify the functions here, their defaults are described + // in their pseudo-implementations above. +}; diff --git a/src/customisations/helpers/UIComponents.ts b/src/customisations/helpers/UIComponents.ts new file mode 100644 index 0000000000..11b8a40969 --- /dev/null +++ b/src/customisations/helpers/UIComponents.ts @@ -0,0 +1,22 @@ +/* +Copyright 2021 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 { UIComponent } from "../../settings/UIFeature"; +import { ComponentVisibilityCustomisations } from "../ComponentVisibility"; + +export function shouldShowComponent(component: UIComponent): boolean { + return ComponentVisibilityCustomisations.shouldShowComponent?.(component) ?? true; +} diff --git a/src/settings/UIFeature.ts b/src/settings/UIFeature.ts index 82dfdf0bb5..847da2537e 100644 --- a/src/settings/UIFeature.ts +++ b/src/settings/UIFeature.ts @@ -33,3 +33,8 @@ export enum UIFeature { AdvancedSettings = "UIFeature.advancedSettings", RoomHistorySettings = "UIFeature.roomHistorySettings", } + +export enum UIComponent { + InviteUsers = "UIComponent.sendInvites", + CreateRooms = "UIComponent.roomCreation", +}