cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

commit 89faac0c833ef155d213342643f5f0091aaae221
parent ebe9feb8f811ebb6f4b4f8efc1e2649f7f7aae05
Author: MTRNord <mtrnord1@gmail.com>
Date:   Sat,  6 May 2023 15:36:14 +0200

Improve font sizes, fix stories for input, render non encrypted images, fix avatar sizes

Diffstat:
Msrc/app/sdk/api/apiTypes.ts | 27+++++++++++++++++++++++++++
Msrc/app/sdk/client.ts | 4++++
Msrc/app/sdk/room.ts | 4++--
Msrc/components/avatar/avatar.tsx | 2+-
Msrc/components/events/messageEvent.tsx | 39++++++++++++++++++++++++++++++++++++---
Msrc/components/input/chat/input.stories.tsx | 24------------------------
Msrc/index.scss | 4++++
Mtailwind.config.js | 10++++++++++
8 files changed, 84 insertions(+), 30 deletions(-)

diff --git a/src/app/sdk/api/apiTypes.ts b/src/app/sdk/api/apiTypes.ts @@ -311,6 +311,12 @@ export interface IRoomMessageTextContent extends IRoomMessageContent<"m.text"> { formatted_body?: string; } +export interface IRoomMessageImageContent extends IRoomMessageContent<"m.image"> { + info?: IImageInfo; + url?: string; + file?: IEncryptedFile; +} + export interface IRoomMessageEvent<Content = any> extends IRoomEvent<Content> { } export interface IRoomMessageTextEvent extends IRoomMessageEvent<IRoomMessageTextContent> { } @@ -319,6 +325,10 @@ export function isRoomMessageTextEvent(event: IRoomEvent): event is IRoomMessage return event.type === "m.room.message" && event.content.msgtype === "m.text"; } +export function isRoomMessageImageEvent(event: IRoomEvent): event is IRoomMessageEvent<IRoomMessageImageContent> { + return event.type === "m.room.message" && event.content.msgtype === "m.image"; +} + export function isRoomMessageEvent(event: IRoomEvent): event is IRoomMessageEvent { return event.type === "m.room.message"; } @@ -368,12 +378,29 @@ export interface IThumbnailInfo { w: number; } +export interface IEncryptedFile { + v: string; + key: { + alg: string; + ext: boolean; + k: string; + key_ops: string[]; + kty: string; + }; + iv: string; + hashes: { + [key: string]: string; + }; + url: string; +} + export interface IImageInfo { h: number; mimetype: string; size: number; thumbnail_info?: IThumbnailInfo; thumbnail_url?: string; + thumbnail_file?: IEncryptedFile; w: number; } diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts @@ -134,6 +134,10 @@ export class MatrixClient extends EventEmitter { return this.access_token !== undefined; } + public convertMXC(url: string): string { + return `${this.hostname}/_matrix/media/r0/download/${url.substring(6)}`; + } + public setCurrentRoom(roomID?: string) { if (roomID !== this.currentRoom) { this.currentRoom = roomID; diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts @@ -71,7 +71,7 @@ export class Room extends EventEmitter { if (isRoomAvatarEvent(event)) { const rawAvatarURL = event.content.url; if (rawAvatarURL?.startsWith("mxc://")) { - avatarURL = `${this.hostname}/_matrix/media/r0/download/${rawAvatarURL.substring(6)}`; + avatarURL = this.client.convertMXC(rawAvatarURL); } } }); @@ -197,7 +197,7 @@ export class Room extends EventEmitter { if (event.state_key === userID && event.content.membership == "join") { const rawAvatarURL = event.content.avatar_url; if (rawAvatarURL?.startsWith("mxc://")) { - avatarURL = `${this.hostname}/_matrix/media/r0/download/${rawAvatarURL.substring(6)}`; + avatarURL = this.client.convertMXC(rawAvatarURL); } } } diff --git a/src/components/avatar/avatar.tsx b/src/components/avatar/avatar.tsx @@ -23,7 +23,7 @@ const Avatar: FC<AvatarProps> = memo(({ avatarUrl, displayname, dm = false, onli if (avatarUrl) { return ( <div className="flex relative min-w-[2rem] min-h-[2rem] justify-center items-center m-0 mr-3 text-xl rounded-full text-white"> - <img className="rounded-full w-8 h-8" alt={displayname} src={avatarUrl} /> + <img className="rounded-full w-8 h-8 object-cover" alt={displayname} src={avatarUrl} /> { dm ? ( diff --git a/src/components/events/messageEvent.tsx b/src/components/events/messageEvent.tsx @@ -1,8 +1,8 @@ -import { memo } from "react"; -import { IRoomEvent, isRoomMessageTextEvent } from "../../app/sdk/api/apiTypes"; +import { memo, useContext, useEffect, useState } from "react"; +import { IRoomEvent, isRoomMessageImageEvent, isRoomMessageTextEvent } from "../../app/sdk/api/apiTypes"; import { FC } from "react"; import Avatar from "../avatar/avatar"; -import { useRoom } from "../../app/sdk/client"; +import { MatrixContext, useRoom } from "../../app/sdk/client"; import Linkify from "linkify-react"; import DOMPurify from "dompurify"; @@ -34,6 +34,7 @@ const linkifyOptions = { const MessageEvent: FC<MessageEventProps> = memo(({ event, roomID, hasPreviousEvent }) => { const room = useRoom(roomID); + const client = useContext(MatrixContext); const renderCorrectMessage = (event: IRoomEvent) => { if (isRoomMessageTextEvent(event)) { @@ -113,6 +114,38 @@ const MessageEvent: FC<MessageEventProps> = memo(({ event, roomID, hasPreviousEv </div> ) } + } else if (isRoomMessageImageEvent(event)) { + const [url, setUrl] = useState<string | undefined>(undefined); + + useEffect(() => { + if (event.content.url) { + setUrl(client.convertMXC(event.content.url)); + } else { + // Image is encrypted and we need to download and decrypt it + + } + }, [event.content.url]) + + return ( + <div className={!hasPreviousEvent ? "flex flex-row gap-4 p-2 pb-1 hover:bg-gray-200 rounded-md duration-200 ease-in-out items-start" : "flex flex-row p-2 pb-1 pt-0 hover:bg-gray-200 rounded-md duration-200 ease-in-out"}> + {!hasPreviousEvent && <Avatar + displayname={room?.getMemberName(event.sender) || ""} + avatarUrl={room?.getMemberAvatar(event.sender)} + online={room?.isOnline() || false} + dm={room?.isDM() || false} + />} + <div className={!hasPreviousEvent ? "flex flex-col gap-1" : "ml-[3.7rem]"}> + {!hasPreviousEvent && <h2 className="text-sm font-medium text-red-500 whitespace-normal">{room?.getMemberName(event.sender)}</h2>} + {/* TODO: Loading circle while image is loading */} + <img + src={url} + alt={event.content.body} + title={event.content.body} + className="rounded-md object-cover border-slate-400 border-2 max-h-[512px] max-w-[512px] h-[unset]" + /> + </div> + </div> + ) } else { return ( <div className={!hasPreviousEvent ? "flex flex-row gap-4 p-2 pb-1 hover:bg-gray-200 rounded-md duration-200 ease-in-out items-start" : "flex flex-row p-2 pb-1 pt-0 hover:bg-gray-200 rounded-md duration-200 ease-in-out"}> diff --git a/src/components/input/chat/input.stories.tsx b/src/components/input/chat/input.stories.tsx @@ -1,9 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; import ChatInput from './input'; import { BADGE } from '@geometricpanda/storybook-addon-badges'; -import { $getRoot, EditorState, LexicalEditor } from 'lexical'; -import { TRANSFORMERS, $convertToMarkdownString } from "@lexical/markdown"; -import { $generateHtmlFromNodes } from "@lexical/html"; const meta: Meta<typeof ChatInput> = { title: 'Chat/Input', @@ -18,14 +15,6 @@ const meta: Meta<typeof ChatInput> = { name: "Namespace", description: "The Namespace of the editor.", }, - onChange: { - description: "The onChange handler of the editor.", - required: true, - }, - onError: { - description: "The error handler of the editor.", - required: true, - }, } }; @@ -34,19 +23,6 @@ type Story = StoryObj<typeof ChatInput>; export const Default: Story = { args: { - onChange: (editorState: EditorState, editor: LexicalEditor) => { - editorState.read(() => { - // Read the contents of the EditorState here. - const root = $getRoot(); - // make it markdown - const markdown = $convertToMarkdownString(TRANSFORMERS, root); - const html = $generateHtmlFromNodes(editor, null); - - console.log(markdown); - console.log(html); - }); - }, - onError: (error) => { console.error(error); }, namespace: "Editor" } }; \ No newline at end of file diff --git a/src/index.scss b/src/index.scss @@ -4,6 +4,10 @@ html { height: 100%; } +:root { + font-size: 15px; +} + code { @apply rounded font-normal text-red-500 bg-slate-100 p-0.5; } diff --git a/tailwind.config.js b/tailwind.config.js @@ -12,6 +12,16 @@ export default { sans: ['Inter var', ...defaultTheme.fontFamily.sans], }, }, + fontSize: { + xs: '0.694rem', + sm: '0.833rem', + base: '1rem', + xl: '1.2rem', + '2xl': '1.44rem', + '3xl': '1.728rem', + '4xl': '2.074rem', + '5xl': '2.488rem', + } }, plugins: [ require('@tailwindcss/forms'),