mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 22:22:24 -05:00
fix: announce each version once, and don't flash old notes on reopen
- A downgrade leaves the recorded version alone, so going back up to it isn't announced again. - The recorded version is claimed by moving it on from the value read, so of two processes starting at once only one notifies. - The dialog clears the last notes before it paints. - Clearer wording for fresh installs in the docs and for the picture move in the 1.2.0 post.
This commit is contained in:
1 parent
aab83df886
commit
58eb011793
5 files changed
+65
-31
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "../db";
|
||||
import { RELEASE_NOTES_URL } from "../config";
|
||||
@@ -63,13 +64,8 @@ export async function isQuietRelease(version: string, baseUrl = RELEASE_NOTES_UR
|
||||
}
|
||||
}
|
||||
|
||||
async function readVersionSetting(key: string): Promise<string | null> {
|
||||
const row = await prisma.setting.findUnique({ where: { key } });
|
||||
return typeof row?.value === "string" && SEMVER.test(row.value) ? row.value : null;
|
||||
}
|
||||
|
||||
function writeVersionSetting(key: string, value: string) {
|
||||
return prisma.setting.upsert({ where: { key }, create: { key, value }, update: { value } });
|
||||
function asVersion(value: Prisma.JsonValue | undefined): string | null {
|
||||
return typeof value === "string" && SEMVER.test(value) ? value : null;
|
||||
}
|
||||
|
||||
/** At startup: when this version is newer than the one that ran last, every user gets a notification that opens
|
||||
@@ -81,24 +77,46 @@ export async function announceVersionUpdate(
|
||||
isQuiet: (version: string) => Promise<boolean> = isQuietRelease,
|
||||
): Promise<boolean> {
|
||||
if (!version || !SEMVER.test(version)) return false;
|
||||
const previous = await readVersionSetting(LAST_RUN_VERSION_KEY);
|
||||
if (previous === version) return false;
|
||||
const updated = previous ? compareVersions(version, previous) > 0 : (await prisma.user.count()) > 0;
|
||||
const since = (await readVersionSetting(LAST_ANNOUNCED_VERSION_KEY)) ?? previous;
|
||||
const [lastRun, lastAnnounced] = await Promise.all([
|
||||
prisma.setting.findUnique({ where: { key: LAST_RUN_VERSION_KEY } }),
|
||||
prisma.setting.findUnique({ where: { key: LAST_ANNOUNCED_VERSION_KEY } }),
|
||||
]);
|
||||
const previous = asVersion(lastRun?.value);
|
||||
// Equal is a restart. Lower is a downgrade: the higher version stays recorded, so going back up to it isn't
|
||||
// announced a second time.
|
||||
if (previous && compareVersions(version, previous) <= 0) return false;
|
||||
const updated = previous !== null || (await prisma.user.count()) > 0;
|
||||
const since = asVersion(lastAnnounced?.value) ?? previous;
|
||||
const quiet = updated && (await isQuiet(version));
|
||||
const users = updated && !quiet ? await prisma.user.findMany({ select: { id: true } }) : [];
|
||||
|
||||
if (updated && (await isQuiet(version))) {
|
||||
await prisma.$transaction([
|
||||
writeVersionSetting(LAST_RUN_VERSION_KEY, version),
|
||||
...(since ? [writeVersionSetting(LAST_ANNOUNCED_VERSION_KEY, since)] : []),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
const users = updated ? await prisma.user.findMany({ select: { id: true } }) : [];
|
||||
await prisma.$transaction([
|
||||
writeVersionSetting(LAST_RUN_VERSION_KEY, version),
|
||||
writeVersionSetting(LAST_ANNOUNCED_VERSION_KEY, version),
|
||||
prisma.notification.createMany({
|
||||
return prisma.$transaction(async (tx) => {
|
||||
// The version is claimed by moving it on from the one read above, so of two processes starting at once only
|
||||
// one notifies.
|
||||
const claimed = lastRun
|
||||
? (
|
||||
await tx.setting.updateMany({
|
||||
where: { key: LAST_RUN_VERSION_KEY, value: { equals: lastRun.value as Prisma.InputJsonValue } },
|
||||
data: { value: version },
|
||||
})
|
||||
).count === 1
|
||||
: (
|
||||
await tx.setting.createMany({
|
||||
data: [{ key: LAST_RUN_VERSION_KEY, value: version }],
|
||||
skipDuplicates: true,
|
||||
})
|
||||
).count === 1;
|
||||
if (!claimed) return false;
|
||||
// A quiet release leaves the next announcement's notes starting where the last one's ended.
|
||||
const announced = quiet ? since : version;
|
||||
if (announced) {
|
||||
await tx.setting.upsert({
|
||||
where: { key: LAST_ANNOUNCED_VERSION_KEY },
|
||||
create: { key: LAST_ANNOUNCED_VERSION_KEY, value: announced },
|
||||
update: { value: announced },
|
||||
});
|
||||
}
|
||||
await tx.notification.createMany({
|
||||
data: users.map((user) => ({
|
||||
userId: user.id,
|
||||
type: "system",
|
||||
@@ -107,9 +125,9 @@ export async function announceVersionUpdate(
|
||||
internalPath: whatsNewPath(version, since),
|
||||
createdAt: now,
|
||||
})),
|
||||
}),
|
||||
]);
|
||||
return updated;
|
||||
});
|
||||
return users.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
const curatedSchema = z.object({
|
||||
|
||||
@@ -240,6 +240,21 @@ describe("version update notification", () => {
|
||||
expect((await updates())[0].internalPath).toBe("?whats-new=1.2.0");
|
||||
});
|
||||
|
||||
it("doesn't announce a version again after going down from it and back up", async () => {
|
||||
await ranBefore("1.1.0");
|
||||
expect(await announce("1.2.0")).toBe(true);
|
||||
expect(await announce("1.1.0")).toBe(false);
|
||||
expect(await announce("1.2.0")).toBe(false);
|
||||
expect(await updates()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("notifies once when two processes start on the new version together", async () => {
|
||||
await ranBefore("1.1.0");
|
||||
const results = await Promise.all([announce("1.2.0"), announce("1.2.0")]);
|
||||
expect(results.filter(Boolean)).toHaveLength(1);
|
||||
expect(await updates()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("skips a quiet release, and covers it in the next announcement's notes", async () => {
|
||||
await ranBefore("1.2.0");
|
||||
expect(await announce("1.2.1", ["1.2.1"])).toBe(false);
|
||||
|
||||
+1
-1
@@ -259,7 +259,7 @@ ordinary notification (the System tab). To post one, add an entry to `entries` a
|
||||
## Release notes (What's new)
|
||||
|
||||
The first time a backend starts on a newer version than it last ran, every user gets a notification that opens
|
||||
the What's new dialog (admins can reopen it from Update Checker in Administration). A fresh install isn't told. There's no need to add a "Welcome to
|
||||
the What's new dialog (admins can reopen it from Update Checker in Administration). A fresh installation doesn't get this notification. There's no need to add a "Welcome to
|
||||
X.Y.Z" entry to `system-notifications.json` for this any more.
|
||||
|
||||
The dialog fetches its content from `main`, so it can be written after the release is out:
|
||||
|
||||
@@ -397,7 +397,8 @@ export default function WhatsNewDialog({ request, onClose }: Props) {
|
||||
const [failed, setFailed] = React.useState(false);
|
||||
const [zoomed, setZoomed] = React.useState<ReleaseNoteItem["images"][0] | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Before paint, so reopening shows the loading state rather than the last notes for a frame.
|
||||
React.useLayoutEffect(() => {
|
||||
if (!request) return;
|
||||
let active = true;
|
||||
setNotes(null);
|
||||
|
||||
@@ -116,6 +116,6 @@ Pull the new images and restart:
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
The first start after the update moves existing pictures next to their models. On a big library that can take a few
|
||||
minutes, and Administration shows how far it has got. Everyone on your instance then gets a notification that opens the
|
||||
The first start after the update moves existing pictures next to their models. For a large library, the move can take
|
||||
a few minutes, and Administration shows its progress. Everyone on your instance then gets a notification that opens the
|
||||
release notes in the app.
|
||||
Reference in new issue
Block a user