mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-08 06:03:03 -04:00
feat(timezones): add a database option (#272)
* add a database option for timezones * forgot to remove wrong comments lol
This commit is contained in:
parent
2aae71abf7
commit
43e8870dbc
3 changed files with 266 additions and 41 deletions
|
@ -8,9 +8,10 @@ import * as DataStore from "@api/DataStore";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
|
||||||
import { Button, Forms, SearchableSelect, useMemo, useState } from "@webpack/common";
|
import { Button, Forms, SearchableSelect, useEffect, useMemo, useState } from "@webpack/common";
|
||||||
|
|
||||||
import { DATASTORE_KEY, timezones } from ".";
|
import { DATASTORE_KEY, settings, timezones } from ".";
|
||||||
|
import { getTimezone, setTimezone, setUserDatabaseTimezone } from "./database";
|
||||||
|
|
||||||
export async function setUserTimezone(userId: string, timezone: string | null) {
|
export async function setUserTimezone(userId: string, timezone: string | null) {
|
||||||
timezones[userId] = timezone;
|
timezones[userId] = timezone;
|
||||||
|
@ -19,9 +20,24 @@ export async function setUserTimezone(userId: string, timezone: string | null) {
|
||||||
|
|
||||||
const cl = classNameFactory("vc-timezone-");
|
const cl = classNameFactory("vc-timezone-");
|
||||||
|
|
||||||
export function SetTimezoneModal({ userId, modalProps }: { userId: string, modalProps: ModalProps; }) {
|
export function SetTimezoneModal({ userId, modalProps, database }: { userId: string, modalProps: ModalProps; database?: boolean; }) {
|
||||||
const [currentValue, setCurrentValue] = useState<string | null>(timezones[userId] ?? null);
|
const [currentValue, setCurrentValue] = useState<string | null>(timezones[userId] ?? null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!database) return;
|
||||||
|
|
||||||
|
const localTimezone = timezones[userId];
|
||||||
|
const shouldUseDatabase =
|
||||||
|
settings.store.useDatabase &&
|
||||||
|
(settings.store.preferDatabaseOverLocal || localTimezone == null);
|
||||||
|
|
||||||
|
if (shouldUseDatabase) {
|
||||||
|
getTimezone(userId).then(setCurrentValue);
|
||||||
|
} else {
|
||||||
|
setCurrentValue(localTimezone);
|
||||||
|
}
|
||||||
|
}, [userId, settings.store.useDatabase, settings.store.preferDatabaseOverLocal, database]);
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return Intl.supportedValuesOf("timeZone").map(timezone => {
|
return Intl.supportedValuesOf("timeZone").map(timezone => {
|
||||||
const offset = new Intl.DateTimeFormat(undefined, { timeZone: timezone, timeZoneName: "short" })
|
const offset = new Intl.DateTimeFormat(undefined, { timeZone: timezone, timeZoneName: "short" })
|
||||||
|
@ -59,6 +75,7 @@ export function SetTimezoneModal({ userId, modalProps }: { userId: string, modal
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<ModalFooter className={cl("modal-footer")}>
|
<ModalFooter className={cl("modal-footer")}>
|
||||||
|
{!database && (
|
||||||
<Button
|
<Button
|
||||||
color={Button.Colors.RED}
|
color={Button.Colors.RED}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
@ -68,11 +85,18 @@ export function SetTimezoneModal({ userId, modalProps }: { userId: string, modal
|
||||||
>
|
>
|
||||||
Delete Timezone
|
Delete Timezone
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
color={Button.Colors.BRAND}
|
color={Button.Colors.BRAND}
|
||||||
disabled={currentValue === null}
|
disabled={currentValue === null}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await setUserTimezone(userId, currentValue!);
|
if (database) {
|
||||||
|
await setUserDatabaseTimezone(userId, currentValue);
|
||||||
|
await setTimezone(currentValue!);
|
||||||
|
} else {
|
||||||
|
await setUserTimezone(userId, currentValue);
|
||||||
|
}
|
||||||
|
|
||||||
modalProps.onClose();
|
modalProps.onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
125
src/equicordplugins/timezones/database.tsx
Normal file
125
src/equicordplugins/timezones/database.tsx
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
type CacheEntry = {
|
||||||
|
value: string | null;
|
||||||
|
expires: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
import { DataStore } from "@api/index";
|
||||||
|
import { openModal } from "@utils/modal";
|
||||||
|
import { OAuth2AuthorizeModal, showToast, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
|
export const DOMAIN = "https://timezone.creations.works";
|
||||||
|
export const REDIRECT_URI = `${DOMAIN}/auth/discord/callback`;
|
||||||
|
export const CLIENT_ID = "1377021506810417173";
|
||||||
|
|
||||||
|
export const DATASTORE_KEY = "vencord-database-timezones";
|
||||||
|
export let databaseTimezones: Record<string, CacheEntry> = {};
|
||||||
|
(async () => {
|
||||||
|
databaseTimezones = await DataStore.get<Record<string, CacheEntry>>(DATASTORE_KEY) || {};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const pendingRequests: Record<string, Promise<string | null>> = {};
|
||||||
|
|
||||||
|
export async function setUserDatabaseTimezone(userId: string, timezone: string | null) {
|
||||||
|
databaseTimezones[userId] = {
|
||||||
|
value: timezone,
|
||||||
|
expires: Date.now() + 60 * 60 * 1000 // 1 hour
|
||||||
|
};
|
||||||
|
await DataStore.set(DATASTORE_KEY, databaseTimezones);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTimezone(userId: string): Promise<string | null> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const cached = databaseTimezones[userId];
|
||||||
|
if (cached && now < cached.expires) return cached.value;
|
||||||
|
|
||||||
|
if (!pendingRequests[userId]) {
|
||||||
|
pendingRequests[userId] = (async () => {
|
||||||
|
const res = await fetch(`${DOMAIN}/get?id=${userId}`, {
|
||||||
|
headers: { Accept: "application/json" }
|
||||||
|
});
|
||||||
|
|
||||||
|
let value: string | null = null;
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json();
|
||||||
|
if (json?.timezone && typeof json.timezone === "string") {
|
||||||
|
value = json.timezone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserDatabaseTimezone(userId, value);
|
||||||
|
delete pendingRequests[userId];
|
||||||
|
return value;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingRequests[userId];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setTimezone(timezone: string): Promise<boolean> {
|
||||||
|
const res = await fetch(`${DOMAIN}/set?timezone=${encodeURIComponent(timezone)}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json"
|
||||||
|
},
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTimezone(): Promise<boolean> {
|
||||||
|
const res = await fetch(`${DOMAIN}/delete`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json"
|
||||||
|
},
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authModal(callback?: () => void) {
|
||||||
|
openModal(modalProps => (
|
||||||
|
<OAuth2AuthorizeModal
|
||||||
|
{...modalProps}
|
||||||
|
clientId={CLIENT_ID}
|
||||||
|
redirectUri={REDIRECT_URI}
|
||||||
|
responseType="code"
|
||||||
|
scopes={["identify"]}
|
||||||
|
permissions={0n}
|
||||||
|
cancelCompletesFlow={false}
|
||||||
|
callback={async (res: any) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(res.location);
|
||||||
|
|
||||||
|
const r = await fetch(url, {
|
||||||
|
credentials: "include",
|
||||||
|
headers: { Accept: "application/json" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await r.json();
|
||||||
|
if (!r.ok) {
|
||||||
|
showToast(json.message ?? "Authorization failed", Toasts.Type.FAILURE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast("Authorization successful!", Toasts.Type.SUCCESS);
|
||||||
|
callback?.();
|
||||||
|
} catch (e) {
|
||||||
|
showToast("Unexpected error during authorization", Toasts.Type.FAILURE);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
|
@ -10,13 +10,14 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import * as DataStore from "@api/DataStore";
|
import * as DataStore from "@api/DataStore";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs, EquicordDevs } from "@utils/constants";
|
||||||
import { openModal } from "@utils/modal";
|
import { openModal } from "@utils/modal";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Menu, Tooltip, useEffect, useState } from "@webpack/common";
|
import { Button, Menu, showToast, Toasts, Tooltip, useEffect, UserStore, useState } from "@webpack/common";
|
||||||
import { Message, User } from "discord-types/general";
|
import { Message, User } from "discord-types/general";
|
||||||
|
|
||||||
|
import { authModal, deleteTimezone, getTimezone, setUserDatabaseTimezone } from "./database";
|
||||||
import { SetTimezoneModal } from "./TimezoneModal";
|
import { SetTimezoneModal } from "./TimezoneModal";
|
||||||
|
|
||||||
export const DATASTORE_KEY = "vencord-timezones";
|
export const DATASTORE_KEY = "vencord-timezones";
|
||||||
|
@ -46,6 +47,51 @@ export const settings = definePluginSettings({
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Show time in profiles",
|
description: "Show time in profiles",
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
|
||||||
|
useDatabase: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Enable database for getting user timezones",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
preferDatabaseOverLocal: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Prefer database over local storage for timezones",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
|
||||||
|
setDatabaseTimezone: {
|
||||||
|
description: "Set your timezone on the database",
|
||||||
|
type: OptionType.COMPONENT,
|
||||||
|
component: () => (
|
||||||
|
<Button onClick={() => {
|
||||||
|
authModal(async () => {
|
||||||
|
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}>
|
||||||
|
Set Timezone on Database
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
resetDatabaseTimezone: {
|
||||||
|
description: "Reset your timezone on the database",
|
||||||
|
type: OptionType.COMPONENT,
|
||||||
|
component: () => (
|
||||||
|
<Button
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
onClick={() => {
|
||||||
|
authModal(async () => {
|
||||||
|
await setUserDatabaseTimezone(UserStore.getCurrentUser().id, null);
|
||||||
|
await deleteTimezone();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Database Timezones
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -66,23 +112,33 @@ interface Props {
|
||||||
}
|
}
|
||||||
const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Props) => {
|
const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Props) => {
|
||||||
const [currentTime, setCurrentTime] = useState(timestamp || Date.now());
|
const [currentTime, setCurrentTime] = useState(timestamp || Date.now());
|
||||||
const timezone = timezones[userId];
|
const [timezone, setTimezone] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer: NodeJS.Timeout;
|
const localTimezone = timezones[userId];
|
||||||
|
const shouldUseDatabase =
|
||||||
|
settings.store.useDatabase &&
|
||||||
|
(settings.store.preferDatabaseOverLocal || localTimezone == null);
|
||||||
|
|
||||||
|
if (shouldUseDatabase) {
|
||||||
|
getTimezone(userId).then(setTimezone);
|
||||||
|
} else {
|
||||||
|
setTimezone(localTimezone);
|
||||||
|
}
|
||||||
|
}, [userId, settings.store.useDatabase, settings.store.preferDatabaseOverLocal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (type !== "profile") return;
|
||||||
|
|
||||||
if (type === "profile") {
|
|
||||||
setCurrentTime(Date.now());
|
setCurrentTime(Date.now());
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds();
|
const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds();
|
||||||
|
const timer = setTimeout(() => {
|
||||||
timer = setTimeout(() => {
|
|
||||||
setCurrentTime(Date.now());
|
setCurrentTime(Date.now());
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
|
||||||
|
|
||||||
return () => timer && clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [type, currentTime]);
|
}, [type, currentTime]);
|
||||||
|
|
||||||
if (!timezone) return null;
|
if (!timezone) return null;
|
||||||
|
@ -94,8 +150,9 @@ const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Prop
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "numeric",
|
minute: "numeric"
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position="top"
|
position="top"
|
||||||
|
@ -107,22 +164,19 @@ const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Prop
|
||||||
tooltipClassName="timezone-tooltip"
|
tooltipClassName="timezone-tooltip"
|
||||||
text={longTime}
|
text={longTime}
|
||||||
>
|
>
|
||||||
{toolTipProps => {
|
{toolTipProps => (
|
||||||
return (
|
|
||||||
<span
|
<span
|
||||||
{...toolTipProps}
|
{...toolTipProps}
|
||||||
className={type === "message" ? `timezone-message-item ${classes.timestamp}` : "timezone-profile-item"}
|
className={type === "message" ? `timezone-message-item ${classes.timestamp}` : "timezone-profile-item"}
|
||||||
>
|
>
|
||||||
{
|
{type === "message" ? `(${shortTime})` : shortTime}
|
||||||
type === "message" ? `(${shortTime})` : shortTime
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}, { noop: true });
|
}, { noop: true });
|
||||||
|
|
||||||
|
|
||||||
const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: { user: User; }) => {
|
const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: { user: User; }) => {
|
||||||
if (user?.id == null) return;
|
if (user?.id == null) return;
|
||||||
|
|
||||||
|
@ -136,11 +190,33 @@ const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: {
|
||||||
|
|
||||||
children.push(<Menu.MenuSeparator />, setTimezoneItem);
|
children.push(<Menu.MenuSeparator />, setTimezoneItem);
|
||||||
|
|
||||||
|
if (settings.store.useDatabase) {
|
||||||
|
const refreshTimezoneItem = (
|
||||||
|
<Menu.MenuItem
|
||||||
|
label="Refresh Timezone"
|
||||||
|
id="refresh-timezone"
|
||||||
|
action={async () => {
|
||||||
|
showToast("Refreshing timezone...", Toasts.Type.CLOCK);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const timezone = await getTimezone(user.id);
|
||||||
|
setUserDatabaseTimezone(user.id, timezone);
|
||||||
|
timezones[user.id] = timezone;
|
||||||
|
showToast("Timezone refreshed successfully!", Toasts.Type.SUCCESS);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to refresh timezone:", error);
|
||||||
|
showToast("Failed to refresh timezone.", Toasts.Type.FAILURE);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
children.push(refreshTimezoneItem);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Timezones",
|
name: "Timezones",
|
||||||
authors: [Devs.Aria],
|
authors: [Devs.Aria, EquicordDevs.creations],
|
||||||
description: "Shows the local time of users in profiles and message headers",
|
description: "Shows the local time of users in profiles and message headers",
|
||||||
contextMenus: {
|
contextMenus: {
|
||||||
"user-context": userContextMenuPatch
|
"user-context": userContextMenuPatch
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue