Skip to content
This repository has been archived by the owner on Jan 30, 2025. It is now read-only.

Add support to customize UI components rendering #12

Closed
wants to merge 11 commits into from
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ The module can also change what room/user/entity the user is looking at, and joi
From the `RuntimeModule` instance, modules can listen for `WrapperLifecycle.Wrapper` to provide a wrapper react component.
It would wrap the `MatrixChat` component and let any consumer add a header, a footer.

### UI Component management

From the `RuntimeModule` instance, modules can listen to `UiComponentLifecycle.ShouldShowComponent` that allows to
control if some components should be shown or not.

## Contributing / developing

Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for the mechanics of the contribution process.
Expand Down
7 changes: 7 additions & 0 deletions src/ModuleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,11 @@ export interface ModuleApi {
* @returns The config value verbatim.
*/
getConfigValue<T>(namespace: string, key: string): T | undefined;

/**
* Get the user-id of the logged-in user
*
* @returns MXID for the logged-in user, or undefined if not logged in
*/
getUserId(): string | undefined;
}
87 changes: 87 additions & 0 deletions src/lifecycles/UIComponentLifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
Copyright 2023 Mikhail Aheichyk
Copyright 2023 Nordeck IT + Consulting GmbH.

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.
*/

/**
* UI Component lifecycle events.
*/
export enum UIComponentLifecycle {
/**
* An event to check if component should be shown by the Element.
*/
ShouldShowComponent = "should_show_component",
}

/**
* Opts object containing the result of whether the component should be shown or not.
*/
export type ShouldShowUIComponentOps = {
/**
* Should be true to show the component, false to hide it. If not defined, then the default value from
* the Element should be applied.
*/
shouldShowComponent: boolean | undefined;
};

/**
* Listener for "should_show_component" request
*/
export type ShouldShowUIComponentListener = (
shouldShowUIComponentOps: ShouldShowUIComponentOps,
component: UIComponent,
) => void;

export enum UIComponent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seem to be a copy/paste of UIFeature.ts in the matrix-react-sdk.
Before considering this i'd want to explore ways to keep the two in sync. I'm actually unsure what value this brings over the existing possible customisations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is true, I have mentioned this in my first comment. Maybe a solution could be to change react-sdk to use a UIComponent from this PR? Or any other idea?

Based on this matrix-org/matrix-react-sdk#9931 (comment) we may think that component visibility customization API is deprecated in favor of module API.

This solution would simplify our deployment, maybe quite a lot. We plan to put all visibility customizations and landing for guests in a separate module.

/**
* Components that lead to a user being invited.
*/
InviteUsers = "UIComponent.sendInvites",

/**
* Components that lead to a room being created that aren't already
* guarded by some other condition (ie: "only if you can edit this
* space" is *not* guarded by this component, but "start DM" is).
*/
CreateRooms = "UIComponent.roomCreation",

/**
* Components that lead to a Space being created that aren't already
* guarded by some other condition (ie: "only if you can add subspaces"
* is *not* guarded by this component, but "create new space" is).
*/
CreateSpaces = "UIComponent.spaceCreation",
Comment on lines +48 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe a more appropriate avenue for this kind of feature would be to piggy back off user permissions on a server as described in matrix-org/synapse#7731

I appreciate this issue has not received a lot of attention in recent times, but it seems like a more forward looking way to handle this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably could be used if implemented already, but it seems just and idea at the moment. But we would like some users (guests with special user ids) not to be able to invite, create rooms or spaces. Would be good to use a module API for that.

Copy link
Contributor

@germain-gg germain-gg Jul 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already possible using a feature flag UIFeature.widgets as defined in UIFeature.ts. See settings.md in the matrix-react-sdk for usage

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UIFeature.widgets seems to toggle rendering of the integration manager. How should this be connected with InviteUsers, CreateRooms, CreateSpaces functionality to be hidden or shown based on user ids? Could you please clarify?


/**
* Components that lead to the public room directory.
*/
ExploreRooms = "UIComponent.exploreRooms",

/**
* Components that lead to the user being able to easily add widgets
* and integrations to the room, such as from the room information card.
*/
AddIntegrations = "UIComponent.addIntegrations",
germain-gg marked this conversation as resolved.
Show resolved Hide resolved

/**
* Component that lead to the user being able to search, dial, explore rooms
*/
FilterContainer = "UIComponent.filterContainer",

/**
* Components that lead the user to room options menu.
*/
RoomOptionsMenu = "UIComponent.roomOptionsMenu",
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
}
3 changes: 2 additions & 1 deletion src/lifecycles/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ limitations under the License.
import { RoomViewLifecycle } from "./RoomViewLifecycle";
import { WidgetLifecycle } from "./WidgetLifecycle";
import { WrapperLifecycle } from "./WrapperLifecycle";
import { UIComponentLifecycle } from "./UIComponentLifecycle";

export type AnyLifecycle = RoomViewLifecycle | WidgetLifecycle | WrapperLifecycle;
export type AnyLifecycle = RoomViewLifecycle | WidgetLifecycle | WrapperLifecycle | UIComponentLifecycle;
55 changes: 55 additions & 0 deletions test/lifecycles/UiComponentLifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
Copyright 2023 Mikhail Aheichyk
Copyright 2023 Nordeck IT + Consulting GmbH.

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 {
UIComponentLifecycle,
ShouldShowUIComponentOps,
ShouldShowUIComponentListener,
UIComponent,
} from "../../src/lifecycles/UIComponentLifecycle";
import { RuntimeModule } from "../../src/RuntimeModule";

describe("UiComponentLifecycle", () => {
let module: RuntimeModule;

beforeAll(() => {
module = new (class extends RuntimeModule {
public constructor() {
super(undefined as any);

this.on(UIComponentLifecycle.ShouldShowComponent, this.shouldShowComponent);
}

protected shouldShowComponent: ShouldShowUIComponentListener = (
shouldShowUiComponentOps: ShouldShowUIComponentOps,
component: UIComponent,
) => {
shouldShowUiComponentOps.shouldShowComponent = component !== UIComponent.CreateRooms;
};
})();
});

it("should handle should_show_component request", () => {
const opts: ShouldShowUIComponentOps = { shouldShowComponent: undefined };

module.emit(UIComponentLifecycle.ShouldShowComponent, opts, UIComponent.CreateRooms, "@user-id");
expect(opts.shouldShowComponent).toBe(false);

module.emit(UIComponentLifecycle.ShouldShowComponent, opts, UIComponent.ExploreRooms, "@user-id");
expect(opts.shouldShowComponent).toBe(true);
});
});