From 1b145a5695c40d1c9cdb2a47929c9875a47ec536 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Sun, 23 Jun 2024 20:42:04 -0700 Subject: [PATCH] use components from vc-timezones --- src/plugins/timezones/api.ts | 13 +- src/plugins/timezones/cache.ts | 25 ++- src/plugins/timezones/components.tsx | 88 ++++++++++ src/plugins/timezones/index.tsx | 230 +++++---------------------- src/plugins/timezones/settings.tsx | 18 ++- src/plugins/timezones/styles.css | 43 +++++ src/plugins/timezones/utils.ts | 24 ++- 7 files changed, 229 insertions(+), 212 deletions(-) create mode 100644 src/plugins/timezones/components.tsx create mode 100644 src/plugins/timezones/styles.css diff --git a/src/plugins/timezones/api.ts b/src/plugins/timezones/api.ts index 321c14ea1..c9feafe3e 100644 --- a/src/plugins/timezones/api.ts +++ b/src/plugins/timezones/api.ts @@ -1,5 +1,12 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent"; import { Logger } from "@utils/Logger"; + import settings from "./settings"; export type Snowflake = string; @@ -22,10 +29,10 @@ export async function fetchTimezonesBulk(ids: Snowflake[]): Promise = {}; + const parsed: Record = {}; for (const userId of Object.keys(json)) { - parsed[userId] = json[userId].timezoneId; + parsed[userId] = json[userId]?.timezoneId ?? null; } return parsed; @@ -48,7 +55,7 @@ export async function fetchTimezone(userId: Snowflake): Promise 3) { new Logger("Timezones").warn("Bulk queue fetch ran out of retries!"); return; } - const callbacks = existingCallbacks ?? BulkFetchQueue; - if (!existingCallbacks) BulkFetchQueue = {}; + const callbacks = retryQueue ?? BulkFetchQueue; + if (!retryQueue) BulkFetchQueue = {}; const timezones = await fetchTimezonesBulk(Object.keys(callbacks)); if (!timezones) { @@ -47,9 +54,10 @@ export async function getUserTimezone( immediate: boolean = false, force: boolean = false, ): Promise { - const overwrites = settings.store.timezoneOverwrites as TimezoneOverwrites; + const overwrites = settings.store.timezoneOverwrites ?? {} as TimezoneOverwrites; + const useApi = settings.store.enableApi; const overwrite = overwrites[userId]; - if (overwrite) return overwrite ?? null; + if (overwrite || !useApi) return overwrite ?? null; if (!force) { const cachedTimezone = await DataStore.get(userId, TimezoneCache); @@ -61,9 +69,10 @@ export async function getUserTimezone( if (immediate) { let tries = 3; while (tries-- > 0) { - let timezone = await fetchTimezone(userId); + const timezone = await fetchTimezone(userId); if (timezone === undefined) continue; + DataStore.set(userId, timezone, TimezoneCache).catch(_ => _); return timezone; } diff --git a/src/plugins/timezones/components.tsx b/src/plugins/timezones/components.tsx new file mode 100644 index 000000000..aa27d3f20 --- /dev/null +++ b/src/plugins/timezones/components.tsx @@ -0,0 +1,88 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { ErrorBoundary } from "@components/index"; +import { findByPropsLazy } from "@webpack"; +import { React, Tooltip, useEffect, useState } from "@webpack/common"; + +import { Snowflake } from "./api"; +import { getUserTimezone } from "./cache"; +import { formatTimestamp } from "./utils"; + +// Based on Syncxv's vc-timezones user plugin // + +const messageClasses = findByPropsLazy("timestamp", "compact", "contentOnly"); + +interface LocalTimestampProps { + userId: Snowflake; + timestamp?: Date; + type: "message" | "profile"; +} + +export function LocalTimestamp(props: LocalTimestampProps): JSX.Element { + return + + ; +} + +function LocalTimestampInner(props: LocalTimestampProps): JSX.Element | null { + const [timezone, setTimezone] = useState(); + const [timestamp, setTimestamp] = useState(props.timestamp ?? Date.now()); + + useEffect(() => { + if (!timezone) { + getUserTimezone(props.userId, props.type === "profile").then(setTimezone); + return; + } + + let timer: NodeJS.Timeout; + + if (props.type === "profile") { + setTimestamp(Date.now()); + + const now = new Date(); + const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds(); + + timer = setTimeout(() => setTimestamp(Date.now()), delay); + } + + return () => timer && clearTimeout(timer); + }, [timezone, timestamp]); + + if (!timezone) return null; + + const longTime = formatTimestamp(timezone, timestamp, true); + const shortTime = formatTimestamp(timezone, timestamp, false); + const shortTimeFormatted = props.type === "message" + ? `• ${shortTime}` + : shortTime; + + const classes = props.type === "message" + ? `timezone-message-item ${messageClasses.timestamp}` + : "timezone-profile-item"; + + + return <> + + {toolTipProps => <> + + {shortTimeFormatted} + + } + + ; +} diff --git a/src/plugins/timezones/index.tsx b/src/plugins/timezones/index.tsx index 7fdac13aa..17ffe9cf7 100644 --- a/src/plugins/timezones/index.tsx +++ b/src/plugins/timezones/index.tsx @@ -6,22 +6,15 @@ import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { SearchableSelect, Text, Toasts, useEffect, UserStore, useState } from "@webpack/common"; import { Message, User } from "discord-types/general"; +import { LocalTimestamp } from "./components"; import settings, { SettingsComponent } from "./settings"; -import { CogWheel, DeleteIcon } from "@components/Icons"; -import { classes } from "@utils/misc"; -import { useForceUpdater } from "@utils/react"; - -const classNames = findByPropsLazy("customStatusSection"); -const styles = findByPropsLazy("timestampInline"); export default definePlugin({ name: "Timezones", description: "Set and display the local times of you and other users via TimezoneDB", - authors: [Devs.rushii, Devs.mantikafasi, Devs.Aria, Devs.Arjix], + authors: [Devs.rushii, Devs.Aria, Devs.mantikafasi, Devs.Arjix], settings, settingsAboutComponent: SettingsComponent, @@ -44,197 +37,50 @@ export default definePlugin({ // replace: "return [$1, $self.getProfileTimezonesComponent(arguments[0])] }", // }, // }, - { - // TODO: fix this - // thank you https://github.com/Syncxv/vc-timezones/blob/master/index.tsx for saving me from painful work - find: ".badgesContainer,{", + // { + // // TODO: fix this + // // thank you https://github.com/Syncxv/vc-timezones/blob/master/index.tsx for saving me from painful work + // find: ".badgesContainer,{", + // replacement: { + // match: /id:\(0,\i\.getMessageTimestampId\)\(\i\),timestamp.{1,50}}\),/, + // replace: "$&,$self.getTimezonesComponent(arguments[0]),", + // }, + // }, + + // Based on Syncxv's vc-timezones user plugin // + ...[".NITRO_BANNER,", "=!1,canUsePremiumCustomization:"].map(find => ({ + find, replacement: { - match: /id:\(0,\i\.getMessageTimestampId\)\(\i\),timestamp.{1,50}}\),/, - replace: "$&,$self.getTimezonesComponent(arguments[0]),", + match: /(?<=hasProfileEffect.+?)children:\[/, + replace: "$&$self.renderProfileTimezone(arguments[0]),", + }, + })), + { + find: "\"Message Username\"", + replacement: { + // thanks https://github.com/Syncxv/vc-timezones/pull/4 + match: /(?<=isVisibleOnlyOnHover.+?)id:.{1,11},timestamp.{1,50}}\),/, + replace: "$&,$self.renderMessageTimezone(arguments[0]),", }, }, ], - // TODO: make this not ugly (port vc-timezones plugin) - getProfileTimezonesComponent: ({ user }: { user: User; }) => { - const { displayInProfile } = settings.use(["displayInProfile"]); + renderProfileTimezone: (props?: { user?: User; }) => { + if (!settings.store.displayInProfile || !props?.user?.id) return null; - const [timezone, setTimezone] = useState(); - const [isInEditMode, setIsInEditMode] = useState(false); - const [timezones, setTimezones] = useState([]); - - const forceUpdate = useForceUpdater(); - - useEffect(() => { - useTimezones().then(setTimezones); - getUserTimezone(user.id, preference).then(tz => setTimezone(tz)); - - // Rerender every 10 seconds to stay in sync. - const interval = setInterval(forceUpdate, 10 * 1000); - - return () => clearInterval(interval); - }, [preference]); - - if (!showInProfile) - return null; - - return ( - - {!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 }))} - value={timezone ? { label: timezone, value: timezone } : undefined} - onChange={value => { - setTimezone(value); - }} - /> - - )} - - - { - 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 && - { - DataStore.update(DATASTORE_KEY, (oldValue: TimezoneDB | undefined) => { - oldValue = oldValue || {}; - delete oldValue[user.id]; - return oldValue; - }).then(async () => { - Toasts.show({ - type: Toasts.Type.SUCCESS, - message: "Timezone removed!", - id: Toasts.genId(), - }); - setIsInEditMode(false); - setTimezone(await getUserTimezone(user.id, preference)); - }).catch(err => { - console.error(err); - Toasts.show({ - type: Toasts.Type.FAILURE, - message: "Something went wrong, please try again later.", - id: Toasts.genId(), - }); - }); - }} - color="var(--red-360)" - height="16" - width="16" - /> - } - - - ); + return ; }, - getTimezonesComponent: ({ message }: { message: Message; }) => { - console.log(message); + renderMessageTimezone: (props?: { message?: Message; }) => { + if (!settings.store.displayInChat || !props?.message) return null; - const { showInChat, preference } = settings.use(["preference", "showInChat"]); - const [timeString, setTimeString] = useState(); - - if (!showInChat || message.author.id === UserStore.getCurrentUser()?.id) - return null; - - useEffect(() => { - if (!showInChat) return; - - (async function() { - const timezone = await getUserTimezone(message.author.id, preference); - const timestamp = (message.timestamp as unknown) as Date; // discord-types is outdated - setTimeString(timezone && "• " + getTimeString(timezone, timestamp)); - })(); - }, [showInChat, preference]); - - return <> - - {timeString} - - ; + return ; }, }); diff --git a/src/plugins/timezones/settings.tsx b/src/plugins/timezones/settings.tsx index 92161694a..68293be67 100644 --- a/src/plugins/timezones/settings.tsx +++ b/src/plugins/timezones/settings.tsx @@ -17,9 +17,10 @@ */ import { definePluginSettings } from "@api/Settings"; -import { OptionType } from "@utils/types"; -import { Text } from "@webpack/common"; import { Link } from "@components/Link"; +import { IPluginOptionComponentProps, OptionType } from "@utils/types"; +import { Text } from "@webpack/common"; + import { Snowflake } from "./api"; export type TimezoneOverwrites = Record; @@ -48,8 +49,13 @@ const settings = definePluginSettings({ timezoneOverwrites: { type: OptionType.COMPONENT, description: "Local overwrites for users' timezones", - component: () => <> // TODO: settings component to manage local overwrites, - } + component: props => <> + + , + }, }); export default settings; @@ -69,3 +75,7 @@ export function SettingsComponent(): JSX.Element { ; } + +function TimezoneOverwritesSetting(props: IPluginOptionComponentProps): JSX.Element { + return <>; +} diff --git a/src/plugins/timezones/styles.css b/src/plugins/timezones/styles.css new file mode 100644 index 000000000..f0f961a39 --- /dev/null +++ b/src/plugins/timezones/styles.css @@ -0,0 +1,43 @@ +/** Based on Syncxv's vc-timezones user plugin **/ + +.timezone-profile-item { + position: absolute; + right: 0; + bottom: 0; + margin: 28px 16px 4px; + background: var(--profile-body-background-color, var(--background-primary)); + border-radius: 4px; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + color: var(--text-normal); +} + +[class*="topSection"] .timezone-profile-item { + margin: 16px; +} + +.timezone-message-item { + margin-left: 4px; +} + +.vc-timezone-modal-header { + justify-content: space-between; + align-content: center; +} + +.vc-timezone-modal-header h1 { + margin: 0; +} + +.vc-timezone-modal-content { + padding: 1em; +} + +.vc-timezone-modal-footer { + gap: 16px; +} + +.timezone-tooltip { + max-width: none !important; + white-space: nowrap +} diff --git a/src/plugins/timezones/utils.ts b/src/plugins/timezones/utils.ts index acd2175a7..1064e279e 100644 --- a/src/plugins/timezones/utils.ts +++ b/src/plugins/timezones/utils.ts @@ -4,19 +4,33 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { findStoreLazy } from "@webpack"; -import { Logger } from "@utils/Logger"; import { makeLazy } from "@utils/lazy"; +import { Logger } from "@utils/Logger"; +import { findStoreLazy } from "@webpack"; const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore"); const TIMEZONE_LIST = "https://gist.githubusercontent.com/ArjixWasTaken/e321f856f98676505efb90aad82feff1/raw/91034ee32eff93a7cb62d10702f6b1d01e0309e6/timezones.json"; -export function formatTimestamp(timezone: string, timestamp: Date = new Date()): string | undefined { +export function formatTimestamp( + timezone: string, + timestamp: number | Date | undefined, + long: boolean, +): string | undefined { try { const locale = UserSettingsProtoStore.settings.localization.locale.value; + const options: Intl.DateTimeFormatOptions = !long + ? { hour: "numeric", minute: "numeric" } + : { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + }; + const formatter = new Intl.DateTimeFormat(locale, { - hour: "numeric", - minute: "numeric", + ...options, timeZone: timezone, });