mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-05 01:10:40 +00:00
Compare commits
44 Commits
d85e37e938
...
midhun/mod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dc1431270 | ||
|
|
f4e8e79af8 | ||
|
|
507eaa02df | ||
|
|
b94d40f166 | ||
|
|
c2d68f8dc0 | ||
|
|
3be766d79c | ||
|
|
335491eabc | ||
|
|
2449557aa8 | ||
|
|
eebf227cf4 | ||
|
|
ebc9e3ace6 | ||
|
|
61306a1e4a | ||
|
|
a9fed64637 | ||
|
|
8a875e8c6d | ||
|
|
620ba9231d | ||
|
|
f2104b5ec0 | ||
|
|
1c0738be0f | ||
|
|
c78461db0b | ||
|
|
2b05d51e41 | ||
|
|
6f6b3bdd8f | ||
|
|
da11cff6ff | ||
|
|
302b6567ea | ||
|
|
b8c79f46ee | ||
|
|
0e8a617beb | ||
|
|
a94328a125 | ||
|
|
4d7d06bfc0 | ||
|
|
c31d4fea8d | ||
|
|
a5f3876a38 | ||
|
|
206905c2f5 | ||
|
|
51499fa106 | ||
|
|
1ebead1c8a | ||
|
|
738eac9b90 | ||
|
|
2dd743dea0 | ||
|
|
ced886aa07 | ||
|
|
de5a75777f | ||
|
|
809b41aa59 | ||
|
|
b6b1658805 | ||
|
|
afa340eb18 | ||
|
|
7ac4a4a2d4 | ||
|
|
66bf1dd469 | ||
|
|
9ae447f14f | ||
|
|
a02a5ac849 | ||
|
|
e4dee7ab63 | ||
|
|
9129c35407 | ||
|
|
4b701b55b1 |
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@element-hq/element-web-module-api": "1.4.1",
|
||||
"@element-hq/element-web-module-api": "1.5.0",
|
||||
"@fontsource/inconsolata": "^5",
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
|
||||
@@ -211,6 +211,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_SpaceButton_withIcon .mx_SpaceButton_icon {
|
||||
background-color: $panel-actions;
|
||||
}
|
||||
|
||||
&.mx_SpaceButton_home .mx_SpaceButton_icon::before {
|
||||
mask-image: url("@vector-im/compound-design-tokens/icons/home-solid.svg");
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const notLoggedInMap: Record<Exclude<Views, Views.LOGGED_IN>, ScreenName> = {
|
||||
[Views.LOCK_STOLEN]: "SessionLockStolen",
|
||||
};
|
||||
|
||||
const loggedInPageTypeMap: Record<PageType, ScreenName> = {
|
||||
const loggedInPageTypeMap: Record<PageType | string, ScreenName> = {
|
||||
[PageType.HomePage]: "Home",
|
||||
[PageType.RoomView]: "Room",
|
||||
[PageType.UserView]: "User",
|
||||
@@ -48,10 +48,10 @@ export default class PosthogTrackers {
|
||||
}
|
||||
|
||||
private view: Views = Views.LOADING;
|
||||
private pageType?: PageType;
|
||||
private pageType?: PageType | string;
|
||||
private override?: ScreenName;
|
||||
|
||||
public trackPageChange(view: Views, pageType: PageType | undefined, durationMs: number): void {
|
||||
public trackPageChange(view: Views, pageType: PageType | string | undefined, durationMs: number): void {
|
||||
this.view = view;
|
||||
this.pageType = pageType;
|
||||
if (this.override) return;
|
||||
|
||||
@@ -68,7 +68,8 @@ import { monitorSyncedPushRules } from "../../utils/pushRules/monitorSyncedPushR
|
||||
import { type ConfigOptions } from "../../SdkConfig";
|
||||
import { MatrixClientContextProvider } from "./MatrixClientContextProvider";
|
||||
import { Landmark, LandmarkNavigation } from "../../accessibility/LandmarkNavigation";
|
||||
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext.ts";
|
||||
import { ModuleApi } from "../../modules/Api.ts";
|
||||
import { SDKContext } from "../../contexts/SDKContext.ts";
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// 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> {
|
||||
public render(): React.ReactNode {
|
||||
let pageElement;
|
||||
|
||||
const moduleRenderer = this.props.page_type
|
||||
? ModuleApi.instance.navigation.locationRenderers.get(this.props.page_type)
|
||||
: undefined;
|
||||
|
||||
switch (this.props.page_type) {
|
||||
case PageTypes.RoomView:
|
||||
pageElement = (
|
||||
@@ -690,7 +695,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
key={this.props.currentRoomId || "roomview"}
|
||||
justCreatedOpts={this.props.roomJustCreatedOpts}
|
||||
forceTimeline={this.props.forceTimeline}
|
||||
roomViewStore={SdkContextClass.instance.roomViewStore}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
@@ -706,6 +710,13 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
);
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
if (moduleRenderer) {
|
||||
pageElement = moduleRenderer();
|
||||
} else {
|
||||
console.warn(`Couldn't render page type "${this.props.page_type}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapperClasses = classNames({
|
||||
@@ -747,20 +758,22 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
)}
|
||||
<SpacePanel />
|
||||
{!useNewRoomList && <BackdropPanel backgroundImage={this.state.backgroundImage} />}
|
||||
<div
|
||||
className="mx_LeftPanel_wrapper--user"
|
||||
ref={this._resizeContainer}
|
||||
data-collapsed={shouldUseMinimizedUI ? true : undefined}
|
||||
>
|
||||
<LeftPanel
|
||||
pageType={this.props.page_type as PageTypes}
|
||||
isMinimized={shouldUseMinimizedUI || false}
|
||||
resizeNotifier={this.context.resizeNotifier}
|
||||
/>
|
||||
</div>
|
||||
{!moduleRenderer && (
|
||||
<div
|
||||
className="mx_LeftPanel_wrapper--user"
|
||||
ref={this._resizeContainer}
|
||||
data-collapsed={shouldUseMinimizedUI ? true : undefined}
|
||||
>
|
||||
<LeftPanel
|
||||
pageType={this.props.page_type as PageTypes}
|
||||
isMinimized={shouldUseMinimizedUI || false}
|
||||
resizeNotifier={this.context.resizeNotifier}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />
|
||||
{!moduleRenderer && <ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />}
|
||||
<div className="mx_RoomView_wrapper">{pageElement}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -140,6 +140,7 @@ import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/ShareP
|
||||
import Markdown from "../../Markdown";
|
||||
import { sanitizeHtmlParams } from "../../Linkify";
|
||||
import { isOnlyAdmin } from "../../utils/membership";
|
||||
import { ModuleApi } from "../../modules/Api.ts";
|
||||
|
||||
// legacy export
|
||||
export { default as Views } from "../../Views";
|
||||
@@ -175,9 +176,11 @@ interface IProps {
|
||||
interface IState {
|
||||
// the master view we are showing.
|
||||
view: Views;
|
||||
// What the LoggedInView would be showing if visible
|
||||
// What the LoggedInView would be showing if visible.
|
||||
// A member of the enum for standard pages or a string for those provided by
|
||||
// a module.
|
||||
// eslint-disable-next-line camelcase
|
||||
page_type?: PageType;
|
||||
page_type?: PageType | string;
|
||||
// The ID of the room we're viewing. This is either populated directly
|
||||
// in the case where we view a room by ID or by RoomView when it resolves
|
||||
// what ID an alias points at.
|
||||
@@ -1922,7 +1925,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
subAction: params?.action,
|
||||
});
|
||||
} else {
|
||||
logger.info(`Ignoring showScreen for '${screen}'`);
|
||||
if (ModuleApi.instance.navigation.locationRenderers.get(screen)) {
|
||||
this.setState({ page_type: screen });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import { type CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { debounce, throttle } from "lodash";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import { type RoomViewProps } from "@element-hq/element-web-module-api";
|
||||
|
||||
import shouldHideEvent from "../../shouldHideEvent";
|
||||
import { _t } from "../../languageHandler";
|
||||
@@ -148,7 +149,7 @@ if (DEBUG) {
|
||||
debuglog = logger.log.bind(console);
|
||||
}
|
||||
|
||||
interface IRoomProps {
|
||||
interface IRoomProps extends RoomViewProps {
|
||||
threepidInvite?: IThreepidInvite;
|
||||
oobData?: IOOBData;
|
||||
|
||||
@@ -158,19 +159,17 @@ interface IRoomProps {
|
||||
|
||||
// Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU)
|
||||
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||
|
||||
/**
|
||||
* The RoomViewStore instance for the room to be displayed.
|
||||
* Only necessary if RoomView should get it's RoomViewStore through the MultiRoomViewStore.
|
||||
* Omitting this will mean that RoomView renders for the room held in SDKContext.RoomViewStore.
|
||||
*/
|
||||
roomViewStore: RoomViewStore;
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
export { MainSplitContentType };
|
||||
|
||||
export interface IRoomState {
|
||||
/**
|
||||
* The RoomViewStore instance for the room we are displaying
|
||||
*/
|
||||
roomViewStore: RoomViewStore;
|
||||
room?: Room;
|
||||
roomId?: string;
|
||||
roomAlias?: string;
|
||||
@@ -389,6 +388,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
private messagePanel: TimelinePanel | null = null;
|
||||
private roomViewBody = createRef<HTMLDivElement>();
|
||||
|
||||
private roomViewStore: RoomViewStore;
|
||||
|
||||
public static contextType = SDKContext;
|
||||
declare public context: React.ContextType<typeof SDKContext>;
|
||||
|
||||
@@ -401,9 +402,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
throw new Error("Unable to create RoomView without MatrixClient");
|
||||
}
|
||||
|
||||
if (props.roomId) {
|
||||
this.roomViewStore = this.context.multiRoomViewStore.getRoomViewStoreForRoom(props.roomId);
|
||||
} else {
|
||||
this.roomViewStore = context.roomViewStore;
|
||||
}
|
||||
|
||||
const llMembers = context.client.hasLazyLoadMembersEnabled();
|
||||
this.state = {
|
||||
roomViewStore: props.roomViewStore,
|
||||
roomId: undefined,
|
||||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
@@ -535,7 +541,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
};
|
||||
|
||||
private getMainSplitContentType = (room: Room): MainSplitContentType => {
|
||||
if (this.state.roomViewStore.isViewingCall() || isVideoRoom(room)) {
|
||||
if (this.roomViewStore.isViewingCall() || isVideoRoom(room)) {
|
||||
return MainSplitContentType.Call;
|
||||
}
|
||||
if (this.context.widgetLayoutStore.hasMaximisedWidget(room)) {
|
||||
@@ -549,8 +555,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
return;
|
||||
}
|
||||
|
||||
const roomLoadError = this.state.roomViewStore.getRoomLoadError() ?? undefined;
|
||||
if (!initial && !roomLoadError && this.state.roomId !== this.state.roomViewStore.getRoomId()) {
|
||||
const roomLoadError = this.roomViewStore.getRoomLoadError() ?? undefined;
|
||||
if (!initial && !roomLoadError && this.state.roomId !== this.roomViewStore.getRoomId()) {
|
||||
// RoomView explicitly does not support changing what room
|
||||
// is being viewed: instead it should just be re-mounted when
|
||||
// switching rooms. Therefore, if the room ID changes, we
|
||||
@@ -564,7 +570,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
// it was, it means we're about to be unmounted.
|
||||
return;
|
||||
}
|
||||
const roomViewStore = this.state.roomViewStore;
|
||||
const roomViewStore = this.roomViewStore;
|
||||
const roomId = roomViewStore.getRoomId() ?? null;
|
||||
const roomAlias = roomViewStore.getRoomAlias() ?? undefined;
|
||||
const roomLoading = roomViewStore.isRoomLoading();
|
||||
@@ -611,7 +617,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
newState.showRightPanel = false;
|
||||
}
|
||||
|
||||
const initialEventId = this.state.roomViewStore.getInitialEventId() ?? this.state.initialEventId;
|
||||
const initialEventId = this.roomViewStore.getInitialEventId() ?? this.state.initialEventId;
|
||||
if (initialEventId) {
|
||||
let initialEvent = room?.findEventById(initialEventId);
|
||||
// The event does not exist in the current sync data
|
||||
@@ -637,13 +643,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
action: Action.ShowThread,
|
||||
rootEvent: thread.rootEvent,
|
||||
initialEvent,
|
||||
highlighted: this.state.roomViewStore.isInitialEventHighlighted(),
|
||||
scroll_into_view: this.state.roomViewStore.initialEventScrollIntoView(),
|
||||
highlighted: this.roomViewStore.isInitialEventHighlighted(),
|
||||
scroll_into_view: this.roomViewStore.initialEventScrollIntoView(),
|
||||
});
|
||||
} else {
|
||||
newState.initialEventId = initialEventId;
|
||||
newState.isInitialEventHighlighted = this.state.roomViewStore.isInitialEventHighlighted();
|
||||
newState.initialEventScrollIntoView = this.state.roomViewStore.initialEventScrollIntoView();
|
||||
newState.isInitialEventHighlighted = this.roomViewStore.isInitialEventHighlighted();
|
||||
newState.initialEventScrollIntoView = this.roomViewStore.initialEventScrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,7 +909,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
this.context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
}
|
||||
// Start listening for RoomViewStore updates
|
||||
this.state.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
this.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
|
||||
this.context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
|
||||
@@ -1020,7 +1026,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
window.removeEventListener("beforeunload", this.onPageUnload);
|
||||
|
||||
this.state.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
this.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
|
||||
this.context.rightPanelStore.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||
@@ -1048,6 +1054,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
// clean up if this was a local room
|
||||
this.context.client?.store.removeRoom(this.state.room.roomId);
|
||||
}
|
||||
|
||||
if (this.props.roomId) this.context.multiRoomViewStore.removeRoomViewStore(this.props.roomId);
|
||||
}
|
||||
|
||||
private onRightPanelStoreUpdate = (): void => {
|
||||
@@ -2070,7 +2078,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
if (!this.state.room || !this.context?.client) return null;
|
||||
const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId());
|
||||
return (
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||
<LocalRoomCreateLoader
|
||||
localRoom={localRoom}
|
||||
names={names}
|
||||
@@ -2082,7 +2090,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
private renderLocalRoomView(localRoom: LocalRoom): ReactNode {
|
||||
return (
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||
<LocalRoomView
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
localRoom={localRoom}
|
||||
@@ -2098,7 +2106,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode {
|
||||
return (
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||
<WaitingForThirdPartyRoomView
|
||||
resizeNotifier={this.context.resizeNotifier}
|
||||
roomView={this.roomView}
|
||||
@@ -2640,7 +2648,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||
{showChatEffects && this.roomView.current && (
|
||||
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||
|
||||
@@ -68,6 +68,8 @@ import { ThreadsActivityCentre } from "./threads-activity-centre/";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation";
|
||||
import { KeyboardShortcut } from "../settings/KeyboardShortcut";
|
||||
import { ModuleApi } from "../../../modules/Api.ts";
|
||||
import { useModuleSpacePanelItems } from "../../../modules/ExtrasApi.ts";
|
||||
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement";
|
||||
|
||||
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
|
||||
@@ -290,6 +292,8 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
|
||||
const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces();
|
||||
const activeSpaces = activeSpace ? [activeSpace] : [];
|
||||
|
||||
const moduleSpaceItems = useModuleSpacePanelItems(ModuleApi.instance.extras);
|
||||
|
||||
const metaSpacesSection = metaSpaces
|
||||
.filter((key) => !(key === MetaSpace.VideoRooms && !SettingsStore.getValue("feature_video_rooms")))
|
||||
.map((key) => {
|
||||
@@ -341,6 +345,27 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
|
||||
</Draggable>
|
||||
))}
|
||||
{children}
|
||||
{moduleSpaceItems.map((item) => (
|
||||
<li
|
||||
key={item.spaceKey}
|
||||
className={classNames("mx_SpaceItem", {
|
||||
collapsed: isPanelCollapsed,
|
||||
})}
|
||||
role="treeitem"
|
||||
aria-selected={false} // TODO
|
||||
>
|
||||
<SpaceButton
|
||||
{...item}
|
||||
isNarrow={isPanelCollapsed}
|
||||
size="32px"
|
||||
selected={activeSpace === item.spaceKey}
|
||||
onClick={() => {
|
||||
SpaceStore.instance.setActiveSpace(item.spaceKey);
|
||||
item.onSelected?.();
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{shouldShowComponent(UIComponent.CreateSpaces) && (
|
||||
<CreateSpaceButton isPanelCollapsed={isPanelCollapsed} setPanelCollapsed={setPanelCollapsed} />
|
||||
)}
|
||||
|
||||
@@ -52,6 +52,7 @@ type ButtonProps<T extends keyof HTMLElementTagNameMap> = Omit<
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
label: string;
|
||||
icon?: JSX.Element;
|
||||
contextMenuTooltip?: string;
|
||||
notificationState?: NotificationState;
|
||||
isNarrow?: boolean;
|
||||
@@ -65,6 +66,7 @@ export const SpaceButton = <T extends keyof HTMLElementTagNameMap>({
|
||||
space,
|
||||
spaceKey: _spaceKey,
|
||||
className,
|
||||
icon,
|
||||
selected,
|
||||
label,
|
||||
contextMenuTooltip,
|
||||
@@ -84,7 +86,7 @@ export const SpaceButton = <T extends keyof HTMLElementTagNameMap>({
|
||||
|
||||
let avatar = (
|
||||
<div className="mx_SpaceButton_avatarPlaceholder">
|
||||
<div className="mx_SpaceButton_icon" />
|
||||
<div className="mx_SpaceButton_icon">{icon}</div>
|
||||
</div>
|
||||
);
|
||||
if (space) {
|
||||
@@ -143,6 +145,7 @@ export const SpaceButton = <T extends keyof HTMLElementTagNameMap>({
|
||||
mx_SpaceButton_active: selected,
|
||||
mx_SpaceButton_hasMenuOpen: menuDisplayed,
|
||||
mx_SpaceButton_narrow: isNarrow,
|
||||
mx_SpaceButton_withIcon: Boolean(icon),
|
||||
})}
|
||||
aria-label={label}
|
||||
title={!isNarrow || menuDisplayed ? undefined : label}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createContext } from "react";
|
||||
|
||||
import { type IRoomState } from "../components/structures/RoomView";
|
||||
import { Layout } from "../settings/enums/Layout";
|
||||
import { type RoomViewStore } from "../stores/RoomViewStore";
|
||||
|
||||
export enum TimelineRenderingType {
|
||||
Room = "Room",
|
||||
@@ -29,11 +30,12 @@ export enum MainSplitContentType {
|
||||
Call,
|
||||
}
|
||||
|
||||
const RoomContext = createContext<
|
||||
IRoomState & {
|
||||
threadId?: string;
|
||||
}
|
||||
>({
|
||||
export interface RoomContextType extends IRoomState {
|
||||
threadId?: string;
|
||||
roomViewStore: RoomViewStore;
|
||||
}
|
||||
|
||||
const RoomContext = createContext<RoomContextType>({
|
||||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
shouldPeek: true,
|
||||
|
||||
@@ -25,6 +25,7 @@ import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore";
|
||||
import { OidcClientStore } from "../stores/oidc/OidcClientStore";
|
||||
import WidgetStore from "../stores/WidgetStore";
|
||||
import ResizeNotifier from "../utils/ResizeNotifier";
|
||||
import { MultiRoomViewStore } from "../stores/MultiRoomViewStore";
|
||||
|
||||
// This context is available to components under MatrixChat,
|
||||
// the context must not be used by components outside a SdkContextClass tree.
|
||||
@@ -66,6 +67,7 @@ export class SdkContextClass {
|
||||
protected _UserProfilesStore?: UserProfilesStore;
|
||||
protected _OidcClientStore?: OidcClientStore;
|
||||
protected _ResizeNotifier?: ResizeNotifier;
|
||||
protected _MultiRoomViewStore?: MultiRoomViewStore;
|
||||
|
||||
/**
|
||||
* Automatically construct stores which need to be created eagerly so they can register with
|
||||
@@ -183,6 +185,13 @@ export class SdkContextClass {
|
||||
return this._ResizeNotifier;
|
||||
}
|
||||
|
||||
public get multiRoomViewStore(): MultiRoomViewStore {
|
||||
if (!this._MultiRoomViewStore) {
|
||||
this._MultiRoomViewStore = new MultiRoomViewStore(defaultDispatcher, this);
|
||||
}
|
||||
return this._MultiRoomViewStore;
|
||||
}
|
||||
|
||||
public onLoggedOut(): void {
|
||||
this._UserProfilesStore = undefined;
|
||||
}
|
||||
|
||||
54
src/modules/AccountDataApi.ts
Normal file
54
src/modules/AccountDataApi.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Watchable, type AccountDataApi as IAccountDataApi } from "@element-hq/element-web-module-api";
|
||||
import { ClientEvent, type MatrixEvent, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
|
||||
export class AccountDataApi implements IAccountDataApi {
|
||||
public get(eventType: string): Watchable<unknown> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
return new AccountDataWatchable(cli, eventType);
|
||||
}
|
||||
|
||||
public async set(eventType: string, content: any): Promise<void> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
//@ts-expect-error: JS-SDK accepts known event-types, intentionally allow arbitrary types.
|
||||
await cli.setAccountData(eventType, content);
|
||||
}
|
||||
|
||||
public async delete(eventType: string): Promise<void> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
//@ts-expect-error: JS-SDK accepts known event-types, intentionally allow arbitrary types.
|
||||
await cli.deleteAccountData(eventType);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountDataWatchable extends Watchable<unknown> {
|
||||
public constructor(
|
||||
private cli: MatrixClient,
|
||||
private eventType: string,
|
||||
) {
|
||||
//@ts-expect-error: JS-SDK accepts known event-types, intentionally allow arbitrary types.
|
||||
super(cli.getAccountData(eventType)?.getContent());
|
||||
}
|
||||
|
||||
private onAccountData = (event: MatrixEvent): void => {
|
||||
if (event.getType() === this.eventType) {
|
||||
this.value = event.getContent();
|
||||
}
|
||||
};
|
||||
|
||||
protected onFirstWatch(): void {
|
||||
this.cli.on(ClientEvent.AccountData, this.onAccountData);
|
||||
}
|
||||
|
||||
protected onLastWatch(): void {
|
||||
this.cli.off(ClientEvent.AccountData, this.onAccountData);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ import { WatchableProfile } from "./Profile.ts";
|
||||
import { NavigationApi } from "./Navigation.ts";
|
||||
import { openDialog } from "./Dialog.tsx";
|
||||
import { overwriteAccountAuth } from "./Auth.ts";
|
||||
import { ElementWebExtrasApi } from "./ExtrasApi.ts";
|
||||
import { ElementWebBuiltinsApi } from "./BuiltinsApi.tsx";
|
||||
import { ClientApi } from "./ClientApi.ts";
|
||||
|
||||
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
|
||||
let used = false;
|
||||
@@ -79,7 +82,10 @@ export class ModuleApi implements Api {
|
||||
public readonly config = new ConfigApi();
|
||||
public readonly i18n = new I18nApi();
|
||||
public readonly customComponents = new CustomComponentsApi();
|
||||
public readonly extras = new ElementWebExtrasApi();
|
||||
public readonly builtins = new ElementWebBuiltinsApi();
|
||||
public readonly rootNode = document.getElementById("matrixchat")!;
|
||||
public readonly client = new ClientApi();
|
||||
|
||||
public createRoot(element: Element): Root {
|
||||
return createRoot(element);
|
||||
|
||||
75
src/modules/BuiltinsApi.tsx
Normal file
75
src/modules/BuiltinsApi.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { type RoomViewProps, type BuiltinsApi } from "@element-hq/element-web-module-api";
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
interface RoomViewPropsWithRoomId extends RoomViewProps {
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
interface RoomAvatarProps {
|
||||
room: Room;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
interface Components {
|
||||
roomView: React.ComponentType<RoomViewPropsWithRoomId>;
|
||||
roomAvatar: React.ComponentType<RoomAvatarProps>;
|
||||
}
|
||||
|
||||
export class ElementWebBuiltinsApi implements BuiltinsApi {
|
||||
private _roomView?: React.ComponentType<RoomViewPropsWithRoomId>;
|
||||
private _roomAvatar?: React.ComponentType<RoomAvatarProps>;
|
||||
|
||||
/**
|
||||
* Sets the components used by the API.
|
||||
*
|
||||
* This only really exists here because referencing these components directly causes a nightmare of
|
||||
* circular dependencies that break the whole app, so instead we avoid referencing it here
|
||||
* and pass it in from somewhere it's already referenced (see related comment in app.tsx).
|
||||
*
|
||||
* @param component The components used by the api, see {@link Components}
|
||||
*/
|
||||
public setComponents(components: Components): void {
|
||||
this._roomView = components.roomView;
|
||||
this._roomAvatar = components.roomAvatar;
|
||||
}
|
||||
|
||||
public getRoomViewComponent(): React.ComponentType<RoomViewPropsWithRoomId> {
|
||||
if (!this._roomView) {
|
||||
throw new Error("No RoomView component has been set");
|
||||
}
|
||||
|
||||
return this._roomView;
|
||||
}
|
||||
|
||||
public getRoomAvatarComponent(): React.ComponentType<RoomAvatarProps> {
|
||||
if (!this._roomAvatar) {
|
||||
throw new Error("No RoomAvatar component has been set");
|
||||
}
|
||||
|
||||
return this._roomAvatar;
|
||||
}
|
||||
|
||||
public renderRoomView(roomId: string): React.ReactNode {
|
||||
const Component = this.getRoomViewComponent();
|
||||
return <Component roomId={roomId} />;
|
||||
}
|
||||
|
||||
public renderRoomAvatar(roomId: string, size?: string): React.ReactNode {
|
||||
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (!room) {
|
||||
throw new Error(`No room such room: ${roomId}`);
|
||||
}
|
||||
const Component = this.getRoomAvatarComponent();
|
||||
return <Component room={room} size={size} />;
|
||||
}
|
||||
}
|
||||
25
src/modules/ClientApi.ts
Normal file
25
src/modules/ClientApi.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
import type { ClientApi as IClientApi, Room } from "@element-hq/element-web-module-api";
|
||||
import { Room as ModuleRoom } from "./models/Room";
|
||||
import { AccountDataApi } from "./AccountDataApi";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
|
||||
export class ClientApi implements IClientApi {
|
||||
private accountDataApi?: AccountDataApi;
|
||||
|
||||
public getRoom(roomId: string): Room | null {
|
||||
const sdkRoom = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (sdkRoom) return new ModuleRoom(sdkRoom);
|
||||
return null;
|
||||
}
|
||||
|
||||
public get accountData(): AccountDataApi {
|
||||
this.accountDataApi ??= new AccountDataApi();
|
||||
return this.accountDataApi;
|
||||
}
|
||||
}
|
||||
50
src/modules/ExtrasApi.ts
Normal file
50
src/modules/ExtrasApi.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { type SpacePanelItemProps, type ExtrasApi } from "@element-hq/element-web-module-api";
|
||||
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { useTypedEventEmitter } from "../hooks/useEventEmitter";
|
||||
|
||||
export interface ModuleSpacePanelItem extends SpacePanelItemProps {
|
||||
spaceKey: string;
|
||||
}
|
||||
|
||||
enum ExtrasApiEvent {
|
||||
SpacePanelItemsChanged = "SpacePanelItemsChanged",
|
||||
}
|
||||
|
||||
interface EmittedEvents {
|
||||
[ExtrasApiEvent.SpacePanelItemsChanged]: () => void;
|
||||
}
|
||||
|
||||
export class ElementWebExtrasApi extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents> implements ExtrasApi {
|
||||
public spacePanelItems = new Map<string, SpacePanelItemProps>();
|
||||
|
||||
public setSpacePanelItem(spacekey: string, item: SpacePanelItemProps): void {
|
||||
this.spacePanelItems.set(spacekey, item);
|
||||
this.emit(ExtrasApiEvent.SpacePanelItemsChanged);
|
||||
}
|
||||
}
|
||||
|
||||
export function useModuleSpacePanelItems(api: ElementWebExtrasApi): ModuleSpacePanelItem[] {
|
||||
const getItems = (): ModuleSpacePanelItem[] => {
|
||||
return Array.from(api.spacePanelItems.entries()).map(([spaceKey, item]) => ({
|
||||
spaceKey,
|
||||
...item,
|
||||
}));
|
||||
};
|
||||
|
||||
const [items, setItems] = useState<ModuleSpacePanelItem[]>(getItems);
|
||||
|
||||
useTypedEventEmitter(api, ExtrasApiEvent.SpacePanelItemsChanged, () => {
|
||||
setItems(getItems());
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -5,8 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type NavigationApi as INavigationApi } from "@element-hq/element-web-module-api";
|
||||
|
||||
import type {
|
||||
LocationRenderFunction,
|
||||
NavigationApi as INavigationApi,
|
||||
OpenRoomOptions,
|
||||
} from "@element-hq/element-web-module-api";
|
||||
import { navigateToPermalink } from "../utils/permalinks/navigator.ts";
|
||||
import { parsePermalink } from "../utils/permalinks/Permalinks.ts";
|
||||
import dispatcher from "../dispatcher/dispatcher.ts";
|
||||
@@ -14,28 +17,32 @@ import { Action } from "../dispatcher/actions.ts";
|
||||
import type { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload.ts";
|
||||
|
||||
export class NavigationApi implements INavigationApi {
|
||||
public locationRenderers = new Map<string, LocationRenderFunction>();
|
||||
|
||||
public async toMatrixToLink(link: string, join = false): Promise<void> {
|
||||
navigateToPermalink(link);
|
||||
|
||||
const parts = parsePermalink(link);
|
||||
if (parts?.roomIdOrAlias) {
|
||||
if (parts.roomIdOrAlias.startsWith("#")) {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_alias: parts.roomIdOrAlias,
|
||||
via_servers: parts.viaServers ?? undefined,
|
||||
auto_join: join,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
} else {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: parts.roomIdOrAlias,
|
||||
via_servers: parts.viaServers ?? undefined,
|
||||
auto_join: join,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
}
|
||||
this.openRoom(parts.roomIdOrAlias, {
|
||||
viaServers: parts.viaServers ?? undefined,
|
||||
autoJoin: join,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public registerLocationRenderer(path: string, renderer: LocationRenderFunction): void {
|
||||
this.locationRenderers.set(path, renderer);
|
||||
}
|
||||
|
||||
public openRoom(roomIdOrAlias: string, opts: OpenRoomOptions = {}): void {
|
||||
const key = roomIdOrAlias.startsWith("#") ? "room_alias" : "room_id";
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
[key]: roomIdOrAlias,
|
||||
via_servers: opts.viaServers,
|
||||
auto_join: opts.autoJoin,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
45
src/modules/models/Room.ts
Normal file
45
src/modules/models/Room.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Room as IRoom, Watchable } from "@element-hq/element-web-module-api";
|
||||
import { RoomEvent, type Room as SdkRoom } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
export class Room implements IRoom {
|
||||
public name: Watchable<string>;
|
||||
|
||||
public constructor(private sdkRoom: SdkRoom) {
|
||||
this.name = new WatchableName(sdkRoom);
|
||||
}
|
||||
|
||||
public getLastActiveTimestamp(): number {
|
||||
return this.sdkRoom.getLastActiveTimestamp();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.sdkRoom.roomId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom watchable for room name.
|
||||
*/
|
||||
class WatchableName extends Watchable<string> {
|
||||
public constructor(private sdkRoom: SdkRoom) {
|
||||
super(sdkRoom.name);
|
||||
}
|
||||
|
||||
private onNameUpdate = (): void => {
|
||||
super.value = this.sdkRoom.name;
|
||||
};
|
||||
protected onFirstWatch(): void {
|
||||
this.sdkRoom.on(RoomEvent.Name, this.onNameUpdate);
|
||||
}
|
||||
|
||||
protected onLastWatch(): void {
|
||||
this.sdkRoom.off(RoomEvent.Name, this.onNameUpdate);
|
||||
}
|
||||
}
|
||||
67
src/stores/MultiRoomViewStore.ts
Normal file
67
src/stores/MultiRoomViewStore.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { RoomViewStore } from "./RoomViewStore";
|
||||
import { type MatrixDispatcher } from "../dispatcher/dispatcher";
|
||||
import { type SdkContextClass } from "../contexts/SDKContext";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
|
||||
/**
|
||||
* Acts as a cache of many RoomViewStore instances, creating them as necessary
|
||||
* given a room ID.
|
||||
*/
|
||||
export class MultiRoomViewStore {
|
||||
/**
|
||||
* Map from room-id to RVS instance.
|
||||
*/
|
||||
private stores: Map<string, RoomViewStore> = new Map();
|
||||
|
||||
public constructor(
|
||||
private dispatcher: MatrixDispatcher,
|
||||
private sdkContextClass: SdkContextClass,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a RVS instance for the room identified by the given roomId.
|
||||
*/
|
||||
public getRoomViewStoreForRoom(roomId: string): RoomViewStore {
|
||||
// Get existing store / create new store
|
||||
const store = this.stores.has(roomId)
|
||||
? this.stores.get(roomId)!
|
||||
: new RoomViewStore(this.dispatcher, this.sdkContextClass, roomId);
|
||||
|
||||
// RoomView component does not render the room unless you call viewRoom
|
||||
store.viewRoom({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
|
||||
// Cache the store, okay to do even if the store is already in the map
|
||||
this.stores.set(roomId, store);
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a RVS instance that was created by {@link getRoomViewStoreForRoom}.
|
||||
*/
|
||||
public removeRoomViewStore(roomId: string): void {
|
||||
const didRemove = this.stores.delete(roomId);
|
||||
if (!didRemove) {
|
||||
logger.warn(`removeRoomViewStore called with ${roomId} but no store exists for this room.`);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const id of this.stores.keys()) {
|
||||
this.removeRoomViewStore(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +153,7 @@ export class RoomViewStore extends EventEmitter {
|
||||
public constructor(
|
||||
dis: MatrixDispatcher,
|
||||
private readonly stores: SdkContextClass,
|
||||
private readonly lockedToRoomId?: string,
|
||||
) {
|
||||
super();
|
||||
this.resetDispatcher(dis);
|
||||
@@ -187,7 +188,7 @@ export class RoomViewStore extends EventEmitter {
|
||||
|
||||
const lastRoomId = this.state.roomId;
|
||||
this.state = Object.assign(this.state, newState);
|
||||
if (lastRoomId !== this.state.roomId) {
|
||||
if (!this.lockedToRoomId && lastRoomId !== this.state.roomId) {
|
||||
if (lastRoomId) this.emitForRoom(lastRoomId, false);
|
||||
if (this.state.roomId) this.emitForRoom(this.state.roomId, true);
|
||||
|
||||
@@ -204,6 +205,9 @@ export class RoomViewStore extends EventEmitter {
|
||||
}
|
||||
|
||||
private onDispatch(payload: ActionPayload): void {
|
||||
if (this.lockedToRoomId && payload.room_id && this.lockedToRoomId !== payload.room_id) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-line @typescript-eslint/naming-convention
|
||||
switch (payload.action) {
|
||||
// view_room:
|
||||
@@ -324,7 +328,7 @@ export class RoomViewStore extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private async viewRoom(payload: ViewRoomPayload): Promise<void> {
|
||||
public async viewRoom(payload: ViewRoomPayload): Promise<void> {
|
||||
if (payload.room_id) {
|
||||
const room = MatrixClientPeg.safeGet().getRoom(payload.room_id);
|
||||
|
||||
|
||||
@@ -12,15 +12,15 @@ import { NotificationLevel } from "./NotificationLevel";
|
||||
import { arrayDiff } from "../../utils/arrays";
|
||||
import { type RoomNotificationState } from "./RoomNotificationState";
|
||||
import { NotificationState, NotificationStateEvents } from "./NotificationState";
|
||||
import { type FetchRoomFn } from "./ListNotificationState";
|
||||
import { DefaultTagID } from "../room-list/models";
|
||||
import RoomListStore from "../room-list/RoomListStore";
|
||||
import { RoomNotificationStateStore } from "./RoomNotificationStateStore";
|
||||
|
||||
export class SpaceNotificationState extends NotificationState {
|
||||
public rooms: Room[] = []; // exposed only for tests
|
||||
private states: { [spaceId: string]: RoomNotificationState } = {};
|
||||
|
||||
public constructor(private getRoomFn: FetchRoomFn) {
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class SpaceNotificationState extends NotificationState {
|
||||
state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
|
||||
}
|
||||
for (const newRoom of diff.added) {
|
||||
const state = this.getRoomFn(newRoom);
|
||||
const state = RoomNotificationStateStore.instance.getRoomState(newRoom);
|
||||
state.on(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
|
||||
this.states[newRoom.roomId] = state;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import RoomListStore from "../room-list/RoomListStore";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import DMRoomMap from "../../utils/DMRoomMap";
|
||||
import { type FetchRoomFn } from "../notifications/ListNotificationState";
|
||||
import { SpaceNotificationState } from "../notifications/SpaceNotificationState";
|
||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||
import { DefaultTagID } from "../room-list/models";
|
||||
@@ -63,6 +62,7 @@ import { type ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePage
|
||||
import { type SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload";
|
||||
import { type AfterLeaveRoomPayload } from "../../dispatcher/payloads/AfterLeaveRoomPayload";
|
||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||
import { ModuleApi } from "../../modules/Api.ts";
|
||||
|
||||
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
|
||||
|
||||
@@ -111,10 +111,6 @@ export const getChildOrder = (
|
||||
return [validOrder(order) ?? NaN, ts, roomId]; // NaN has lodash sort it at the end in asc
|
||||
};
|
||||
|
||||
const getRoomFn: FetchRoomFn = (room: Room) => {
|
||||
return RoomNotificationStateStore.instance.getRoomState(room);
|
||||
};
|
||||
|
||||
type SpaceStoreActions =
|
||||
| SettingUpdatedPayload
|
||||
| ViewRoomPayload
|
||||
@@ -258,7 +254,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
|
||||
if (!space || !this.matrixClient || space === this.activeSpace) return;
|
||||
|
||||
let cliSpace: Room | null = null;
|
||||
if (!isMetaSpace(space)) {
|
||||
if (ModuleApi.instance.extras.spacePanelItems.has(space)) {
|
||||
// it's a "space" provided by a module: that's good enough
|
||||
} else if (!isMetaSpace(space)) {
|
||||
cliSpace = this.matrixClient.getRoom(space);
|
||||
if (!cliSpace?.isSpaceRoom()) return;
|
||||
} else if (!this.enabledMetaSpaces.includes(space)) {
|
||||
@@ -293,6 +291,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
|
||||
context_switch: true,
|
||||
metricsTrigger: "WebSpaceContextSwitch",
|
||||
});
|
||||
} else if (ModuleApi.instance.extras.spacePanelItems.has(space)) {
|
||||
// module will handle this
|
||||
} else {
|
||||
defaultDispatcher.dispatch<ViewHomePagePayload>({
|
||||
action: Action.ViewHomePage,
|
||||
@@ -1214,7 +1214,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
|
||||
const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY) as MetaSpace;
|
||||
const valid =
|
||||
lastSpaceId &&
|
||||
(!isMetaSpace(lastSpaceId) ? this.matrixClient.getRoom(lastSpaceId) : enabledMetaSpaces[lastSpaceId]);
|
||||
(ModuleApi.instance.extras.spacePanelItems.has(lastSpaceId) ||
|
||||
(!isMetaSpace(lastSpaceId) ? this.matrixClient.getRoom(lastSpaceId) : enabledMetaSpaces[lastSpaceId]));
|
||||
if (valid) {
|
||||
// don't context switch here as it may break permalinks
|
||||
this.setActiveSpace(lastSpaceId, false);
|
||||
@@ -1369,7 +1370,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
|
||||
return this.notificationStateMap.get(key)!;
|
||||
}
|
||||
|
||||
const state = new SpaceNotificationState(getRoomFn);
|
||||
const state = new SpaceNotificationState();
|
||||
this.notificationStateMap.set(key, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Room, type HierarchyRoom } from "matrix-js-sdk/src/matrix";
|
||||
import { type HierarchyRoom } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../languageHandler";
|
||||
|
||||
@@ -42,7 +42,7 @@ export const getMetaSpaceName = (spaceKey: MetaSpace, allRoomsInHome = false): s
|
||||
}
|
||||
};
|
||||
|
||||
export type SpaceKey = MetaSpace | Room["roomId"];
|
||||
export type SpaceKey = string;
|
||||
|
||||
export interface ISuggestedRoom extends HierarchyRoom {
|
||||
viaServers: string[];
|
||||
|
||||
@@ -30,6 +30,9 @@ import { ModuleRunner } from "../modules/ModuleRunner";
|
||||
import { parseQs } from "./url_utils";
|
||||
import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting, onNewScreen } from "./routing";
|
||||
import { UserFriendlyError } from "../languageHandler";
|
||||
import { ModuleApi } from "../modules/Api";
|
||||
import { RoomView } from "../components/structures/RoomView";
|
||||
import RoomAvatar from "../components/views/avatars/RoomAvatar";
|
||||
|
||||
logger.log(`Application is running in ${process.env.NODE_ENV} mode`);
|
||||
|
||||
@@ -53,6 +56,10 @@ function onTokenLoginCompleted(): void {
|
||||
}
|
||||
|
||||
export async function loadApp(fragParams: QueryDict, matrixChatRef: React.Ref<MatrixChat>): Promise<ReactElement> {
|
||||
// XXX: This lives here because certain components import so many things that importing it in a sensible place (eg.
|
||||
// the builtins module or init.tsx) causes a circular dependency.
|
||||
ModuleApi.instance.builtins.setComponents({ roomView: RoomView, roomAvatar: RoomAvatar });
|
||||
|
||||
initRouting();
|
||||
const platform = PlatformPeg.get();
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import { type MockedObject } from "jest-mock";
|
||||
import { type EventTimeline, EventType, type MatrixClient, type MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { type IRoomState, MainSplitContentType } from "../../src/components/structures/RoomView";
|
||||
import { TimelineRenderingType } from "../../src/contexts/RoomContext";
|
||||
import { MainSplitContentType } from "../../src/components/structures/RoomView";
|
||||
import { type RoomContextType, TimelineRenderingType } from "../../src/contexts/RoomContext";
|
||||
import { Layout } from "../../src/settings/enums/Layout";
|
||||
import { mkEvent } from "./test-utils";
|
||||
import { SdkContextClass } from "../../src/contexts/SDKContext";
|
||||
@@ -43,7 +43,7 @@ export const makeRoomWithStateEvents = (
|
||||
return room1;
|
||||
};
|
||||
|
||||
export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoomState {
|
||||
export function getRoomContext(room: Room, override: Partial<RoomContextType>): RoomContextType {
|
||||
return {
|
||||
roomViewStore: SdkContextClass.instance.roomViewStore,
|
||||
room,
|
||||
|
||||
@@ -650,6 +650,7 @@ export function mkStubRoom(
|
||||
getJoinedMembers: jest.fn().mockReturnValue([]),
|
||||
getLiveTimeline: jest.fn().mockReturnValue(stubTimeline),
|
||||
getLastLiveEvent: jest.fn().mockReturnValue(undefined),
|
||||
getLastActiveTimestamp: jest.fn().mockReturnValue(1183140000),
|
||||
getMember: jest.fn().mockReturnValue({
|
||||
userId: "@member:domain.bla",
|
||||
name: "Member",
|
||||
|
||||
@@ -15,7 +15,7 @@ import { render } from "jest-matrix-react";
|
||||
|
||||
import MessagePanel, { shouldFormContinuation } from "../../../../src/components/structures/MessagePanel";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||
import RoomContext, { type RoomContextType, TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import * as TestUtilsMatrix from "../../../test-utils";
|
||||
import {
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
mockClientPushProcessor,
|
||||
} from "../../../test-utils";
|
||||
import type ResizeNotifier from "../../../../src/utils/ResizeNotifier";
|
||||
import { type IRoomState } from "../../../../src/components/structures/RoomView";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext.ts";
|
||||
@@ -92,9 +91,9 @@ describe("MessagePanel", function () {
|
||||
showAvatarChanges: false,
|
||||
showDisplaynameChanges: true,
|
||||
showHiddenEvents: false,
|
||||
} as unknown as IRoomState;
|
||||
} as unknown as RoomContextType;
|
||||
|
||||
const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) => (
|
||||
const getComponent = (props = {}, roomContext: Partial<RoomContextType> = {}) => (
|
||||
<ScopedRoomContextProvider {...defaultRoomContext} {...roomContext}>
|
||||
<MessagePanel {...defaultProps} {...props} />
|
||||
</ScopedRoomContextProvider>
|
||||
|
||||
@@ -89,7 +89,6 @@ describe("RoomView", () => {
|
||||
let cli: MockedObject<MatrixClient>;
|
||||
let room: Room;
|
||||
let rooms: Map<string, Room>;
|
||||
let roomCount = 0;
|
||||
let stores: SdkContextClass;
|
||||
let crypto: CryptoApi;
|
||||
|
||||
@@ -100,7 +99,9 @@ describe("RoomView", () => {
|
||||
mockPlatformPeg({ reload: () => {} });
|
||||
cli = mocked(stubClient());
|
||||
|
||||
room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
|
||||
const roomName = (expect.getState().currentTestName ?? "").replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
||||
|
||||
room = new Room(`!${roomName}:example.org`, cli, "@alice:example.org");
|
||||
jest.spyOn(room, "findPredecessor");
|
||||
room.getPendingEvents = () => [];
|
||||
rooms = new Map();
|
||||
@@ -158,7 +159,6 @@ describe("RoomView", () => {
|
||||
threepidInvite={undefined as any}
|
||||
forceTimeline={false}
|
||||
ref={ref}
|
||||
roomViewStore={stores.roomViewStore}
|
||||
/>
|
||||
</SDKContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
@@ -197,7 +197,6 @@ describe("RoomView", () => {
|
||||
threepidInvite={undefined}
|
||||
forceTimeline={false}
|
||||
onRegistered={jest.fn()}
|
||||
roomViewStore={stores.roomViewStore}
|
||||
/>
|
||||
</SDKContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
@@ -211,6 +210,26 @@ describe("RoomView", () => {
|
||||
return ref.current!;
|
||||
};
|
||||
|
||||
it("gets a room view store from MultiRoomViewStore when given a room ID", async () => {
|
||||
stores.multiRoomViewStore.getRoomViewStoreForRoom = jest.fn().mockReturnValue(stores.roomViewStore);
|
||||
|
||||
const ref = createRef<RoomView>();
|
||||
render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<SDKContext.Provider value={stores}>
|
||||
<RoomView
|
||||
threepidInvite={undefined as any}
|
||||
forceTimeline={false}
|
||||
ref={ref}
|
||||
roomId="!room:example.dummy"
|
||||
/>
|
||||
</SDKContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
expect(stores.multiRoomViewStore.getRoomViewStoreForRoom).toHaveBeenCalledWith("!room:example.dummy");
|
||||
});
|
||||
|
||||
it("should show member list right panel phase on Action.ViewUser without `payload.member`", async () => {
|
||||
const spy = jest.spyOn(stores.rightPanelStore, "showOrHidePhase");
|
||||
await renderRoomView(false);
|
||||
@@ -707,7 +726,7 @@ describe("RoomView", () => {
|
||||
});
|
||||
|
||||
it("should switch rooms when edit is clicked on a search result for a different room", async () => {
|
||||
const room2 = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
|
||||
const room2 = new Room(`!roomswitchtest:example.org`, cli, "@alice:example.org");
|
||||
rooms.set(room2.roomId, room2);
|
||||
|
||||
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
|
||||
|
||||
@@ -26,8 +26,8 @@ import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalink
|
||||
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
|
||||
import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../../test-utils";
|
||||
import { mkThread } from "../../../test-utils/threads";
|
||||
import { type IRoomState } from "../../../../src/components/structures/RoomView";
|
||||
import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import type { RoomContextType } from "../../../../src/contexts/RoomContext.ts";
|
||||
|
||||
jest.mock("../../../../src/utils/Feedback");
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("ThreadPanel", () => {
|
||||
mockRoom.getLastLiveEvent.mockReturnValue(mockEvent);
|
||||
const roomContextObject = {
|
||||
room: mockRoom,
|
||||
} as unknown as IRoomState;
|
||||
} as unknown as RoomContextType;
|
||||
const { container } = render(
|
||||
<ScopedRoomContextProvider {...roomContextObject}>
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
|
||||
@@ -1099,12 +1099,12 @@ exports[`RoomView invites renders an invite room 1`] = `
|
||||
class="mx_RoomPreviewBar_message"
|
||||
>
|
||||
<h3>
|
||||
Do you want to join !2:example.org?
|
||||
Do you want to join !roomviewinvitesrendersaninviteroom:example.org?
|
||||
</h3>
|
||||
<p>
|
||||
<span
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="4"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
@@ -1185,7 +1185,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
aria-label="Open room settings"
|
||||
aria-live="off"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="5"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
@@ -1212,7 +1212,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
<span
|
||||
class="mx_RoomHeader_truncated mx_lineClamp"
|
||||
>
|
||||
!12:example.org
|
||||
!roomviewshouldnotdisplaythetimelinewhentheroomencryptionisloading:example.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1397,7 +1397,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
aria-label="Open room settings"
|
||||
aria-live="off"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="5"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
@@ -1424,7 +1424,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
<span
|
||||
class="mx_RoomHeader_truncated mx_lineClamp"
|
||||
>
|
||||
!12:example.org
|
||||
!roomviewshouldnotdisplaythetimelinewhentheroomencryptionisloading:example.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1787,7 +1787,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
aria-label="Open room settings"
|
||||
aria-live="off"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="4"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
@@ -1814,7 +1814,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
<span
|
||||
class="mx_RoomHeader_truncated mx_lineClamp"
|
||||
>
|
||||
!17:example.org
|
||||
!roomviewvideoroomsshouldrenderjoinedvideoroomview:example.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,10 +15,9 @@ import RecordingPlayback, {
|
||||
PlaybackLayout,
|
||||
} from "../../../../../src/components/views/audio_messages/RecordingPlayback";
|
||||
import { Playback } from "../../../../../src/audio/Playback";
|
||||
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { createAudioContext } from "../../../../../src/audio/compat";
|
||||
import { flushPromises } from "../../../../test-utils";
|
||||
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
|
||||
jest.mock("../../../../../src/WorkerManager", () => ({
|
||||
@@ -54,7 +53,10 @@ describe("<RecordingPlayback />", () => {
|
||||
|
||||
const mockChannelData = new Float32Array();
|
||||
|
||||
const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState;
|
||||
const defaultRoom = {
|
||||
roomId: "!room:server.org",
|
||||
timelineRenderingType: TimelineRenderingType.File,
|
||||
} as RoomContextType;
|
||||
const getComponent = (props: React.ComponentProps<typeof RecordingPlayback>, room = defaultRoom) =>
|
||||
render(
|
||||
<ScopedRoomContextProvider {...room}>
|
||||
|
||||
@@ -31,8 +31,7 @@ import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMe
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { ConnectionState } from "../../../../../src/models/Call";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext";
|
||||
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import RoomContext from "../../../../../src/contexts/RoomContext";
|
||||
import RoomContext, { type RoomContextType } from "../../../../../src/contexts/RoomContext";
|
||||
|
||||
describe("<RoomCallBanner />", () => {
|
||||
let client: Mocked<MatrixClient>;
|
||||
@@ -51,7 +50,7 @@ describe("<RoomCallBanner />", () => {
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
let roomContext: IRoomState;
|
||||
let roomContext: RoomContextType;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
@@ -79,7 +78,7 @@ describe("<RoomCallBanner />", () => {
|
||||
...RoomContext,
|
||||
roomId: room.roomId,
|
||||
roomViewStore: mockRoomViewStore,
|
||||
} as unknown as IRoomState;
|
||||
} as unknown as RoomContextType;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -27,8 +27,7 @@ import { mocked } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { canEditContent } from "../../../../../src/utils/EventUtils";
|
||||
import { copyPlaintext, getSelectedText } from "../../../../../src/utils/strings";
|
||||
import MessageContextMenu from "../../../../../src/components/views/context_menus/MessageContextMenu";
|
||||
@@ -711,18 +710,18 @@ describe("MessageContextMenu", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function createRightClickMenuWithContent(eventContent: object, context?: Partial<IRoomState>): RenderResult {
|
||||
function createRightClickMenuWithContent(eventContent: object, context?: Partial<RoomContextType>): RenderResult {
|
||||
return createMenuWithContent(eventContent, { rightClick: true }, context);
|
||||
}
|
||||
|
||||
function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial<IRoomState>): RenderResult {
|
||||
function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial<RoomContextType>): RenderResult {
|
||||
return createMenu(mxEvent, { rightClick: true }, context);
|
||||
}
|
||||
|
||||
function createMenuWithContent(
|
||||
eventContent: object,
|
||||
props?: Partial<MessageContextMenu["props"]>,
|
||||
context?: Partial<IRoomState>,
|
||||
context?: Partial<RoomContextType>,
|
||||
): RenderResult {
|
||||
// XXX: We probably shouldn't be assuming all events are going to be message events, but considering this
|
||||
// test is for the Message context menu, it's a fairly safe assumption.
|
||||
@@ -739,7 +738,7 @@ function makeDefaultRoom(): Room {
|
||||
function createMenu(
|
||||
mxEvent: MatrixEvent,
|
||||
props?: Partial<MessageContextMenu["props"]>,
|
||||
context: Partial<IRoomState> = {},
|
||||
context: Partial<RoomContextType> = {},
|
||||
beacons: Map<BeaconIdentifier, Beacon> = new Map(),
|
||||
room: Room = makeDefaultRoom(),
|
||||
): RenderResult {
|
||||
@@ -754,7 +753,7 @@ function createMenu(
|
||||
client.getRoom = jest.fn().mockReturnValue(room);
|
||||
|
||||
return render(
|
||||
<ScopedRoomContextProvider {...(context as IRoomState)}>
|
||||
<ScopedRoomContextProvider {...(context as RoomContextType)}>
|
||||
<MessageContextMenu mxEvent={mxEvent} onFinished={jest.fn()} {...props} />
|
||||
</ScopedRoomContextProvider>,
|
||||
);
|
||||
|
||||
@@ -26,8 +26,7 @@ import {
|
||||
} from "../../../../test-utils";
|
||||
import DateSeparator from "../../../../../src/components/views/messages/DateSeparator";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext";
|
||||
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import RoomContext from "../../../../../src/contexts/RoomContext";
|
||||
import RoomContext, { type RoomContextType } from "../../../../../src/contexts/RoomContext";
|
||||
|
||||
jest.mock("../../../../../src/settings/SettingsStore");
|
||||
|
||||
@@ -50,7 +49,7 @@ describe("DateSeparator", () => {
|
||||
...RoomContext,
|
||||
roomId,
|
||||
roomViewStore: mockRoomViewStore,
|
||||
} as unknown as IRoomState;
|
||||
} as unknown as RoomContextType;
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
timestampToEvent: jest.fn(),
|
||||
|
||||
@@ -29,8 +29,7 @@ import {
|
||||
makeBeaconInfoEvent,
|
||||
} from "../../../../test-utils";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import RoomContext, { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
@@ -115,8 +114,8 @@ describe("<MessageActionBar />", () => {
|
||||
canSendMessages: true,
|
||||
canReact: true,
|
||||
room,
|
||||
} as unknown as IRoomState;
|
||||
const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) =>
|
||||
} as unknown as RoomContextType;
|
||||
const getComponent = (props = {}, roomContext: Partial<RoomContextType> = {}) =>
|
||||
render(
|
||||
<ScopedRoomContextProvider {...defaultRoomContext} {...roomContext}>
|
||||
<MessageActionBar {...defaultProps} {...props} />
|
||||
|
||||
@@ -27,12 +27,12 @@ import {
|
||||
import DocumentOffset from "../../../../../src/editor/offset";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer";
|
||||
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import Autocompleter, { type IProviderCompletions } from "../../../../../src/autocomplete/Autocompleter";
|
||||
import NotifProvider from "../../../../../src/autocomplete/NotifProvider";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
|
||||
|
||||
describe("<EditMessageComposer/>", () => {
|
||||
const userId = "@alice:server.org";
|
||||
@@ -75,7 +75,7 @@ describe("<EditMessageComposer/>", () => {
|
||||
|
||||
const defaultRoomContext = getRoomContext(room, {});
|
||||
|
||||
const getComponent = (editState: EditorStateTransfer, roomContext: IRoomState = defaultRoomContext) =>
|
||||
const getComponent = (editState: EditorStateTransfer, roomContext: RoomContextType = defaultRoomContext) =>
|
||||
render(<EditMessageComposerWithMatrixClient editState={editState} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
|
||||
@@ -30,14 +30,13 @@ import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing";
|
||||
|
||||
import EventTile, { type EventTileProps } from "../../../../../src/components/views/rooms/EventTile";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { filterConsole, flushPromises, getRoomContext, mkEvent, mkMessage, stubClient } from "../../../../test-utils";
|
||||
import { mkThread } from "../../../../test-utils/threads";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import dis from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import PinningUtils from "../../../../../src/utils/PinningUtils";
|
||||
import { Layout } from "../../../../../src/settings/enums/Layout";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
@@ -52,7 +51,7 @@ describe("EventTile", () => {
|
||||
|
||||
/** wrap the EventTile up in context providers, and with basic properties, as it would be by MessagePanel normally. */
|
||||
function WrappedEventTile(props: {
|
||||
roomContext: IRoomState;
|
||||
roomContext: RoomContextType;
|
||||
eventTilePropertyOverrides?: Partial<EventTileProps>;
|
||||
}) {
|
||||
return (
|
||||
@@ -71,7 +70,7 @@ describe("EventTile", () => {
|
||||
function getComponent(
|
||||
overrides: Partial<EventTileProps> = {},
|
||||
renderingType: TimelineRenderingType = TimelineRenderingType.Room,
|
||||
roomContext: Partial<IRoomState> = {},
|
||||
roomContext: Partial<RoomContextType> = {},
|
||||
) {
|
||||
const context = getRoomContext(room, {
|
||||
timelineRenderingType: renderingType,
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import MessageComposer from "../../../../../src/components/views/rooms/MessageComposer";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import ResizeNotifier from "../../../../../src/utils/ResizeNotifier";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import { LocalRoom } from "../../../../../src/models/LocalRoom";
|
||||
@@ -36,6 +35,7 @@ import { addTextToComposerRTL } from "../../../../test-utils/composer";
|
||||
import UIStore, { UI_EVENTS } from "../../../../../src/stores/UIStore";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
|
||||
|
||||
const openStickerPicker = async (): Promise<void> => {
|
||||
await userEvent.click(screen.getByLabelText("More options"));
|
||||
@@ -155,7 +155,7 @@ describe("MessageComposer", () => {
|
||||
});
|
||||
|
||||
describe("when receiving a »reply_to_event«", () => {
|
||||
let roomContext: IRoomState;
|
||||
let roomContext: RoomContextType;
|
||||
let resizeNotifier: ResizeNotifier;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -458,7 +458,7 @@ function wrapAndRender(
|
||||
canSendMessages,
|
||||
tombstone,
|
||||
narrow,
|
||||
} as unknown as IRoomState;
|
||||
} as unknown as RoomContextType;
|
||||
|
||||
const defaultProps = {
|
||||
room,
|
||||
|
||||
@@ -11,10 +11,10 @@ import { render, screen, waitFor } from "jest-matrix-react";
|
||||
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { createTestClient, getRoomContext, mkStubRoom } from "../../../../test-utils";
|
||||
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import MessageComposerButtons from "../../../../../src/components/views/rooms/MessageComposerButtons";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
|
||||
|
||||
describe("MessageComposerButtons", () => {
|
||||
// @ts-ignore - we're deliberately not implementing the whole interface here, but
|
||||
@@ -50,7 +50,7 @@ describe("MessageComposerButtons", () => {
|
||||
|
||||
function wrapAndRender(component: React.ReactElement, narrow: boolean) {
|
||||
const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any;
|
||||
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, { narrow });
|
||||
const defaultRoomContext: RoomContextType = getRoomContext(mockRoom, { narrow });
|
||||
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
|
||||
@@ -22,16 +22,16 @@ import {
|
||||
} from "../../../../test-utils";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import NewRoomIntro from "../../../../../src/components/views/rooms/NewRoomIntro";
|
||||
import { type IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import { DirectoryMember } from "../../../../../src/utils/direct-messages";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
|
||||
|
||||
const renderNewRoomIntro = (client: MatrixClient, room: Room | LocalRoom) => {
|
||||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<ScopedRoomContextProvider {...({ room, roomId: room.roomId } as unknown as IRoomState)}>
|
||||
<ScopedRoomContextProvider {...({ room, roomId: room.roomId } as unknown as RoomContextType)}>
|
||||
<NewRoomIntro />
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>,
|
||||
|
||||
@@ -41,8 +41,7 @@ import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext";
|
||||
import { type IRoomState } from "../../../../../../src/components/structures/RoomView";
|
||||
import RoomContext from "../../../../../../src/contexts/RoomContext";
|
||||
import RoomContext, { type RoomContextType } from "../../../../../../src/contexts/RoomContext";
|
||||
import RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore";
|
||||
import { RightPanelPhases } from "../../../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import LegacyCallHandler from "../../../../../../src/LegacyCallHandler";
|
||||
@@ -85,7 +84,7 @@ describe("RoomHeader", () => {
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
let roomContext: IRoomState;
|
||||
let roomContext: RoomContextType;
|
||||
|
||||
function getWrapper(): RenderOptions {
|
||||
return {
|
||||
@@ -121,7 +120,7 @@ describe("RoomHeader", () => {
|
||||
...RoomContext,
|
||||
roomId: ROOM_ID,
|
||||
roomViewStore: mockRoomViewStore,
|
||||
} as unknown as IRoomState;
|
||||
} as unknown as RoomContextType;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import SendMessageComposer, {
|
||||
isQuickReaction,
|
||||
} from "../../../../../src/components/views/rooms/SendMessageComposer";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import EditorModel from "../../../../../src/editor/model";
|
||||
import { createPartCreator } from "../../../editor/mock";
|
||||
import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../../test-utils";
|
||||
@@ -25,7 +25,7 @@ import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import DocumentOffset from "../../../../../src/editor/offset";
|
||||
import { Layout } from "../../../../../src/settings/enums/Layout";
|
||||
import { type IRoomState, MainSplitContentType } from "../../../../../src/components/structures/RoomView";
|
||||
import { MainSplitContentType } from "../../../../../src/components/structures/RoomView";
|
||||
import { mockPlatformPeg } from "../../../../test-utils/platform";
|
||||
import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room";
|
||||
import { addTextToComposer } from "../../../../test-utils/composer";
|
||||
@@ -37,7 +37,7 @@ jest.mock("../../../../../src/utils/local-room", () => ({
|
||||
}));
|
||||
|
||||
describe("<SendMessageComposer/>", () => {
|
||||
const defaultRoomContext: IRoomState = {
|
||||
const defaultRoomContext: RoomContextType = {
|
||||
roomViewStore: SdkContextClass.instance.roomViewStore,
|
||||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
|
||||
@@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { type EventTimeline, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getRoomContext, mkEvent, mkStubRoom, stubClient } from "../../../../../test-utils";
|
||||
import { type IRoomState } from "../../../../../../src/components/structures/RoomView";
|
||||
import EditorStateTransfer from "../../../../../../src/utils/EditorStateTransfer";
|
||||
import type { RoomContextType } from "../../../../../../src/contexts/RoomContext";
|
||||
|
||||
export function createMocks(eventContent = "Replying <strong>to</strong> this new content") {
|
||||
const mockClient = stubClient();
|
||||
@@ -31,7 +31,7 @@ export function createMocks(eventContent = "Replying <strong>to</strong> this ne
|
||||
return eventId === mockEvent.getId() ? mockEvent : null;
|
||||
});
|
||||
|
||||
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {
|
||||
const defaultRoomContext: RoomContextType = getRoomContext(mockRoom, {
|
||||
liveTimeline: { getEvents: (): MatrixEvent[] => [] } as unknown as EventTimeline,
|
||||
});
|
||||
|
||||
|
||||
72
test/unit-tests/modules/AccountDataApi-test.ts
Normal file
72
test/unit-tests/modules/AccountDataApi-test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ClientEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { AccountDataApi } from "../../../src/modules/AccountDataApi";
|
||||
import { mkEvent, stubClient } from "../../test-utils/test-utils";
|
||||
|
||||
describe("AccountDataApi", () => {
|
||||
describe("AccountDataWatchable", () => {
|
||||
it("should return content of account data event on get()", () => {
|
||||
const cli = stubClient();
|
||||
const api = new AccountDataApi();
|
||||
// Mock cli to return a event
|
||||
const content = { foo: "bar" };
|
||||
const event = mkEvent({ content, type: "m.test", user: "@foobar:matrix.org", event: true });
|
||||
cli.getAccountData = () => event;
|
||||
expect(api.get("m.test").value).toStrictEqual(content);
|
||||
});
|
||||
|
||||
it("should update value on event", () => {
|
||||
const cli = stubClient();
|
||||
const api = new AccountDataApi();
|
||||
// Mock cli to return a event
|
||||
const content = { foo: "bar" };
|
||||
const event = mkEvent({ content, type: "m.test", user: "@foobar:matrix.org", event: true });
|
||||
cli.getAccountData = () => event;
|
||||
|
||||
const watchable = api.get("m.test");
|
||||
expect(watchable.value).toStrictEqual(content);
|
||||
|
||||
const fn = jest.fn();
|
||||
watchable.watch(fn);
|
||||
|
||||
// Let's say that the account data event changed
|
||||
const event2 = mkEvent({
|
||||
content: { foo: "abc" },
|
||||
type: "m.test",
|
||||
user: "@foobar:matrix.org",
|
||||
event: true,
|
||||
});
|
||||
cli.emit(ClientEvent.AccountData, event2);
|
||||
// Watchable value should have been updated
|
||||
expect(watchable.value).toStrictEqual({ foo: "abc" });
|
||||
// Watched callbacks should be called
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Make sure unwatch removed the event listener
|
||||
cli.off = jest.fn();
|
||||
watchable.unwatch(fn);
|
||||
expect(cli.off).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set account data via js-sdk on set()", async () => {
|
||||
const cli = stubClient();
|
||||
const api = new AccountDataApi();
|
||||
await api.set("m.test", { foo: "bar" });
|
||||
expect(cli.setAccountData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should delete account data via js-sdk on set()", async () => {
|
||||
const cli = stubClient();
|
||||
const api = new AccountDataApi();
|
||||
await api.delete("m.test");
|
||||
expect(cli.deleteAccountData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
53
test/unit-tests/modules/BuiltinsApi-test.tsx
Normal file
53
test/unit-tests/modules/BuiltinsApi-test.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import { ElementWebBuiltinsApi } from "../../../src/modules/BuiltinsApi.tsx";
|
||||
import { stubClient } from "../../test-utils/test-utils";
|
||||
|
||||
const Avatar: React.FC<{ room: { roomId: string }; size: string }> = ({ room, size }) => {
|
||||
return (
|
||||
<div>
|
||||
Avatar, {room.roomId}, {size}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe("ElementWebBuiltinsApi", () => {
|
||||
it("returns the RoomView component thats been set", () => {
|
||||
const builtinsApi = new ElementWebBuiltinsApi();
|
||||
const sentinel = {};
|
||||
builtinsApi.setComponents({ roomView: sentinel, roomAvatar: Avatar } as any);
|
||||
expect(builtinsApi.getRoomViewComponent()).toBe(sentinel);
|
||||
});
|
||||
|
||||
it("returns rendered RoomView component", () => {
|
||||
const builtinsApi = new ElementWebBuiltinsApi();
|
||||
const RoomView = () => <div>hello world</div>;
|
||||
builtinsApi.setComponents({ roomView: RoomView, roomAvatar: Avatar } as any);
|
||||
const { container } = render(<> {builtinsApi.renderRoomView("!foo:m.org")}</>);
|
||||
expect(container).toHaveTextContent("hello world");
|
||||
});
|
||||
|
||||
it("returns rendered RoomAvatar component", () => {
|
||||
stubClient();
|
||||
const builtinsApi = new ElementWebBuiltinsApi();
|
||||
builtinsApi.setComponents({ roomView: {}, roomAvatar: Avatar } as any);
|
||||
const { container } = render(<> {builtinsApi.renderRoomAvatar("!foo:m.org", "50")}</>);
|
||||
expect(container).toHaveTextContent("Avatar");
|
||||
expect(container).toHaveTextContent("!foo:m.org");
|
||||
expect(container).toHaveTextContent("50");
|
||||
});
|
||||
|
||||
it("should throw error if called before components are set", () => {
|
||||
stubClient();
|
||||
const builtinsApi = new ElementWebBuiltinsApi();
|
||||
expect(() => builtinsApi.renderRoomAvatar("!foo:m.org")).toThrow("No RoomAvatar component has been set");
|
||||
expect(() => builtinsApi.renderRoomView("!foo:m.org")).toThrow("No RoomView component has been set");
|
||||
});
|
||||
});
|
||||
20
test/unit-tests/modules/ClientApi-test.ts
Normal file
20
test/unit-tests/modules/ClientApi-test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ClientApi } from "../../../src/modules/ClientApi";
|
||||
import { Room } from "../../../src/modules/models/Room";
|
||||
import { stubClient } from "../../test-utils/test-utils";
|
||||
|
||||
describe("ClientApi", () => {
|
||||
it("should return module room from getRoom()", () => {
|
||||
stubClient();
|
||||
const client = new ClientApi();
|
||||
const moduleRoom = client.getRoom("!foo:matrix.org");
|
||||
expect(moduleRoom).toBeInstanceOf(Room);
|
||||
expect(moduleRoom?.id).toStrictEqual("!foo:matrix.org");
|
||||
});
|
||||
});
|
||||
@@ -37,5 +37,25 @@ describe("NavigationApi", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should dispatch correct action on openRoom", () => {
|
||||
const spy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
// Non alias
|
||||
api.openRoom("!foo:m.org");
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "view_room",
|
||||
room_id: "!foo:m.org",
|
||||
}),
|
||||
);
|
||||
// Alias
|
||||
api.openRoom("#bar:m.org");
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "view_room",
|
||||
room_alias: "#bar:m.org",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
50
test/unit-tests/modules/models/Room-test.ts
Normal file
50
test/unit-tests/modules/models/Room-test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Room } from "../../../../src/modules/models/Room";
|
||||
import { mkRoom, stubClient } from "../../../test-utils";
|
||||
|
||||
describe("Room", () => {
|
||||
it("should return id from sdk room", () => {
|
||||
const cli = stubClient();
|
||||
const sdkRoom = mkRoom(cli, "!foo:m.org");
|
||||
const room = new Room(sdkRoom);
|
||||
expect(room.id).toStrictEqual("!foo:m.org");
|
||||
});
|
||||
|
||||
it("should return last timestamp from sdk room", () => {
|
||||
const cli = stubClient();
|
||||
const sdkRoom = mkRoom(cli, "!foo:m.org");
|
||||
const room = new Room(sdkRoom);
|
||||
expect(room.getLastActiveTimestamp()).toStrictEqual(sdkRoom.getLastActiveTimestamp());
|
||||
});
|
||||
|
||||
describe("watchableName", () => {
|
||||
it("should return name from sdkRoom", () => {
|
||||
const cli = stubClient();
|
||||
const sdkRoom = mkRoom(cli, "!foo:m.org");
|
||||
sdkRoom.name = "Foo Name";
|
||||
const room = new Room(sdkRoom);
|
||||
expect(room.name.value).toStrictEqual("Foo Name");
|
||||
});
|
||||
|
||||
it("should add/remove event listener on sdk room", () => {
|
||||
const cli = stubClient();
|
||||
const sdkRoom = mkRoom(cli, "!foo:m.org");
|
||||
sdkRoom.name = "Foo Name";
|
||||
|
||||
const room = new Room(sdkRoom);
|
||||
const fn = jest.fn();
|
||||
|
||||
room.name.watch(fn);
|
||||
expect(sdkRoom.on).toHaveBeenCalledTimes(1);
|
||||
|
||||
room.name.unwatch(fn);
|
||||
expect(sdkRoom.off).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
101
test/unit-tests/stores/MultiRoomViewStore-test.ts
Normal file
101
test/unit-tests/stores/MultiRoomViewStore-test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MultiRoomViewStore } from "../../../src/stores/MultiRoomViewStore";
|
||||
import { RoomViewStore } from "../../../src/stores/RoomViewStore";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import type { MatrixDispatcher } from "../../../src/dispatcher/dispatcher";
|
||||
import { TestSdkContext } from "../TestSdkContext";
|
||||
|
||||
jest.mock("../../../src/stores/RoomViewStore");
|
||||
|
||||
describe("MultiRoomViewStore", () => {
|
||||
let multiRoomViewStore: MultiRoomViewStore;
|
||||
let mockDispatcher: MatrixDispatcher;
|
||||
let mockSdkContext: TestSdkContext;
|
||||
let mockRoomViewStore: jest.Mocked<RoomViewStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create mock dispatcher
|
||||
mockDispatcher = {
|
||||
dispatch: jest.fn(),
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
} as unknown as MatrixDispatcher;
|
||||
|
||||
// Create mock SDK context
|
||||
mockSdkContext = new TestSdkContext();
|
||||
|
||||
// Create mock RoomViewStore instance
|
||||
mockRoomViewStore = {
|
||||
viewRoom: jest.fn(),
|
||||
dispose: jest.fn(),
|
||||
} as any;
|
||||
|
||||
(RoomViewStore as jest.MockedClass<typeof RoomViewStore>).mockImplementation(() => mockRoomViewStore as any);
|
||||
|
||||
// Create the MultiRoomViewStore instance
|
||||
multiRoomViewStore = new MultiRoomViewStore(mockDispatcher, mockSdkContext);
|
||||
});
|
||||
|
||||
describe("getRoomViewStoreForRoom", () => {
|
||||
it("should create a new RoomViewStore for a room that doesn't exist in cache", () => {
|
||||
const roomId = "!room1:example.com";
|
||||
|
||||
const result = multiRoomViewStore.getRoomViewStoreForRoom(roomId);
|
||||
|
||||
expect(RoomViewStore).toHaveBeenCalledWith(mockDispatcher, mockSdkContext, roomId);
|
||||
expect(mockRoomViewStore.viewRoom).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
expect(result).toBe(mockRoomViewStore);
|
||||
});
|
||||
|
||||
it("should return existing RoomViewStore for a room that exists in cache", () => {
|
||||
const roomId = "!room1:example.com";
|
||||
|
||||
// First call creates the store
|
||||
const firstResult = multiRoomViewStore.getRoomViewStoreForRoom(roomId);
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Should return the same store
|
||||
const secondResult = multiRoomViewStore.getRoomViewStoreForRoom(roomId);
|
||||
|
||||
expect(RoomViewStore).not.toHaveBeenCalled();
|
||||
expect(mockRoomViewStore.viewRoom).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
expect(secondResult).toBe(firstResult);
|
||||
expect(secondResult).toBe(mockRoomViewStore);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeRoomViewStore", () => {
|
||||
it("should remove an existing RoomViewStore from cache", () => {
|
||||
const roomId = "!room1:example.com";
|
||||
|
||||
multiRoomViewStore.getRoomViewStoreForRoom(roomId);
|
||||
multiRoomViewStore.removeRoomViewStore(roomId);
|
||||
|
||||
// New store should be created now
|
||||
jest.clearAllMocks();
|
||||
(RoomViewStore as jest.MockedClass<typeof RoomViewStore>).mockImplementation(
|
||||
() => mockRoomViewStore as any,
|
||||
);
|
||||
|
||||
multiRoomViewStore.getRoomViewStoreForRoom(roomId);
|
||||
expect(RoomViewStore).toHaveBeenCalledWith(mockDispatcher, mockSdkContext, roomId);
|
||||
});
|
||||
});
|
||||
});
|
||||
11
yarn.lock
11
yarn.lock
@@ -1602,10 +1602,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.16.1.tgz#28bdbde426051cc2a3228a36e7196e0a254569d3"
|
||||
integrity sha512-g3v/QFuNy8YVRGrKC5SxjIYvgBh6biOHgejhJT2Jk/yjOOUEuP0y2PBaADm+suPD9BB/Vk1jPxFk2uEIpEzhpA==
|
||||
|
||||
"@element-hq/element-web-module-api@1.4.1":
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.4.1.tgz#a46526d58985190f9989bf1686ea872687d3c6e1"
|
||||
integrity sha512-A8yaQtX7QoKThzzZVU+VYOFhpiNyppEMuIQijK48RvhVp1nwmy0cTD6u/6Yn64saNwJjtna+Oy+Qzo/TfwwhxQ==
|
||||
"@element-hq/element-web-module-api@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.5.0.tgz#077a528917f4eb558059a2a5286b9bb6a2fb1690"
|
||||
integrity sha512-WI/iMADRouXp9WhQy5jov6Z4eKKlHEPh20DKoCsKZ9dWaYcW/MiBhzi09PZxay+o0RLZXA6aDPxpxaIX3lZXag==
|
||||
|
||||
"@element-hq/element-web-playwright-common@^2.0.0":
|
||||
version "2.0.0"
|
||||
@@ -4085,13 +4085,14 @@
|
||||
|
||||
"@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@vector-im/matrix-wysiwyg@2.40.0":
|
||||
version "2.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.40.0.tgz#53c9ca5ea907d91e4515da64f20a82e5586b882c"
|
||||
integrity sha512-8LRFLs5PEKYs4lOL7aJ4lL/hGCrvEvOYkCR3JggXYXDVMtX4LmfdlKYucSAe98pCmqAAbLRvlRcR1bTOYvM8ug==
|
||||
dependencies:
|
||||
"@vector-im/matrix-wysiwyg-wasm" "link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm"
|
||||
"@vector-im/matrix-wysiwyg-wasm" "link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm"
|
||||
|
||||
"@vitest/expect@3.2.4":
|
||||
version "3.2.4"
|
||||
|
||||
Reference in New Issue
Block a user