Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Plugin: CustomMessageTimestamps #3189

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/plugins/customMessageTimestamps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# CustomMessageTimestamps

This plugin allows you to customize the timestamps in chat messages and tooltips.

## Formats

- Cozy: The timestamp by your username when chat is in cozy mode.
![cozy](https://github.com/user-attachments/assets/a883b21b-346b-4e36-9660-771eff6898c9)


- Compact: The timestamp to the left of messages when chat is in compact mode and when you send multiple messages in cozy mode.
![compact](https://github.com/user-attachments/assets/9944495f-ff21-4da5-b6f2-0ee0e0a7bf99)


- Tooltip: The timestamp in the tooltip when hovering over a message.
![tootip](https://github.com/user-attachments/assets/5fcc6c0e-ad52-4a4e-8660-b373f5020d11)

## Placeholders

- **[calendar]**: Replaced with moment.js's calendar time, which dynamically adjusts based on the date.
- **[relative]**: Replaced with moment.js's relative time, such as "4 hours ago".

## Timeframes for Calendar Time

- **Same day**: Format for today's dates (e.g., "Today, 2:30 PM").
- **Last day**: Format for yesterday's dates (e.g., "Yesterday, 2:30 PM").
- **Last week**: Format for dates within the last week (e.g., "Monday, 2:30 PM").
- **Same else**: Format for older dates (e.g., "01/01/2024, 2:30 PM").

![settings](https://github.com/user-attachments/assets/daf4bd95-5a52-4376-9226-2a3afd043368)

For more information on formatting options, refer to the [Moment.js formatting documentation](https://momentjs.com/docs/#/displaying/format/).
237 changes: 237 additions & 0 deletions src/plugins/customMessageTimestamps/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { definePluginSettings, useSettings } from "@api/Settings";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { Forms, moment, TextInput, useEffect, useState } from "@webpack/common";

type TimeFormat = {
name: string;
description: string;
default: string;
offset: number;
};
type TimeRowProps = {
id: string;
format: TimeFormat;
onChange: (key: string, value: string) => void;
pluginSettings: any;
};

const timeFormats: Record<string, TimeFormat> = {
cozyFormat: {
name: "Cozy mode",
description: "Time format to use in messages on cozy mode",
default: "[calendar]",
offset: 0,
},
compactFormat: {
name: "Compact mode",
description: "Time format on compact mode and hovering messages",
default: "LT",
offset: 0,
},
tooltipFormat: {
name: "Tooltip",
description: "Time format to use on tooltips",
default: "LLLL • [relative]",
offset: 0,
},
sameDayFormat: {
name: "Same day",
description: "[calendar] format for today",
default: "HH:mm:ss",
offset: 0,
},
lastDayFormat: {
name: "Last day",
description: "[calendar] format for yesterday",
default: "[yesterday] HH:mm:ss",
offset: -1000 * 60 * 60 * 24,
},
lastWeekFormat: {
name: "Last week",
description: "[calendar] format for last week",
default: "ddd DD.MM.YYYY HH:mm:ss",
offset: -1000 * 60 * 60 * 24 * 7,
},
sameElseFormat: {
name: "Same else",
description: "[calendar] format for older dates",
default: "ddd DD.MM.YYYY HH:mm:ss",
offset: -1000 * 60 * 60 * 24 * 31,
}
};

const format = (date: Date, formatTemplate: string): string => {
const mmt = moment(date);

const sameDayFormat = settings.store?.formats?.sameDayFormat || timeFormats.sameDayFormat.default;
const lastDayFormat = settings.store?.formats?.lastDayFormat || timeFormats.lastDayFormat.default;
const lastWeekFormat = settings.store?.formats?.lastWeekFormat || timeFormats.lastWeekFormat.default;
const sameElseFormat = settings.store?.formats?.sameElseFormat || timeFormats.sameElseFormat.default;

return mmt.format(formatTemplate)
.replace("calendar", () => mmt.calendar(null, {
sameDay: sameDayFormat,
lastDay: lastDayFormat,
lastWeek: lastWeekFormat,
sameElse: sameElseFormat
}))
.replace("relative", () => mmt.fromNow());
};

const TimeRow = (props: TimeRowProps) => {
const [state, setState] = useState(props.pluginSettings?.[props.id] || props.format.default);
const [preview, setPreview] = useState("");

const handleChange = (value: string) => {
setState(value);
props.onChange(props.id, value);
};

const updatePreview = () => setPreview(format(new Date(Date.now() + props.format.offset), state || props.format.default));

useEffect(() => {
updatePreview();
const interval = setInterval(updatePreview, 1000);
return () => clearInterval(interval);
}, [state]);

return (
<div style={{ padding: "0 0 20px 0" }}>
<Forms.FormTitle tag="h4">{props.format.name}</Forms.FormTitle>
<Forms.FormText>{props.format.description}</Forms.FormText>
<TextInput value={state} onChange={handleChange}/>
<Forms.FormText style={{ color: "yellow", marginTop: "10px" }}>{preview}</Forms.FormText>
</div>
);
};

const settings = definePluginSettings({
formats: {
type: OptionType.COMPONENT,
description: "Customize the timestamp formats",
component: componentProps => {
const [settingsState, setSettingsState] = useState(useSettings().plugins?.CustomMessageTimestamps?.formats ?? {});

const setNewValue = (key: string, value: string) => {
const newSettings = { ...settingsState, [key]: value };
setSettingsState(newSettings);
componentProps.setValue(newSettings);
};

return (
<Forms.FormSection>
{Object.entries(timeFormats).map(([key, value]) => (
<div key={key}>
{key === "sameDayFormat" && (
<div style={{ padding: "0 0 20px 0" }}>
<Forms.FormDivider style={{ marginBottom: "10px" }}/>
<Forms.FormTitle tag="h1">Calendar formats</Forms.FormTitle>
<Forms.FormText>
How to format the [calendar] value if used in the above timestamps.
</Forms.FormText>
</div>
)}
<TimeRow
id={key}
format={value}
onChange={setNewValue}
pluginSettings={settingsState}
/>
</div>
))}
</Forms.FormSection>
);
}
}
}).withPrivateSettings<{
formats: {
cozyFormat: string;
compactFormat: string;
tooltipFormat: string;
sameDayFormat: string;
lastDayFormat: string;
lastWeekFormat: string;
sameElseFormat: string;
};
}>();

export default definePlugin({
name: "CustomMessageTimestamps",
description: "Custom timestamps on messages and tooltips",
authors: [
Devs.Rini,
Devs.nvhhr,
Devs.Suffocate
],
settings,
settingsAboutComponent: () => (
<div
style={{
backgroundColor: "var(--info-help-background)",
border: "1px solid var(--info-help-foreground)",
borderRadius: "5px",
padding: "5px",
marginTop: "10px",
marginBottom: "10px"
}}
>
<Forms.FormTitle tag="h2">How to use:</Forms.FormTitle>
<Forms.FormText>
<Link href="https://momentjs.com/docs/#/displaying/format/">Moment.js formatting documentation</Link>
<p>
Additionally you can use these in your inputs:<br/>
<b>[calendar]</b> enables dynamic date formatting such
as &quot;Today&quot; or &quot;Yesterday&quot;.<br/>
<b>[relative]</b> gives you times such as &quot;4 hours ago&quot;.<br/>
</p>
</Forms.FormText>
</div>
),
patches: [{
find: "#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}",
replacement: [
{
match: /(\i)\?\(0,\i\.\i\)\((\i),"LT"\):\(0,\i\.\i\)\(\i\)/,
replace: "$self.renderTimestamp($2,$1?'compact':'cozy')",
},
{
match: /(?<=text:)\(0,\i.\i\)\((\i),"LLLL"\)(?=,)/,
replace: "$self.renderTimestamp($1,'tooltip')",
},
]
}],

renderTimestamp: (date: Date, type: "cozy" | "compact" | "tooltip") => {
const forceUpdater = useForceUpdater();
let formatTemplate: string;

switch (type) {
case "cozy":
formatTemplate = settings.use(["formats"]).formats?.cozyFormat || timeFormats.cozyFormat.default;
break;
case "compact":
formatTemplate = settings.use(["formats"]).formats?.compactFormat || timeFormats.compactFormat.default;
break;
case "tooltip":
formatTemplate = settings.use(["formats"]).formats?.tooltipFormat || timeFormats.tooltipFormat.default;
}

useEffect(() => {
if (formatTemplate.includes("calendar") || formatTemplate.includes("relative")) {
const interval = setInterval(forceUpdater, 30000);
return () => clearInterval(interval);
}
}, []);

return format(date, formatTemplate);
}
});
17 changes: 1 addition & 16 deletions src/plugins/replyTimestamp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,8 @@ import "./style.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { DateUtils, Timestamp } from "@webpack/common";
import type { Message } from "discord-types/general";
import type { HTMLAttributes } from "react";

const MessageClasses = findByPropsLazy("separator", "latin24CompactTimeStamp");

function Sep(props: HTMLAttributes<HTMLElement>) {
return <i className={MessageClasses.separator} aria-hidden={true} {...props} />;
}

const enum ReferencedMessageState {
LOADED = 0,
Expand All @@ -44,14 +36,7 @@ function ReplyTimestamp({
compact={DateUtils.isSameDay(refTimestamp, baseTimestamp)}
timestamp={refTimestamp}
isInline={false}
>
<Sep>[</Sep>
{DateUtils.isSameDay(refTimestamp, baseTimestamp)
? DateUtils.dateFormat(refTimestamp, "LT")
: DateUtils.calendarFormat(refTimestamp)
}
<Sep>]</Sep>
</Timestamp>
/>
);
}

Expand Down
8 changes: 8 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "jamesbt365",
id: 158567567487795200n,
},
nvhhr: {
name: "nvhhr",
id: 165098921071345666n
},
Suffocate: {
name: "Suffocate",
id: 772601756776923187n
},
} satisfies Record<string, Dev>);

// iife so #__PURE__ works correctly
Expand Down