Skip to content

Commit 42f8247

Browse files
dbkrMidhunSureshR
andauthored
Experimental Module API Additions (#30863)
* Module API experiments * Move ResizerNotifier into SDKContext so we don't have to pass it into RoomView * Add the MultiRoomViewStore * Make RoomViewStore able to take a roomId prop * Different interface to add space panel items A bit less flexible but probably simpler and will help keep things actually consistent rather than just allowing modules to stick any JSX into the space panel (which means they also have to worry about styling if they *do* want it to be consistent). * Allow space panel items to be updated and manage which one is selected, allowing module "spaces" to be considered spaces * Remove fetchRoomFn from SpaceNotificationStore which didn't really seem to have any point as it was only called from one place * Switch to using module api via .instance * Fairly awful workaround to actually break the dependency nightmare * Add test for multiroomviewstore * add test * Make room names deterministic So the tests don't fail if you add other tests or run them individually * Add test for builtinsapi * Update module api * RVS is not needed as prop anymore Since it's passed through context * Add roomId to prop * Remove RoomViewStore from state This is now accessed through class field * Fix test * No need to pass RVS from LoggedInView * Add RoomContextType * Implement new builtins api * Add tests * Fix import * Fix circular dependency issue * Fix import * Add more tests * Improve comment * room-id is optional * Update license * Add implementation for AccountDataApi * Add implementation for Room * Add implementation for ClientApi * Create ClientApi in Api.ts * Write tests * Use nullish coalescing assignment * Implement openRoom in NavigationApi * Write tests * Add implementation for StoresApi * Write tests * Fix circular dependency * Add comments in lieu of type and fix else block * Change to class field --------- Co-authored-by: R Midhun Suresh <[email protected]>
1 parent 514dd07 commit 42f8247

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1088
-154
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
},
8383
"dependencies": {
8484
"@babel/runtime": "^7.12.5",
85-
"@element-hq/element-web-module-api": "1.4.1",
85+
"@element-hq/element-web-module-api": "1.5.0",
8686
"@element-hq/web-shared-components": "file:packages/shared-components",
8787
"@fontsource/inconsolata": "^5",
8888
"@fontsource/inter": "^5",

res/css/structures/_SpacePanel.pcss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ Please see LICENSE files in the repository root for full details.
211211
}
212212
}
213213

214+
&.mx_SpaceButton_withIcon .mx_SpaceButton_icon {
215+
background-color: $panel-actions;
216+
}
217+
214218
&.mx_SpaceButton_home .mx_SpaceButton_icon::before {
215219
mask-image: url("@vector-im/compound-design-tokens/icons/home-solid.svg");
216220
}

src/PosthogTrackers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const notLoggedInMap: Record<Exclude<Views, Views.LOGGED_IN>, ScreenName> = {
3131
[Views.LOCK_STOLEN]: "SessionLockStolen",
3232
};
3333

34-
const loggedInPageTypeMap: Record<PageType, ScreenName> = {
34+
const loggedInPageTypeMap: Record<PageType | string, ScreenName> = {
3535
[PageType.HomePage]: "Home",
3636
[PageType.RoomView]: "Room",
3737
[PageType.UserView]: "User",
@@ -48,10 +48,10 @@ export default class PosthogTrackers {
4848
}
4949

5050
private view: Views = Views.LOADING;
51-
private pageType?: PageType;
51+
private pageType?: PageType | string;
5252
private override?: ScreenName;
5353

54-
public trackPageChange(view: Views, pageType: PageType | undefined, durationMs: number): void {
54+
public trackPageChange(view: Views, pageType: PageType | string | undefined, durationMs: number): void {
5555
this.view = view;
5656
this.pageType = pageType;
5757
if (this.override) return;

src/components/structures/LoggedInView.tsx

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ import { monitorSyncedPushRules } from "../../utils/pushRules/monitorSyncedPushR
6868
import { type ConfigOptions } from "../../SdkConfig";
6969
import { MatrixClientContextProvider } from "./MatrixClientContextProvider";
7070
import { Landmark, LandmarkNavigation } from "../../accessibility/LandmarkNavigation";
71-
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext.ts";
71+
import { ModuleApi } from "../../modules/Api.ts";
72+
import { SDKContext } from "../../contexts/SDKContext.ts";
7273

7374
// We need to fetch each pinned message individually (if we don't already have it)
7475
// so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -679,6 +680,10 @@ class LoggedInView extends React.Component<IProps, IState> {
679680
public render(): React.ReactNode {
680681
let pageElement;
681682

683+
const moduleRenderer = this.props.page_type
684+
? ModuleApi.instance.navigation.locationRenderers.get(this.props.page_type)
685+
: undefined;
686+
682687
switch (this.props.page_type) {
683688
case PageTypes.RoomView:
684689
pageElement = (
@@ -690,7 +695,6 @@ class LoggedInView extends React.Component<IProps, IState> {
690695
key={this.props.currentRoomId || "roomview"}
691696
justCreatedOpts={this.props.roomJustCreatedOpts}
692697
forceTimeline={this.props.forceTimeline}
693-
roomViewStore={SdkContextClass.instance.roomViewStore}
694698
/>
695699
);
696700
break;
@@ -706,6 +710,13 @@ class LoggedInView extends React.Component<IProps, IState> {
706710
);
707711
}
708712
break;
713+
default: {
714+
if (moduleRenderer) {
715+
pageElement = moduleRenderer();
716+
} else {
717+
console.warn(`Couldn't render page type "${this.props.page_type}"`);
718+
}
719+
}
709720
}
710721

711722
const wrapperClasses = classNames({
@@ -747,20 +758,22 @@ class LoggedInView extends React.Component<IProps, IState> {
747758
)}
748759
<SpacePanel />
749760
{!useNewRoomList && <BackdropPanel backgroundImage={this.state.backgroundImage} />}
750-
<div
751-
className="mx_LeftPanel_wrapper--user"
752-
ref={this._resizeContainer}
753-
data-collapsed={shouldUseMinimizedUI ? true : undefined}
754-
>
755-
<LeftPanel
756-
pageType={this.props.page_type as PageTypes}
757-
isMinimized={shouldUseMinimizedUI || false}
758-
resizeNotifier={this.context.resizeNotifier}
759-
/>
760-
</div>
761+
{!moduleRenderer && (
762+
<div
763+
className="mx_LeftPanel_wrapper--user"
764+
ref={this._resizeContainer}
765+
data-collapsed={shouldUseMinimizedUI ? true : undefined}
766+
>
767+
<LeftPanel
768+
pageType={this.props.page_type as PageTypes}
769+
isMinimized={shouldUseMinimizedUI || false}
770+
resizeNotifier={this.context.resizeNotifier}
771+
/>
772+
</div>
773+
)}
761774
</div>
762775
</div>
763-
<ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />
776+
{!moduleRenderer && <ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />}
764777
<div className="mx_RoomView_wrapper">{pageElement}</div>
765778
</div>
766779
</div>

src/components/structures/MatrixChat.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/ShareP
140140
import Markdown from "../../Markdown";
141141
import { sanitizeHtmlParams } from "../../Linkify";
142142
import { isOnlyAdmin } from "../../utils/membership";
143+
import { ModuleApi } from "../../modules/Api.ts";
143144

144145
// legacy export
145146
export { default as Views } from "../../Views";
@@ -175,9 +176,11 @@ interface IProps {
175176
interface IState {
176177
// the master view we are showing.
177178
view: Views;
178-
// What the LoggedInView would be showing if visible
179+
// What the LoggedInView would be showing if visible.
180+
// A member of the enum for standard pages or a string for those provided by
181+
// a module.
179182
// eslint-disable-next-line camelcase
180-
page_type?: PageType;
183+
page_type?: PageType | string;
181184
// The ID of the room we're viewing. This is either populated directly
182185
// in the case where we view a room by ID or by RoomView when it resolves
183186
// what ID an alias points at.
@@ -1921,8 +1924,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
19211924
userId: userId,
19221925
subAction: params?.action,
19231926
});
1924-
} else {
1925-
logger.info(`Ignoring showScreen for '${screen}'`);
1927+
} else if (ModuleApi.instance.navigation.locationRenderers.get(screen)) {
1928+
this.setState({ page_type: screen });
19261929
}
19271930
}
19281931

src/components/structures/RoomView.tsx

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { type CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
4444
import { debounce, throttle } from "lodash";
4545
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
4646
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
47+
import { type RoomViewProps } from "@element-hq/element-web-module-api";
4748

4849
import shouldHideEvent from "../../shouldHideEvent";
4950
import { _t } from "../../languageHandler";
@@ -148,7 +149,7 @@ if (DEBUG) {
148149
debuglog = logger.log.bind(console);
149150
}
150151

151-
interface IRoomProps {
152+
interface IRoomProps extends RoomViewProps {
152153
threepidInvite?: IThreepidInvite;
153154
oobData?: IOOBData;
154155

@@ -158,19 +159,17 @@ interface IRoomProps {
158159

159160
// Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU)
160161
onRegistered?(credentials: IMatrixClientCreds): void;
162+
161163
/**
162-
* The RoomViewStore instance for the room to be displayed.
164+
* Only necessary if RoomView should get it's RoomViewStore through the MultiRoomViewStore.
165+
* Omitting this will mean that RoomView renders for the room held in SDKContext.RoomViewStore.
163166
*/
164-
roomViewStore: RoomViewStore;
167+
roomId?: string;
165168
}
166169

167170
export { MainSplitContentType };
168171

169172
export interface IRoomState {
170-
/**
171-
* The RoomViewStore instance for the room we are displaying
172-
*/
173-
roomViewStore: RoomViewStore;
174173
room?: Room;
175174
roomId?: string;
176175
roomAlias?: string;
@@ -389,6 +388,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
389388
private messagePanel: TimelinePanel | null = null;
390389
private roomViewBody = createRef<HTMLDivElement>();
391390

391+
private roomViewStore: RoomViewStore;
392+
392393
public static contextType = SDKContext;
393394
declare public context: React.ContextType<typeof SDKContext>;
394395

@@ -401,9 +402,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
401402
throw new Error("Unable to create RoomView without MatrixClient");
402403
}
403404

405+
if (props.roomId) {
406+
this.roomViewStore = this.context.multiRoomViewStore.getRoomViewStoreForRoom(props.roomId);
407+
} else {
408+
this.roomViewStore = context.roomViewStore;
409+
}
410+
404411
const llMembers = context.client.hasLazyLoadMembersEnabled();
405412
this.state = {
406-
roomViewStore: props.roomViewStore,
407413
roomId: undefined,
408414
roomLoading: true,
409415
peekLoading: false,
@@ -535,7 +541,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
535541
};
536542

537543
private getMainSplitContentType = (room: Room): MainSplitContentType => {
538-
if (this.state.roomViewStore.isViewingCall() || isVideoRoom(room)) {
544+
if (this.roomViewStore.isViewingCall() || isVideoRoom(room)) {
539545
return MainSplitContentType.Call;
540546
}
541547
if (this.context.widgetLayoutStore.hasMaximisedWidget(room)) {
@@ -549,8 +555,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
549555
return;
550556
}
551557

552-
const roomLoadError = this.state.roomViewStore.getRoomLoadError() ?? undefined;
553-
if (!initial && !roomLoadError && this.state.roomId !== this.state.roomViewStore.getRoomId()) {
558+
const roomLoadError = this.roomViewStore.getRoomLoadError() ?? undefined;
559+
if (!initial && !roomLoadError && this.state.roomId !== this.roomViewStore.getRoomId()) {
554560
// RoomView explicitly does not support changing what room
555561
// is being viewed: instead it should just be re-mounted when
556562
// switching rooms. Therefore, if the room ID changes, we
@@ -564,7 +570,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
564570
// it was, it means we're about to be unmounted.
565571
return;
566572
}
567-
const roomViewStore = this.state.roomViewStore;
573+
const roomViewStore = this.roomViewStore;
568574
const roomId = roomViewStore.getRoomId() ?? null;
569575
const roomAlias = roomViewStore.getRoomAlias() ?? undefined;
570576
const roomLoading = roomViewStore.isRoomLoading();
@@ -611,7 +617,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
611617
newState.showRightPanel = false;
612618
}
613619

614-
const initialEventId = this.state.roomViewStore.getInitialEventId() ?? this.state.initialEventId;
620+
const initialEventId = this.roomViewStore.getInitialEventId() ?? this.state.initialEventId;
615621
if (initialEventId) {
616622
let initialEvent = room?.findEventById(initialEventId);
617623
// The event does not exist in the current sync data
@@ -637,13 +643,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
637643
action: Action.ShowThread,
638644
rootEvent: thread.rootEvent,
639645
initialEvent,
640-
highlighted: this.state.roomViewStore.isInitialEventHighlighted(),
641-
scroll_into_view: this.state.roomViewStore.initialEventScrollIntoView(),
646+
highlighted: this.roomViewStore.isInitialEventHighlighted(),
647+
scroll_into_view: this.roomViewStore.initialEventScrollIntoView(),
642648
});
643649
} else {
644650
newState.initialEventId = initialEventId;
645-
newState.isInitialEventHighlighted = this.state.roomViewStore.isInitialEventHighlighted();
646-
newState.initialEventScrollIntoView = this.state.roomViewStore.initialEventScrollIntoView();
651+
newState.isInitialEventHighlighted = this.roomViewStore.isInitialEventHighlighted();
652+
newState.initialEventScrollIntoView = this.roomViewStore.initialEventScrollIntoView();
647653
}
648654
}
649655

@@ -903,7 +909,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
903909
this.context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
904910
}
905911
// Start listening for RoomViewStore updates
906-
this.state.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
912+
this.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
907913

908914
this.context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
909915

@@ -1020,7 +1026,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
10201026

10211027
window.removeEventListener("beforeunload", this.onPageUnload);
10221028

1023-
this.state.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
1029+
this.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
10241030

10251031
this.context.rightPanelStore.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
10261032
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
@@ -1048,6 +1054,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
10481054
// clean up if this was a local room
10491055
this.context.client?.store.removeRoom(this.state.room.roomId);
10501056
}
1057+
1058+
if (this.props.roomId) this.context.multiRoomViewStore.removeRoomViewStore(this.props.roomId);
10511059
}
10521060

10531061
private onRightPanelStoreUpdate = (): void => {
@@ -2070,7 +2078,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
20702078
if (!this.state.room || !this.context?.client) return null;
20712079
const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId());
20722080
return (
2073-
<ScopedRoomContextProvider {...this.state}>
2081+
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
20742082
<LocalRoomCreateLoader
20752083
localRoom={localRoom}
20762084
names={names}
@@ -2082,7 +2090,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
20822090

20832091
private renderLocalRoomView(localRoom: LocalRoom): ReactNode {
20842092
return (
2085-
<ScopedRoomContextProvider {...this.state}>
2093+
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
20862094
<LocalRoomView
20872095
e2eStatus={this.state.e2eStatus}
20882096
localRoom={localRoom}
@@ -2098,7 +2106,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
20982106

20992107
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode {
21002108
return (
2101-
<ScopedRoomContextProvider {...this.state}>
2109+
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
21022110
<WaitingForThirdPartyRoomView
21032111
resizeNotifier={this.context.resizeNotifier}
21042112
roomView={this.roomView}
@@ -2640,7 +2648,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
26402648
}
26412649

26422650
return (
2643-
<ScopedRoomContextProvider {...this.state}>
2651+
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
26442652
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
26452653
{showChatEffects && this.roomView.current && (
26462654
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />

src/components/views/spaces/SpacePanel.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ import { ThreadsActivityCentre } from "./threads-activity-centre/";
6868
import AccessibleButton from "../elements/AccessibleButton";
6969
import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation";
7070
import { KeyboardShortcut } from "../settings/KeyboardShortcut";
71+
import { ModuleApi } from "../../../modules/Api.ts";
72+
import { useModuleSpacePanelItems } from "../../../modules/ExtrasApi.ts";
7173
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement";
7274

7375
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
@@ -290,6 +292,8 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
290292
const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces();
291293
const activeSpaces = activeSpace ? [activeSpace] : [];
292294

295+
const moduleSpaceItems = useModuleSpacePanelItems(ModuleApi.instance.extras);
296+
293297
const metaSpacesSection = metaSpaces
294298
.filter((key) => !(key === MetaSpace.VideoRooms && !SettingsStore.getValue("feature_video_rooms")))
295299
.map((key) => {
@@ -341,6 +345,27 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
341345
</Draggable>
342346
))}
343347
{children}
348+
{moduleSpaceItems.map((item) => (
349+
<li
350+
key={item.spaceKey}
351+
className={classNames("mx_SpaceItem", {
352+
collapsed: isPanelCollapsed,
353+
})}
354+
role="treeitem"
355+
aria-selected={false} // TODO
356+
>
357+
<SpaceButton
358+
{...item}
359+
isNarrow={isPanelCollapsed}
360+
size="32px"
361+
selected={activeSpace === item.spaceKey}
362+
onClick={() => {
363+
SpaceStore.instance.setActiveSpace(item.spaceKey);
364+
item.onSelected?.();
365+
}}
366+
/>
367+
</li>
368+
))}
344369
{shouldShowComponent(UIComponent.CreateSpaces) && (
345370
<CreateSpaceButton isPanelCollapsed={isPanelCollapsed} setPanelCollapsed={setPanelCollapsed} />
346371
)}

0 commit comments

Comments
 (0)