commit 265b9437b5c71835eb8e0ee4c991d23cc18f4052
parent 04fa313faa4bc5e98603ce4eb2fe833d28d8ab37
Author: MTRNord <mtrnord1@gmail.com>
Date: Fri, 11 Feb 2022 16:11:49 +0100
Make storage async, use pure files on server side and indexeddb (plus fallbacks) on client side
Diffstat:
18 files changed, 354 insertions(+), 164 deletions(-)
diff --git a/.eslintrc.json b/.eslintrc.json
@@ -13,6 +13,8 @@
"unicorn/prevent-abbreviations": "off",
"unicorn/filename-case": "off",
"unicorn/prefer-ternary": "off",
- "unicorn/no-lonely-if": "off"
+ "unicorn/no-lonely-if": "off",
+ "unicorn/empty-brace-spaces": "off",
+ "unicorn/prefer-node-protocol": "off"
}
}
\ No newline at end of file
diff --git a/components/ClientContext.tsx b/components/ClientContext.tsx
@@ -1,12 +1,16 @@
import React, { Context } from "react";
-import Client from "../helpers/matrix_client";
-import Storage from "../helpers/storage";
+import MatrixClient from "../helpers/matrix_client";
-// Actual user client
-const client = new Client(new Storage("main"));
+type ContextData = {
+ client?: MatrixClient;
+ guest_client?: MatrixClient;
+ is_generating_guest: boolean;
+};
-// Client used if logged in for the Homepage to prevent excessive room joining
-const guest_client = new Client(new Storage("guest"));
-const ClientContext: Context<{ client: Client; guest_client: Client; is_generating_guest: boolean; }> = React.createContext<{ client: Client; guest_client: Client; is_generating_guest: boolean; }>({ client: client, guest_client: guest_client, is_generating_guest: false });
+const ClientContext: Context<ContextData> = React.createContext<ContextData>({
+ client: undefined,
+ guest_client: undefined,
+ is_generating_guest: false
+});
-export { ClientContext, client, guest_client };
-\ No newline at end of file
+export { ClientContext };
+\ No newline at end of file
diff --git a/components/Header.tsx b/components/Header.tsx
@@ -81,8 +81,7 @@ export default class Header extends PureComponent<Props, State> {
<nav className='flex lg:flex-shrink-0 my-4'>
<ClientContext.Consumer>
{value => {
- const isGuest = value.client.isGuest === null || value.client.isGuest === undefined ? true : value.client.isGuest;
- console.log("isGuest1:", isGuest);
+ const isGuest = value.client?.isGuest === null || value.client?.isGuest === undefined ? true : value.client.isGuest;
if (typeof window !== "undefined" && isGuest) {
return (
<>
@@ -91,10 +90,10 @@ export default class Header extends PureComponent<Props, State> {
</>
);
} else if (typeof window !== "undefined") {
- if (this.state.directory_data.some(thing => thing.mxid == this.context.client.userId)) {
+ if (this.state.directory_data.some(thing => thing.mxid == this.context.client?.userId)) {
return (
<>
- <span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href={"/profile/" + encodeURIComponent(this.context.client.userId!)}>{i18n?.t('Profile') ?? 'Profile'}</Link></span>
+ <span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href={"/profile/" + encodeURIComponent(this.context.client?.userId ?? "")}>{i18n?.t('Profile') ?? 'Profile'}</Link></span>
<span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href="/logout/">{i18n?.t('Logout') ?? 'Logout'}</Link></span>
</>
);
@@ -109,8 +108,7 @@ export default class Header extends PureComponent<Props, State> {
<ClientContext.Consumer>
{value => {
- const isGuest = value.client.isGuest === null || value.client.isGuest === undefined ? true : value.client.isGuest;
- console.log("isGuest2:", isGuest);
+ const isGuest = value.client?.isGuest === null || value.client?.isGuest === undefined ? true : value.client.isGuest;
if (typeof window !== "undefined" && !isGuest) {
return (<span className='lg:opacity-100 opacity-0 inline-block bg-gray-900 dark:bg-gray-200 w-[1px] lg:h-7 h-0'></span>);
} else {
@@ -123,10 +121,9 @@ export default class Header extends PureComponent<Props, State> {
<div className='flex'>
<ClientContext.Consumer>
{value => {
- const isGuest = value.client.isGuest === null || value.client.isGuest === undefined ? true : value.client.isGuest;
- console.log("isGuest3:", isGuest);
+ const isGuest = value.client?.isGuest === null || value.client?.isGuest === undefined ? true : value.client.isGuest;
if (typeof window !== "undefined" && !isGuest) {
- if (this.state.directory_data.some(thing => thing.mxid == this.context.client.userId)) {
+ if (this.state.directory_data.some(thing => thing.mxid == this.context.client?.userId)) {
return <Link href="/submit"><a className='inline-flex justify-center items-center text-teal-400 hover:text-teal-200 bg-transparent relative h-14 min-w-[9.25rem] z-[2] cursor-pointer font-bold'>{i18n?.t('Submit') ?? 'Submit'}</a></Link>;
} else {
return <a className='inline-flex justify-center items-center text-teal-400 hover:text-teal-200 bg-transparent relative h-14 min-w-[9.25rem] z-[2] cursor-pointer font-bold'>{i18n?.t('Setup Account') ?? 'Setup Account'}</a>;
diff --git a/components/submit_page/main.tsx b/components/submit_page/main.tsx
@@ -150,7 +150,7 @@ class MainSubmissionForm extends PureComponent<Props, State> {
toast.dismiss();
- if (this.context.client.isGuest) {
+ if (this.context.client?.isGuest) {
toast.error(() => <div><h2 className="text-xl text-white">{i18n?.t("Error")}</h2><br />{i18n?.t("You are not logged in!")}</div>, {
autoClose: false
});
@@ -184,7 +184,7 @@ class MainSubmissionForm extends PureComponent<Props, State> {
const nsfw = `${index}_nsfw`;
const file = this.props.files[index];
- if (!this.context.client.profileRoomId) {
+ if (!this.context.client?.profileRoomId) {
toast.error(() => <div><h2 className="text-xl text-white">{i18n?.t("Error")}</h2><br />{i18n?.t("Unable to find your profile Room. Please log in again!")}</div>, {
autoClose: false
});
@@ -229,8 +229,14 @@ class MainSubmissionForm extends PureComponent<Props, State> {
sender: this.context.client.userId!
});
}
- const token = await this.context.client.getOpenidToken();
- await fetch("/api/submitSearch", { method: "POST", body: JSON.stringify({ access_token: token, user_id: this.context.client.userId, docs: posts_for_search }) });
+ const token = await this.context.client?.getOpenidToken();
+ await fetch("/api/submitSearch", {
+ method: "POST", body: JSON.stringify({
+ access_token: token,
+ user_id: this.context.client?.userId,
+ docs: posts_for_search
+ })
+ });
await this.props.router.replace("/");
this.setState({ submit_in_process: false });
}
@@ -374,7 +380,7 @@ class MainSubmissionForm extends PureComponent<Props, State> {
private async generateThumbnailsAndUpload(image_infos: { width: number; height: number; img: HTMLImageElement; }[]): Promise<{ index: number; meta: ThumbnailData; }[]> {
const thumbnails = [];
- if (!this.context.client.isGuest) {
+ if (!this.context.client?.isGuest) {
const range = [...Array(this.props.files.length).keys()]; // eslint-disable-line unicorn/new-for-builtins
for (const index of range) {
const file = this.props.files[index];
@@ -399,8 +405,10 @@ class MainSubmissionForm extends PureComponent<Props, State> {
thumbnails.push({ index: index, meta: thumbnail_data.thumbnail_meta });
}
- const result = await this.context.client.uploadFile(thumbnail_data.thumbnail);
- thumbnail_data.thumbnail_meta["m.thumbnail"]![0].url = result;
+ const result = await this.context.client?.uploadFile(thumbnail_data.thumbnail);
+ if (result) {
+ thumbnail_data.thumbnail_meta["m.thumbnail"]![0].url = result;
+ }
thumbnails.push({ index: index, meta: thumbnail_data.thumbnail_meta });
}
}
@@ -409,11 +417,11 @@ class MainSubmissionForm extends PureComponent<Props, State> {
private async doUpload() {
const urls = [];
- if (!this.context.client.isGuest) {
+ if (!this.context.client?.isGuest) {
const range = [...Array(this.props.files.length).keys()]; // eslint-disable-line unicorn/new-for-builtins
for (const index of range) {
const file = this.props.files[index];
- const result = await this.context.client.uploadFile(file);
+ const result = await this.context.client?.uploadFile(file);
urls.push({ index: index, url: result });
}
}
diff --git a/helpers/matrix_client.ts b/helpers/matrix_client.ts
@@ -7,12 +7,6 @@ export const constMatrixArtServer = process.env.NEXT_PUBLIC_DEFAULT_SERVER_URL ?
export default class MatrixClient {
private joinedRooms: Map<string, string> = new Map<string, string>(); // room alias -> room ID
private userProfileCache: Map<string, { displayname?: string; avatar_url?: string; }> = new Map(); // user_id -> {display_name; avatar;}
- private serverUrl?: string;
- private _userId?: string;
- private _accessToken?: string;
- private _isGuest?: boolean;
- private _profileRoomId?: string;
- private serverName?: string;
get userId(): string | undefined {
return this._userId;
}
@@ -27,32 +21,42 @@ export default class MatrixClient {
return this.joinedRooms.get(`#${this.userId}`) ?? this._profileRoomId;
}
- constructor(private storage: Storage) {
- this.serverUrl = this.storage.getItem("serverUrl");
- this._userId = this.storage.getItem("userId");
- this._accessToken = this.storage.getItem("accessToken");
- this._isGuest = this.storage.getItem("isGuest") === undefined ? undefined : (this.storage.getItem("isGuest") === "true");
- this.serverName = this.storage.getItem("serverName");
- this._profileRoomId = this.storage.getItem("profileRoomId");
+ private constructor(
+ private storage: Storage,
+ private serverUrl?: string,
+ private _userId?: string,
+ private _accessToken?: string,
+ private _isGuest?: boolean,
+ private serverName?: string,
+ private _profileRoomId?: string
+ ) {
}
- private saveAuthState() {
- if (!this.storage) {
- return;
- }
- this.storage.setOrDelete("serverUrl", this.serverUrl);
- this.storage.setOrDelete("userId", this._userId);
- this.storage.setOrDelete("accessToken", this._accessToken);
- this.storage.setOrDelete("serverName", this.serverName);
- this.storage.setOrDelete("isGuest", this._isGuest?.toString());
+ public static async init(storage: Storage): Promise<MatrixClient> {
+ const serverUrl = await storage.getItem("serverUrl");
+ const _userId = await storage.getItem("userId");
+ const _accessToken = await storage.getItem("accessToken");
+ const _isGuest = await storage.getItem("isGuest") === undefined ? undefined : (await storage.getItem("isGuest") === "true");
+ const serverName = await storage.getItem("serverName");
+ const _profileRoomId = await storage.getItem("profileRoomId");
+ return new MatrixClient(storage, serverUrl, _userId, _accessToken, _isGuest, serverName, _profileRoomId);
+ }
+
+ private async saveAuthState() {
+ await this.storage.setOrDelete("serverUrl", this.serverUrl);
+ await this.storage.setOrDelete("userId", this._userId);
+ await this.storage.setOrDelete("accessToken", this._accessToken);
+ await this.storage.setOrDelete("serverName", this.serverName);
+ await this.storage.setOrDelete("isGuest", this._isGuest?.toString());
}
- private generateToken(len: number) {
+ private async generateToken(len: number) {
var arr = new Uint8Array(len / 2);
if (typeof window !== "undefined") {
window.crypto.getRandomValues(arr);
} else {
- require("crypto").randomFillSync(arr); // eslint-disable-line unicorn/prefer-module
+ const crypto = await import("crypto");
+ crypto.randomFillSync(arr);
}
/* eslint-disable unicorn/prefer-spread */
return Array.from(arr, (num) => {
@@ -311,7 +315,7 @@ export default class MatrixClient {
}
);
if (isMyself) {
- this.storage?.setOrDelete("profileRoomId", data.room_id);
+ await this.storage?.setOrDelete("profileRoomId", data.room_id);
}
this.joinedRooms.set(roomAlias, data.room_id);
return data.room_id;
@@ -355,7 +359,7 @@ export default class MatrixClient {
);
this.joinedRooms.set(roomAlias, data.room_id);
- this.storage?.setOrDelete("profileRoomId", data.room_id);
+ await this.storage?.setOrDelete("profileRoomId", data.room_id);
return data.room_id;
} else {
throw error;
diff --git a/helpers/storage.ts b/helpers/storage.ts
@@ -1,92 +1,128 @@
-const STORAGE_VERSION = 4;
+const STORAGE_VERSION = 5;
export default class Storage {
- private prefix;
- private nodeLocalStorage?: any;
- constructor(prefix: string) {
- this.prefix = prefix;
+ private constructor(private prefix: string, private localFolder?: string) { }
+
+ public static async init(prefix: string): Promise<Storage> {
if (typeof window === "undefined") {
- var LocalStorage = require('node-localstorage').LocalStorage; // eslint-disable-line unicorn/prefer-module
- var path = require('node:path'); // eslint-disable-line unicorn/prefer-module
- var fs = require('node:fs'); // eslint-disable-line unicorn/prefer-module
+ const path = await import('path');
+ const fs = await import('fs');
const dir = path.join(process.cwd(), "localstorage");
- if (!fs.existsSync(dir)) {
+ const dir_with_prefix = path.join(dir, prefix);
+ try {
+ await fs.promises.stat(dir);
+ await fs.promises.stat(dir_with_prefix);
+ } catch {
try {
- fs.mkdirSync(dir);
+ await fs.promises.mkdir(dir);
+ await fs.promises.mkdir(dir_with_prefix);
} catch { console.log("race while folder was checked"); }
}
- this.nodeLocalStorage = new LocalStorage(dir);
+ return new Storage(prefix, dir);
+ } else {
+ return new Storage(prefix);
}
}
- private ensureVersion() {
+
+ private async ensureVersion() {
let version;
if (typeof window !== "undefined") {
- version = window.localStorage.getItem("version");
+ const localforage = await import('localforage');
+ version = await localforage.getItem("version");
} else {
try {
- version = this.nodeLocalStorage?.getItem("version");
+ const fs = await import('fs');
+ const path = await import('path');
+ version = await fs.promises.readFile(path.join(this.localFolder!, "version"), { encoding: "utf8" });
} catch {
//No-op
}
}
if (version === undefined || version === null || version !== STORAGE_VERSION.toString()) {
if (typeof window !== "undefined") {
- window.localStorage.clear();
- window.localStorage.setItem("version", STORAGE_VERSION.toString());
+ const localforage = await import('localforage');
+ await localforage.clear();
+ await localforage.setItem("version", STORAGE_VERSION.toString());
} else {
- this.nodeLocalStorage?.clear();
- this.nodeLocalStorage?.setItem("version", STORAGE_VERSION.toString());
+ const fs = await import('fs');
+ const path = await import('path');
+ try {
+ const stat = await fs.promises.stat(path.join(this.localFolder!, "version"));
+ if (stat.isFile()) {
+ await fs.promises.unlink(path.join(this.localFolder!, "version"));
+ }
+ } catch { }
+ await fs.promises.writeFile(path.join(this.localFolder!, "version"), STORAGE_VERSION.toString(), { encoding: "utf8", flag: "w+" });
}
}
}
- getItem(key: string): string | undefined {
+ async getItem(key: string): Promise<string | undefined> {
this.ensureVersion();
if (typeof window !== "undefined") {
- const item = window.localStorage.getItem(this.prefix + key);
+ const localforage = await import('localforage');
+ const item = await localforage.getItem(this.prefix + key) as string | null;
if (item === null) {
return undefined;
}
return item;
} else {
try {
- return this.nodeLocalStorage?.getItem(this.prefix + key);
+ const fs = await import('fs');
+ const path = await import('path');
+ try {
+ const stat = await fs.promises.stat(path.join(this.localFolder!, this.prefix, key));
+ if (stat.isFile()) {
+ return fs.promises.readFile(path.join(this.localFolder!, this.prefix, key), { encoding: "utf8" });
+ }
+ } catch { }
} catch {
//No-op
}
}
}
- setItem(key: string, value: string): void {
+ async setItem(key: string, value: string): Promise<void> {
this.ensureVersion();
if (typeof window !== "undefined") {
- return window.localStorage.setItem(this.prefix + key, value);
+ const localforage = await import('localforage');
+ await localforage.setItem(this.prefix + key, value);
} else {
try {
- return this.nodeLocalStorage?.setItem(this.prefix + key, value);
+ const fs = await import('fs');
+ const path = await import('path');
+ return await fs.promises.writeFile(path.join(this.localFolder!, this.prefix, key), value, { encoding: "utf8", flag: "w+" });
} catch {
//No-op
}
}
}
- removeItem(key: string): void {
+ async removeItem(key: string): Promise<void> {
this.ensureVersion();
if (typeof window !== "undefined") {
- return window.localStorage.removeItem(this.prefix + key);
+ const localforage = await import('localforage');
+ await localforage.removeItem(this.prefix + key);
} else {
try {
- return this.nodeLocalStorage?.removeItem(this.prefix + key);
+ const fs = await import('fs');
+ const path = await import('path');
+ try {
+ const stat = await fs.promises.stat(path.join(this.localFolder!, this.prefix, key));
+ if (stat.isFile()) {
+ await fs.promises.unlink(path.join(this.localFolder!, this.prefix, key));
+ }
+ } catch { }
} catch {
//No-op
}
}
}
- setOrDelete(key: string, value: string | undefined): void {
+ async setOrDelete(key: string, value: string | undefined): Promise<void> {
if (value) {
- this.setItem(key, value);
+ await this.setItem(key, value);
} else {
- this.removeItem(key);
+ await this.removeItem(key);
}
}
}
\ No newline at end of file
diff --git a/next.config.js b/next.config.js
@@ -27,6 +27,15 @@ const securityHeaders = [
module.exports = {
i18n,
reactStrictMode: true,
+ webpack: (config) => {
+ config.resolve.fallback = {
+ ...config.resolve.fallback,
+ fs: false,
+ path: false
+ }
+
+ return config;
+ },
async headers() {
return [
{
diff --git a/package-lock.json b/package-lock.json
@@ -10,6 +10,7 @@
"cors": "2.8.5",
"feed": "yshrsmz/feed#c50d7a82c5b6141039830d615b19b760e173e231",
"lightgallery": "2.4.0",
+ "localforage": "1.10.0",
"meilisearch": "0.24.0",
"next": "12.0.10",
"next-i18next": "10.2.0",
@@ -30,6 +31,7 @@
"@babel/plugin-transform-typescript": "7.16.8",
"@playwright/test": "1.18.1",
"@types/cors": "2.8.12",
+ "@types/localforage": "^0.0.34",
"@types/node": "17.0.17",
"@types/node-localstorage": "1.3.0",
"@types/offscreencanvas": "2019.6.4",
@@ -1398,6 +1400,16 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "node_modules/@types/localforage": {
+ "version": "0.0.34",
+ "resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz",
+ "integrity": "sha1-XjHDLdh5HsS5/z70fJy1Wy0NlDg=",
+ "deprecated": "This is a stub types definition for localforage (https://github.com/localForage/localForage). localforage provides its own type definitions, so you don't need @types/localforage installed!",
+ "dev": true,
+ "dependencies": {
+ "localforage": "*"
+ }
+ },
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@@ -4657,6 +4669,11 @@
"minimatch": "^3.0.4"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -5584,6 +5601,14 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lie": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+ "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/lightgallery": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/lightgallery/-/lightgallery-2.4.0.tgz",
@@ -5607,6 +5632,14 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
+ "node_modules/localforage": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
+ "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
+ "dependencies": {
+ "lie": "3.1.1"
+ }
+ },
"node_modules/locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
@@ -10017,6 +10050,15 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "@types/localforage": {
+ "version": "0.0.34",
+ "resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz",
+ "integrity": "sha1-XjHDLdh5HsS5/z70fJy1Wy0NlDg=",
+ "dev": true,
+ "requires": {
+ "localforage": "*"
+ }
+ },
"@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@@ -12586,6 +12628,11 @@
"minimatch": "^3.0.4"
}
},
+ "immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+ },
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -13282,6 +13329,14 @@
"type-check": "~0.4.0"
}
},
+ "lie": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+ "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
+ "requires": {
+ "immediate": "~3.0.5"
+ }
+ },
"lightgallery": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/lightgallery/-/lightgallery-2.4.0.tgz",
@@ -13299,6 +13354,14 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
+ "localforage": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
+ "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
+ "requires": {
+ "lie": "3.1.1"
+ }
+ },
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
diff --git a/package.json b/package.json
@@ -21,6 +21,7 @@
"cors": "2.8.5",
"feed": "yshrsmz/feed#c50d7a82c5b6141039830d615b19b760e173e231",
"lightgallery": "2.4.0",
+ "localforage": "1.10.0",
"meilisearch": "0.24.0",
"next": "12.0.10",
"next-i18next": "10.2.0",
@@ -41,6 +42,7 @@
"@babel/plugin-transform-typescript": "7.16.8",
"@playwright/test": "1.18.1",
"@types/cors": "2.8.12",
+ "@types/localforage": "0.0.34",
"@types/node": "17.0.17",
"@types/node-localstorage": "1.3.0",
"@types/offscreencanvas": "2019.6.4",
@@ -60,4 +62,4 @@
"optionalDependencies": {
"fsevents": "2.3.2"
}
-}
+}
+\ No newline at end of file
diff --git a/pages/_app.tsx b/pages/_app.tsx
@@ -1,59 +1,86 @@
import '../styles/globals.css';
import 'react-toastify/dist/ReactToastify.min.css';
import type { AppProps } from 'next/app';
-import { client, ClientContext, guest_client } from '../components/ClientContext';
-import React from 'react';
+import { ClientContext, } from '../components/ClientContext';
+import React, { PureComponent } from 'react';
import Head from 'next/head';
import { ToastContainer } from 'react-toastify';
import { appWithTranslation } from 'next-i18next';
import Footer from '../components/Footer';
import dynamic from 'next/dynamic';
+import MatrixClient from "../helpers/matrix_client";
+import Storage from "../helpers/storage";
const HeaderNoSSR = dynamic(
() => import('../components/Header'),
{ ssr: false }
);
-function MyApp({ Component, pageProps }: AppProps) {
- 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={{
- client: client,
- guest_client: guest_client,
- is_generating_guest: false,
- }}
- >
- <div className='min-h-full flex flex-col justify-between bg-[#f8f8f8] dark:bg-[#06070D]'>
- <HeaderNoSSR></HeaderNoSSR>
- <main className='mb-auto lg:pt-20 pt-52 z-0'>
- <Component {...pageProps} />
- </main>
- <Footer></Footer>
- </div>
- </ClientContext.Provider>
- </React.StrictMode>
- <ToastContainer
- position="top-center"
- autoClose={5000}
- hideProgressBar={false}
- newestOnTop={false}
- closeOnClick
- rtl={false}
- pauseOnFocusLoss
- draggable
- pauseOnHover
- theme="colored"
- />
- </>
- );
+type State = {
+ client?: MatrixClient;
+ guest_client?: MatrixClient;
+};
+
+class MyApp extends PureComponent<AppProps, State> {
+ constructor(props: AppProps) {
+ super(props);
+ this.state = {};
+ }
+
+ async componentDidMount() {
+ // Actual user client
+ const client = await MatrixClient.init(await Storage.init("main"));
+
+ // Client used if logged in for the Homepage to prevent excessive room joining
+ const guest_client = await MatrixClient.init(await Storage.init("guest"));
+ this.setState({
+ client: client,
+ guest_client: guest_client
+ });
+ }
+
+ render() {
+ const Component = this.props.Component;
+ 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={{
+ client: this.state.client,
+ guest_client: this.state.guest_client,
+ is_generating_guest: false,
+ }}
+ >
+ <div className='min-h-full flex flex-col justify-between bg-[#f8f8f8] dark:bg-[#06070D]'>
+ <HeaderNoSSR></HeaderNoSSR>
+ <main className='mb-auto lg:pt-20 pt-52 z-0'>
+ <Component {...this.props.pageProps} />
+ </main>
+ <Footer></Footer>
+ </div>
+ </ClientContext.Provider>
+ </React.StrictMode>
+ <ToastContainer
+ position="top-center"
+ autoClose={5000}
+ hideProgressBar={false}
+ newestOnTop={false}
+ closeOnClick
+ rtl={false}
+ pauseOnFocusLoss
+ draggable
+ pauseOnHover
+ theme="colored"
+ />
+ </>
+ );
+ }
}
export default appWithTranslation(MyApp);
diff --git a/pages/api/posts/feed.rss.ts b/pages/api/posts/feed.rss.ts
@@ -3,10 +3,10 @@ import Cors from 'cors';
import { NextApiRequest, NextApiResponse } from "next";
import { Feed } from "feed";
import { get_data } from "../directory";
-import { client } from "../../../components/ClientContext";
-import { constMatrixArtServer } from "../../../helpers/matrix_client";
+import MatrixClient, { constMatrixArtServer } from "../../../helpers/matrix_client";
import { MatrixImageEvents } from "../../../helpers/event_types";
import { isImageGalleryEvent } from "../../../components/FrontPageImage";
+import Storage from "../../../helpers/storage";
// Initialize the cors middleware
const cors = initMiddleware(
@@ -41,6 +41,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try {
const directory = await get_data();
+ const client = await MatrixClient.init(await Storage.init("main"));
if (!client?.accessToken) {
try {
diff --git a/pages/api/submitSearch.ts b/pages/api/submitSearch.ts
@@ -21,7 +21,7 @@ export type SearchMedia = {
license: string;
sender: string;
nsfw: string;
- mxc_url: string;
+ mxc_url?: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
diff --git a/pages/index.tsx b/pages/index.tsx
@@ -1,13 +1,14 @@
import React, { PureComponent } from 'react';
import { MatrixEventBase, MatrixImageEvents } from '../helpers/event_types';
import Head from 'next/head';
-import { client, ClientContext } from '../components/ClientContext';
+import { ClientContext } from '../components/ClientContext';
import FrontPageImage, { isImageGalleryEvent } from '../components/FrontPageImage';
import { GetServerSideProps, InferGetServerSidePropsType, } from 'next';
import { get_data } from './api/directory';
-import { constMatrixArtServer } from '../helpers/matrix_client';
+import MatrixClient, { constMatrixArtServer } from '../helpers/matrix_client';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { i18n, WithTranslation } from 'next-i18next';
+import Storage from "../helpers/storage";
type Props = InferGetServerSidePropsType<typeof getServerSideProps> & WithTranslation & {
};
@@ -124,7 +125,8 @@ export const getServerSideProps: GetServerSideProps = async ({ res, locale }) =>
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
);
- console.log(`locale: ${locale ?? 'en'}`);
+
+ const client = await MatrixClient.init(await Storage.init("main"));
try {
const data = await get_data();
if (!client?.accessToken) {
diff --git a/pages/login.tsx b/pages/login.tsx
@@ -76,18 +76,24 @@ class Login extends PureComponent<Props, State> {
this.setState({
loading: true
});
- await this.context.client.login(serverUrl, this.state.mxid, this.state.password, true);
+ await this.context.client?.login(serverUrl, this.state.mxid, this.state.password, true);
if (this.state.generateProfile) {
try {
- await this.context.client.followUser(`#${this.context.client.userId}`);
+ await this.context.client?.followUser(`#${this.context.client?.userId}`);
} catch (error: any) {
this.setState(
{ error: error.message }
);
}
try {
- const token = await this.context.client.getOpenidToken();
- const resp = await fetch("/api/directory", { method: "POST", body: JSON.stringify({ access_token: token, user_id: this.context.client.userId, user_room: `#${this.context.client.userId}` }) });
+ const token = await this.context.client?.getOpenidToken();
+ const resp = await fetch("/api/directory", {
+ method: "POST", body: JSON.stringify({
+ access_token: token,
+ user_id: this.context.client?.userId,
+ user_room: `#${this.context.client?.userId}`
+ })
+ });
const body = await resp.json();
if (body.error_code) {
if (body.error_code !== "001") {
diff --git a/pages/logout.tsx b/pages/logout.tsx
@@ -20,8 +20,8 @@ class Logout extends PureComponent<Props, State> {
async componentDidMount() {
if (typeof window !== "undefined") {
- if (!this.context.client.isGuest) {
- await this.context.client.logout(false);
+ if (!this.context.client?.isGuest) {
+ await this.context.client?.logout(false);
}
}
await this.props.router.replace("/");
diff --git a/pages/post/[id].tsx b/pages/post/[id].tsx
@@ -8,9 +8,9 @@ import { GetServerSideProps, InferGetServerSidePropsType } from "next";
import Head from "next/head";
import { NextRouter, withRouter } from "next/router";
import { PureComponent, ReactNode } from "react";
-import { client, ClientContext } from "../../components/ClientContext";
+import { ClientContext } from "../../components/ClientContext";
import { ImageEvent, ImageGalleryEvent, MatrixEventBase, MatrixImageEvents } from "../../helpers/event_types";
-import { constMatrixArtServer } from "../../helpers/matrix_client";
+import MatrixClient, { constMatrixArtServer } from "../../helpers/matrix_client";
import { get_data } from "../api/directory";
import { isImageEvent, isImageGalleryEvent } from '../../components/FrontPageImage';
import Link from 'next/link';
@@ -18,6 +18,7 @@ import { Blurhash } from 'react-blurhash';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { i18n } from 'next-i18next';
import { toast } from 'react-toastify';
+import Storage from "../../helpers/storage";
type Props = InferGetServerSidePropsType<typeof getServerSideProps> & {
router: NextRouter;
@@ -88,7 +89,7 @@ class Post extends PureComponent<Props, State> {
return;
}
console.log("hi");
- const client = this.context.client.isGuest ? this.context.client : this.context.guest_client;
+ const client = this.context.client?.isGuest ? this.context.client : this.context.guest_client;
this.setState({
isLoadingImages: true,
});
@@ -100,19 +101,30 @@ class Post extends PureComponent<Props, State> {
try {
roomId = await client?.followUser(user.public_user_room);
} catch {
+ this.setState({
+ isLoadingImages: false,
+ hasFullyLoaded: true
+ });
console.error("Unbable to join room");
continue;
}
+ if (!roomId) {
+ this.setState({
+ isLoadingImages: false,
+ hasFullyLoaded: true
+ });
+ return;
+ }
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 == undefined) {
+ const image_event = events?.find((event) => (event.type === "m.image_gallery" || event.type === "m.image") && event.event_id === event_id);
+ if (!image_event) {
continue;
}
try {
- const profile = await client.getProfile(image_event.sender);
+ const profile = await client?.getProfile(image_event.sender);
this.setState({
image_event: image_event as MatrixImageEvents,
- displayname: profile.displayname ?? image_event.sender,
+ displayname: profile?.displayname ?? image_event.sender,
});
} catch (error) {
@@ -239,7 +251,7 @@ class Post extends PureComponent<Props, State> {
<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="cursor-pointer mt-0 mb-4 text-xl text-gray-900 dark:text-gray-200 font-normal inline-flex">
- {avatar_url ? <span className="block object-cover rounded-full mr-4"> <img className="object-cover rounded-full" src={this.context.client.downloadLink(avatar_url)!} height="24" width="24" alt={displayname} title={displayname} /> </span> : undefined}
+ {avatar_url ? <span className="block object-cover rounded-full mr-4"> <img className="object-cover rounded-full" src={this.context.client?.downloadLink(avatar_url)} height="24" width="24" alt={displayname} title={displayname} /> </span> : undefined}
<Link href={"/profile/" + encodeURIComponent(image_event.sender)} passHref><span className='hover:text-teal-400'>{displayname}</span></Link>
</h3>
{isImageEvent(image_event) ? (image_event.content['matrixart.license'] ? <h3 className='cursor-pointer mt-0 mb-4 text-l text-gray-600 dark:text-gray-400 font-normal'>{i18n?.t("License:")} <a href={this.getLicenseUrl(image_event.content['matrixart.license'])} title={this.getLicenseName(image_event.content['matrixart.license'])}>{this.getLicenseName(image_event.content['matrixart.license'])}</a></h3> : undefined) : undefined}
@@ -289,7 +301,7 @@ class Post extends PureComponent<Props, State> {
renderSingleImageEvent(imageEvent: ImageEvent, caption: string) {
const url = this.context.client?.downloadLink(imageEvent.content["m.file"].url);
- const thumbnail_url = this.context.client.thumbnailLink(imageEvent.content['m.file'].url, "scale", imageEvent.content['m.image'].width - 1, imageEvent.content['m.image'].height - 1);
+ const thumbnail_url = this.context.client?.thumbnailLink(imageEvent.content['m.file'].url, "scale", imageEvent.content['m.image'].width - 1, imageEvent.content['m.image'].height - 1);
if (!url || !thumbnail_url) {
return <></>;
@@ -445,6 +457,8 @@ export const getServerSideProps: GetServerSideProps = async ({ res, locale, quer
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
);
+
+ const client = await MatrixClient.init(await Storage.init("main"));
if (event_id && event_id.startsWith("$")) {
try {
const data = await get_data();
diff --git a/pages/profile/[userid].tsx b/pages/profile/[userid].tsx
@@ -78,10 +78,10 @@ class Profile extends PureComponent<Props, State> {
return;
}
try {
- const profile = await this.context.client.getProfile(this.props.mxid);
+ const profile = await this.context.client?.getProfile(this.props.mxid);
this.setState({
- displayname: profile.displayname ?? this.props.event.sender,
- avatar_url: profile.avatar_url,
+ displayname: profile?.displayname ?? this.props.event.sender,
+ avatar_url: profile?.avatar_url,
});
} catch (error) {
console.debug(`Failed to fetch profile for user ${this.props.mxid}:`, error);
@@ -89,7 +89,7 @@ class Profile extends PureComponent<Props, State> {
await this.loadEvents();
this.setState({
- isLoggedInUser: this.props.mxid === this.context.client.userId && !this.context.client.isGuest
+ isLoggedInUser: this.props.mxid === this.context.client?.userId && !this.context.client.isGuest
});
}
@@ -112,7 +112,7 @@ class Profile extends PureComponent<Props, State> {
if (isLoadingImages || hasFullyLoaded) {
return;
}
- const client = this.context.client.isGuest ? this.context.client : this.context.guest_client;
+ const client = this.context.client?.isGuest ? this.context.client : this.context.guest_client;
this.setState({
isLoadingImages: true,
});
@@ -121,15 +121,26 @@ class Profile extends PureComponent<Props, State> {
try {
roomId = await client?.followUser("#" + this.props.mxid);
} catch {
+ this.setState({
+ hasFullyLoaded: true,
+ isLoadingImages: false
+ });
console.error("Unbable to join room");
return;
}
+ if (!roomId) {
+ this.setState({
+ hasFullyLoaded: true,
+ isLoadingImages: false
+ });
+ return;
+ }
const events = await client?.getTimeline(roomId, 100, { limit: 30, types: ["m.image", "m.image_gallery", "matrixart.profile", "matrixart.profile_banner"] });
- const profile_event = events.find((event) => event.type === "matrixart.profile" && event.sender === this.props.mxid);
- const filtered_events = events.filter(event => event.type !== "m.room.member" && event.type !== "m.room.topic" && event.type !== "m.room.name" && event.type !== "m.room.power_levels");
- console.log("Adding", filtered_events.length, "items");
+ const profile_event = events?.find((event) => event.type === "matrixart.profile" && event.sender === this.props.mxid);
+ const filtered_events = events?.filter(event => event.type !== "m.room.member" && event.type !== "m.room.topic" && event.type !== "m.room.name" && event.type !== "m.room.power_levels");
+ console.log("Adding", filtered_events?.length, "items");
this.setState({
- events: filtered_events,
+ events: filtered_events ?? [],
profile_event: profile_event as MatrixArtProfile | undefined,
});
} catch (error) {
@@ -171,7 +182,7 @@ class Profile extends PureComponent<Props, State> {
editingUsername: true
});
} else {
- await this.context.client.setDisplayname(this.state.displayname);
+ await this.context.client?.setDisplayname(this.state.displayname);
this.setState({
editingUsername: false
});
@@ -185,8 +196,11 @@ class Profile extends PureComponent<Props, State> {
return;
}
const file = files[0];
- const mxc = await this.context.client.uploadFile(file);
- await this.context.client.setAvatarUrl(mxc);
+ const mxc = await this.context.client?.uploadFile(file);
+ if (!mxc) {
+ return;
+ }
+ await this.context.client?.setAvatarUrl(mxc);
this.setState({
avatar_url: mxc,
});
@@ -212,7 +226,7 @@ class Profile extends PureComponent<Props, State> {
<meta property="og:type" content="website" key="og-type" />
</Head>
{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)})`
+ 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)})`
}}
className="fixed top-14 w-full h-[32.5rem] bg-cover lg:bg-[position:50%]"
></div> : undefined}
@@ -231,7 +245,7 @@ class Profile extends PureComponent<Props, State> {
avatar_url ? (
<>
<label htmlFor="avatar-upload" className="rounded-md flex justify-center items-center cursor-pointer grayscale-0 hover:grayscale transition-all ease-in-out duration-300" 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} />
+ <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 className="invert-0 duration-300 transition-all ease-in-out hover:invert" /></div>
</label>
<input className="hidden" id="avatar-upload" type="file" accept="image/*" onChange={this.handleAvatarUpload.bind(this)} />
@@ -246,7 +260,7 @@ class Profile extends PureComponent<Props, State> {
)
)
: (avatar_url ?
- <img className="block object-cover rounded-md" src={this.context.client.downloadLink(avatar_url)!} height="100" width="100" alt={displayname} title={displayname} />
+ <img className="block object-cover rounded-md" src={this.context.client?.downloadLink(avatar_url)!} height="100" width="100" alt={displayname} title={displayname} />
:
<div className="rounded-md bg-slate-500 flex justify-center items-center" style={{ height: "100px", width: "100px" }}></div>
)
diff --git a/pages/submit.tsx b/pages/submit.tsx
@@ -31,7 +31,7 @@ class Submit extends PureComponent<Props, State> implements DropCallbacks {
onDropError: (fileRejections: FileRejection[]) => void;
async componentDidMount() {
- if (this.context.client.isGuest) {
+ if (this.context.client?.isGuest) {
this.props.router.replace("/login");
}
}