mirror of
https://github.com/RobinRMC/VencordPlus.git
synced 2025-05-10 17:35:39 +02:00
Add TimezoneDB
This commit is contained in:
parent
a500375e55
commit
03822eb59e
3 changed files with 422 additions and 0 deletions
120
src/plusplugins/Timezones/Utils.ts
Normal file
120
src/plusplugins/Timezones/Utils.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { VENCORD_USER_AGENT } from "@utils/constants";
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
export const DATASTORE_KEY = "plugins.Timezones.savedTimezones";
|
||||
|
||||
import { CustomTimezonePreference } from "./settings";
|
||||
|
||||
export interface TimezoneDB {
|
||||
[userId: string]: string;
|
||||
}
|
||||
|
||||
export const API_URL = "https://timezonedb.catvibers.me";
|
||||
const Cache: Record<string, string> = {};
|
||||
|
||||
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
||||
|
||||
export function getTimeString(timezone: string, timestamp = new Date()): string {
|
||||
try {
|
||||
const locale = UserSettingsProtoStore.settings.localization.locale.value;
|
||||
return new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "numeric", timeZone: timezone }).format(timestamp); // we hate javascript
|
||||
} catch (e) {
|
||||
return "Error"; // incase it gets invalid timezone from api, probably not gonna happen but if it does this will prevent discord from crashing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// A map of ids and callbacks that should be triggered on fetch
|
||||
const requestQueue: Record<string, ((timezone: string) => void)[]> = {};
|
||||
|
||||
|
||||
async function bulkFetchTimezones(ids: string[]): Promise<TimezoneDB | undefined> {
|
||||
try {
|
||||
const req = await fetch(`${API_URL}/api/user/bulk`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-User-Agent": VENCORD_USER_AGENT
|
||||
},
|
||||
body: JSON.stringify(ids),
|
||||
});
|
||||
|
||||
return await req.json()
|
||||
.then((res: { [userId: string]: { timezoneId: string; } | null; }) => {
|
||||
const tzs = (Object.keys(res).map(userId => {
|
||||
return res[userId] && { [userId]: res[userId]!.timezoneId };
|
||||
}).filter(Boolean) as TimezoneDB[]).reduce((acc, cur) => ({ ...acc, ...cur }), {});
|
||||
|
||||
Object.assign(Cache, tzs);
|
||||
return tzs;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Timezone fetching failed: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Executes all queued requests and calls their callbacks
|
||||
const bulkFetch = debounce(async () => {
|
||||
const ids = Object.keys(requestQueue);
|
||||
const timezones = await bulkFetchTimezones(ids);
|
||||
if (!timezones) {
|
||||
// retry after 15 seconds
|
||||
setTimeout(bulkFetch, 15000);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const id of ids) {
|
||||
// Call all callbacks for the id
|
||||
requestQueue[id].forEach(c => c(timezones[id]));
|
||||
delete requestQueue[id];
|
||||
}
|
||||
});
|
||||
|
||||
export function getUserTimezone(discordID: string, strategy: CustomTimezonePreference):
|
||||
Promise<string | undefined> {
|
||||
|
||||
return new Promise(res => {
|
||||
const timezone = (DataStore.get(DATASTORE_KEY) as Promise<TimezoneDB | undefined>).then(tzs => tzs?.[discordID]);
|
||||
timezone.then(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);
|
||||
// If not already added, then add it and call the debounced function to make sure the request gets executed
|
||||
else {
|
||||
requestQueue[discordID] = [res];
|
||||
bulkFetch();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
return Intl.supportedValuesOf("timeZone");
|
||||
} catch { }
|
||||
}
|
||||
|
||||
return await fetch(timezonesLink).then(tzs => tzs.json());
|
||||
};
|
243
src/plusplugins/Timezones/index.tsx
Normal file
243
src/plusplugins/Timezones/index.tsx
Normal file
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { Devs, VENCORD_USER_AGENT } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { React, SearchableSelect, Text, Toasts, UserStore } from "@webpack/common";
|
||||
import { Message, User } from "discord-types/general";
|
||||
|
||||
import settings from "./settings";
|
||||
const classNames = findByPropsLazy("customStatusSection");
|
||||
|
||||
|
||||
import { CogWheel, DeleteIcon } from "@components/Icons";
|
||||
import { makeLazy } from "@utils/lazy";
|
||||
import { classes } from "@utils/misc";
|
||||
import { useForceUpdater } from "@utils/react";
|
||||
|
||||
import { API_URL, DATASTORE_KEY, getAllTimezones, getTimeString, getUserTimezone, TimezoneDB } from "./Utils";
|
||||
const styles = findByPropsLazy("timestampInline");
|
||||
|
||||
const useTimezones = makeLazy(getAllTimezones);
|
||||
|
||||
export default definePlugin({
|
||||
settings,
|
||||
|
||||
name: "Timezones",
|
||||
description: "Allows you to see and set the timezones of other users.",
|
||||
authors: [Devs.mantikafasi, Devs.Arjix],
|
||||
|
||||
commands: [
|
||||
{
|
||||
name: "timezone",
|
||||
description: "Sends link to a website that shows timezone string, useful if you want to know your friends timezone",
|
||||
execute: () => {
|
||||
return { content: "https://gh.lewisakura.moe/timezone/" };
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
settingsAboutComponent: () => {
|
||||
const href = `${API_URL}?client_mod=${encodeURIComponent(VENCORD_USER_AGENT)}`;
|
||||
return (
|
||||
<Text variant="text-md/normal">
|
||||
A plugin that displays the local time for specific users using their timezone. <br />
|
||||
Timezones can either be set manually or fetched automatically from the <a href={href}>TimezoneDB</a>
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "copyMetaData:\"User Tag\"",
|
||||
replacement: {
|
||||
|
||||
match: /return(\(0,.\.jsx\)\(.\.default,{className:.+?}\)]}\)}\))/,
|
||||
replace: "return [$1, $self.getProfileTimezonesComponent(arguments[0])]"
|
||||
},
|
||||
},
|
||||
{
|
||||
// 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]),"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
getProfileTimezonesComponent: ({ user }: { user: User; }) => {
|
||||
const { preference, showTimezonesInProfile } = settings.use(["preference", "showTimezonesInProfile"]);
|
||||
|
||||
const [timezone, setTimezone] = React.useState<string | undefined>();
|
||||
const [isInEditMode, setIsInEditMode] = React.useState(false);
|
||||
const [timezones, setTimezones] = React.useState<string[]>([]);
|
||||
|
||||
const forceUpdate = useForceUpdater();
|
||||
|
||||
React.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 (!showTimezonesInProfile)
|
||||
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 >
|
||||
);
|
||||
},
|
||||
|
||||
getTimezonesComponent: ({ message }: { message: Message; }) => {
|
||||
console.log(message);
|
||||
|
||||
const { showTimezonesInChat, preference } = settings.use(["preference", "showTimezonesInChat"]);
|
||||
const [timezone, setTimezone] = React.useState<string | undefined>();
|
||||
|
||||
React.useEffect(() => {
|
||||
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 (
|
||||
<span className={classes(styles.timestampInline, styles.timestamp)}>
|
||||
{timezone && "• " + getTimeString(timezone, message.timestamp.toDate())}
|
||||
</span>);
|
||||
}
|
||||
});
|
59
src/plusplugins/Timezones/settings.tsx
Normal file
59
src/plusplugins/Timezones/settings.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue