From 9d4db95e757fcb6698fc4e1dcf9f9f5704173236 Mon Sep 17 00:00:00 2001 From: creations Date: Sun, 3 Aug 2025 14:39:48 -0400 Subject: [PATCH] refactor timezone management and API error handling, add option to (#319) change the database api url --- .../timezones/TimezoneModal.tsx | 24 +++---- src/equicordplugins/timezones/database.tsx | 69 +++++++++++++------ src/equicordplugins/timezones/index.tsx | 53 +++++++++----- 3 files changed, 91 insertions(+), 55 deletions(-) diff --git a/src/equicordplugins/timezones/TimezoneModal.tsx b/src/equicordplugins/timezones/TimezoneModal.tsx index bd90d378a..eb1d719d9 100644 --- a/src/equicordplugins/timezones/TimezoneModal.tsx +++ b/src/equicordplugins/timezones/TimezoneModal.tsx @@ -10,8 +10,8 @@ import { Margins } from "@utils/margins"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; import { Button, Forms, SearchableSelect, useEffect, useMemo, useState } from "@webpack/common"; -import { DATASTORE_KEY, settings, timezones } from "."; -import { getTimezone, setTimezone, setUserDatabaseTimezone } from "./database"; +import { DATASTORE_KEY, getSystemTimezone, resolveUserTimezone, settings, timezones } from "."; +import { setTimezone, setUserDatabaseTimezone } from "./database"; export async function setUserTimezone(userId: string, timezone: string | null) { timezones[userId] = timezone; @@ -21,19 +21,11 @@ export async function setUserTimezone(userId: string, timezone: string | null) { const cl = classNameFactory("vc-timezone-"); export function SetTimezoneModal({ userId, modalProps, database }: { userId: string, modalProps: ModalProps; database?: boolean; }) { - const [currentValue, setCurrentValue] = useState(timezones[userId] ?? null); + const [currentValue, setCurrentValue] = useState(null); useEffect(() => { - const localTimezone = timezones[userId]; - const shouldUseDatabase = - settings.store.useDatabase && - (settings.store.preferDatabaseOverLocal || !localTimezone); - - const value = shouldUseDatabase - ? getTimezone(userId) ?? localTimezone - : localTimezone; - - setCurrentValue(value ?? Intl.DateTimeFormat().resolvedOptions().timeZone); + const resolvedTimezone = resolveUserTimezone(userId); + setCurrentValue(resolvedTimezone ?? getSystemTimezone()); }, [userId, settings.store.useDatabase, settings.store.preferDatabaseOverLocal]); const options = useMemo(() => { @@ -89,8 +81,10 @@ export function SetTimezoneModal({ userId, modalProps, database }: { userId: str disabled={currentValue === null} onClick={async () => { if (database) { - await setUserDatabaseTimezone(userId, currentValue); - await setTimezone(currentValue!); + const success = await setTimezone(currentValue!); + if (success) { + await setUserDatabaseTimezone(userId, currentValue); + } } else { await setUserTimezone(userId, currentValue); } diff --git a/src/equicordplugins/timezones/database.tsx b/src/equicordplugins/timezones/database.tsx index a313828e8..0f6ea5b3c 100644 --- a/src/equicordplugins/timezones/database.tsx +++ b/src/equicordplugins/timezones/database.tsx @@ -7,11 +7,34 @@ import { openModal } from "@utils/index"; import { OAuth2AuthorizeModal, showToast, Toasts } from "@webpack/common"; +import { settings } from "."; + const databaseTimezones: Record = {}; -const DOMAIN = "https://timezone.creations.works"; -const REDIRECT_URI = `${DOMAIN}/auth/discord/callback`; const CLIENT_ID = "1377021506810417173"; +function getDomain(): string { + return settings.store.databaseUrl || "https://timezone.creations.works"; +} + +function getRedirectUri(): string { + return `${getDomain()}/auth/discord/callback`; +} + +function handleApiError(error: any, defaultMessage: string): void { + console.error(defaultMessage, error); + const message = error?.message || defaultMessage; + showToast(message, Toasts.Type.FAILURE); +} + +async function safeJsonParse(response: Response): Promise { + try { + return await response.json(); + } catch (e) { + console.warn("Failed to parse JSON response:", e); + return { message: "Invalid response format" }; + } +} + export async function setUserDatabaseTimezone(userId: string, timezone: string | null) { databaseTimezones[userId] = { value: timezone }; } @@ -22,7 +45,7 @@ export function getTimezone(userId: string): string | null { export async function loadDatabaseTimezones(): Promise { try { - const res = await fetch(`${DOMAIN}/list`, { + const res = await fetch(`${getDomain()}/list`, { headers: { Accept: "application/json" } }); if (res.ok) { @@ -43,7 +66,7 @@ export async function loadDatabaseTimezones(): Promise { async function checkAuthentication(): Promise { try { - const res = await fetch(`${DOMAIN}/me`, { + const res = await fetch(`${getDomain()}/me`, { credentials: "include", headers: { Accept: "application/json" } }); @@ -73,7 +96,7 @@ async function setTimezoneInternal(timezone: string): Promise { formData.append("timezone", timezone); try { - const res = await fetch(`${DOMAIN}/set`, { + const res = await fetch(`${getDomain()}/set`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", @@ -84,16 +107,15 @@ async function setTimezoneInternal(timezone: string): Promise { }); if (!res.ok) { - const error = await res.json().catch(() => ({ message: "Unknown error" })); - showToast(error.message || "Failed to set timezone", Toasts.Type.FAILURE); + const error = await safeJsonParse(res); + handleApiError(error, "Failed to set timezone"); return false; } showToast("Timezone updated successfully!", Toasts.Type.SUCCESS); return true; } catch (e) { - console.error("Error setting timezone:", e); - showToast("Failed to set timezone", Toasts.Type.FAILURE); + handleApiError(e, "Failed to set timezone"); return false; } } @@ -102,12 +124,19 @@ export async function deleteTimezone(): Promise { const isAuthenticated = await checkAuthentication(); if (!isAuthenticated) { - showToast("You must be logged in to delete your timezone", Toasts.Type.FAILURE); - return false; + return new Promise(resolve => { + authModal(() => { + deleteTimezoneInternal().then(resolve); + }); + }); } + return deleteTimezoneInternal(); +} + +async function deleteTimezoneInternal(): Promise { try { - const res = await fetch(`${DOMAIN}/delete`, { + const res = await fetch(`${getDomain()}/delete`, { method: "DELETE", headers: { Accept: "application/json" @@ -116,16 +145,15 @@ export async function deleteTimezone(): Promise { }); if (!res.ok) { - const error = await res.json().catch(() => ({ message: "Unknown error" })); - showToast(error.message || "Failed to delete timezone", Toasts.Type.FAILURE); + const error = await safeJsonParse(res); + handleApiError(error, "Failed to delete timezone"); return false; } showToast("Timezone deleted successfully!", Toasts.Type.SUCCESS); return true; } catch (e) { - console.error("Error deleting timezone:", e); - showToast("Failed to delete timezone", Toasts.Type.FAILURE); + handleApiError(e, "Failed to delete timezone"); return false; } } @@ -135,7 +163,7 @@ export function authModal(callback?: () => void) { void) { credentials: "include", headers: { Accept: "application/json" } }); - const json = await r.json(); + const json = await safeJsonParse(r); if (!r.ok) { - showToast(json.message ?? "Authorization failed", Toasts.Type.FAILURE); + handleApiError(json, "Authorization failed"); return; } showToast("Authorization successful!", Toasts.Type.SUCCESS); callback?.(); } catch (e) { - console.error("Error during authorization:", e); - showToast("Unexpected error during authorization", Toasts.Type.FAILURE); + handleApiError(e, "Unexpected error during authorization"); } }} /> diff --git a/src/equicordplugins/timezones/index.tsx b/src/equicordplugins/timezones/index.tsx index 5a4dca6a7..4390c7fba 100644 --- a/src/equicordplugins/timezones/index.tsx +++ b/src/equicordplugins/timezones/index.tsx @@ -23,6 +23,22 @@ import { SetTimezoneModal } from "./TimezoneModal"; export let timezones: Record = {}; export const DATASTORE_KEY = "vencord-timezones"; +export function resolveUserTimezone(userId: string): string | null { + const localTimezone = timezones[userId]; + const shouldUseDatabase = + settings.store.useDatabase && + (settings.store.preferDatabaseOverLocal || !localTimezone); + + if (shouldUseDatabase) { + return getTimezone(userId) ?? localTimezone; + } + return localTimezone; +} + +export function getSystemTimezone(): string { + return Intl.DateTimeFormat().resolvedOptions().timeZone; +} + const classes = findByPropsLazy("timestamp", "compact", "contentOnly"); const locale = findByPropsLazy("getLocale"); @@ -63,6 +79,12 @@ export const settings = definePluginSettings({ default: true }, + databaseUrl: { + type: OptionType.STRING, + description: "Database URL for timezone storage", + default: "https://timezone.creations.works" + }, + setDatabaseTimezone: { description: "Set your timezone on the database", type: OptionType.COMPONENT, @@ -84,19 +106,14 @@ export const settings = definePluginSettings({ onClick={async () => { try { await setUserDatabaseTimezone(UserStore.getCurrentUser().id, null); - const success = await deleteTimezone(); - if (success) { - showToast("Database timezone reset successfully!", Toasts.Type.SUCCESS); - } else { - showToast("Failed to reset database timezone", Toasts.Type.FAILURE); - } + await deleteTimezone(); } catch (error) { console.error("Error resetting database timezone:", error); showToast("Failed to reset database timezone", Toasts.Type.FAILURE); } }} > - Reset Database Timezones + Reset Database Timezone ) }, @@ -130,16 +147,7 @@ const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Prop const [timezone, setTimezone] = useState(null); useEffect(() => { - const localTimezone = timezones[userId]; - const shouldUseDatabase = - settings.store.useDatabase && - (settings.store.preferDatabaseOverLocal || !localTimezone); - - if (shouldUseDatabase) { - setTimezone(getTimezone(userId) ?? localTimezone); - } else { - setTimezone(localTimezone); - } + setTimezone(resolveUserTimezone(userId)); }, [userId, settings.store.useDatabase, settings.store.preferDatabaseOverLocal]); useEffect(() => { @@ -153,8 +161,15 @@ const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Prop setCurrentTime(Date.now()); }, delay); - return () => clearTimeout(timer); - }, [type, currentTime]); + const interval = setInterval(() => { + setCurrentTime(Date.now()); + }, 60000); + + return () => { + clearTimeout(timer); + clearInterval(interval); + }; + }, [type]); if (!timezone) return null;