matrix-art

An image gallery for Matrix
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-art.git
Log | Files | Refs | README | LICENSE

commit bca8e2b93a005018fb5b7579cfe0a8857b4f1b4b
parent 0c33d4cdcc79b3f5b93dce1719ad5d3dc66782a3
Author: MTRNord <mtrnord1@gmail.com>
Date:   Mon,  3 Jan 2022 21:56:54 +0100

Calculate the displayname of posters

Diffstat:
Acomponents/FrontPageImage.tsx | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mhelpers/matrix_client.ts | 4++--
Mpages/Home.tsx | 71++++++-----------------------------------------------------------------
Mpages/post/[id].tsx | 28++++++++++++++++++++--------
4 files changed, 158 insertions(+), 75 deletions(-)

diff --git a/components/FrontPageImage.tsx b/components/FrontPageImage.tsx @@ -0,0 +1,129 @@ +import Link from "next/link"; +import { Component } from "react"; +import { ImageEvent, ImageGalleryEvent, MatrixEventBase, MatrixImageEvents } from "../helpers/event_types"; +import { constMatrixArtServer } from "../helpers/matrix_client"; +import { ClientContext } from "./ClientContext"; + +type Props = { + event: MatrixImageEvents; +}; + +type State = { + displayname: string; + error: any; +}; + +export default class FrontPageImage extends Component<Props, State> { + declare context: React.ContextType<typeof ClientContext>; + + constructor(props: Props | Readonly<Props>) { + super(props); + + this.state = { + displayname: this.props.event.sender + } as State; + } + + async componentDidMount() { + // auto-register as a guest if not logged in + if (!this.context.client?.accessToken) { + this.registerAsGuest(); + } else { + console.log("Already logged in"); + if (!this.props.event) { + return; + } + try { + const profile = await this.context.client.getProfile(this.props.event.sender); + this.setState({ + displayname: profile.displayname, + }); + } catch (ex) { + console.debug(`Failed to fetch profile for user ${this.props.event.sender}:`, ex); + } + } + + } + + async registerAsGuest() { + try { + let serverUrl = constMatrixArtServer + "/_matrix/client"; + await this.context.client?.registerAsGuest(serverUrl); + if (typeof window !== "undefined") { + window.location.reload(); + } + } catch (err) { + console.error("Failed to register as guest:", err); + this.setState({ + error: "Failed to register as guest: " + JSON.stringify(err), + }); + } + } + + render() { + const event = this.props.event; + return ( + <> + {isImageGalleryEvent(event) ? this.render_gallery(event) : isImageEvent(event) ? this.render_image(event) : <div key={(event as MatrixEventBase).event_id}></div>} + </> + ); + } + + + render_gallery(event: ImageGalleryEvent) { + const caption = event.content['m.caption'].filter((cap) => { + const possible_html_caption = (cap as { body: string; mimetype: string; }); + return possible_html_caption.body !== undefined && possible_html_caption.mimetype === "text/html"; + }); + let caption_text = ""; + if (caption.length != 0) { + caption_text = (caption[0] as { body: string; mimetype: string; }).body; + } + return event.content['m.image_gallery'].map(image => { + return this.render_image_box(image['m.thumbnail'][0].url, event.event_id + image['m.file'].url, event.event_id, caption_text); + }); + } + + + render_image(event: ImageEvent) { + const caption = event.content['m.caption'].filter((cap) => { + const possible_html_caption = (cap as { body: string; mimetype: string; }); + return possible_html_caption.body !== undefined && possible_html_caption.mimetype === "text/html"; + }); + let caption_text = ""; + if (caption.length != 0) { + caption_text = (caption[0] as { body: string; mimetype: string; }).body; + } + return this.render_image_box(event.content['m.thumbnail'][0].url, event.event_id, event.event_id, caption_text); + } + + render_image_box(thumbnail_url: string, id: string, post_id: string, caption: string) { + // TODO show creators display name instead of mxid and show avatar image + // TODO proper alt text + const direct_link = `/post/${encodeURIComponent(post_id)}`; + return ( + <li className='h-[270px]' key={id}> + <Link href={direct_link} passHref> + <div className='relative h-[270px] cursor-pointer'> + <img alt={caption} title={caption} className='relative max-w-full h-[270px] object-cover align-bottom z-0' src={this.context.client?.thumbnailLink(thumbnail_url, "scale", 270, 270)}></img> + <div className="flex-col max-w-full h-[270px] opacity-0 hover:opacity-100 duration-300 absolute bg-gradient-to-b from-transparent to-black/[.25] inset-0 z-10 flex justify-end items-start text-white p-4"> + <h2 className='truncate max-w-full text-base font-semibold'>{caption}</h2> + <p className='truncate max-w-full text-sm'>{this.state.displayname}</p> + </div> + </div> + </Link> + </li> + ); + } +} +FrontPageImage.contextType = ClientContext; + +// TODO also render the edits properly later on +export function isImageGalleryEvent(event: MatrixImageEvents): event is ImageGalleryEvent { + return event.type === "m.image_gallery" && event.redacted_because === undefined; +} + + +export function isImageEvent(event: MatrixImageEvents): event is ImageEvent { + return event.type === "m.image" && event.redacted_because === undefined; +}; +\ No newline at end of file diff --git a/helpers/matrix_client.ts b/helpers/matrix_client.ts @@ -362,7 +362,7 @@ export default class MatrixClient { return info; } - async getTimeline(roomId: string, limit: number, callback: (arg0: MatrixEvent[]) => void) { + async getTimeline(roomId: string, limit: number, callback: (arg0: MatrixEvent[]) => void | Promise<void>) { if (!this.accessToken) { console.error("No access token"); return []; @@ -386,7 +386,7 @@ export default class MatrixClient { data.chunk.forEach((ev: any) => { msgs.push(ev); }); - callback(msgs); + Promise.resolve(callback(msgs)); seenEvents += msgs.length; if (data.chunk.length < limit) { break; diff --git a/pages/Home.tsx b/pages/Home.tsx @@ -1,13 +1,12 @@ import React, { Component } from 'react'; import { RingLoader } from 'react-spinners'; -import { ImageEvent, ImageGalleryEvent, MatrixEventBase } from '../helpers/event_types'; +import { ImageEvent, ImageGalleryEvent, MatrixEventBase, MatrixImageEvents } from '../helpers/event_types'; import Link from 'next/link'; import Head from 'next/head'; import Header from '../components/Header'; import { ClientContext } from '../components/ClientContext'; import { constMatrixArtServer } from '../helpers/matrix_client'; - -type ImageEvents = ImageEvent | ImageGalleryEvent; +import FrontPageImage from '../components/FrontPageImage'; type Props = { }; @@ -19,7 +18,7 @@ type State = { isLoadingImages: boolean; hasFullyLoaded: boolean; // TODO make sure we parse both extev variants properly - image_events: ImageEvents[] | []; + image_events: MatrixImageEvents[] | []; }; export const centerSpinner = ` @@ -146,7 +145,7 @@ export default class Home extends Component<Props, State>{ </div> <div className='m-10'> <ul className='flex flex-wrap gap-1'> - {image_events.map(event => isImageGalleryEvent(event) ? this.render_gallery(event) : isImageEvent(event) ? this.render_image(event) : <div key={(event as MatrixEventBase).event_id}></div>)} + {image_events.map(event => <FrontPageImage event={event} />)} <li className='grow-[10]'></li> </ul> </div> @@ -158,63 +157,5 @@ export default class Home extends Component<Props, State>{ }; } - - - render_gallery(event: ImageGalleryEvent) { - const caption = event.content['m.caption'].filter((cap) => { - const possible_html_caption = (cap as { body: string; mimetype: string; }); - return possible_html_caption.body !== undefined && possible_html_caption.mimetype === "text/html"; - }); - let caption_text = ""; - if (caption.length != 0) { - caption_text = (caption[0] as { body: string; mimetype: string; }).body; - } - return event.content['m.image_gallery'].map(image => { - return this.render_image_box(image['m.thumbnail'][0].url, event.event_id + image['m.file'].url, event.event_id, event.sender, caption_text); - }); - } - - - render_image(event: ImageEvent) { - const caption = event.content['m.caption'].filter((cap) => { - const possible_html_caption = (cap as { body: string; mimetype: string; }); - return possible_html_caption.body !== undefined && possible_html_caption.mimetype === "text/html"; - }); - let caption_text = ""; - if (caption.length != 0) { - caption_text = (caption[0] as { body: string; mimetype: string; }).body; - } - return this.render_image_box(event.content['m.thumbnail'][0].url, event.event_id, event.event_id, event.sender, caption_text); - } - - render_image_box(thumbnail_url: string, id: string, post_id: string, sender: string, caption: string) { - // TODO show creators display name instead of mxid and show avatar image - // TODO proper alt text - const direct_link = `/post/${encodeURIComponent(post_id)}`; - return ( - <li className='h-[270px]' key={id}> - <Link href={direct_link} passHref> - <div className='relative h-[270px] cursor-pointer'> - <img alt={caption} title={caption} className='relative max-w-full h-[270px] object-cover align-bottom z-0' src={this.context.client?.thumbnailLink(thumbnail_url, "scale", 270, 270)}></img> - <div className="flex-col max-w-full h-[270px] opacity-0 hover:opacity-100 duration-300 absolute bg-gradient-to-b from-transparent to-black/[.25] inset-0 z-10 flex justify-end items-start text-white p-4"> - <h2 className='truncate max-w-full text-base font-semibold'>{caption}</h2> - <p className='truncate max-w-full text-sm'>{sender}</p> - </div> - </div> - </Link> - </li> - ); - } - -} -Home.contextType = ClientContext; - -// TODO also render the edits properly later on -export function isImageGalleryEvent(event: ImageEvents): event is ImageGalleryEvent { - return event.type === "m.image_gallery" && event.redacted_because === undefined; } - - -export function isImageEvent(event: ImageEvents): event is ImageEvent { - return event.type === "m.image" && event.redacted_because === undefined; -}; -\ No newline at end of file +Home.contextType = ClientContext; +\ No newline at end of file diff --git a/pages/post/[id].tsx b/pages/post/[id].tsx @@ -14,7 +14,8 @@ import Header from "../../components/Header"; import { ImageEvent, ImageGalleryEvent, MatrixEventBase, MatrixImageEvents } from "../../helpers/event_types"; import { constMatrixArtServer } from "../../helpers/matrix_client"; import { get_data } from "../api/directory"; -import { isImageEvent, isImageGalleryEvent } from "../Home"; +import { isImageEvent, isImageGalleryEvent } from '../../components/FrontPageImage'; +import Link from 'next/link'; const centerSpinner = ` position: fixed; @@ -30,7 +31,8 @@ type Props = InferGetServerSidePropsType<typeof getServerSideProps> & { type State = { hasFullyLoaded: boolean; isLoadingImages: boolean; - image_event: MatrixImageEvents; + image_event?: MatrixImageEvents; + displayname?: string; error?: any; }; @@ -43,7 +45,7 @@ class Post extends Component<Props, State> { this.state = { hasFullyLoaded: false, isLoadingImages: false - } as State; + }; } async componentDidMount() { @@ -75,12 +77,22 @@ class Post extends Component<Props, State> { for (let user of this.props.directory_data) { // We dont need many events const roomId = await this.context.client?.followUser(user.user_room); - await this.context.client?.getTimeline(roomId, 100, (events) => { + await this.context.client?.getTimeline(roomId, 100, async (events) => { // Filter events by type const image_event = events.filter((event) => (event.type === "m.image_gallery" || event.type === "m.image") && event.event_id === event_id); - this.setState({ - image_event: image_event[0], - }); + try { + const profile = await this.context.client.getProfile(image_event[0].sender); + this.setState({ + image_event: image_event[0], + displayname: profile.displayname, + }); + } catch (ex) { + console.debug(`Failed to fetch profile for user ${image_event[0].sender}:`, ex); + this.setState({ + image_event: image_event[0], + displayname: image_event[0].sender, + }); + } }); } } catch (err) { @@ -151,7 +163,7 @@ class Post extends Component<Props, State> { <div className="grow bg-[#f8f8f8] dark:bg-[#06070D] min-h-[400px] flex flex-col items-center"> <div className="flex flex-col items-start lg:min-w-[60rem] lg:w-[60rem]"> <h1 className="my-4 text-6xl text-gray-900 dark:text-gray-200 font-bold">{post_title}</h1> - <h3 className="my-4 text-l text-gray-900 dark:text-gray-200 font-normal">{image_event.sender}</h3> + <Link href={"/profile/" + encodeURIComponent(image_event.sender)}><h3 className="cursor-pointer mt-0 mb-4 text-l text-gray-900 dark:text-gray-200 font-normal">{this.state.displayname}</h3></Link> {isImageGalleryEvent(image_event) ? this.renderImageGalleryTags(image_event) : isImageEvent(image_event) ? this.renderSingleImageTags(image_event) : <div key={(image_event as MatrixEventBase).event_id + "tags"}></div>} </div> </div>