mirror of
https://github.com/Vendicated/Vencord.git
synced 2025-02-24 15:35:11 +00:00
use components from vc-timezones
This commit is contained in:
parent
925629f85d
commit
1b145a5695
7 changed files with 229 additions and 212 deletions
|
@ -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<Record<Snowf
|
|||
const json: BulkFetchResponse = await req.json();
|
||||
if ("error" in json) throw "API Error: " + json.error;
|
||||
|
||||
let parsed: Record<Snowflake, string | null> = {};
|
||||
const parsed: Record<Snowflake, string | null> = {};
|
||||
|
||||
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<string | null |
|
|||
const json: UserFetchResponse = await req.json();
|
||||
|
||||
if ("error" in json) {
|
||||
if (json.error == "not_found") return null;
|
||||
if (json.error === "not_found") return null;
|
||||
|
||||
throw "API Error: " + json.error;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { createStore } from "@api/DataStore";
|
||||
import { fetchTimezone, fetchTimezonesBulk, Snowflake } from "./api";
|
||||
import settings, { TimezoneOverwrites } from "./settings";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
||||
import { fetchTimezone, fetchTimezonesBulk, Snowflake } from "./api";
|
||||
import settings, { TimezoneOverwrites } from "./settings";
|
||||
|
||||
// TODO: cache invalidation
|
||||
const TimezoneCache = createStore("UsersTimezoneCache", "TimezoneCache");
|
||||
|
||||
|
@ -16,14 +23,14 @@ let BulkFetchQueue: BulkFetchCallbacks = {};
|
|||
// Executes all queued requests and calls their callbacks
|
||||
const debounceProcessBulkQueue = debounce(processBulkQueue, 750);
|
||||
|
||||
async function processBulkQueue(attempt: number = 1, existingCallbacks?: BulkFetchCallbacks) {
|
||||
async function processBulkQueue(attempt: number = 1, retryQueue?: BulkFetchCallbacks) {
|
||||
if (attempt > 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<string | null> {
|
||||
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<string | null>(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;
|
||||
}
|
||||
|
||||
|
|
88
src/plugins/timezones/components.tsx
Normal file
88
src/plugins/timezones/components.tsx
Normal file
|
@ -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 <ErrorBoundary noop={true} wrappedProps={props}>
|
||||
<LocalTimestampInner {...props} />
|
||||
</ErrorBoundary>;
|
||||
}
|
||||
|
||||
function LocalTimestampInner(props: LocalTimestampProps): JSX.Element | null {
|
||||
const [timezone, setTimezone] = useState<string | null>();
|
||||
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 <>
|
||||
<Tooltip
|
||||
position="top"
|
||||
// @ts-ignore
|
||||
delay={750}
|
||||
allowOverflow={false}
|
||||
spacing={8}
|
||||
hideOnClick={true}
|
||||
tooltipClassName="timezone-tooltip"
|
||||
text={longTime}
|
||||
>
|
||||
{toolTipProps => <>
|
||||
<span className={classes} {...toolTipProps}>
|
||||
{shortTimeFormatted}
|
||||
</span>
|
||||
</>}
|
||||
</Tooltip>
|
||||
</>;
|
||||
}
|
|
@ -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<string | undefined>();
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
const [timezones, setTimezones] = useState<string[]>([]);
|
||||
|
||||
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 (
|
||||
<Text variant="text-sm/normal" className={classNames.customStatusSection}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
...(isInEditMode ? {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
} : {}),
|
||||
}}
|
||||
>
|
||||
{!isInEditMode &&
|
||||
<span
|
||||
style={{ fontSize: "1.2em", cursor: (timezone ? "pointer" : "") }}
|
||||
onClick={() => {
|
||||
if (timezone) {
|
||||
Toasts.show({
|
||||
type: Toasts.Type.MESSAGE,
|
||||
message: timezone,
|
||||
id: Toasts.genId(),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(timezone) ? getTimeString(timezone) : "No timezone set"}
|
||||
</span>
|
||||
}
|
||||
|
||||
{isInEditMode && (
|
||||
<span style={{ width: "90%" }}>
|
||||
<SearchableSelect
|
||||
placeholder="Pick a timezone"
|
||||
options={timezones.map(tz => ({ label: tz, value: tz }))}
|
||||
value={timezone ? { label: timezone, value: timezone } : undefined}
|
||||
onChange={value => {
|
||||
setTimezone(value);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span style={
|
||||
isInEditMode ? {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-around",
|
||||
width: "60%",
|
||||
marginTop: "5%",
|
||||
} : {
|
||||
marginLeft: "2%",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<CogWheel
|
||||
style={{ cursor: "pointer", padding: "2px", border: "2px solid grey", borderRadius: "50px" }}
|
||||
onClick={() => {
|
||||
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 &&
|
||||
<DeleteIcon
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
padding: "2px",
|
||||
border: "2px solid grey",
|
||||
borderRadius: "50px",
|
||||
}}
|
||||
onClick={() => {
|
||||
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"
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
</Text>
|
||||
);
|
||||
return <LocalTimestamp
|
||||
userId={props.user.id}
|
||||
type="profile"
|
||||
/>;
|
||||
},
|
||||
|
||||
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<string>();
|
||||
|
||||
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 <>
|
||||
<span className={classes(styles.timestampInline, styles.timestamp)}>
|
||||
{timeString}
|
||||
</span>
|
||||
</>;
|
||||
return <LocalTimestamp
|
||||
userId={props.message.author.id}
|
||||
timestamp={props.message.timestamp as unknown as Date}
|
||||
type="message"
|
||||
/>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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<Snowflake, string>;
|
||||
|
@ -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 => <>
|
||||
<TimezoneOverwritesSetting
|
||||
setValue={props.setValue}
|
||||
setError={props.setError}
|
||||
option={props.option} />
|
||||
</>,
|
||||
},
|
||||
});
|
||||
|
||||
export default settings;
|
||||
|
@ -69,3 +75,7 @@ export function SettingsComponent(): JSX.Element {
|
|||
</Text>
|
||||
</>;
|
||||
}
|
||||
|
||||
function TimezoneOverwritesSetting(props: IPluginOptionComponentProps): JSX.Element {
|
||||
return <></>;
|
||||
}
|
||||
|
|
43
src/plugins/timezones/styles.css
Normal file
43
src/plugins/timezones/styles.css
Normal file
|
@ -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
|
||||
}
|
|
@ -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 formatter = new Intl.DateTimeFormat(locale, {
|
||||
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, {
|
||||
...options,
|
||||
timeZone: timezone,
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue