matrix-art

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

commit 56216abe03d31a7efb3f90a5ce568f02260e00ee
parent 07bba607fc34ac863be7dcd679f3cae1de329ed8
Author: MTRNord <mtrnord1@gmail.com>
Date:   Sun, 30 Jan 2022 20:02:23 +0100

Add ways to change avatar and displayname

Diffstat:
Mcomponents/FrontPageImage.tsx | 2+-
Acomponents/editIcon.tsx | 38++++++++++++++++++++++++++++++++++++++
Acomponents/uploadIcon.tsx | 39+++++++++++++++++++++++++++++++++++++++
Mhelpers/matrix_client.ts | 36++++++++++++++++++++++++++++++++++++
Mpages/profile/[userid].tsx | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
5 files changed, 188 insertions(+), 11 deletions(-)

diff --git a/components/FrontPageImage.tsx b/components/FrontPageImage.tsx @@ -32,7 +32,7 @@ export default class FrontPageImage extends PureComponent<Props, State> { static propTypes = { event: PropTypes.object, - imageHeight: PropTypes.number, + imageHeight: PropTypes.string, show_nsfw: PropTypes.bool }; diff --git a/components/editIcon.tsx b/components/editIcon.tsx @@ -0,0 +1,37 @@ +import { PureComponent } from "react"; +import PropTypes from 'prop-types'; + +type Props = { + onClick?: () => void; + className?: string; +}; + +type State = { + className: string; + onClick: () => void; +}; + +export class EditIcon extends PureComponent<Props, State> { + + constructor(props: Props) { + super(props); + + this.state = { + className: (props.className || "") + " dark:fill-white fill-black", + onClick: props.onClick || (() => { }) + }; + } + static propTypes = { + onClick: PropTypes.func, + className: PropTypes.string + }; + + render() { + return ( + <svg className={this.state.className} onClick={this.state.onClick} xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"> + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" /> + </svg> + ); + } +} +\ No newline at end of file diff --git a/components/uploadIcon.tsx b/components/uploadIcon.tsx @@ -0,0 +1,38 @@ +import { PureComponent } from "react"; +import PropTypes from 'prop-types'; + +type Props = { + onClick?: () => void; + className?: string; +}; + +type State = { + onClick: () => void; + className: string; +}; + +export class UploadIcon extends PureComponent<Props, State> { + + constructor(props: Props) { + super(props); + + this.state = { + className: (props.className || "") + " dark:fill-white fill-black", + onClick: props.onClick || (() => { }) + }; + } + + static propTypes = { + onClick: PropTypes.func, + className: PropTypes.string + }; + + render() { + return ( + <svg className={this.state.className} onClick={this.state.onClick} xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="inherit"> + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" /> + </svg> + ); + } +} +\ No newline at end of file diff --git a/helpers/matrix_client.ts b/helpers/matrix_client.ts @@ -185,6 +185,42 @@ export default class MatrixClient { }); } + async setDisplayname(newDisplayname: string) { + const data = await this.fetchJson( + `${this.serverUrl}/r0/profile/${encodeURIComponent(this.userId!)}/displayname`, + { + method: "PUT", + body: JSON.stringify({ displayname: newDisplayname }), + headers: { Authorization: `Bearer ${this.accessToken}` }, + } + ); + if (this.userProfileCache.has(this.userId!)) { + const old = this.userProfileCache.get(this.userId!); + old.displayname = newDisplayname; + this.userProfileCache.set(this.userId!, old); + } else { + await this.getProfile(this.userId!); + } + } + + async setAvatarUrl(newAvatarUrl: string) { + const data = await this.fetchJson( + `${this.serverUrl}/r0/profile/${encodeURIComponent(this.userId!)}/avatar_url`, + { + method: "PUT", + body: JSON.stringify({ avatar_url: newAvatarUrl }), + headers: { Authorization: `Bearer ${this.accessToken}` }, + } + ); + if (this.userProfileCache.has(this.userId!)) { + const old = this.userProfileCache.get(this.userId!); + old.avatar_url = newAvatarUrl; + this.userProfileCache.set(this.userId!, old); + } else { + await this.getProfile(this.userId!); + } + } + async getProfile(userId: string) { if (this.userProfileCache.has(userId)) { console.debug(`Returning cached copy of ${userId}'s profile`); diff --git a/pages/profile/[userid].tsx b/pages/profile/[userid].tsx @@ -4,14 +4,17 @@ import Link from "next/link"; import { NextRouter, withRouter } from "next/router"; import { PureComponent } from "react"; import { ClientContext } from "../../components/ClientContext"; +import { EditIcon } from "../../components/editIcon"; import Footer from "../../components/Footer"; import FrontPageImage from "../../components/FrontPageImage"; import Header from "../../components/Header"; +import { UploadIcon } from "../../components/uploadIcon"; import { BannerEvent, MatrixArtProfile, MatrixEvent, MatrixEventBase, MatrixImageEvents } from "../../helpers/event_types"; import { constMatrixArtServer } from "../../helpers/matrix_client"; type Props = InferGetServerSidePropsType<typeof getServerSideProps> & { router: NextRouter; + mxid: string; }; type State = { @@ -19,9 +22,12 @@ type State = { avatar_url: string; events: MatrixEvent[] | []; profile_event?: MatrixArtProfile; - error?: any; + error?: string; isLoadingImages: boolean; hasFullyLoaded: boolean; + isLoggedInUser: boolean; + editingUsername: boolean; + editingAbout: boolean; }; class Profile extends PureComponent<Props, State> { @@ -32,7 +38,13 @@ class Profile extends PureComponent<Props, State> { this.state = { displayname: this.props.mxid, - events: [] + avatar_url: "", + events: [], + isLoadingImages: false, + hasFullyLoaded: false, + isLoggedInUser: false, + editingUsername: false, + editingAbout: false, } as State; } @@ -64,6 +76,9 @@ class Profile extends PureComponent<Props, State> { } await this.loadEvents(); } + this.setState({ + isLoggedInUser: this.props.mxid === this.context.client.userId && !this.context.client.isGuest + }); } async registerAsGuest() { @@ -132,9 +147,42 @@ class Profile extends PureComponent<Props, State> { ); } + handleUsernameInputChange(event: { target: any; }) { + this.setState({ + displayname: event.target.value + }); + } + + async onClickEditUsername() { + if (!this.state.editingUsername) { + this.setState({ + editingUsername: true + }); + } else { + await this.context.client.setDisplayname(this.state.displayname); + this.setState({ + editingUsername: false + }); + } + + } + + async handleAvatarUpload(event: { target: { files: FileList | null; }; }) { + const files = event.target.files; + if (!files) { + return; + } + const file = files[0]; + const mxc = await this.context.client.uploadFile(file); + await this.context.client.setAvatarUrl(mxc); + this.setState({ + avatar_url: mxc, + }); + } + render() { const { mxid } = this.props; - const { events, profile_event, avatar_url, displayname } = this.state; + const { events, profile_event, avatar_url, displayname, isLoggedInUser, editingUsername } = this.state; if (!mxid || !mxid?.startsWith("@")) { return this.renderNotFound(); } @@ -153,11 +201,6 @@ class Profile extends PureComponent<Props, State> { </Head> <Header></Header> <main className='w-full mb-auto lg:pt-20 pt-56 z-0 bg-[#f8f8f8] dark:bg-[#06070D]'> - {/*{banner_event ? <div style={{ - backgroundImage: `url(${this.context.client.downloadLink((banner_event as BannerEvent).content["m.file"].url)})` - }} - className="fixed top-14 w-full h-[32.5rem] bg-cover lg:bg-[position:50%]" - ></div> : undefined}*/} {banner_event ? <div style={{ backgroundImage: `url(${this.context.client.thumbnailLink((banner_event as BannerEvent).content["m.file"].url, "scale", (banner_event as BannerEvent).content["m.image"].width - 1, (banner_event as BannerEvent).content["m.image"].height - 1)})` }} @@ -172,11 +215,32 @@ class Profile extends PureComponent<Props, State> { <span> <div className="block relative"> {/* TODO fallback*/} - <img className="block object-cover rounded-md" src={this.context.client.downloadLink(avatar_url)!} height="100" width="100" alt={displayname} title={displayname} /> + { + isLoggedInUser ? + ( + avatar_url ? ( + <> + <label htmlFor="avatar-upload" className="rounded-md flex justify-center items-center cursor-pointer" style={{ height: "100px", width: "100px" }}> + <img className="block object-cover rounded-md" src={this.context.client.downloadLink(avatar_url)!} height="100" width="100" alt={displayname} title={displayname} /> + <div className="min-h-[48px] min-w-[48px] absolute left-[20%] rounded-full bg-slate-700/40 p-1 flex justify-center items-center"><EditIcon /></div> + </label> + <input className="hidden" id="avatar-upload" type="file" accept="image/*" onChange={this.handleAvatarUpload.bind(this)} /> + </> + ) : ( + <> + <label htmlFor="avatar-upload" className="rounded-md bg-slate-500 flex justify-center items-center cursor-pointer" style={{ height: "100px", width: "100px" }}> + <UploadIcon /> + </label> + <input className="hidden" id="avatar-upload" type="file" accept="image/*" onChange={this.handleAvatarUpload.bind(this)} /> + </> + ) + ) + : <img className="block object-cover rounded-md" src={this.context.client.downloadLink(avatar_url)!} height="100" width="100" alt={displayname} title={displayname} /> + } </div> </span> <div className="ml-5 flex flex-col justify-center"> - <h1 className="font-extrabold text-3xl lg:text-5xl text-gray-200 mt-[-1rem] flex items-end">{displayname}</h1> + <h1 className="font-extrabold text-3xl lg:text-5xl text-gray-200 mt-[-1rem] flex items-center gap-1">{editingUsername ? <input onChange={this.handleUsernameInputChange.bind(this)} className="placeholder:text-gray-900 text-gray-900 rounded py-1.5 px-2" type="text" placeholder="Set a displayname" value={displayname}></input> : <span>{displayname}</span>}{isLoggedInUser ? <EditIcon className="cursor-pointer" onClick={this.onClickEditUsername.bind(this)} /> : undefined}</h1> </div> </div> </div>