Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Taj/1976 usersearch 168 1 #87

Merged
merged 9 commits into from
Aug 27, 2024
36 changes: 30 additions & 6 deletions src/components/views/dialogs/InviteDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { SdkContextClass } from "../../../contexts/SDKContext";
import { UserProfilesStore } from "../../../stores/UserProfilesStore";
import { Key } from "../../../Keyboard";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { ModuleRunner } from "../../../modules/ModuleRunner";

// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
Expand Down Expand Up @@ -641,8 +642,19 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial

let foundUser = false;
try {
await MatrixClientPeg.get()
?.searchUserDirectory({ term: this.state.filterText.trim().split(":")[0] ?? this.state.filterText })
const client = MatrixClientPeg.get();

const searchContext = await ModuleRunner.instance.extensions.userSearch.getSearchContext(
client,
SdkContextClass.instance,
);

await client
?.searchUserDirectory(
{ term: this.state.filterText.trim().split(":")[0] ?? this.state.filterText },
searchContext.extraBodyArgs,
searchContext.extraRequestOptions,
)
.then(async (r) => {
this.setState({ busy: false });

Expand Down Expand Up @@ -947,8 +959,14 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
};

private updateSuggestions = async (term: string): Promise<void> => {
MatrixClientPeg.safeGet()
.searchUserDirectory({ term })
const client = MatrixClientPeg.safeGet();
const searchContext = await ModuleRunner.instance.extensions.userSearch.getSearchContext(
client,
SdkContextClass.instance,
);

client
.searchUserDirectory({ term }, searchContext.extraBodyArgs, searchContext.extraRequestOptions)
.then(async (r): Promise<void> => {
if (term !== this.state.filterText) {
// Discard the results - we were probably too slow on the server-side to make
Expand Down Expand Up @@ -1157,8 +1175,14 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
this.setState({ busy: true });

let directoryUsers: any[] = [];
await MatrixClientPeg.get()
?.searchUserDirectory({ term: text })

const client = MatrixClientPeg.get();
const searchContext = await ModuleRunner.instance.extensions.userSearch.getSearchContext(
client,
SdkContextClass.instance,
);
await client
?.searchUserDirectory({ term: text }, searchContext.extraBodyArgs, searchContext.extraRequestOptions)
.then(async (r) => {
this.setState({ busy: false });

Expand Down
14 changes: 13 additions & 1 deletion src/hooks/useUserDirectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { useCallback, useState } from "react";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { DirectoryMember } from "../utils/direct-messages";
import { useLatestResult } from "./useLatestResult";
import { ModuleRunner } from "../modules/ModuleRunner";
import { SdkContextClass } from "../contexts/SDKContext";

export interface IUserDirectoryOpts {
limit: number;
Expand Down Expand Up @@ -49,7 +51,17 @@ export const useUserDirectory = (): {

try {
setLoading(true);
const { results } = await MatrixClientPeg.safeGet().searchUserDirectory(opts);
const client = MatrixClientPeg.safeGet();
const searchContext = await ModuleRunner.instance.extensions.userSearch.getSearchContext(
client,
SdkContextClass.instance,
);
const { results } = await client.searchUserDirectory(
opts,
searchContext.extraBodyArgs,
searchContext.extraRequestOptions,
);

updateResult(
opts,
results.map((user) => new DirectoryMember(user)),
Expand Down
91 changes: 2 additions & 89 deletions src/modules/ModuleRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,100 +17,13 @@ limitations under the License.
import { safeSet } from "matrix-js-sdk/src/utils";
import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations";
import { AnyLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/types";
import {
DefaultCryptoSetupExtensions,
ProvideCryptoSetupExtensions,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions";
import {
DefaultExperimentalExtensions,
ProvideExperimentalExtensions,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions";
import { ExtensionsManager } from "@matrix-org/react-sdk-module-api/lib/extensions/ExtensionsManager";

import { AppModule } from "./AppModule";
import { ModuleFactory } from "./ModuleFactory";

import "./ModuleComponents";

/**
* Handles and manages extensions provided by modules.
*/
class ExtensionsManager {
// Private backing fields for extensions
private cryptoSetupExtension: ProvideCryptoSetupExtensions;
private experimentalExtension: ProvideExperimentalExtensions;

/** `true` if `cryptoSetupExtension` is the default implementation; `false` if it is implemented by a module. */
private hasDefaultCryptoSetupExtension = true;

/** `true` if `experimentalExtension` is the default implementation; `false` if it is implemented by a module. */
private hasDefaultExperimentalExtension = true;

/**
* Create a new instance.
*/
public constructor() {
// Set up defaults
this.cryptoSetupExtension = new DefaultCryptoSetupExtensions();
this.experimentalExtension = new DefaultExperimentalExtensions();
}

/**
* Provides a crypto setup extension.
*
* @returns The registered extension. If no module provides this extension, a default implementation is returned.
*/
public get cryptoSetup(): ProvideCryptoSetupExtensions {
return this.cryptoSetupExtension;
}

/**
* Provides an experimental extension.
*
* @remarks
* This method extension is provided to simplify experimentation and development, and is not intended for production code.
*
* @returns The registered extension. If no module provides this extension, a default implementation is returned.
*/
public get experimental(): ProvideExperimentalExtensions {
return this.experimentalExtension;
}

/**
* Add any extensions provided by the module.
*
* @param module - The appModule to check for extensions.
*
* @throws if an extension is provided by more than one module.
*/
public addExtensions(module: AppModule): void {
const runtimeModule = module.module;

/* Add the cryptoSetup extension if any */
if (runtimeModule.extensions?.cryptoSetup) {
if (this.hasDefaultCryptoSetupExtension) {
this.cryptoSetupExtension = runtimeModule.extensions?.cryptoSetup;
this.hasDefaultCryptoSetupExtension = false;
} else {
throw new Error(
`adding cryptoSetup extension implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`,
);
}
}

/* Add the experimental extension if any */
if (runtimeModule.extensions?.experimental) {
if (this.hasDefaultExperimentalExtension) {
this.experimentalExtension = runtimeModule.extensions?.experimental;
this.hasDefaultExperimentalExtension = false;
} else {
throw new Error(
`adding experimental extension implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`,
);
}
}
}
}

/**
* Handles and coordinates the operation of modules.
*/
Expand Down Expand Up @@ -177,7 +90,7 @@ export class ModuleRunner {
this.modules.push(appModule);

// Check if the new module provides any extensions, and also ensure a given extension is only provided by a single runtime module.
this.extensionsManager.addExtensions(appModule);
this.extensionsManager.addExtensions(appModule.module);
}

/**
Expand Down
11 changes: 10 additions & 1 deletion src/stores/widgets/StopGapWidgetDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,16 @@ export class StopGapWidgetDriver extends WidgetDriver {
public async searchUserDirectory(searchTerm: string, limit?: number): Promise<ISearchUserDirectoryResult> {
const client = MatrixClientPeg.safeGet();

const { limited, results } = await client.searchUserDirectory({ term: searchTerm, limit });
const searchContext = await ModuleRunner.instance.extensions.userSearch.getSearchContext(
client,
SdkContextClass.instance,
);

const { limited, results } = await client.searchUserDirectory(
{ term: searchTerm, limit },
searchContext.extraBodyArgs,
searchContext.extraRequestOptions,
);

return {
limited,
Expand Down
2 changes: 1 addition & 1 deletion test/MatrixClientPeg-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import EventEmitter from "events";
import {
ProvideCryptoSetupExtensions,
SecretStorageKeyDescription,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions";
} from "@matrix-org/react-sdk-module-api/lib/extensions/CryptoSetupExtensions";

import { advanceDateAndTime, stubClient } from "./test-utils";
import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg";
Expand Down
50 changes: 48 additions & 2 deletions test/modules/MockModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ limitations under the License.
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
import { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi";
import { AllExtensions } from "@matrix-org/react-sdk-module-api/lib/types/extensions";
import { ProvideCryptoSetupExtensions } from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions";
import { ProvideExperimentalExtensions } from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions";
import { ProvideCryptoSetupExtensions } from "@matrix-org/react-sdk-module-api/lib/extensions/CryptoSetupExtensions";
import {
ProvideUserSearchExtensions,
SearchContext,
} from "@matrix-org/react-sdk-module-api/lib/extensions/UserSearchExtensions";
import { ProvideExperimentalExtensions } from "@matrix-org/react-sdk-module-api/lib/extensions/ExperimentalExtensions";

import { ModuleRunner } from "../../src/modules/ModuleRunner";

Expand Down Expand Up @@ -95,6 +99,27 @@ class MockModuleWithExperimentalExtension extends RuntimeModule {
}
}

class MockModuleWithUserSearchExtension extends RuntimeModule {
public get apiInstance(): ModuleApi {
return this.moduleApi;
}

moduleName: string = MockModuleWithUserSearchExtension.name;

extensions: AllExtensions = {
userSearch: {
getSearchContext: jest.fn().mockReturnValue(<SearchContext>{
extraBodyArgs: {},
extraRequestOptions: {},
}),
} as ProvideUserSearchExtensions,
};

public constructor(moduleApi: ModuleApi) {
super(moduleApi);
}
}

/**
* Register a mock module which implements the cryptoSetup extension.
*
Expand Down Expand Up @@ -136,3 +161,24 @@ export function registerMockModuleWithExperimentalExtension(): MockModuleWithExp
}
return module;
}

/**
* Register a mock module which implements the user search extension.
*
* @returns The registered module.
*/
export function registerMockModuleWithUserSearchExtension(): MockModuleWithUserSearchExtension {
let module: MockModuleWithUserSearchExtension | undefined;

ModuleRunner.instance.registerModule((api) => {
if (module) {
throw new Error("State machine error: ModuleRunner created the module twice");
}
module = new MockModuleWithUserSearchExtension(api);
return module;
});
if (!module) {
throw new Error("State machine error: ModuleRunner did not create module");
}
return module;
}
10 changes: 10 additions & 0 deletions test/modules/ModuleRunner-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
registerMockModule,
registerMockModuleWithCryptoSetupExtension,
registerMockModuleWithExperimentalExtension,
registerMockModuleWithUserSearchExtension,
} from "./MockModule";
import { ModuleRunner } from "../../src/modules/ModuleRunner";

Expand Down Expand Up @@ -97,5 +98,14 @@ describe("ModuleRunner", () => {
"adding experimental extension implementation from module MockModuleWithExperimentalExtension but an implementation was already provided",
);
});

it("must not allow multiple modules to provide user search extension", async () => {
registerMockModuleWithUserSearchExtension();
const t = () => registerMockModuleWithUserSearchExtension();
expect(t).toThrow(Error);
expect(t).toThrow(
"adding userSearch extension implementation from module MockModuleWithUserSearchExtension but an implementation was already provided",
);
});
});
});
4 changes: 2 additions & 2 deletions test/stores/widgets/StopGapWidgetDriver-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ describe("StopGapWidgetDriver", () => {
results: [{ userId: "@user", displayName: "Name", avatarUrl: "mxc://" }],
});

expect(client.searchUserDirectory).toHaveBeenCalledWith({ term: "foo", limit: undefined });
expect(client.searchUserDirectory).toHaveBeenCalledWith({ term: "foo", limit: undefined }, {}, {});
});

it("searches for users with a custom limit", async () => {
Expand All @@ -444,7 +444,7 @@ describe("StopGapWidgetDriver", () => {
results: [],
});

expect(client.searchUserDirectory).toHaveBeenCalledWith({ term: "foo", limit: 25 });
expect(client.searchUserDirectory).toHaveBeenCalledWith({ term: "foo", limit: 25 }, {}, {});
});
});

Expand Down
Loading