Merge pull request #126 from matrix-org/matrixtwo/groups

Add rudimentary group support
This commit is contained in:
Jorik Schellekens 2020-09-23 12:03:04 +01:00 committed by GitHub
commit dd372dbb50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 223 additions and 78 deletions

View File

@ -25,4 +25,5 @@ limitations under the License.
.avatarNoCrop {
border-radius: 0;
border: 0;
}

View File

@ -16,7 +16,7 @@ limitations under the License.
import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import { Room, User } from '../matrix-cypher';
import { Group, Room, User } from '../matrix-cypher';
import { getMediaQueryFromMCX } from '../utils/cypher-wrapper';
import logo from '../imgs/chat-icon.svg';
@ -35,12 +35,15 @@ const Avatar: React.FC<IProps> = ({ className, avatarUrl, label }: IProps) => {
setSrc(avatarUrl);
}, [avatarUrl]);
const _className = classNames('avatar', className, {
avatarNoCrop: src === logo,
});
return (
<img
src={src}
onError={(): void => setSrc(logo)}
alt={label}
className={classNames('avatar', className)}
className={_className}
/>
);
};
@ -73,4 +76,17 @@ export const RoomAvatar: React.FC<IPropsRoomAvatar> = ({
/>
);
interface IPropsGroupAvatar {
group: Group;
}
export const GroupAvatar: React.FC<IPropsGroupAvatar> = ({
group,
}: IPropsGroupAvatar) => (
<Avatar
avatarUrl={getMediaQueryFromMCX(group.avatar_url)}
label={group.name}
/>
);
export default Avatar;

View File

@ -0,0 +1,26 @@
/*
Copyright 2020 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.
*/
.groupPreview {
> .avatar {
margin-bottom: 8px;
}
> h1 {
font-size: 24px;
margin-bottom: 4px;
}
}

View File

@ -0,0 +1,44 @@
/*
Copyright 2020 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 React from 'react';
import { Group } from '../matrix-cypher';
import { GroupAvatar } from './Avatar';
import './GroupPreview.scss';
interface IProps {
group: Group;
}
const GroupPreview: React.FC<IProps> = ({ group }: IProps) => {
const description = group.long_description
? group.long_description
: group.short_description
? group.short_description
: null;
return (
<div className="groupPreview">
<GroupAvatar group={group} />
<h1>{group.name}</h1>
{description ? <p>{description}</p> : null}
</div>
);
};
export default GroupPreview;

View File

@ -22,6 +22,7 @@ import InviteTile from './InviteTile';
import { SafeLink, LinkKind } from '../parser/types';
import UserPreview, { WrappedInviterPreview } from './UserPreview';
import EventPreview from './EventPreview';
import GroupPreview from './GroupPreview';
import HomeserverOptions from './HomeserverOptions';
import DefaultPreview from './DefaultPreview';
import Toggle from './Toggle';
@ -31,6 +32,7 @@ import {
getRoomFromAlias,
getRoomFromPermalink,
getUser,
getGroup,
} from '../utils/cypher-wrapper';
import { ClientContext } from '../contexts/ClientContext';
import useHSs from '../utils/getHS';
@ -86,6 +88,13 @@ const invite = async ({
/>
);
case LinkKind.GroupId:
return (
<GroupPreview
group={await getGroup(clientAddress, link.identifier)}
/>
);
default:
// Todo Implement events
return <></>;

View File

@ -16,28 +16,24 @@ limitations under the License.
/* eslint-disable import/first */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
import any from 'promise.any';
any.shim()
any.shim();
import VersionSchema from './schemas/VersionSchema';
import WellKnownSchema from './schemas/WellKnownSchema';
import UserSchema, { User } from './schemas/UserSchema';
import RoomAliasSchema, {
RoomAlias,
} from './schemas/RoomAliasSchema';
import RoomAliasSchema, { RoomAlias } from './schemas/RoomAliasSchema';
import PublicRoomsSchema, {
PublicRooms,
Room,
} from './schemas/PublicRoomsSchema';
import EventSchema, {
Event,
} from './schemas/EventSchema';
import EventSchema, { Event } from './schemas/EventSchema';
import GroupSchema, { Group } from './schemas/GroupSchema';
import { ensure } from './utils/promises';
import { prefixFetch, parseJSON } from './utils/fetch';
/*
* A client is a resolved homeserver name wrapped in a lambda'd fetch
*/
@ -46,7 +42,7 @@ export type Client = (path: string) => Promise<Response>;
/*
* Confirms that the target homeserver is properly configured and operational
*/
export const validateHS = (host: string) =>
export const validateHS = (host: string): Promise<string> =>
prefixFetch(host)('/_matrix/client/versions')
.then(parseJSON)
.then(VersionSchema.parse)
@ -56,47 +52,43 @@ export const validateHS = (host: string) =>
* Discovers the correct domain name for the host according to the spec's
* discovery rules
*/
export const discoverServer = (host: string) =>
export const discoverServer = (host: string): Promise<string> =>
prefixFetch(host)('/.well-known/matrix/client')
.then(resp => resp.ok
? resp.json()
.then(WellKnownSchema.parse)
.then(content => {
if (content === undefined) return host;
else if (
'm.homeserver' in content && content['m.homeserver']
) {
return content['m.homeserver'].base_url
} else {
return host
}
})
: ensure(
resp.status === 404,
() => host,
),
.then((resp) =>
resp.ok
? resp
.json()
.then(WellKnownSchema.parse)
.then((content) => {
if (content === undefined) return host;
else if (
'm.homeserver' in content &&
content['m.homeserver']
) {
return content['m.homeserver'].base_url;
} else {
return host;
}
})
: ensure(resp.status === 404, () => host)
)
.then(validateHS)
.then(validateHS);
/*
* Takes a hs domain and resolves it to it's current domain and returns a
* client
*/
export async function client(host: string): Promise<Client> {
return prefixFetch(await discoverServer(host))
return prefixFetch(await discoverServer(host));
}
/*
* Gets the details for a user
*/
export function getUserDetails(
client: Client,
userId: string,
): Promise<User> {
export function getUserDetails(client: Client, userId: string): Promise<User> {
return client(`/_matrix/client/r0/profile/${userId}`)
.then(parseJSON)
.then(UserSchema.parse)
.then(UserSchema.parse);
}
/*
@ -104,7 +96,7 @@ export function getUserDetails(
*/
export function getRoomIdFromAlias(
client: Client,
roomAlias: string,
roomAlias: string
): Promise<RoomAlias> {
const encodedRoomAlias = encodeURIComponent(roomAlias);
return client(`/_matrix/client/r0/directory/room/${encodedRoomAlias}`)
@ -112,23 +104,6 @@ export function getRoomIdFromAlias(
.then(RoomAliasSchema.parse);
}
/*
* Gets the details of a room if that room is public
*/
export function getRoomDetails(clients: Client[], roomId: string): Promise<Room> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return Promise.any(clients.map(client => searchPublicRooms(client, roomId)));
}
/*
* Gets a list of all public rooms on a hs
*/
export function getPublicRooms(client: Client): Promise<PublicRooms> {
return getPublicRoomsUnsafe(client)
.then(PublicRoomsSchema.parse)
}
/*
* Similar to getPubliRooms however id doesn't confirm the data returned from
* the hs is correct
@ -138,8 +113,14 @@ export function getPublicRooms(client: Client): Promise<PublicRooms> {
*/
export function getPublicRoomsUnsafe(client: Client): Promise<PublicRooms> {
// TODO: Do not assume server will return all results in one go
return client('/_matrix/client/r0/publicRooms')
.then(parseJSON)
return client('/_matrix/client/r0/publicRooms').then(parseJSON);
}
/*
* Gets a list of all public rooms on a hs
*/
export function getPublicRooms(client: Client): Promise<PublicRooms> {
return getPublicRoomsUnsafe(client).then(PublicRoomsSchema.parse);
}
/*
@ -147,20 +128,33 @@ export function getPublicRoomsUnsafe(client: Client): Promise<PublicRooms> {
*/
export function searchPublicRooms(
client: Client,
roomId: string,
roomId: string
): Promise<Room> {
// we use the unsage version here because the safe one is sloooow
return getPublicRoomsUnsafe(client)
.then(rooms => {
const [match] = rooms.chunk.filter(
chunk => chunk.room_id === roomId,
);
return match !== undefined
? Promise.resolve(match)
: Promise.reject(new Error(
`This server knowns no public room with id ${roomId}`,
));
});
return getPublicRoomsUnsafe(client).then((rooms) => {
const [match] = rooms.chunk.filter((chunk) => chunk.room_id === roomId);
return match !== undefined
? Promise.resolve(match)
: Promise.reject(
new Error(
`This server knowns no public room with id ${roomId}`
)
);
});
}
/*
* Gets the details of a room if that room is public
*/
export function getRoomDetails(
clients: Client[],
roomId: string
): Promise<Room> {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return Promise.any(
clients.map((client) => searchPublicRooms(client, roomId))
);
}
/*
@ -169,24 +163,33 @@ export function searchPublicRooms(
export async function getEvent(
client: Client,
roomIdOrAlias: string,
eventId: string,
eventId: string
): Promise<Event> {
return client(`/_matrix/client/r0/rooms/${roomIdOrAlias}/event/${eventId}`)
.then(parseJSON)
.then(EventSchema.parse);
}
/*
* Gets community information
*/
export async function getGroupDetails(
client: Client,
groupId: string
): Promise<Group> {
return client(`/_matrix/client/r0/groups/${groupId}/profile`)
.then(parseJSON)
.then(GroupSchema.parse);
}
/*
* Gets an mxc resource
*/
export function convertMXCtoMediaQuery(
clientURL: string,
mxc: string,
): string {
export function convertMXCtoMediaQuery(clientURL: string, mxc: string): string {
// mxc://matrix.org/EqMZYbAYhREvHXvYFyfxOlkf
const matches = mxc.match(/mxc:\/\/(.+)\/(.+)/)
const matches = mxc.match(/mxc:\/\/(.+)\/(.+)/);
if (!matches) {
throw new Error(`mxc invalid: ${JSON.stringify({mxc})}`);
throw new Error(`mxc invalid: ${JSON.stringify({ mxc })}`);
}
return `${clientURL}/_matrix/media/r0/download/${matches[1]}/${matches[2]}`;

View File

@ -0,0 +1,27 @@
/*
Copyright 2020 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 { object, string, TypeOf } from 'zod';
const GroupSchema = object({
name: string(),
avatar_url: string().optional(),
short_description: string().optional(),
long_description: string().optional(),
});
export type Group = TypeOf<typeof GroupSchema>;
export default GroupSchema;

View File

@ -15,10 +15,10 @@ limitations under the License.
*/
export * from './EventSchema';
export * from './GroupSchema';
export * from './PublicRoomsSchema';
export * from './RoomAliasSchema';
export * from './UserSchema';
export * from './VersionSchema';
export * from './WellKnownSchema';
export * from './index';

View File

@ -24,10 +24,12 @@ import {
Room,
RoomAlias,
User,
Group,
getRoomIdFromAlias,
searchPublicRooms,
getUserDetails,
convertMXCtoMediaQuery,
getGroupDetails,
} from '../matrix-cypher';
import { LinkKind, Permalink } from '../parser/types';
@ -72,6 +74,11 @@ export const fallbackRoom = ({
};
};
export const fallbackGroup = (groupId: string): Group => ({
name: groupId,
short_description: `The ${groupId} group`,
});
/*
* Tries to fetch room details from an alias. If it fails it uses
* a `fallbackRoom`
@ -169,3 +176,15 @@ export function getMediaQueryFromMCX(mxc?: string): string {
return '';
}
}
export async function getGroup(
clientURL: string,
groupId: string
): Promise<Group> {
try {
const resolvedClient = await client(clientURL);
return await getGroupDetails(resolvedClient, groupId);
} catch {
return fallbackGroup(groupId);
}
}