diff --git a/src/plugins/Timezones/Utils.ts b/src/plugins/Timezones/Utils.ts index 13801729b..c84e00e97 100644 --- a/src/plugins/Timezones/Utils.ts +++ b/src/plugins/Timezones/Utils.ts @@ -23,15 +23,14 @@ import { VENCORD_USER_AGENT } from "@utils/constants"; import { debounce } from "@utils/debounce"; import { findLazy } from "@webpack"; export const DATASTORE_KEY = "plugins.Timezones.savedTimezones"; -import type { timezones } from "./all_timezones"; - +import { CustomTimezonePreference } from "./settings"; export interface TimezoneDB { - [userId: string]: typeof timezones[number]; + [userId: string]: string; } export const API_URL = "https://timezonedb.catvibers.me"; -const Cache: Record = {}; +const Cache: Record = {}; export function getTimeString(timezone: string, timestamp = new Date()): string { const locale = PreloadedUserSettings.getCurrentValue().localization.locale.value; @@ -41,7 +40,7 @@ export function getTimeString(timezone: string, timestamp = new Date()): string // A map of ids and callbacks that should be triggered on fetch -const requestQueue: Record void)[]> = {}; +const requestQueue: Record void)[]> = {}; async function bulkFetchTimezones(ids: string[]): Promise { @@ -58,7 +57,7 @@ async function bulkFetchTimezones(ids: string[]): Promise { const tzs = (Object.keys(res).map(userId => { - return res[userId] && { [userId]: res[userId]!.timezoneId as typeof timezones[number] }; + return res[userId] && { [userId]: res[userId]!.timezoneId }; }).filter(Boolean) as TimezoneDB[]).reduce((acc, cur) => ({ ...acc, ...cur }), {}); Object.assign(Cache, tzs); @@ -87,11 +86,20 @@ const bulkFetch = debounce(async () => { } }); -export function getUserTimezone(discordID: string): Promise { +export function getUserTimezone(discordID: string, strategy: CustomTimezonePreference): + Promise { + return new Promise(res => { const timezone = (DataStore.get(DATASTORE_KEY) as Promise).then(tzs => tzs?.[discordID]); timezone.then(tz => { - if (tz) res(tz); + if (strategy == CustomTimezonePreference.Always) { + if (tz) res(tz); + else res(undefined); + return; + } + + if (tz && strategy == CustomTimezonePreference.Secondary) + res(tz); else { if (discordID in Cache) res(Cache[discordID]); else if (discordID in requestQueue) requestQueue[discordID].push(res); @@ -104,3 +112,18 @@ export function getUserTimezone(discordID: string): Promise => { + if (typeof Intl !== "undefined" && "supportedValuesOf" in Intl) { + try { + // @ts-expect-error fuck you typescript + return Intl.supportedValuesOf('timeZone'); + } catch { } + } + + return await fetch(timezonesLink).then(tzs => tzs.json()); +}; diff --git a/src/plugins/Timezones/index.tsx b/src/plugins/Timezones/index.tsx index 9cc19824a..61f7b62b8 100644 --- a/src/plugins/Timezones/index.tsx +++ b/src/plugins/Timezones/index.tsx @@ -17,158 +17,41 @@ */ -import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import * as DataStore from "@api/DataStore"; import { Devs, VENCORD_USER_AGENT } from "@utils/constants"; -import { classes, useForceUpdater } from "@utils/misc"; -import definePlugin, { OptionType } from "@utils/types"; +import { classes, makeLazy, useForceUpdater } from "@utils/misc"; +import definePlugin from "@utils/types"; import { findByCodeLazy, findByPropsLazy } from "@webpack"; import { React, SearchableSelect, Text, Toasts, UserStore } from "@webpack/common"; import { Message, User } from "discord-types/general"; - +import settings from "./settings"; const EditIcon = findByCodeLazy("M19.2929 9.8299L19.9409 9.18278C21.353 7.77064 21.353 5.47197 19.9409"); const DeleteIcon = findByCodeLazy("M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"); const classNames = findByPropsLazy("customStatusSection"); -import { timezones } from "./all_timezones"; -import { API_URL, DATASTORE_KEY, getTimeString, getUserTimezone, TimezoneDB } from "./Utils"; +import { API_URL, DATASTORE_KEY, getAllTimezones, getTimeString, getUserTimezone, TimezoneDB } from "./Utils"; const styles = findByPropsLazy("timestampInline"); - +const useTimezones = makeLazy(getAllTimezones); export default definePlugin({ - name: "Timezones", - description: "Shows the timezones of users", + settings, + + name: "User Timezones", + description: "Allows you to see and set the timezones of other users.", authors: [Devs.mantikafasi, Devs.Arjix], - options: { - showTimezonesInChat: { - type: OptionType.BOOLEAN, - description: "Show timezones in chat", - default: true, - }, - - showTimezonesInProfile: { - type: OptionType.BOOLEAN, - description: "Show timezones in profile", - default: true, - }, - }, settingsAboutComponent: () => { const href = `${API_URL}?client_mod=${encodeURIComponent(VENCORD_USER_AGENT)}`; return ( A plugin that displays the local time for specific users using their timezone.
- By default the timezone will be fetched from the open(href)}>TimezoneDB (if available),
- but you can override that with a custom timezone. + Timezones can either be set manually or fetched automatically from the TimezoneDB
); }, - commands: [ - { - name: "settimezone", - description: "Set a users timezone", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - name: "user", - description: "User to set timezone for", - type: ApplicationCommandOptionType.USER, - required: true - }, - { - name: "timezone", - description: "Timezone id to set (see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)", - type: ApplicationCommandOptionType.STRING, - required: true - } - - - ], - execute(args, ctx) { - const user: string | undefined = findOption(args, "user"); - const timezone = (findOption(args, "timezone") as string | undefined)?.trim() as typeof timezones[number] | undefined; - - // Kinda hard to happen, but just to be safe... - if (!user || !timezone) return sendBotMessage(ctx.channel.id, { content: "PLease provider both a user and a timezone." }); - - - if (timezone && !timezones.includes(timezone)) { - sendBotMessage(ctx.channel.id, { content: "Invalid timezone.\nPlease look at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" }); - return; - } - - DataStore.update(DATASTORE_KEY, (oldValue: TimezoneDB | undefined) => { - oldValue = oldValue || {}; - - oldValue[user] = timezone; - return oldValue; - }).then(() => { - sendBotMessage(ctx.channel.id, { content: "Timezone set!" }); - }).catch(err => { - console.error(err); - sendBotMessage(ctx.channel.id, { content: "Something went wrong, please try again later." }); - }); - }, - }, - { - name: "deletetimezone", - description: "Delete a users timezone", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - name: "user", - description: "User to delete timezone for", - type: ApplicationCommandOptionType.USER, - required: true - }, - ], - execute(args, ctx) { - const user: string | undefined = findOption(args, "user"); - if (!user) return sendBotMessage(ctx.channel.id, { content: "Please provide a user." }) && undefined; - - DataStore.update(DATASTORE_KEY, (oldValue: TimezoneDB | undefined) => { - oldValue = oldValue || {}; - - if (!Object.prototype.hasOwnProperty.call(oldValue, user)) - sendBotMessage(ctx.channel.id, { content: "No timezones were set for this user." }); - else { - delete oldValue[user]; - sendBotMessage(ctx.channel.id, { content: "Timezone removed!." }); - } - - return oldValue; - }).catch(err => { - console.error(err); - sendBotMessage(ctx.channel.id, { content: "Something went wrong, please try again later." }); - }); - } - }, - { - name: "gettimezones", - description: "Get all timezones", - inputType: ApplicationCommandInputType.BUILT_IN, - execute(args, ctx) { - DataStore.get(DATASTORE_KEY).then((timezones: TimezoneDB | undefined) => { - if (!timezones || Object.keys(timezones).length === 0) { - sendBotMessage(ctx.channel.id, { content: "No timezones are set." }); - return; - } - - sendBotMessage(ctx.channel.id, { - content: "Timezones for " + Object.keys(timezones).length + " users:\n" + Object.keys(timezones).map(user => { - return `<@${user}> - ${timezones[user]}`; - }).join("\n") - }); - }).catch(err => { - console.error(err); - sendBotMessage(ctx.channel.id, { content: "Something went wrong, please try again later." }); - }); - } - } - ], patches: [ { @@ -176,37 +59,39 @@ export default definePlugin({ replacement: { match: /(?<=return\s*\(0,\w{1,3}\.jsxs?\)\(.+!\w{1,3}&&)(\[{0,1}\(0,\w{1,3}.jsxs?\)\(.+?\{.+?\}\)*\]{0,1})/, - // DONT EVER ASK ME HOW THIS WORKS I DONT KNOW EITHER I STOLE IT FROM TYMEN - replace: "[$1, Vencord.Plugins.plugins.Timezones.getTimezonesComponent(e)]" + replace: "[$1, $self.getTimezonesComponent(e)]" }, }, { find: "().customStatusSection", replacement: { - // Inserts the timezone component right below the custom status. match: /user:(\w),nickname:\w,.*?children.*?\(\)\.customStatusSection.*?\}\),/, replace: "$&$self.getProfileTimezonesComponent({user:$1})," } } ], - getProfileTimezonesComponent: (e: any) => { - const user = e.user as User; + getProfileTimezonesComponent: ({ user }: { user: User; }) => { + const { preference } = settings.use(["preference"]); const [timezone, setTimezone] = React.useState(); const [isInEditMode, setIsInEditMode] = React.useState(false); + const [timezones, setTimezones] = React.useState([]); + const forceUpdate = useForceUpdater(); React.useEffect(() => { - getUserTimezone(user.id).then(timezone => setTimezone(timezone)); + useTimezones().then(setTimezones); + getUserTimezone(user.id, preference).then(tz => setTimezone(tz)); - // Rerender every second to stay in sync. - setInterval(forceUpdate, 1000); - }, [user.id]); + // Rerender every 10 seconds to stay in sync. + const interval = setInterval(forceUpdate, 10 * 1000); - if (!Vencord.Settings.plugins.Timezones.showTimezonesInProfile) { + return () => clearInterval(interval); + }, [preference]); + + if (!Vencord.Settings.plugins.Timezones.showTimezonesInProfile) return null; - } return ( - {!isInEditMode && { - if (timezone) { - Toasts.show({ - type: Toasts.Type.MESSAGE, - message: timezone, - id: Toasts.genId() - }); - } - }}>{(timezone) ? getTimeString(timezone) : "No timezone set"}} + {!isInEditMode && + { + if (timezone) { + Toasts.show({ + type: Toasts.Type.MESSAGE, + message: timezone, + id: Toasts.genId() + }); + } + }} + > + {(timezone) ? getTimeString(timezone) : "No timezone set"} + + } {isInEditMode && ( ({ label: tz, value: tz }))} - onChange={value => setTimezone(value)} + value={timezone ? { label: timezone, value: timezone } : undefined} + onChange={value => { setTimezone(value); }} /> )} - { - if (isInEditMode) { - if (timezone) { - DataStore.update(DATASTORE_KEY, (oldValue: TimezoneDB | undefined) => { - oldValue = oldValue || {}; - oldValue[user.id] = timezone as typeof timezones[number]; - return oldValue; - }).then(() => { - Toasts.show({ - type: Toasts.Type.SUCCESS, - message: "Timezone set!", - id: Toasts.genId() - }); - setIsInEditMode(false); - }).catch(err => { - console.error(err); - Toasts.show({ - type: Toasts.Type.FAILURE, - message: "Something went wrong, please try again later.", - id: Toasts.genId() - }); - }); - } else { - setIsInEditMode(false); - } - } else { + if (!isInEditMode) { setIsInEditMode(true); + return; } + + if (!timezone) { + setIsInEditMode(false); + return; + } + + DataStore.update(DATASTORE_KEY, (oldValue: TimezoneDB | undefined) => { + oldValue = oldValue || {}; + oldValue[user.id] = timezone; + return oldValue; + }).then(() => { + Toasts.show({ + type: Toasts.Type.SUCCESS, + message: "Timezone set!", + id: Toasts.genId() + }); + + setIsInEditMode(false); + }).catch(err => { + console.error(err); + Toasts.show({ + type: Toasts.Type.FAILURE, + message: "Something went wrong, please try again later.", + id: Toasts.genId() + }); + }); }} color="var(--primary-330)" height="16" width="16" /> + {isInEditMode && { console.error(err); Toasts.show({ @@ -322,18 +218,20 @@ export default definePlugin({ ); - } - , - - getTimezonesComponent: ({ message, user }: { message: Message, user: any; }) => { - if (Vencord.Settings.plugins.showTimezonesInChat || user || message.author.id === UserStore.getCurrentUser().id) - return null; + }, + getTimezonesComponent: ({ message }: { message: Message; }) => { + const { showTimezonesInChat, preference } = settings.use(["preference", "showTimezonesInChat"]); const [timezone, setTimezone] = React.useState(); React.useEffect(() => { - getUserTimezone(message.author.id).then(timezone => setTimezone(timezone)); - }, [message.author.id]); + if (!showTimezonesInChat) return; + + getUserTimezone(message.author.id, preference).then(tz => setTimezone(tz)); + }, [showTimezonesInChat, preference]); + + if (!showTimezonesInChat || message.author.id === UserStore.getCurrentUser()?.id) + return null; return ( diff --git a/src/plugins/Timezones/settings.tsx b/src/plugins/Timezones/settings.tsx new file mode 100644 index 000000000..feee1a4d3 --- /dev/null +++ b/src/plugins/Timezones/settings.tsx @@ -0,0 +1,41 @@ +import { definePluginSettings } from "@api/settings"; +import { OptionType } from "@utils/types"; + +export enum CustomTimezonePreference { + Never, + Secondary, + Always +} + +export default definePluginSettings({ + preference: { + type: OptionType.SELECT, + description: "When to use custom timezones over TimezoneDB.", + options: [ + { + label: "Never use custom timezones.", + value: CustomTimezonePreference.Never, + }, + { + label: "Prefer custom timezones over TimezoneDB", + value: CustomTimezonePreference.Secondary, + default: true, + }, + { + label: "Always use custom timezones.", + value: CustomTimezonePreference.Always, + }, + ], + default: CustomTimezonePreference.Secondary, + }, + showTimezonesInChat: { + type: OptionType.BOOLEAN, + description: "Show timezones in chat", + default: true, + }, + showTimezonesInProfile: { + type: OptionType.BOOLEAN, + description: "Show timezones in profile", + default: true, + }, +});