diff --git a/external/lang b/external/lang index 30964859..296a3d98 160000 --- a/external/lang +++ b/external/lang @@ -1 +1 @@ -Subproject commit 309648592801a3bb5c1fa1702753f8dadde56cae +Subproject commit 296a3d982c6a596176c9bbbd55020cd6ce760b9a diff --git a/package.json b/package.json index 29008844..80239d6a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.5.3-5", + "version": "0.5.3-7", "scripts": { "dev": "node scripts/setup_assets.js --check && vite", "pull": "node scripts/setup_assets.js", diff --git a/src/assets/changelogs.ts b/src/assets/changelogs.ts new file mode 100644 index 00000000..88b60f6a --- /dev/null +++ b/src/assets/changelogs.ts @@ -0,0 +1,38 @@ +type Element = + | string + | { + type: "image"; + src: string; + }; + +export interface ChangelogPost { + date: Date; + title: string; + content: Element[]; +} + +export const changelogEntries: Record = { + 1: { + date: new Date("2022-06-12T20:39:16.674Z"), + title: "Secure your account with 2FA", + content: [ + "Two-factor authentication is now available to all users, you can now head over to settings to enable recovery codes and an authenticator app.", + { + type: "image", + src: "https://autumn.revolt.chat/attachments/E21kwmuJGcASgkVLiSIW0wV3ggcaOWjW0TQF7cdFNY/image.png", + }, + "Once enabled, you will be prompted on login.", + { + type: "image", + src: "https://autumn.revolt.chat/attachments/LWRYoKR2tE1ggW_Lzm547P1pnrkNgmBaoCAfWvHE74/image.png", + }, + "Other authentication methods coming later, stay tuned!", + ], + }, +}; + +export const changelogEntryArray = Object.keys(changelogEntries).map( + (index) => changelogEntries[index as unknown as number], +); + +export const latestChangelog = changelogEntryArray.length; diff --git a/src/context/modals/components/Changelog.tsx b/src/context/modals/components/Changelog.tsx new file mode 100644 index 00000000..059ed776 --- /dev/null +++ b/src/context/modals/components/Changelog.tsx @@ -0,0 +1,105 @@ +import dayjs from "dayjs"; +import styled from "styled-components"; + +import { Text } from "preact-i18n"; +import { useMemo, useState } from "preact/hooks"; + +import { CategoryButton, Column, Modal } from "@revoltchat/ui"; +import type { Action } from "@revoltchat/ui/esm/components/design/atoms/display/Modal"; + +import { noopTrue } from "../../../lib/js"; + +import { + changelogEntries, + changelogEntryArray, + ChangelogPost, +} from "../../../assets/changelogs"; +import { ModalProps } from "../types"; + +const Image = styled.img` + border-radius: var(--border-radius); +`; + +function RenderLog({ post }: { post: ChangelogPost }) { + return ( + + {post.content.map((entry) => + typeof entry === "string" ? ( + {entry} + ) : ( + + ), + )} + + ); +} + +/** + * Changelog rendering modal + */ +export default function Changelog({ + initial, + onClose, +}: ModalProps<"changelog">) { + const [log, setLog] = useState(initial); + + const entry = useMemo( + () => (log ? changelogEntries[log] : undefined), + [log], + ); + + const actions = useMemo(() => { + const arr: Action[] = [ + { + palette: "primary", + children: , + onClick: noopTrue, + }, + ]; + + if (log) { + arr.push({ + palette: "plain-secondary", + children: , + onClick: () => { + setLog(undefined); + return false; + }, + }); + } + + return arr; + }, [log]); + + return ( + + ) + } + description={ + entry ? ( + dayjs(entry.date).calendar() + ) : ( + + ) + } + actions={actions} + onClose={onClose}> + {entry ? ( + + ) : ( + + {changelogEntryArray.map((entry, index) => ( + setLog(index + 1)}> + {entry.title} + + ))} + + )} + + ); +} diff --git a/src/context/modals/index.tsx b/src/context/modals/index.tsx index 824c31f2..dd10f78d 100644 --- a/src/context/modals/index.tsx +++ b/src/context/modals/index.tsx @@ -8,6 +8,7 @@ import { import type { Client, API } from "revolt.js"; import { ulid } from "ulid"; +import Changelog from "./components/Changelog"; import MFAEnableTOTP from "./components/MFAEnableTOTP"; import MFAFlow from "./components/MFAFlow"; import MFARecovery from "./components/MFARecovery"; @@ -118,6 +119,7 @@ class ModalControllerExtended extends ModalController { } export const modalController = new ModalControllerExtended({ + changelog: Changelog, mfa_flow: MFAFlow, mfa_recovery: MFARecovery, mfa_enable_totp: MFAEnableTOTP, diff --git a/src/context/modals/types.ts b/src/context/modals/types.ts index bbe43c37..5b1e061d 100644 --- a/src/context/modals/types.ts +++ b/src/context/modals/types.ts @@ -28,6 +28,10 @@ export type Modal = { type: "out_of_date"; version: string; } + | { + type: "changelog"; + initial?: number; + } | { type: "test"; } diff --git a/src/context/revoltjs/SyncManager.tsx b/src/context/revoltjs/SyncManager.tsx index 33cb78c6..6ed39af4 100644 --- a/src/context/revoltjs/SyncManager.tsx +++ b/src/context/revoltjs/SyncManager.tsx @@ -18,7 +18,10 @@ export default function SyncManager() { // Sync settings from Revolt. useEffect(() => { if (client) { - state.sync.pull(client); + state.sync + .pull(client) + .catch(console.error) + .finally(() => state.changelog.checkForUpdates()); } }, [client]); diff --git a/src/mobx/State.ts b/src/mobx/State.ts index 4168809e..6a9c7cfb 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -11,6 +11,7 @@ import { legacyMigrateForwards, LegacyState } from "./legacy/redux"; import Persistent from "./interfaces/Persistent"; import Syncable from "./interfaces/Syncable"; import Auth from "./stores/Auth"; +import Changelog from "./stores/Changelog"; import Draft from "./stores/Draft"; import Experiments from "./stores/Experiments"; import Layout from "./stores/Layout"; @@ -32,6 +33,7 @@ export const MIGRATIONS = { */ export default class State { auth: Auth; + changelog: Changelog; draft: Draft; locale: LocaleOptions; experiments: Experiments; @@ -54,6 +56,7 @@ export default class State { */ constructor() { this.auth = new Auth(); + this.changelog = new Changelog(); this.draft = new Draft(); this.locale = new LocaleOptions(); this.experiments = new Experiments(); @@ -147,6 +150,7 @@ export default class State { () => stringify(store.toJSON()), async (value) => { try { + console.log(id, "updated!"); // Save updated store to local storage. await localforage.setItem(id, JSON.parse(value)); diff --git a/src/mobx/stores/Changelog.ts b/src/mobx/stores/Changelog.ts new file mode 100644 index 00000000..410e1518 --- /dev/null +++ b/src/mobx/stores/Changelog.ts @@ -0,0 +1,72 @@ +import { action, makeAutoObservable, runInAction } from "mobx"; + +import { modalController } from "../../context/modals"; + +import { latestChangelog } from "../../assets/changelogs"; +import Persistent from "../interfaces/Persistent"; +import Store from "../interfaces/Store"; +import Syncable from "../interfaces/Syncable"; + +export interface Data { + viewed?: number; +} + +/** + * Keeps track of viewed changelog items + */ +export default class Changelog implements Store, Persistent, Syncable { + /** + * Last viewed changelog ID + */ + private viewed: number; + + /** + * Construct new Layout store. + */ + constructor() { + this.viewed = 0; + makeAutoObservable(this); + } + + get id() { + return "changelog"; + } + + toJSON() { + return { + viewed: this.viewed, + }; + } + + @action hydrate(data: Data) { + if (data.viewed) { + this.viewed = data.viewed; + } + } + + apply(_key: string, data: unknown, _revision: number): void { + this.hydrate(data as Data); + } + + toSyncable(): { [key: string]: object } { + return { + changelog: this.toJSON(), + }; + } + + /** + * Check whether there are new updates + */ + checkForUpdates() { + if (this.viewed < latestChangelog) { + modalController.push({ + type: "changelog", + initial: latestChangelog, + }); + + runInAction(() => { + this.viewed = latestChangelog; + }); + } + } +} diff --git a/src/mobx/stores/Sync.ts b/src/mobx/stores/Sync.ts index a0d4fcfb..52d1de9c 100644 --- a/src/mobx/stores/Sync.ts +++ b/src/mobx/stores/Sync.ts @@ -19,7 +19,8 @@ export type SyncKeys = | "appearance" | "locale" | "notifications" - | "ordering"; + | "ordering" + | "changelog"; export const SYNC_KEYS: SyncKeys[] = [ "theme", @@ -27,6 +28,7 @@ export const SYNC_KEYS: SyncKeys[] = [ "locale", "notifications", "ordering", + "changelog", ]; export interface Data { diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index fd30d6b8..25ed1148 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -4,6 +4,7 @@ import { Globe, LogOut, Desktop, + ListUl, } from "@styled-icons/boxicons-regular"; import { Bell, @@ -258,6 +259,10 @@ export default observer(() => { category="pages" custom={ <> + + + +