feat: add changelog modal

This commit is contained in:
Paul Makles 2022-06-12 22:19:41 +01:00
parent 5eabd2861f
commit cd8ab6739b
11 changed files with 239 additions and 4 deletions

2
external/lang vendored

@ -1 +1 @@
Subproject commit 309648592801a3bb5c1fa1702753f8dadde56cae Subproject commit 296a3d982c6a596176c9bbbd55020cd6ce760b9a

View file

@ -1,5 +1,5 @@
{ {
"version": "0.5.3-5", "version": "0.5.3-7",
"scripts": { "scripts": {
"dev": "node scripts/setup_assets.js --check && vite", "dev": "node scripts/setup_assets.js --check && vite",
"pull": "node scripts/setup_assets.js", "pull": "node scripts/setup_assets.js",

38
src/assets/changelogs.ts Normal file
View file

@ -0,0 +1,38 @@
type Element =
| string
| {
type: "image";
src: string;
};
export interface ChangelogPost {
date: Date;
title: string;
content: Element[];
}
export const changelogEntries: Record<number, ChangelogPost> = {
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;

View file

@ -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 (
<Column>
{post.content.map((entry) =>
typeof entry === "string" ? (
<span>{entry}</span>
) : (
<Image src={entry.src} />
),
)}
</Column>
);
}
/**
* 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: <Text id="app.special.modals.actions.close" />,
onClick: noopTrue,
},
];
if (log) {
arr.push({
palette: "plain-secondary",
children: <Text id="app.special.modals.changelogs.older" />,
onClick: () => {
setLog(undefined);
return false;
},
});
}
return arr;
}, [log]);
return (
<Modal
title={
entry?.title ?? (
<Text id="app.special.modals.changelogs.title" />
)
}
description={
entry ? (
dayjs(entry.date).calendar()
) : (
<Text id="app.special.modals.changelogs.description" />
)
}
actions={actions}
onClose={onClose}>
{entry ? (
<RenderLog post={entry} />
) : (
<Column>
{changelogEntryArray.map((entry, index) => (
<CategoryButton
key={index}
onClick={() => setLog(index + 1)}>
{entry.title}
</CategoryButton>
))}
</Column>
)}
</Modal>
);
}

View file

@ -8,6 +8,7 @@ import {
import type { Client, API } from "revolt.js"; import type { Client, API } from "revolt.js";
import { ulid } from "ulid"; import { ulid } from "ulid";
import Changelog from "./components/Changelog";
import MFAEnableTOTP from "./components/MFAEnableTOTP"; import MFAEnableTOTP from "./components/MFAEnableTOTP";
import MFAFlow from "./components/MFAFlow"; import MFAFlow from "./components/MFAFlow";
import MFARecovery from "./components/MFARecovery"; import MFARecovery from "./components/MFARecovery";
@ -118,6 +119,7 @@ class ModalControllerExtended extends ModalController<Modal> {
} }
export const modalController = new ModalControllerExtended({ export const modalController = new ModalControllerExtended({
changelog: Changelog,
mfa_flow: MFAFlow, mfa_flow: MFAFlow,
mfa_recovery: MFARecovery, mfa_recovery: MFARecovery,
mfa_enable_totp: MFAEnableTOTP, mfa_enable_totp: MFAEnableTOTP,

View file

@ -28,6 +28,10 @@ export type Modal = {
type: "out_of_date"; type: "out_of_date";
version: string; version: string;
} }
| {
type: "changelog";
initial?: number;
}
| { | {
type: "test"; type: "test";
} }

View file

@ -18,7 +18,10 @@ export default function SyncManager() {
// Sync settings from Revolt. // Sync settings from Revolt.
useEffect(() => { useEffect(() => {
if (client) { if (client) {
state.sync.pull(client); state.sync
.pull(client)
.catch(console.error)
.finally(() => state.changelog.checkForUpdates());
} }
}, [client]); }, [client]);

View file

@ -11,6 +11,7 @@ import { legacyMigrateForwards, LegacyState } from "./legacy/redux";
import Persistent from "./interfaces/Persistent"; import Persistent from "./interfaces/Persistent";
import Syncable from "./interfaces/Syncable"; import Syncable from "./interfaces/Syncable";
import Auth from "./stores/Auth"; import Auth from "./stores/Auth";
import Changelog from "./stores/Changelog";
import Draft from "./stores/Draft"; import Draft from "./stores/Draft";
import Experiments from "./stores/Experiments"; import Experiments from "./stores/Experiments";
import Layout from "./stores/Layout"; import Layout from "./stores/Layout";
@ -32,6 +33,7 @@ export const MIGRATIONS = {
*/ */
export default class State { export default class State {
auth: Auth; auth: Auth;
changelog: Changelog;
draft: Draft; draft: Draft;
locale: LocaleOptions; locale: LocaleOptions;
experiments: Experiments; experiments: Experiments;
@ -54,6 +56,7 @@ export default class State {
*/ */
constructor() { constructor() {
this.auth = new Auth(); this.auth = new Auth();
this.changelog = new Changelog();
this.draft = new Draft(); this.draft = new Draft();
this.locale = new LocaleOptions(); this.locale = new LocaleOptions();
this.experiments = new Experiments(); this.experiments = new Experiments();
@ -147,6 +150,7 @@ export default class State {
() => stringify(store.toJSON()), () => stringify(store.toJSON()),
async (value) => { async (value) => {
try { try {
console.log(id, "updated!");
// Save updated store to local storage. // Save updated store to local storage.
await localforage.setItem(id, JSON.parse(value)); await localforage.setItem(id, JSON.parse(value));

View file

@ -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<Data>, 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;
});
}
}
}

View file

@ -19,7 +19,8 @@ export type SyncKeys =
| "appearance" | "appearance"
| "locale" | "locale"
| "notifications" | "notifications"
| "ordering"; | "ordering"
| "changelog";
export const SYNC_KEYS: SyncKeys[] = [ export const SYNC_KEYS: SyncKeys[] = [
"theme", "theme",
@ -27,6 +28,7 @@ export const SYNC_KEYS: SyncKeys[] = [
"locale", "locale",
"notifications", "notifications",
"ordering", "ordering",
"changelog",
]; ];
export interface Data { export interface Data {

View file

@ -4,6 +4,7 @@ import {
Globe, Globe,
LogOut, LogOut,
Desktop, Desktop,
ListUl,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { import {
Bell, Bell,
@ -258,6 +259,10 @@ export default observer(() => {
category="pages" category="pages"
custom={ custom={
<> <>
<ButtonItem compact>
<ListUl size={20} />
<Text id="app.settings.pages.changelog" />
</ButtonItem>
<a <a
href="https://github.com/revoltchat" href="https://github.com/revoltchat"
target="_blank" target="_blank"