From 78880af9f70a20cc71c0f540001b5bf8f8061ba2 Mon Sep 17 00:00:00 2001 From: Jake <134332787+justjxke@users.noreply.github.com> Date: Mon, 29 Sep 2025 16:49:01 +0100 Subject: [PATCH] fix changelog settings logging every single setting in Equicord (#424) * add serialization for settings and a myraid of other fixes rewrites. * test dummy settings * fix issue with commits not being logged, rewrite some elements * remove dummy setting --------- Co-authored-by: Jake --- .../tabs/changelog/changelogManager.ts | 213 +++++++++++++++--- .../settings/tabs/changelog/index.tsx | 134 +++++++---- 2 files changed, 273 insertions(+), 74 deletions(-) diff --git a/src/components/settings/tabs/changelog/changelogManager.ts b/src/components/settings/tabs/changelog/changelogManager.ts index 406426633..c7ea9730c 100644 --- a/src/components/settings/tabs/changelog/changelogManager.ts +++ b/src/components/settings/tabs/changelog/changelogManager.ts @@ -35,9 +35,144 @@ const LAST_SEEN_HASH_KEY = "EquicordChangelog_LastSeenHash"; const KNOWN_PLUGINS_KEY = "EquicordChangelog_KnownPlugins"; const KNOWN_SETTINGS_KEY = "EquicordChangelog_KnownSettings"; const LAST_REPO_CHECK_KEY = "EquicordChangelog_LastRepoCheck"; +const GITHUB_COMPARE_ENDPOINT = "https://api.github.com/repos"; type KnownPluginSettingsMap = Map>; +function normalizeRepoUrl(repoUrl: string | null | undefined): string | null { + if (!repoUrl) return null; + try { + const normalized = repoUrl.replace(/^git\+/, ""); + const url = new URL(normalized); + if (!url.hostname.endsWith("github.com")) return null; + const segments = url.pathname.replace(/\.git$/, "").split("/").filter(Boolean); + if (segments.length < 2) return null; + return `${segments[0]}/${segments[1]}`; + } catch { + return null; + } +} + +async function fetchCommitsBetween( + repoSlug: string, + fromHash: string, + toHash: string, +): Promise { + if (!repoSlug || typeof fetch !== "function") return []; + try { + const res = await fetch( + `${GITHUB_COMPARE_ENDPOINT}/${repoSlug}/compare/${fromHash}...${toHash}`, + { + headers: { + Accept: "application/vnd.github+json", + "Cache-Control": "no-cache", + }, + }, + ); + + if (!res.ok) return []; + const data = await res.json(); + if (!data || !Array.isArray(data.commits)) return []; + + return data.commits.map((commit: any) => { + const message: string = commit?.commit?.message ?? ""; + const summary = message.split("\n")[0] || "No message"; + const authorName = + commit?.commit?.author?.name || + commit?.author?.login || + "Unknown"; + const timestamp = commit?.commit?.author?.date + ? Date.parse(commit.commit.author.date) + : undefined; + + return { + hash: commit?.sha || "", + author: authorName, + message: summary, + timestamp: Number.isNaN(timestamp) ? undefined : timestamp, + } as ChangelogEntry; + }); + } catch (err) { + console.warn("Failed to fetch commits between hashes", err); + return []; + } +} + +function toStringSet(value: unknown): Set { + const result = new Set(); + + const addValue = (entry: unknown) => { + if (entry === undefined || entry === null) return; + result.add(typeof entry === "string" ? entry : String(entry)); + }; + + if (value instanceof Set) { + value.forEach(addValue); + } else if (value instanceof Map) { + value.forEach(addValue); + } else if (Array.isArray(value)) { + value.forEach(addValue); + } else if (typeof value === "string") { + addValue(value); + } else if (value && typeof value === "object") { + Object.values(value as Record).forEach(addValue); + } + + return result; +} + +function normalizeKnownSettings(value: unknown): KnownPluginSettingsMap { + const map: KnownPluginSettingsMap = new Map(); + + const assign = (plugin: unknown, settings: unknown) => { + if (plugin === undefined || plugin === null) return; + map.set(String(plugin), toStringSet(settings)); + }; + + if (!value) { + return map; + } + + if (value instanceof Map) { + value.forEach((settings, plugin) => assign(plugin, settings)); + return map; + } + + if (Array.isArray(value)) { + value.forEach(entry => { + if (Array.isArray(entry) && entry.length > 0) { + assign(entry[0], entry[1]); + } + }); + return map; + } + + if (typeof value === "object") { + Object.entries(value as Record).forEach( + ([plugin, settings]) => assign(plugin, settings), + ); + } + + return map; +} + +function serializeKnownSettings( + map: KnownPluginSettingsMap, +): Record { + return Object.fromEntries( + Array.from(map.entries()).map(([plugin, settings]) => [ + plugin, + Array.from(settings), + ]), + ); +} + +async function persistKnownSettings( + map: KnownPluginSettingsMap, +): Promise { + await DataStore.set(KNOWN_SETTINGS_KEY, serializeKnownSettings(map)); +} + function isMapLike(value: any): value is Map { return ( value && @@ -204,28 +339,34 @@ function getCurrentSettings(pluginList: string[]): KnownPluginSettingsMap { export async function getKnownSettings(): Promise { const mapData = (await DataStore.get(KNOWN_SETTINGS_KEY)) as any; - let map: KnownPluginSettingsMap; - if (mapData === undefined) { const knownPlugins = await getKnownPlugins(); - const Plugins = [...Object.keys(plugins), ...Array.from(knownPlugins)]; - map = getCurrentSettings(Plugins); - await DataStore.set(KNOWN_SETTINGS_KEY, map); - } else { - // convert the plain object back to a map with set value(s) - map = new Map(); - if (mapData && typeof mapData === "object") { - for (const [plugin, settings] of Object.entries(mapData)) { - if (Array.isArray(settings)) { - map.set(plugin, new Set(settings)); - } else if (settings && typeof settings === "object") { - // should fix serialization issues - map.set(plugin, new Set(Object.values(settings))); - } - } - } + const pluginNames = [ + ...new Set([ + ...Object.keys(plugins), + ...Array.from(knownPlugins), + ]), + ]; + const initialMap = getCurrentSettings(pluginNames); + await persistKnownSettings(initialMap); + return initialMap; } - return map; + + const normalized = normalizeKnownSettings(mapData); + + if ( + mapData instanceof Map || + Array.isArray(mapData) || + (mapData && + typeof mapData === "object" && + Object.values(mapData).some(value => + value instanceof Set || value instanceof Map, + )) + ) { + await persistKnownSettings(normalized); + } + + return normalized; } export async function getNewSettings(): Promise> { @@ -234,8 +375,11 @@ export async function getNewSettings(): Promise> { const newSettings = new Map(); map.forEach((settings, plugin) => { + const known = knownSettings.get(plugin); + if (!known) return; + const filteredSettings = [...settings].filter( - setting => !knownSettings.get(plugin)?.has(setting), + setting => !known.has(setting), ); if (filteredSettings.length > 0) { newSettings.set(plugin, filteredSettings); @@ -245,32 +389,37 @@ export async function getNewSettings(): Promise> { return newSettings; } +export async function getCommitsSinceLastSeen( + repoUrl: string, +): Promise { + const lastSeenHash = await getLastSeenHash(); + if (!lastSeenHash || lastSeenHash === "unknown" || lastSeenHash === gitHash) + return []; + + const repoSlug = normalizeRepoUrl(repoUrl); + if (!repoSlug) return []; + + return fetchCommitsBetween(repoSlug, lastSeenHash, gitHash); +} + export async function updateKnownSettings(): Promise { const currentSettings = getCurrentSettings(Object.keys(plugins)); const knownSettings = await getKnownSettings(); - const allSettings = new Map(); + const mergedSettings: KnownPluginSettingsMap = new Map(); new Set([...currentSettings.keys(), ...knownSettings.keys()]).forEach( plugin => { - allSettings.set( + mergedSettings.set( plugin, new Set([ - ...(currentSettings.get(plugin) || []), ...(knownSettings.get(plugin) || []), + ...(currentSettings.get(plugin) || []), ]), ); }, ); - // convert map to serialized storage format - const serializableSettings = Object.fromEntries( - Array.from(allSettings.entries()).map(([plugin, settings]) => [ - plugin, - Array.from(settings), - ]), - ); - - await DataStore.set(KNOWN_SETTINGS_KEY, serializableSettings); + await persistKnownSettings(mergedSettings); } export async function getNewPlugins(): Promise { diff --git a/src/components/settings/tabs/changelog/index.tsx b/src/components/settings/tabs/changelog/index.tsx index 22ee5dfc6..e39d70b01 100644 --- a/src/components/settings/tabs/changelog/index.tsx +++ b/src/components/settings/tabs/changelog/index.tsx @@ -24,6 +24,7 @@ import { clearIndividualLog, formatTimestamp, getChangelogHistory, + getCommitsSinceLastSeen, getLastRepositoryCheckHash, getNewPlugins, getNewSettings, @@ -295,6 +296,32 @@ function ChangelogContent() { } }, []); + const ensureLocalUpdateLogged = React.useCallback(async () => { + if (repoPending || repoErr) return false; + const repoUrl = repo; + if (!repoUrl) return false; + + try { + const commits = await getCommitsSinceLastSeen(repoUrl); + if (commits.length === 0) return false; + + const newPlgs = await getNewPlugins(); + const updatedPlgs = await getUpdatedPlugins(); + const newSettings = await getNewSettings(); + + await saveUpdateSession(commits, newPlgs, updatedPlgs, newSettings); + + setChangelog(commits); + setNewPlugins(newPlgs); + setUpdatedPlugins(updatedPlgs); + await loadChangelogHistory(); + return true; + } catch (err) { + console.error("Failed to log local update:", err); + return false; + } + }, [repo, repoErr, repoPending, loadChangelogHistory]); + // check if the repository was recently refreshed React.useEffect(() => { const checkRecentStatus = async () => { @@ -339,52 +366,66 @@ function ChangelogContent() { if (lastRepoCheck === currentRepoHash) { setIsLoading(false); setRecentlyChecked(true); - Toasts.show({ - message: "Already up to date with repository", - id: Toasts.genId(), - type: Toasts.Type.MESSAGE, - options: { - position: Toasts.Position.BOTTOM, - }, - }); + const logged = await ensureLocalUpdateLogged(); + if (!logged) { + setChangelog([]); + Toasts.show({ + message: "Already up to date with repository", + id: Toasts.genId(), + type: Toasts.Type.MESSAGE, + options: { + position: Toasts.Position.BOTTOM, + }, + }); + } return; } if (updates.ok && updates.value) { - setChangelog(updates.value); + if (updates.value.length > 0) { + setChangelog(updates.value); - // Load current new/updated plugins and settings - const newPlgs = await getNewPlugins(); - const updatedPlgs = await getUpdatedPlugins(); - const newSettings = await getNewSettings(); - setNewPlugins(newPlgs); - setUpdatedPlugins(updatedPlgs); + const newPlgs = await getNewPlugins(); + const updatedPlgs = await getUpdatedPlugins(); + const newSettings = await getNewSettings(); + setNewPlugins(newPlgs); + setUpdatedPlugins(updatedPlgs); - // always save the current fetch session to history to track repo state - await saveUpdateSession( - updates.value, - newPlgs, - updatedPlgs, - newSettings, - true, // forceLog = true for repository fetches - ); - await loadChangelogHistory(); - setRecentlyChecked(true); + await saveUpdateSession( + updates.value, + newPlgs, + updatedPlgs, + newSettings, + true, + ); + await loadChangelogHistory(); + setRecentlyChecked(true); - Toasts.show({ - message: - updates.value.length > 0 - ? `Found ${updates.value.length} commit${updates.value.length === 1 ? "" : "s"} from repository` + Toasts.show({ + message: `Found ${updates.value.length} commit${updates.value.length === 1 ? "" : "s"} from repository`, + id: Toasts.genId(), + type: Toasts.Type.SUCCESS, + options: { + position: Toasts.Position.BOTTOM, + }, + }); + } else { + const logged = await ensureLocalUpdateLogged(); + setRecentlyChecked(true); + Toasts.show({ + message: logged + ? "Logged commits from your latest update" : "Repository is up to date with your local copy", - id: Toasts.genId(), - type: - updates.value.length > 0 - ? Toasts.Type.SUCCESS - : Toasts.Type.MESSAGE, - options: { - position: Toasts.Position.BOTTOM, - }, - }); + id: Toasts.genId(), + type: logged ? Toasts.Type.SUCCESS : Toasts.Type.MESSAGE, + options: { + position: Toasts.Position.BOTTOM, + }, + }); + if (!logged) { + setChangelog([]); + } + } } else if (!updates.ok) { throw new Error( updates.error?.message || "Failed to fetch from repository", @@ -414,18 +455,27 @@ function ChangelogContent() { React.useEffect(() => { const loadInitialData = async () => { if (!repoPending && !repoErr) { - // load new plugins first, then fetch the commits await loadNewPlugins(); - await fetchChangelog(); + const logged = await ensureLocalUpdateLogged(); + if (!logged) { + await fetchChangelog(); + } else { + setIsLoading(false); + } } else if (!repoPending) { // perseverance await loadNewPlugins(); setIsLoading(false); } }; - loadInitialData(); - }, [repoPending, repoErr, fetchChangelog, loadNewPlugins]); + }, [ + repoPending, + repoErr, + fetchChangelog, + loadNewPlugins, + ensureLocalUpdateLogged, + ]); const toggleLogExpanded = (logId: string) => { const newExpanded = new Set(expandedLogs);