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:
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>