From f017172c3b76f6968b13872117003652a0df427d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 17 May 2022 13:17:28 +0100 Subject: [PATCH 1/5] Add public room directory hook --- src/hooks/usePublicRoomDirectory.ts | 164 +++++++++++++++++++++ test/hooks/usePublicRoomDirectory-test.tsx | 84 +++++++++++ 2 files changed, 248 insertions(+) create mode 100644 src/hooks/usePublicRoomDirectory.ts create mode 100644 test/hooks/usePublicRoomDirectory-test.tsx diff --git a/src/hooks/usePublicRoomDirectory.ts b/src/hooks/usePublicRoomDirectory.ts new file mode 100644 index 00000000000..50b7447157f --- /dev/null +++ b/src/hooks/usePublicRoomDirectory.ts @@ -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([]); + + const [roomServer, setRoomServer] = useState(undefined); + const [instanceId, setInstanceId] = useState(undefined); + const [protocols, setProtocols] = useState(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 => { + if (!query.length) { + setPublicRooms([]); + return; + } + + 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; +}; diff --git a/test/hooks/usePublicRoomDirectory-test.tsx b/test/hooks/usePublicRoomDirectory-test.tsx new file mode 100644 index 00000000000..563d9eb97df --- /dev/null +++ b/test/hooks/usePublicRoomDirectory-test.tsx @@ -0,0 +1,84 @@ +/* +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
onClick(roomDirectory)}> + { (!ready || loading) && `ready: ${ready}, loading: ${loading}` } + { publicRooms[0] && ( + `Name: ${publicRooms[0].name}` + ) } +
; +} + +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( { + hook.search({ + limit: 1, + query, + }); + }} />); + + expect(wrapper.text()).toBe("ready: false, loading: false"); + + await act(async () => { + await sleep(100); + wrapper.simulate("click"); + return act(() => sleep(500)); + }); + + expect(wrapper.text()).toContain(query); + }); +}); From 8df4df8416522c7b79da9447280b98f8f7e56d63 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 17 May 2022 13:40:38 +0100 Subject: [PATCH 2/5] Add useUserDirectory hook --- src/hooks/useUserDirectory.ts | 63 ++++++++++++++++++++++ test/hooks/useUserDirectory-test.tsx | 81 ++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 src/hooks/useUserDirectory.ts create mode 100644 test/hooks/useUserDirectory-test.tsx diff --git a/src/hooks/useUserDirectory.ts b/src/hooks/useUserDirectory.ts new file mode 100644 index 00000000000..9a327f02c4b --- /dev/null +++ b/src/hooks/useUserDirectory.ts @@ -0,0 +1,63 @@ +/* +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([]); + + const [loading, setLoading] = useState(false); + + const search = useCallback(async ({ + limit = 20, + query: term, + }: IUserDirectoryOpts): Promise => { + if (!term.length) { + setUsers([]); + return; + } + + 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; +}; diff --git a/test/hooks/useUserDirectory-test.tsx b/test/hooks/useUserDirectory-test.tsx new file mode 100644 index 00000000000..aa87c239129 --- /dev/null +++ b/test/hooks/useUserDirectory-test.tsx @@ -0,0 +1,81 @@ +/* +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
onClick(userDirectory)}> + { users[0] + ? ( + `Name: ${users[0].name}` + ) + : `ready: ${ready}, loading: ${loading}` } +
; +} + +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( { + hook.search({ + limit: 1, + query, + }); + }} />); + + expect(wrapper.text()).toBe("ready: true, loading: false"); + + await act(async () => { + await sleep(100); + wrapper.simulate("click"); + return act(() => sleep(500)); + }); + + expect(wrapper.text()).toContain(query); + }); +}); From 550d1f94ce1d71998acb98bf9374fea70b86f4b0 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 17 May 2022 13:55:16 +0100 Subject: [PATCH 3/5] fix lint --- src/hooks/useUserDirectory.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hooks/useUserDirectory.ts b/src/hooks/useUserDirectory.ts index 9a327f02c4b..dffa6e8622c 100644 --- a/src/hooks/useUserDirectory.ts +++ b/src/hooks/useUserDirectory.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { useCallback, useState } from "react"; + import { MatrixClientPeg } from "../MatrixClientPeg"; import { DirectoryMember } from "../utils/direct-messages"; From 6bb715ede8b557be2e7a1537d38e8838e64c3628 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 18 May 2022 08:44:37 +0100 Subject: [PATCH 4/5] Add conditional chaining and fix return statements --- src/hooks/usePublicRoomDirectory.ts | 4 ++-- src/hooks/useUserDirectory.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/usePublicRoomDirectory.ts b/src/hooks/usePublicRoomDirectory.ts index 50b7447157f..24cc8f541a1 100644 --- a/src/hooks/usePublicRoomDirectory.ts +++ b/src/hooks/usePublicRoomDirectory.ts @@ -70,9 +70,9 @@ export const usePublicRoomDirectory = () => { limit = 20, query, }: IPublicRoomsOpts): Promise => { - if (!query.length) { + if (!query?.length) { setPublicRooms([]); - return; + return true; } const opts: IRoomDirectoryOptions = { limit }; diff --git a/src/hooks/useUserDirectory.ts b/src/hooks/useUserDirectory.ts index dffa6e8622c..cb7307af2ac 100644 --- a/src/hooks/useUserDirectory.ts +++ b/src/hooks/useUserDirectory.ts @@ -33,9 +33,9 @@ export const useUserDirectory = () => { limit = 20, query: term, }: IUserDirectoryOpts): Promise => { - if (!term.length) { + if (!term?.length) { setUsers([]); - return; + return true; } try { From c20dbe0bdd3cb5f8e3289bf72338787ffe8d585d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 18 May 2022 08:53:42 +0100 Subject: [PATCH 5/5] Add more tests --- test/hooks/usePublicRoomDirectory-test.tsx | 40 ++++++++++++++++++++-- test/hooks/useUserDirectory-test.tsx | 39 +++++++++++++++++++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/test/hooks/usePublicRoomDirectory-test.tsx b/test/hooks/usePublicRoomDirectory-test.tsx index 563d9eb97df..3cd76d11248 100644 --- a/test/hooks/usePublicRoomDirectory-test.tsx +++ b/test/hooks/usePublicRoomDirectory-test.tsx @@ -74,11 +74,47 @@ describe("usePublicRoomDirectory", () => { expect(wrapper.text()).toBe("ready: false, loading: false"); await act(async () => { - await sleep(100); + await sleep(1); wrapper.simulate("click"); - return act(() => sleep(500)); + return act(() => sleep(1)); }); expect(wrapper.text()).toContain(query); }); + + it("should work with empty queries", async () => { + const wrapper = mount( { + 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( { + hook.search({ + limit: 1, + query, + }); + }} />); + await act(async () => { + await sleep(1); + wrapper.simulate("click"); + return act(() => sleep(1)); + }); + + expect(wrapper.text()).toBe(""); + }); }); diff --git a/test/hooks/useUserDirectory-test.tsx b/test/hooks/useUserDirectory-test.tsx index aa87c239129..bcd2861dfee 100644 --- a/test/hooks/useUserDirectory-test.tsx +++ b/test/hooks/useUserDirectory-test.tsx @@ -71,11 +71,46 @@ describe("useUserDirectory", () => { expect(wrapper.text()).toBe("ready: true, loading: false"); await act(async () => { - await sleep(100); + await sleep(1); wrapper.simulate("click"); - return act(() => sleep(500)); + return act(() => sleep(1)); }); expect(wrapper.text()).toContain(query); }); + + it("should work with empty queries", async () => { + const query = ""; + + const wrapper = mount( { + 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( { + 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"); + }); });