From cc175b34cba9a36303c6b01e3998700ecd5c692d Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 17 Sep 2020 15:41:32 +0100 Subject: [PATCH 1/3] Add redimentary group support --- src/components/Avatar.scss | 1 + src/components/Avatar.tsx | 20 ++- src/components/GroupPreview.scss | 26 ++++ src/components/GroupPreview.tsx | 46 +++++++ src/components/LinkPreview.tsx | 9 ++ src/matrix-cypher/matrix-cypher.ts | 155 ++++++++++++----------- src/matrix-cypher/schemas/GroupSchema.ts | 27 ++++ src/matrix-cypher/schemas/index.ts | 2 +- src/utils/cypher-wrapper.ts | 19 +++ 9 files changed, 225 insertions(+), 80 deletions(-) create mode 100644 src/components/GroupPreview.scss create mode 100644 src/components/GroupPreview.tsx create mode 100644 src/matrix-cypher/schemas/GroupSchema.ts diff --git a/src/components/Avatar.scss b/src/components/Avatar.scss index 794064a..6767f2a 100644 --- a/src/components/Avatar.scss +++ b/src/components/Avatar.scss @@ -25,4 +25,5 @@ limitations under the License. .avatarNoCrop { border-radius: 0; + border: 0; } diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index f150c63..222790c 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -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 = ({ className, avatarUrl, label }: IProps) => { setSrc(avatarUrl); }, [avatarUrl]); + const _className = classNames('avatar', className, { + avatarNoCrop: src === logo, + }); return ( setSrc(logo)} alt={label} - className={classNames('avatar', className)} + className={_className} /> ); }; @@ -73,4 +76,17 @@ export const RoomAvatar: React.FC = ({ /> ); +interface IPropsGroupAvatar { + group: Group; +} + +export const GroupAvatar: React.FC = ({ + group, +}: IPropsGroupAvatar) => ( + +); + export default Avatar; diff --git a/src/components/GroupPreview.scss b/src/components/GroupPreview.scss new file mode 100644 index 0000000..aae3aba --- /dev/null +++ b/src/components/GroupPreview.scss @@ -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; + } +} diff --git a/src/components/GroupPreview.tsx b/src/components/GroupPreview.tsx new file mode 100644 index 0000000..91596cf --- /dev/null +++ b/src/components/GroupPreview.tsx @@ -0,0 +1,46 @@ +/* +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 = ({ group }: IProps) => { + const description = group.long_description + ? group.long_description + : group.short_description + ? group.short_description + : null; + + const descriptionP = description ?

{description}

: null; + + return ( +
+ +

{group.name}

+ {descriptionP} +
+ ); +}; + +export default GroupPreview; diff --git a/src/components/LinkPreview.tsx b/src/components/LinkPreview.tsx index ae4170c..0372221 100644 --- a/src/components/LinkPreview.tsx +++ b/src/components/LinkPreview.tsx @@ -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 ( + + ); + default: // Todo Implement events return <>; diff --git a/src/matrix-cypher/matrix-cypher.ts b/src/matrix-cypher/matrix-cypher.ts index 75387a2..4eafb03 100644 --- a/src/matrix-cypher/matrix-cypher.ts +++ b/src/matrix-cypher/matrix-cypher.ts @@ -14,30 +14,24 @@ See the License for the specific language governing permissions and 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 +40,7 @@ export type Client = (path: string) => Promise; /* * Confirms that the target homeserver is properly configured and operational */ -export const validateHS = (host: string) => +export const validateHS = (host: string): Promise => prefixFetch(host)('/_matrix/client/versions') .then(parseJSON) .then(VersionSchema.parse) @@ -56,47 +50,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 => 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 { - return prefixFetch(await discoverServer(host)) + return prefixFetch(await discoverServer(host)); } /* * Gets the details for a user */ -export function getUserDetails( - client: Client, - userId: string, -): Promise { +export function getUserDetails(client: Client, userId: string): Promise { return client(`/_matrix/client/r0/profile/${userId}`) .then(parseJSON) - .then(UserSchema.parse) + .then(UserSchema.parse); } /* @@ -104,7 +94,7 @@ export function getUserDetails( */ export function getRoomIdFromAlias( client: Client, - roomAlias: string, + roomAlias: string ): Promise { const encodedRoomAlias = encodeURIComponent(roomAlias); return client(`/_matrix/client/r0/directory/room/${encodedRoomAlias}`) @@ -112,23 +102,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 { - // 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 { - return getPublicRoomsUnsafe(client) - .then(PublicRoomsSchema.parse) -} - /* * Similar to getPubliRooms however id doesn't confirm the data returned from * the hs is correct @@ -138,8 +111,14 @@ export function getPublicRooms(client: Client): Promise { */ export function getPublicRoomsUnsafe(client: Client): Promise { // 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 { + return getPublicRoomsUnsafe(client).then(PublicRoomsSchema.parse); } /* @@ -147,20 +126,33 @@ export function getPublicRoomsUnsafe(client: Client): Promise { */ export function searchPublicRooms( client: Client, - roomId: string, + roomId: string ): Promise { // 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 { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + return Promise.any( + clients.map((client) => searchPublicRooms(client, roomId)) + ); } /* @@ -169,24 +161,33 @@ export function searchPublicRooms( export async function getEvent( client: Client, roomIdOrAlias: string, - eventId: string, + eventId: string ): Promise { 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 { + 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]}`; diff --git a/src/matrix-cypher/schemas/GroupSchema.ts b/src/matrix-cypher/schemas/GroupSchema.ts new file mode 100644 index 0000000..e17c116 --- /dev/null +++ b/src/matrix-cypher/schemas/GroupSchema.ts @@ -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; +export default GroupSchema; diff --git a/src/matrix-cypher/schemas/index.ts b/src/matrix-cypher/schemas/index.ts index f7fa3c3..34281fb 100644 --- a/src/matrix-cypher/schemas/index.ts +++ b/src/matrix-cypher/schemas/index.ts @@ -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'; - diff --git a/src/utils/cypher-wrapper.ts b/src/utils/cypher-wrapper.ts index 7e1093e..0e30ba0 100644 --- a/src/utils/cypher-wrapper.ts +++ b/src/utils/cypher-wrapper.ts @@ -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 { + try { + const resolvedClient = await client(clientURL); + return await getGroupDetails(resolvedClient, groupId); + } catch { + return fallbackGroup(groupId); + } +} From 79a0a1c7baa4ada98a20c931f74460a5988ef394 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 17 Sep 2020 15:45:59 +0100 Subject: [PATCH 2/3] Fix eslint rule --- src/matrix-cypher/matrix-cypher.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matrix-cypher/matrix-cypher.ts b/src/matrix-cypher/matrix-cypher.ts index 4eafb03..c24947a 100644 --- a/src/matrix-cypher/matrix-cypher.ts +++ b/src/matrix-cypher/matrix-cypher.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +/* eslint-disable import/first */ + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore import any from 'promise.any'; From 645d0ab6fc0b057593f6e39f06b412c664b58565 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 23 Sep 2020 12:02:26 +0100 Subject: [PATCH 3/3] inline small

ternary --- src/components/GroupPreview.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/GroupPreview.tsx b/src/components/GroupPreview.tsx index 91596cf..f90ecf2 100644 --- a/src/components/GroupPreview.tsx +++ b/src/components/GroupPreview.tsx @@ -32,13 +32,11 @@ const GroupPreview: React.FC = ({ group }: IProps) => { ? group.short_description : null; - const descriptionP = description ?

{description}

: null; - return (

{group.name}

- {descriptionP} + {description ?

{description}

: null}
); };