Merge pull request #126 from matrix-org/matrixtwo/groups
Add rudimentary group support
This commit is contained in:
commit
dd372dbb50
9 changed files with 223 additions and 78 deletions
|
@ -25,4 +25,5 @@ limitations under the License.
|
|||
|
||||
.avatarNoCrop {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
26
src/components/GroupPreview.scss
Normal file
26
src/components/GroupPreview.scss
Normal 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;
|
||||
}
|
||||
}
|
44
src/components/GroupPreview.tsx
Normal file
44
src/components/GroupPreview.tsx
Normal 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;
|
|
@ -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 <></>;
|
||||
|
|
|
@ -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((resp) =>
|
||||
resp.ok
|
||||
? resp
|
||||
.json()
|
||||
.then(WellKnownSchema.parse)
|
||||
.then(content => {
|
||||
.then((content) => {
|
||||
if (content === undefined) return host;
|
||||
else if (
|
||||
'm.homeserver' in content && content['m.homeserver']
|
||||
'm.homeserver' in content &&
|
||||
content['m.homeserver']
|
||||
) {
|
||||
return content['m.homeserver'].base_url
|
||||
return content['m.homeserver'].base_url;
|
||||
} else {
|
||||
return host
|
||||
return host;
|
||||
}
|
||||
})
|
||||
: ensure(
|
||||
resp.status === 404,
|
||||
() => 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,46 +128,68 @@ 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 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}`,
|
||||
));
|
||||
: 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))
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the details of an event from the homeserver
|
||||
*/
|
||||
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]}`;
|
||||
|
|
27
src/matrix-cypher/schemas/GroupSchema.ts
Normal file
27
src/matrix-cypher/schemas/GroupSchema.ts
Normal 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;
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue