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