matrix-art

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

commit 00b82ada54c080f05d4e85227bf7c3c0f38f24c6
parent c069ba29e3e6af66fdde75018eeeab9a1322592c
Author: MTRNord <mtrnord1@gmail.com>
Date:   Thu, 13 Jan 2022 23:36:33 +0100

Serverside render as much as possible. Should fix meta tags and make stuff faster. Might break private posts. (Those dont exist yet so it is impossible to test)

Diffstat:
Mhelpers/matrix_client.ts | 6+++---
Dpages/Home.tsx | 164-------------------------------------------------------------------------------
Mpages/_app.tsx | 26+++++++++++++++++++++++++-
Mpages/index.tsx | 124++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mpages/post/[id].tsx | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
5 files changed, 181 insertions(+), 221 deletions(-)

diff --git a/helpers/matrix_client.ts b/helpers/matrix_client.ts @@ -365,7 +365,7 @@ export default class MatrixClient { return info; } - async getTimeline(roomId: string, limit: number, callback: (arg0: MatrixEvent[]) => void | Promise<void>) { + async getTimeline(roomId: string, limit: number) { if (!this.accessToken) { console.error("No access token"); return []; @@ -373,6 +373,7 @@ export default class MatrixClient { limit = limit || 100; let seenEvents = 0; let from; + let msgs: MatrixEvent[] = []; while (seenEvents < limit) { let fromQuery = ``; if (from) { @@ -385,17 +386,16 @@ export default class MatrixClient { } ); from = data.end; - let msgs: MatrixEvent[] = []; for (const ev of data.chunk) { msgs.push(ev); } - await Promise.resolve(callback(msgs)); seenEvents += msgs.length; if (data.chunk.length < limit) { break; } seenEvents += 1; // just in case, to stop infinite loops } + return msgs; } /** diff --git a/pages/Home.tsx b/pages/Home.tsx @@ -1,163 +0,0 @@ -import React, { PureComponent } from 'react'; -import { MatrixEventBase, MatrixImageEvents } from '../helpers/event_types'; -import Head from 'next/head'; -import Header from '../components/Header'; -import { ClientContext } from '../components/ClientContext'; -import { constMatrixArtServer } from '../helpers/matrix_client'; -import FrontPageImage from '../components/FrontPageImage'; -import Footer from '../components/Footer'; - -type Props = { -}; - -type State = { - directory_data: { _id: string; user_id: string; user_room: string; }[]; - error?: any; - directoryIsLoaded: boolean; - isLoadingImages: boolean; - hasFullyLoaded: boolean; - // TODO make sure we parse both extev variants properly - image_events: MatrixImageEvents[] | []; -}; - - -export default class Home extends PureComponent<Props, State>{ - declare context: React.ContextType<typeof ClientContext>; - - constructor(props: Props) { - super(props); - - this.state = { - isLoadingImages: false, - directoryIsLoaded: false, - hasFullyLoaded: false, - image_events: [] - } as State; - } - - async componentDidUpdate() { - await this.loadEvents(); - } - - async componentDidMount() { - // auto-register as a guest if not logged in - if (!this.context.client?.accessToken) { - this.registerAsGuest(); - } else { - console.log("Already logged in"); - try { - const resp = await fetch('/api/directory'); - const data = await resp.json(); - this.setState({ directory_data: data.data, directoryIsLoaded: true }); - } catch (error) { - this.setState({ - hasFullyLoaded: true, - error - }); - } - } - } - - async registerAsGuest() { - try { - let serverUrl = constMatrixArtServer + "/_matrix/client"; - await this.context.client?.registerAsGuest(serverUrl); - await this.context.guest_client?.registerAsGuest(serverUrl); - if (typeof window !== "undefined") { - window.location.reload(); - } - } catch (error) { - console.error("Failed to register as guest:", error); - this.setState({ - error: "Failed to register as guest: " + JSON.stringify(error), - }); - } - } - - async loadEvents() { - const { directoryIsLoaded, directory_data, isLoadingImages, hasFullyLoaded } = this.state; - if (!directoryIsLoaded || isLoadingImages || hasFullyLoaded) { - return; - } - const client = this.context.client.isGuest ? this.context.client : this.context.guest_client; - this.setState({ - isLoadingImages: true, - }); - try { - for (let user of directory_data) { - // We dont need many events - const roomId = await client?.followUser(user.user_room); - await client?.getTimeline(roomId, 100, (events) => { - // Filter events by type - const image_events = events.filter((event) => event.type == "m.image_gallery" || event.type == "m.image") as MatrixImageEvents[]; - console.log("Adding", image_events.length, "items"); - this.setState({ - image_events: image_events, - }); - }); - } - } catch (error) { - this.setState({ - error: JSON.stringify(error), - }); - } finally { - this.setState({ - hasFullyLoaded: true, - isLoadingImages: false - }); - } - } - - render() { - const { error, hasFullyLoaded, image_events } = this.state; - - if (error) { - return ( - <div>Error: {error.message}</div> - ); - } else if (!hasFullyLoaded) { - return ( - <div className='h-full bg-[#f8f8f8] dark:bg-[#06070D]'> - <Head> - <title key="title">Matrix Art | Home</title> - <meta property="og:title" content="Matrix Art | Home" key="og-title" /> - <meta property="og:type" content="website" key="og-type" /> - </Head> - <Header></Header> - <main className='w-full lg:pt-20 pt-52 z-0'> - <div className="m-0 w-full"> - <div className="loader fixed top-[50%] left-[50%] transform translate-x-[-50%] translate-y-[-50%]">Loading...</div> - </div> - </main> - </div> - ); - } else { - return ( - <div className='h-full flex flex-col justify-between bg-[#f8f8f8] dark:bg-[#06070D]'> - <Head> - <title key="title">Matrix Art | Home</title> - <meta property="og:title" content="Matrix Art | Home" key="og-title" /> - <meta property="og:type" content="website" key="og-type" /> - </Head> - <Header></Header> - <main className='mb-auto lg:pt-20 pt-52 z-0'> - <div className='z-[100] sticky lg:top-20 top-52 bg-[#fefefe]/[.95] dark:bg-[#14181E]/[.95]'> - <div className='h-16 px-10 w-full relative grid grid-cols-[1fr_auto_1fr] items-center' id='section-grid'> - <h1 className='text-xl text-gray-900 dark:text-gray-200 font-bold'>Home</h1> - </div> - </div> - <div className='m-10'> - <ul className='flex flex-wrap gap-1'> - {image_events.map(event => <FrontPageImage event={event} key={(event as MatrixEventBase).event_id} />)} - <li className='grow-[10]'></li> - </ul> - </div> - </main> - <Footer></Footer> - </div> - ); - }; - } - -} -Home.contextType = ClientContext; -\ No newline at end of file diff --git a/pages/_app.tsx b/pages/_app.tsx @@ -1,8 +1,32 @@ import '../styles/globals.css'; import type { AppProps } from 'next/app'; +import { useState } from 'react'; +import { client, ClientContext, guest_client } from '../components/ClientContext'; +import React from 'react'; +import Head from 'next/head'; function MyApp({ Component, pageProps }: AppProps) { - return <Component {...pageProps} />; + const [value, setValue] = useState({ + client: client, + guest_client: guest_client, + }); + return ( + <> + <Head> + <title key="title">Matrix Art</title> + <meta property="og:title" content="Matrix Art" key="og-title" /> + <meta property="og:type" content="website" key="og-type" /> + <meta key="viewport" name="viewport" content="initial-scale=1.0, width=device-width" /> + </Head> + <React.StrictMode> + <ClientContext.Provider + value={value} + > + <Component {...pageProps}/> + </ClientContext.Provider> + </React.StrictMode> + </> + ); } export default MyApp; diff --git a/pages/index.tsx b/pages/index.tsx @@ -1,45 +1,104 @@ -import Head from 'next/head'; import React, { PureComponent } from 'react'; -import { guest_client, client, ClientContext } from '../components/ClientContext'; -import MatrixClient from '../helpers/matrix_client'; -import Home from './Home'; - -type Props = { +import { MatrixEventBase, MatrixImageEvents } from '../helpers/event_types'; +import Head from 'next/head'; +import Header from '../components/Header'; +import { client, ClientContext } from '../components/ClientContext'; +import FrontPageImage from '../components/FrontPageImage'; +import Footer from '../components/Footer'; +import { NextRouter, withRouter } from 'next/router'; +import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; +import { get_data } from './api/directory'; +type Props = InferGetServerSidePropsType<typeof getServerSideProps> & { + router: NextRouter; }; type State = { - value: { client: MatrixClient; guest_client: MatrixClient; }; + error?: any; + // TODO make sure we parse both extev variants properly + image_events: MatrixImageEvents[] | []; }; -class App extends PureComponent<Props, State> { - constructor(props: Props | Readonly<Props>) { + + +class Home extends PureComponent<Props, State>{ + declare context: React.ContextType<typeof ClientContext>; + + constructor(props: Props) { super(props); + this.state = { - value: { - client: client, - guest_client: guest_client, - }, - }; + image_events: props.image_events, + } as State; } render() { - return ( - <> - <Head> - <title key="title">Matrix Art</title> - <meta property="og:title" content="Matrix Art" key="og-title" /> - <meta property="og:type" content="website" key="og-type" /> - <meta key="viewport" name="viewport" content="initial-scale=1.0, width=device-width" /> - </Head> - <React.StrictMode> - <ClientContext.Provider - value={this.state.value} - > - <Home /> - </ClientContext.Provider> - </React.StrictMode> - </> - ); + const { error, image_events } = this.state; + + if (error) { + return ( + <div>Error: {error.message}</div> + ); + } else { + return ( + <div className='h-full flex flex-col justify-between bg-[#f8f8f8] dark:bg-[#06070D]'> + <Head> + <title key="title">Matrix Art | Home</title> + <meta property="og:title" content="Matrix Art | Home" key="og-title" /> + <meta property="og:type" content="website" key="og-type" /> + </Head> + <Header></Header> + <main className='mb-auto lg:pt-20 pt-52 z-0'> + <div className='z-[100] sticky lg:top-20 top-52 bg-[#fefefe]/[.95] dark:bg-[#14181E]/[.95]'> + <div className='h-16 px-10 w-full relative grid grid-cols-[1fr_auto_1fr] items-center' id='section-grid'> + <h1 className='text-xl text-gray-900 dark:text-gray-200 font-bold'>Home</h1> + </div> + </div> + <div className='m-10'> + <ul className='flex flex-wrap gap-1'> + {image_events.map(event => <FrontPageImage event={event} key={(event as MatrixEventBase).event_id} />)} + <li className='grow-[10]'></li> + </ul> + </div> + </main> + <Footer></Footer> + </div> + ); + } } + } +Home.contextType = ClientContext; + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { res } = context; + + res.setHeader( + 'Cache-Control', + 'public, s-maxage=10, stale-while-revalidate=59' + ); + let image_events: MatrixImageEvents[] = []; + try { + const data = await get_data(); + + // TODO fix this. It is super inefficent. + for (let user of data) { + // TODO check what happens if this is a non public image. + // We dont need many events + const roomId = await client?.followUser(user.user_room); + const events = await client?.getTimeline(roomId, 100); + // Filter events by type + image_events = [...image_events, ...(events.filter((event) => event.type == "m.image_gallery" || event.type == "m.image") as MatrixImageEvents[])]; + console.log("Adding", image_events.length, "items"); + } + + + return { + props: { + image_events: image_events + } + }; + } catch { + return { notFound: true, props: {} }; + } +}; -export default App; +export default withRouter(Home); +\ No newline at end of file diff --git a/pages/post/[id].tsx b/pages/post/[id].tsx @@ -8,7 +8,7 @@ import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import Head from "next/head"; import { NextRouter, withRouter } from "next/router"; import { PureComponent, ReactNode } from "react"; -import { ClientContext } from "../../components/ClientContext"; +import { client, ClientContext } from "../../components/ClientContext"; import Header from "../../components/Header"; import { ImageEvent, ImageGalleryEvent, MatrixEventBase, MatrixImageEvents } from "../../helpers/event_types"; import { constMatrixArtServer } from "../../helpers/matrix_client"; @@ -36,7 +36,9 @@ class Post extends PureComponent<Props, State> { super(props); this.state = { - hasFullyLoaded: false, + hasFullyLoaded: props.hasFullyLoaded, + image_event: props.image_event, + displayname: props.displayname, isLoadingImages: false }; } @@ -71,25 +73,25 @@ class Post extends PureComponent<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, async (events) => { - // Filter events by type - const image_event = events.find((event) => (event.type === "m.image_gallery" || event.type === "m.image") && event.event_id === event_id); - try { - // TODO this should get handled if null - const profile = await this.context.client.getProfile(image_event!.sender); - this.setState({ - image_event: image_event as MatrixImageEvents, - displayname: profile.displayname, - }); + const events = await this.context.client?.getTimeline(roomId, 100); // Filter events by type + const image_event = events.find((event) => (event.type === "m.image_gallery" || event.type === "m.image") && event.event_id === event_id); + if (image_event == null) { + continue; + } + try { + const profile = await this.context.client.getProfile(image_event.sender); + this.setState({ + image_event: image_event as MatrixImageEvents, + displayname: profile.displayname, + }); - } catch (error) { - console.debug(`Failed to fetch profile for user ${image_event?.sender}:`, error); - this.setState({ - image_event: image_event as MatrixImageEvents, - displayname: image_event?.sender, - }); - } - }); + } catch (error) { + console.debug(`Failed to fetch profile for user ${image_event.sender}:`, error); + this.setState({ + image_event: image_event as MatrixImageEvents, + displayname: image_event.sender, + }); + } } } catch (error) { this.setState({ @@ -314,10 +316,48 @@ export const getServerSideProps: GetServerSideProps = async (context) => { try { const data = await get_data(); + // TODO fix this. It is super inefficent. + for (let user of data) { + // TODO check what happens if this is a non public image. + // We dont need many events + const roomId = await client?.followUser(user.user_room); + const events = await client?.getTimeline(roomId, 100); + // Filter events by type + const image_event = events.find((event) => (event.type === "m.image_gallery" || event.type === "m.image") && event.event_id === event_id); + if (image_event == null) { + continue; + } + try { + const profile = await client.getProfile(image_event.sender); + return { + props: { + directory_data: data, + image_event: image_event as MatrixImageEvents, + event_id: event_id, + hasFullyLoaded: true, + displayname: profile.displayname + } + }; + } catch (error) { + console.debug(`Failed to fetch profile for user ${image_event.sender}:`, error); + return { + props: { + directory_data: data, + image_event: image_event as MatrixImageEvents, + event_id: event_id, + hasFullyLoaded: true, + displayname: image_event.sender + } + }; + } + } + return { props: { - directory_data: data, event_id: event_id + directory_data: data, + event_id: event_id, + hasFullyLoaded: false, } }; } catch {