This commit is contained in:
Angelos Bouklis 2023-04-10 00:07:12 +03:00 committed by GitHub
parent 58f0184f5c
commit 7a22bb5b07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 153 additions and 191 deletions

View file

@ -23,15 +23,14 @@ import { VENCORD_USER_AGENT } from "@utils/constants";
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import { findLazy } from "@webpack"; import { findLazy } from "@webpack";
export const DATASTORE_KEY = "plugins.Timezones.savedTimezones"; export const DATASTORE_KEY = "plugins.Timezones.savedTimezones";
import type { timezones } from "./all_timezones"; import { CustomTimezonePreference } from "./settings";
export interface TimezoneDB { export interface TimezoneDB {
[userId: string]: typeof timezones[number]; [userId: string]: string;
} }
export const API_URL = "https://timezonedb.catvibers.me"; export const API_URL = "https://timezonedb.catvibers.me";
const Cache: Record<string, typeof timezones[number]> = {}; const Cache: Record<string, string> = {};
export function getTimeString(timezone: string, timestamp = new Date()): string { export function getTimeString(timezone: string, timestamp = new Date()): string {
const locale = PreloadedUserSettings.getCurrentValue().localization.locale.value; 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 // A map of ids and callbacks that should be triggered on fetch
const requestQueue: Record<string, ((timezone: typeof timezones[number]) => void)[]> = {}; const requestQueue: Record<string, ((timezone: string) => void)[]> = {};
async function bulkFetchTimezones(ids: string[]): Promise<TimezoneDB | undefined> { async function bulkFetchTimezones(ids: string[]): Promise<TimezoneDB | undefined> {
@ -58,7 +57,7 @@ async function bulkFetchTimezones(ids: string[]): Promise<TimezoneDB | undefined
return await req.json() return await req.json()
.then((res: { [userId: string]: { timezoneId: string; } | null; }) => { .then((res: { [userId: string]: { timezoneId: string; } | null; }) => {
const tzs = (Object.keys(res).map(userId => { 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 }), {}); }).filter(Boolean) as TimezoneDB[]).reduce((acc, cur) => ({ ...acc, ...cur }), {});
Object.assign(Cache, tzs); Object.assign(Cache, tzs);
@ -87,11 +86,20 @@ const bulkFetch = debounce(async () => {
} }
}); });
export function getUserTimezone(discordID: string): Promise<typeof timezones[number] | undefined> { export function getUserTimezone(discordID: string, strategy: CustomTimezonePreference):
Promise<string | undefined> {
return new Promise(res => { return new Promise(res => {
const timezone = (DataStore.get(DATASTORE_KEY) as Promise<TimezoneDB | undefined>).then(tzs => tzs?.[discordID]); const timezone = (DataStore.get(DATASTORE_KEY) as Promise<TimezoneDB | undefined>).then(tzs => tzs?.[discordID]);
timezone.then(tz => { timezone.then(tz => {
if (strategy == CustomTimezonePreference.Always) {
if (tz) res(tz); if (tz) res(tz);
else res(undefined);
return;
}
if (tz && strategy == CustomTimezonePreference.Secondary)
res(tz);
else { else {
if (discordID in Cache) res(Cache[discordID]); if (discordID in Cache) res(Cache[discordID]);
else if (discordID in requestQueue) requestQueue[discordID].push(res); else if (discordID in requestQueue) requestQueue[discordID].push(res);
@ -104,3 +112,18 @@ export function getUserTimezone(discordID: string): Promise<typeof timezones[num
}); });
}); });
} }
const gist = "e321f856f98676505efb90aad82feff1";
const revision = "91034ee32eff93a7cb62d10702f6b1d01e0309e6";
const timezonesLink = `https://gist.githubusercontent.com/ArjixWasTaken/${gist}/raw/${revision}/timezones.json`;
export const getAllTimezones = async (): Promise<string[]> => {
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());
};

View file

@ -17,158 +17,41 @@
*/ */
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { Devs, VENCORD_USER_AGENT } from "@utils/constants"; import { Devs, VENCORD_USER_AGENT } from "@utils/constants";
import { classes, useForceUpdater } from "@utils/misc"; import { classes, makeLazy, useForceUpdater } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { React, SearchableSelect, Text, Toasts, UserStore } from "@webpack/common"; import { React, SearchableSelect, Text, Toasts, UserStore } from "@webpack/common";
import { Message, User } from "discord-types/general"; 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 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 DeleteIcon = findByCodeLazy("M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z");
const classNames = findByPropsLazy("customStatusSection"); const classNames = findByPropsLazy("customStatusSection");
import { timezones } from "./all_timezones"; import { API_URL, DATASTORE_KEY, getAllTimezones, getTimeString, getUserTimezone, TimezoneDB } from "./Utils";
import { API_URL, DATASTORE_KEY, getTimeString, getUserTimezone, TimezoneDB } from "./Utils";
const styles = findByPropsLazy("timestampInline"); const styles = findByPropsLazy("timestampInline");
const useTimezones = makeLazy(getAllTimezones);
export default definePlugin({ export default definePlugin({
name: "Timezones", settings,
description: "Shows the timezones of users",
name: "User Timezones",
description: "Allows you to see and set the timezones of other users.",
authors: [Devs.mantikafasi, Devs.Arjix], 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: () => { settingsAboutComponent: () => {
const href = `${API_URL}?client_mod=${encodeURIComponent(VENCORD_USER_AGENT)}`; const href = `${API_URL}?client_mod=${encodeURIComponent(VENCORD_USER_AGENT)}`;
return ( return (
<Text variant="text-md/normal"> <Text variant="text-md/normal">
A plugin that displays the local time for specific users using their timezone. <br /> A plugin that displays the local time for specific users using their timezone. <br />
By default the timezone will be fetched from the <a href={href} onClick={() => open(href)}>TimezoneDB</a> (if available), <br /> Timezones can either be set manually or fetched automatically from the <a href={href}>TimezoneDB</a>
but you can override that with a custom timezone.
</Text> </Text>
); );
}, },
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: [ patches: [
{ {
@ -176,37 +59,39 @@ export default definePlugin({
replacement: { replacement: {
match: /(?<=return\s*\(0,\w{1,3}\.jsxs?\)\(.+!\w{1,3}&&)(\[{0,1}\(0,\w{1,3}.jsxs?\)\(.+?\{.+?\}\)*\]{0,1})/, 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, $self.getTimezonesComponent(e)]"
replace: "[$1, Vencord.Plugins.plugins.Timezones.getTimezonesComponent(e)]"
}, },
}, },
{ {
find: "().customStatusSection", find: "().customStatusSection",
replacement: { replacement: {
// Inserts the timezone component right below the custom status.
match: /user:(\w),nickname:\w,.*?children.*?\(\)\.customStatusSection.*?\}\),/, match: /user:(\w),nickname:\w,.*?children.*?\(\)\.customStatusSection.*?\}\),/,
replace: "$&$self.getProfileTimezonesComponent({user:$1})," replace: "$&$self.getProfileTimezonesComponent({user:$1}),"
} }
} }
], ],
getProfileTimezonesComponent: (e: any) => { getProfileTimezonesComponent: ({ user }: { user: User; }) => {
const user = e.user as User; const { preference } = settings.use(["preference"]);
const [timezone, setTimezone] = React.useState<string | undefined>(); const [timezone, setTimezone] = React.useState<string | undefined>();
const [isInEditMode, setIsInEditMode] = React.useState(false); const [isInEditMode, setIsInEditMode] = React.useState(false);
const [timezones, setTimezones] = React.useState<string[]>([]);
const forceUpdate = useForceUpdater(); const forceUpdate = useForceUpdater();
React.useEffect(() => { 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. // Rerender every 10 seconds to stay in sync.
setInterval(forceUpdate, 1000); const interval = setInterval(forceUpdate, 10 * 1000);
}, [user.id]);
if (!Vencord.Settings.plugins.Timezones.showTimezonesInProfile) { return () => clearInterval(interval);
}, [preference]);
if (!Vencord.Settings.plugins.Timezones.showTimezonesInProfile)
return null; return null;
}
return ( return (
<Text variant="text-sm/normal" className={classNames.customStatusSection} <Text variant="text-sm/normal" className={classNames.customStatusSection}
@ -220,7 +105,10 @@ export default definePlugin({
} : {}) } : {})
}} }}
> >
{!isInEditMode && <span style={{ fontSize: "1.2em", cursor: (timezone ? "pointer" : "") }} onClick={() => { {!isInEditMode &&
<span
style={{ fontSize: "1.2em", cursor: (timezone ? "pointer" : "") }}
onClick={() => {
if (timezone) { if (timezone) {
Toasts.show({ Toasts.show({
type: Toasts.Type.MESSAGE, type: Toasts.Type.MESSAGE,
@ -228,19 +116,23 @@ export default definePlugin({
id: Toasts.genId() id: Toasts.genId()
}); });
} }
}}>{(timezone) ? getTimeString(timezone) : "No timezone set"}</span>} }}
>
{(timezone) ? getTimeString(timezone) : "No timezone set"}
</span>
}
{isInEditMode && ( {isInEditMode && (
<span style={{ width: "90%" }}> <span style={{ width: "90%" }}>
<SearchableSelect <SearchableSelect
placeholder="Pick a timezone" placeholder="Pick a timezone"
options={timezones.map(tz => ({ label: tz, value: tz }))} options={timezones.map(tz => ({ label: tz, value: tz }))}
onChange={value => setTimezone(value)} value={timezone ? { label: timezone, value: timezone } : undefined}
onChange={value => { setTimezone(value); }}
/> />
</span> </span>
)} )}
<span style={ <span style={
isInEditMode ? { isInEditMode ? {
display: "flex", display: "flex",
@ -257,11 +149,19 @@ export default definePlugin({
<EditIcon <EditIcon
style={{ cursor: "pointer", padding: "2px", border: "2px solid grey", borderRadius: "50px" }} style={{ cursor: "pointer", padding: "2px", border: "2px solid grey", borderRadius: "50px" }}
onClick={() => { onClick={() => {
if (isInEditMode) { if (!isInEditMode) {
if (timezone) { setIsInEditMode(true);
return;
}
if (!timezone) {
setIsInEditMode(false);
return;
}
DataStore.update(DATASTORE_KEY, (oldValue: TimezoneDB | undefined) => { DataStore.update(DATASTORE_KEY, (oldValue: TimezoneDB | undefined) => {
oldValue = oldValue || {}; oldValue = oldValue || {};
oldValue[user.id] = timezone as typeof timezones[number]; oldValue[user.id] = timezone;
return oldValue; return oldValue;
}).then(() => { }).then(() => {
Toasts.show({ Toasts.show({
@ -269,6 +169,7 @@ export default definePlugin({
message: "Timezone set!", message: "Timezone set!",
id: Toasts.genId() id: Toasts.genId()
}); });
setIsInEditMode(false); setIsInEditMode(false);
}).catch(err => { }).catch(err => {
console.error(err); console.error(err);
@ -278,17 +179,12 @@ export default definePlugin({
id: Toasts.genId() id: Toasts.genId()
}); });
}); });
} else {
setIsInEditMode(false);
}
} else {
setIsInEditMode(true);
}
}} }}
color="var(--primary-330)" color="var(--primary-330)"
height="16" height="16"
width="16" width="16"
/> />
{isInEditMode && {isInEditMode &&
<DeleteIcon <DeleteIcon
style={{ cursor: "pointer", padding: "2px", border: "2px solid grey", borderRadius: "50px" }} style={{ cursor: "pointer", padding: "2px", border: "2px solid grey", borderRadius: "50px" }}
@ -304,7 +200,7 @@ export default definePlugin({
id: Toasts.genId() id: Toasts.genId()
}); });
setIsInEditMode(false); setIsInEditMode(false);
setTimezone(await getUserTimezone(user.id)); setTimezone(await getUserTimezone(user.id, preference));
}).catch(err => { }).catch(err => {
console.error(err); console.error(err);
Toasts.show({ Toasts.show({
@ -322,18 +218,20 @@ export default definePlugin({
</span> </span>
</Text > </Text >
); );
} },
,
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<string | undefined>(); const [timezone, setTimezone] = React.useState<string | undefined>();
React.useEffect(() => { React.useEffect(() => {
getUserTimezone(message.author.id).then(timezone => setTimezone(timezone)); if (!showTimezonesInChat) return;
}, [message.author.id]);
getUserTimezone(message.author.id, preference).then(tz => setTimezone(tz));
}, [showTimezonesInChat, preference]);
if (!showTimezonesInChat || message.author.id === UserStore.getCurrentUser()?.id)
return null;
return ( return (
<span className={classes(styles.timestampInline, styles.timestamp)}> <span className={classes(styles.timestampInline, styles.timestamp)}>

View file

@ -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,
},
});