Add public room directory hook (#8626)
parent
e87bda9f37
commit
408f4dfe09
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { IProtocol, IPublicRoomsChunkRoom } from "matrix-js-sdk/src/client";
|
||||
import { IRoomDirectoryOptions } from "matrix-js-sdk/src/@types/requests";
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { Protocols } from "../utils/DirectoryUtils";
|
||||
|
||||
export const ALL_ROOMS = "ALL_ROOMS";
|
||||
const LAST_SERVER_KEY = "mx_last_room_directory_server";
|
||||
const LAST_INSTANCE_KEY = "mx_last_room_directory_instance";
|
||||
|
||||
export interface IPublicRoomsOpts {
|
||||
limit: number;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
let thirdParty: Protocols;
|
||||
|
||||
export const usePublicRoomDirectory = () => {
|
||||
const [publicRooms, setPublicRooms] = useState<IPublicRoomsChunkRoom[]>([]);
|
||||
|
||||
const [roomServer, setRoomServer] = useState<string | null | undefined>(undefined);
|
||||
const [instanceId, setInstanceId] = useState<string | null | undefined>(undefined);
|
||||
const [protocols, setProtocols] = useState<Protocols | null>(null);
|
||||
|
||||
const [ready, setReady] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function initProtocols() {
|
||||
if (!MatrixClientPeg.get()) {
|
||||
// We may not have a client yet when invoked from welcome page
|
||||
setReady(true);
|
||||
} else if (thirdParty) {
|
||||
setProtocols(thirdParty);
|
||||
} else {
|
||||
const response = await MatrixClientPeg.get().getThirdpartyProtocols();
|
||||
thirdParty = response;
|
||||
setProtocols(response);
|
||||
}
|
||||
}
|
||||
|
||||
function setConfig(server: string, instanceId?: string) {
|
||||
if (!ready) {
|
||||
throw new Error("public room configuration not initialised yet");
|
||||
} else {
|
||||
setRoomServer(server);
|
||||
setInstanceId(instanceId ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
const search = useCallback(async ({
|
||||
limit = 20,
|
||||
query,
|
||||
}: IPublicRoomsOpts): Promise<boolean> => {
|
||||
if (!query?.length) {
|
||||
setPublicRooms([]);
|
||||
return true;
|
||||
}
|
||||
|
||||
const opts: IRoomDirectoryOptions = { limit };
|
||||
|
||||
if (roomServer != MatrixClientPeg.getHomeserverName()) {
|
||||
opts.server = roomServer;
|
||||
}
|
||||
|
||||
if (instanceId === ALL_ROOMS) {
|
||||
opts.include_all_networks = true;
|
||||
} else if (instanceId) {
|
||||
opts.third_party_instance_id = instanceId;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
opts.filter = {
|
||||
generic_search_term: query,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { chunk } = await MatrixClientPeg.get().publicRooms(opts);
|
||||
setPublicRooms(chunk);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Could not fetch public rooms for params", opts, e);
|
||||
setPublicRooms([]);
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [roomServer, instanceId]);
|
||||
|
||||
useEffect(() => {
|
||||
initProtocols();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (protocols === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const myHomeserver = MatrixClientPeg.getHomeserverName();
|
||||
const lsRoomServer = localStorage.getItem(LAST_SERVER_KEY);
|
||||
const lsInstanceId = localStorage.getItem(LAST_INSTANCE_KEY);
|
||||
|
||||
let roomServer = myHomeserver;
|
||||
if (
|
||||
SdkConfig.getObject("room_directory")?.get("servers")?.includes(lsRoomServer) ||
|
||||
SettingsStore.getValue("room_directory_servers")?.includes(lsRoomServer)
|
||||
) {
|
||||
roomServer = lsRoomServer;
|
||||
}
|
||||
|
||||
let instanceId: string | null = null;
|
||||
if (roomServer === myHomeserver && (
|
||||
lsInstanceId === ALL_ROOMS ||
|
||||
Object.values(protocols).some((p: IProtocol) => {
|
||||
p.instances.some(i => i.instance_id === lsInstanceId);
|
||||
})
|
||||
)) {
|
||||
instanceId = lsInstanceId;
|
||||
}
|
||||
|
||||
setReady(true);
|
||||
setInstanceId(instanceId);
|
||||
setRoomServer(roomServer);
|
||||
}, [protocols]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(LAST_SERVER_KEY, roomServer);
|
||||
}, [roomServer]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(LAST_INSTANCE_KEY, instanceId);
|
||||
}, [instanceId]);
|
||||
|
||||
return {
|
||||
ready,
|
||||
loading,
|
||||
publicRooms,
|
||||
protocols,
|
||||
roomServer,
|
||||
instanceId,
|
||||
search,
|
||||
setConfig,
|
||||
} as const;
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { DirectoryMember } from "../utils/direct-messages";
|
||||
|
||||
export interface IUserDirectoryOpts {
|
||||
limit: number;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
export const useUserDirectory = () => {
|
||||
const [users, setUsers] = useState<DirectoryMember[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const search = useCallback(async ({
|
||||
limit = 20,
|
||||
query: term,
|
||||
}: IUserDirectoryOpts): Promise<boolean> => {
|
||||
if (!term?.length) {
|
||||
setUsers([]);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { results } = await MatrixClientPeg.get().searchUserDirectory({
|
||||
limit,
|
||||
term,
|
||||
});
|
||||
setUsers(results.map(user => new DirectoryMember(user)));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Could not fetch user in user directory for params", { limit, term }, e);
|
||||
setUsers([]);
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
ready: true,
|
||||
loading,
|
||||
users,
|
||||
search,
|
||||
} as const;
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mount } from "enzyme";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
import { usePublicRoomDirectory } from "../../src/hooks/usePublicRoomDirectory";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../test-utils/test-utils";
|
||||
|
||||
function PublicRoomComponent({ onClick }) {
|
||||
const roomDirectory = usePublicRoomDirectory();
|
||||
|
||||
const {
|
||||
ready,
|
||||
loading,
|
||||
publicRooms,
|
||||
} = roomDirectory;
|
||||
|
||||
return <div onClick={() => onClick(roomDirectory)}>
|
||||
{ (!ready || loading) && `ready: ${ready}, loading: ${loading}` }
|
||||
{ publicRooms[0] && (
|
||||
`Name: ${publicRooms[0].name}`
|
||||
) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
describe("usePublicRoomDirectory", () => {
|
||||
let cli;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
|
||||
MatrixClientPeg.getHomeserverName = () => "matrix.org";
|
||||
cli.getThirdpartyProtocols = () => Promise.resolve({});
|
||||
cli.publicRooms = (({ filter: { generic_search_term: query } }) => Promise.resolve({
|
||||
chunk: [{
|
||||
room_id: "hello world!",
|
||||
name: query,
|
||||
world_readable: true,
|
||||
guest_can_join: true,
|
||||
num_joined_members: 1,
|
||||
}],
|
||||
total_room_count_estimate: 1,
|
||||
}));
|
||||
});
|
||||
|
||||
it("should display public rooms when searching", async () => {
|
||||
const query = "ROOM NAME";
|
||||
|
||||
const wrapper = mount(<PublicRoomComponent onClick={(hook) => {
|
||||
hook.search({
|
||||
limit: 1,
|
||||
query,
|
||||
});
|
||||
}} />);
|
||||
|
||||
expect(wrapper.text()).toBe("ready: false, loading: false");
|
||||
|
||||
await act(async () => {
|
||||
await sleep(1);
|
||||
wrapper.simulate("click");
|
||||
return act(() => sleep(1));
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain(query);
|
||||
});
|
||||
|
||||
it("should work with empty queries", async () => {
|
||||
const wrapper = mount(<PublicRoomComponent onClick={(hook) => {
|
||||
hook.search({
|
||||
limit: 1,
|
||||
query: "",
|
||||
});
|
||||
}} />);
|
||||
|
||||
await act(async () => {
|
||||
await sleep(1);
|
||||
wrapper.simulate("click");
|
||||
return act(() => sleep(1));
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toBe("");
|
||||
});
|
||||
|
||||
it("should recover from a server exception", async () => {
|
||||
cli.publicRooms = () => { throw new Error("Oops"); };
|
||||
const query = "ROOM NAME";
|
||||
|
||||
const wrapper = mount(<PublicRoomComponent onClick={(hook) => {
|
||||
hook.search({
|
||||
limit: 1,
|
||||
query,
|
||||
});
|
||||
}} />);
|
||||
await act(async () => {
|
||||
await sleep(1);
|
||||
wrapper.simulate("click");
|
||||
return act(() => sleep(1));
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toBe("");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mount } from "enzyme";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
import { useUserDirectory } from "../../src/hooks/useUserDirectory";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../test-utils";
|
||||
|
||||
function UserDirectoryComponent({ onClick }) {
|
||||
const userDirectory = useUserDirectory();
|
||||
|
||||
const {
|
||||
ready,
|
||||
loading,
|
||||
users,
|
||||
} = userDirectory;
|
||||
|
||||
return <div onClick={() => onClick(userDirectory)}>
|
||||
{ users[0]
|
||||
? (
|
||||
`Name: ${users[0].name}`
|
||||
)
|
||||
: `ready: ${ready}, loading: ${loading}` }
|
||||
</div>;
|
||||
}
|
||||
|
||||
describe("useUserDirectory", () => {
|
||||
let cli;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
|
||||
MatrixClientPeg.getHomeserverName = () => "matrix.org";
|
||||
cli.getThirdpartyProtocols = () => Promise.resolve({});
|
||||
cli.searchUserDirectory = (({ term: query }) => Promise.resolve({
|
||||
results: [{
|
||||
user_id: "@bob:matrix.org",
|
||||
display_name: query,
|
||||
}] },
|
||||
));
|
||||
});
|
||||
|
||||
it("search for users in the identity server", async () => {
|
||||
const query = "Bob";
|
||||
|
||||
const wrapper = mount(<UserDirectoryComponent onClick={(hook) => {
|
||||
hook.search({
|
||||
limit: 1,
|
||||
query,
|
||||
});
|
||||
}} />);
|
||||
|
||||
expect(wrapper.text()).toBe("ready: true, loading: false");
|
||||
|
||||
await act(async () => {
|
||||
await sleep(1);
|
||||
wrapper.simulate("click");
|
||||
return act(() => sleep(1));
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain(query);
|
||||
});
|
||||
|
||||
it("should work with empty queries", async () => {
|
||||
const query = "";
|
||||
|
||||
const wrapper = mount(<UserDirectoryComponent onClick={(hook) => {
|
||||
hook.search({
|
||||
limit: 1,
|
||||
query,
|
||||
});
|
||||
}} />);
|
||||
await act(async () => {
|
||||
await sleep(1);
|
||||
wrapper.simulate("click");
|
||||
return act(() => sleep(1));
|
||||
});
|
||||
expect(wrapper.text()).toBe("ready: true, loading: false");
|
||||
});
|
||||
|
||||
it("should work with empty queries", async () => {
|
||||
cli.searchUserDirectory = () => { throw new Error("Oops"); };
|
||||
const query = "Bob";
|
||||
|
||||
const wrapper = mount(<UserDirectoryComponent onClick={(hook) => {
|
||||
hook.search({
|
||||
limit: 1,
|
||||
query,
|
||||
});
|
||||
}} />);
|
||||
await act(async () => {
|
||||
await sleep(1);
|
||||
wrapper.simulate("click");
|
||||
return act(() => sleep(1));
|
||||
});
|
||||
expect(wrapper.text()).toBe("ready: true, loading: false");
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue