Skip to content
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
5 changes: 5 additions & 0 deletions web/public/static/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,11 @@
"prefs_users_dialog_password_label": "Password",
"prefs_appearance_title": "Appearance",
"prefs_appearance_language_title": "Language",
"prefs_appearance_datetime_format_title": "Date and time format",
"prefs_appearance_datetime_format_description_locale": "Use your selected language's local date and time format",
"prefs_appearance_datetime_format_description_iso8601": "Use ISO 8601 format (YYYY-MM-DD HH:mm)",
"prefs_appearance_datetime_format_locale": "Locale (default)",
"prefs_appearance_datetime_format_iso8601": "ISO 8601 (YYYY-MM-DD HH:mm)",
"prefs_appearance_theme_title": "Theme",
"prefs_appearance_theme_system": "System (default)",
"prefs_appearance_theme_dark": "Dark mode",
Expand Down
17 changes: 17 additions & 0 deletions web/src/app/Prefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export const THEME = {
SYSTEM: "system",
};

export const DATE_TIME_FORMAT = {
LOCALE: "locale",
ISO_8601: "iso8601",
};

class Prefs {
constructor(dbImpl) {
this.db = dbImpl;
Expand Down Expand Up @@ -55,6 +60,18 @@ class Prefs {
async setTheme(mode) {
await this.db.prefs.put({ key: "theme", value: mode });
}

async dateTimeFormat() {
const dateTimeFormat = await this.db.prefs.get("dateTimeFormat");
if (Object.values(DATE_TIME_FORMAT).includes(dateTimeFormat?.value)) {
return dateTimeFormat.value;
}
return DATE_TIME_FORMAT.LOCALE;
}

async setDateTimeFormat(mode) {
await this.db.prefs.put({ key: "dateTimeFormat", value: mode });
}
}

const prefs = new Prefs(db());
Expand Down
28 changes: 22 additions & 6 deletions web/src/app/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import pop from "../sounds/pop.mp3";
import popSwoosh from "../sounds/pop-swoosh.mp3";
import config from "./config";
import emojisMapped from "./emojisMapped";
import { THEME } from "./Prefs";
import { DATE_TIME_FORMAT, THEME } from "./Prefs";

export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
Expand Down Expand Up @@ -140,14 +140,30 @@ export const hashCode = (s) => {
*/
export const getKebabCaseLangStr = (language) => language.replace(/_/g, "-");

export const formatShortDateTime = (timestamp, language) =>
new Intl.DateTimeFormat(getKebabCaseLangStr(language), {
const pad2 = (value) => `${value}`.padStart(2, "0");

const formatIsoDate = (date) => `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;

const formatIsoDateTime = (date) => `${formatIsoDate(date)} ${pad2(date.getHours())}:${pad2(date.getMinutes())}`;

export const formatShortDateTime = (timestamp, language, dateTimeFormat = DATE_TIME_FORMAT.LOCALE) => {
const date = new Date(timestamp * 1000);
if (dateTimeFormat === DATE_TIME_FORMAT.ISO_8601) {
return formatIsoDateTime(date);
}
return new Intl.DateTimeFormat(getKebabCaseLangStr(language), {
dateStyle: "short",
timeStyle: "short",
}).format(new Date(timestamp * 1000));
}).format(date);
};

export const formatShortDate = (timestamp, language) =>
new Intl.DateTimeFormat(getKebabCaseLangStr(language), { dateStyle: "short" }).format(new Date(timestamp * 1000));
export const formatShortDate = (timestamp, language, dateTimeFormat = DATE_TIME_FORMAT.LOCALE) => {
const date = new Date(timestamp * 1000);
if (dateTimeFormat === DATE_TIME_FORMAT.ISO_8601) {
return formatIsoDate(date);
}
return new Intl.DateTimeFormat(getKebabCaseLangStr(language), { dateStyle: "short" }).format(date);
};

export const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return "0 bytes";
Expand Down
12 changes: 8 additions & 4 deletions web/src/components/Account.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from "react";
import { useContext, useState } from "react";
import { useLiveQuery } from "dexie-react-hooks";
import {
Alert,
CardActions,
Expand Down Expand Up @@ -56,6 +57,7 @@ import { Paragraph } from "./styles";
import { IncorrectPasswordError, UnauthorizedError } from "../app/errors";
import { ProChip } from "./SubscriptionPopup";
import session from "../app/Session";
import prefs from "../app/Prefs";

const Account = () => {
if (!session.exists()) {
Expand Down Expand Up @@ -234,6 +236,7 @@ const ChangePasswordDialog = (props) => {
const AccountType = () => {
const { t, i18n } = useTranslation();
const { account } = useContext(AccountContext);
const dateTimeFormat = useLiveQuery(async () => prefs.dateTimeFormat());
const [upgradeDialogKey, setUpgradeDialogKey] = useState(0);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
const [showPortalError, setShowPortalError] = useState(false);
Expand Down Expand Up @@ -291,7 +294,7 @@ const AccountType = () => {
{account.billing?.paid_until && !account.billing?.cancel_at && (
<Tooltip
title={t("account_basics_tier_paid_until", {
date: formatShortDate(account.billing?.paid_until, i18n.language),
date: formatShortDate(account.billing?.paid_until, i18n.language, dateTimeFormat),
})}
>
<span>
Expand Down Expand Up @@ -336,7 +339,7 @@ const AccountType = () => {
{account.billing?.cancel_at > 0 && (
<Alert severity="warning" sx={{ mt: 1 }}>
{t("account_basics_tier_canceled_subscription", {
date: formatShortDate(account.billing.cancel_at, i18n.language),
date: formatShortDate(account.billing.cancel_at, i18n.language, dateTimeFormat),
})}
</Alert>
)}
Expand Down Expand Up @@ -807,6 +810,7 @@ const Tokens = () => {

const TokensTable = (props) => {
const { t, i18n } = useTranslation();
const dateTimeFormat = useLiveQuery(async () => prefs.dateTimeFormat());
const [snackOpen, setSnackOpen] = useState(false);
const [upsertDialogKey, setUpsertDialogKey] = useState(0);
const [upsertDialogOpen, setUpsertDialogOpen] = useState(false);
Expand Down Expand Up @@ -880,11 +884,11 @@ const TokensTable = (props) => {
{token.token !== session.token() && (token.label || "-")}
</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_expires_header")}>
{token.expires ? formatShortDateTime(token.expires, i18n.language) : <em>{t("account_tokens_table_never_expires")}</em>}
{token.expires ? formatShortDateTime(token.expires, i18n.language, dateTimeFormat) : <em>{t("account_tokens_table_never_expires")}</em>}
</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_last_access_header")}>
<div style={{ display: "flex", alignItems: "center" }}>
<span>{formatShortDateTime(token.last_access, i18n.language)}</span>
<span>{formatShortDateTime(token.last_access, i18n.language, dateTimeFormat)}</span>
<Tooltip
title={t("account_tokens_table_last_origin_tooltip", {
ip: token.last_origin,
Expand Down
19 changes: 13 additions & 6 deletions web/src/components/Notifications.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
import subscriptionManager from "../app/SubscriptionManager";
import notifier from "../app/Notifier";
import prefs from "../app/Prefs";
import priority1 from "../img/priority-1.svg";
import priority2 from "../img/priority-2.svg";
import priority4 from "../img/priority-4.svg";
Expand Down Expand Up @@ -104,6 +105,7 @@ const NotificationList = (props) => {
const { t } = useTranslation();
const pageSize = 20;
const { notifications } = props;
const dateTimeFormat = useLiveQuery(async () => prefs.dateTimeFormat());
const [snackOpen, setSnackOpen] = useState(false);
const [maxCount, setMaxCount] = useState(pageSize);
const count = Math.min(notifications.length, maxCount);
Expand Down Expand Up @@ -139,7 +141,12 @@ const NotificationList = (props) => {
>
<Stack spacing={3}>
{notifications.slice(0, count).map((notification) => (
<NotificationItem key={notification.id} notification={notification} onShowSnack={() => setSnackOpen(true)} />
<NotificationItem
key={notification.id}
notification={notification}
dateTimeFormat={dateTimeFormat}
onShowSnack={() => setSnackOpen(true)}
/>
))}
<Snackbar
open={snackOpen}
Expand Down Expand Up @@ -236,9 +243,9 @@ const NotificationBody = ({ notification }) => {

const NotificationItem = (props) => {
const { t, i18n } = useTranslation();
const { notification } = props;
const { notification, dateTimeFormat } = props;
const { attachment } = notification;
const date = formatShortDateTime(notification.time, i18n.language);
const date = formatShortDateTime(notification.time, i18n.language, dateTimeFormat);
const otherTags = unmatchedTags(notification.tags);
const tags = otherTags.length > 0 ? otherTags.join(", ") : null;
const handleDelete = async () => {
Expand Down Expand Up @@ -309,7 +316,7 @@ const NotificationItem = (props) => {
<NotificationBody notification={notification} />
{maybeActionErrors(notification)}
</Typography>
{attachment && <Attachment attachment={attachment} />}
{attachment && <Attachment attachment={attachment} dateTimeFormat={dateTimeFormat} />}
{tags && (
<Typography sx={{ fontSize: 14 }} color="text.secondary">
{t("notifications_tags")}: {tags}
Expand Down Expand Up @@ -355,7 +362,7 @@ const NotificationItem = (props) => {

const Attachment = (props) => {
const { t, i18n } = useTranslation();
const { attachment } = props;
const { attachment, dateTimeFormat } = props;
const expired = attachment.expires && attachment.expires < Date.now() / 1000;
const expires = attachment.expires && attachment.expires > Date.now() / 1000;
const displayableImage = !expired && isImage(attachment);
Expand All @@ -373,7 +380,7 @@ const Attachment = (props) => {
if (expires) {
infos.push(
t("notifications_attachment_link_expires", {
date: formatShortDateTime(attachment.expires, i18n.language),
date: formatShortDateTime(attachment.expires, i18n.language, dateTimeFormat),
})
);
}
Expand Down
29 changes: 28 additions & 1 deletion web/src/components/Preferences.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import accountApi, { Permission, Role } from "../app/AccountApi";
import { Pref, PrefGroup } from "./Pref";
import { AccountContext } from "./App";
import { Paragraph } from "./styles";
import prefs, { THEME } from "../app/Prefs";
import prefs, { DATE_TIME_FORMAT, THEME } from "../app/Prefs";
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs";
import { UnauthorizedError } from "../app/errors";
Expand Down Expand Up @@ -524,6 +524,7 @@ const Appearance = () => {
</Typography>
<PrefGroup>
<Theme />
<DateTimeFormat />
<Language />
</PrefGroup>
</Card>
Expand Down Expand Up @@ -615,6 +616,32 @@ const Language = () => {
);
};

const DateTimeFormat = () => {
const { t } = useTranslation();
const labelId = "prefDateTimeFormat";
const dateTimeFormat = useLiveQuery(async () => prefs.dateTimeFormat());

const handleChange = async (ev) => {
await prefs.setDateTimeFormat(ev.target.value);
};

const description =
dateTimeFormat === DATE_TIME_FORMAT.ISO_8601
? t("prefs_appearance_datetime_format_description_iso8601")
: t("prefs_appearance_datetime_format_description_locale");

return (
<Pref labelId={labelId} title={t("prefs_appearance_datetime_format_title")} description={description}>
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={dateTimeFormat ?? DATE_TIME_FORMAT.LOCALE} onChange={handleChange} aria-labelledby={labelId}>
<MenuItem value={DATE_TIME_FORMAT.LOCALE}>{t("prefs_appearance_datetime_format_locale")}</MenuItem>
<MenuItem value={DATE_TIME_FORMAT.ISO_8601}>{t("prefs_appearance_datetime_format_iso8601")}</MenuItem>
</Select>
</FormControl>
</Pref>
);
};

const Reservations = () => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
Expand Down
11 changes: 8 additions & 3 deletions web/src/components/SubscriptionPopup.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from "react";
import { useContext, useState } from "react";
import { useLiveQuery } from "dexie-react-hooks";
import {
Button,
TextField,
Expand Down Expand Up @@ -42,9 +43,11 @@ import api from "../app/Api";
import { AccountContext } from "./App";
import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs";
import { UnauthorizedError } from "../app/errors";
import prefs from "../app/Prefs";

export const SubscriptionPopup = (props) => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const dateTimeFormat = useLiveQuery(async () => prefs.dateTimeFormat());
const { account } = useContext(AccountContext);
const navigate = useNavigate();
const [displayNameDialogOpen, setDisplayNameDialogOpen] = useState(false);
Expand Down Expand Up @@ -119,13 +122,15 @@ export const SubscriptionPopup = (props) => {
const message = shuffle([
`Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(
nowSeconds,
"en-US"
i18n.language,
dateTimeFormat
)} right now. Is that early or late?`,
`So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`,
`It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`,
`Alright then, it's ${formatShortDateTime(
nowSeconds,
"en-US"
i18n.language,
dateTimeFormat
)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`,
`There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`,
`I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`,
Expand Down
5 changes: 4 additions & 1 deletion web/src/components/UpgradeDialog.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from "react";
import { useContext, useEffect, useState } from "react";
import { useLiveQuery } from "dexie-react-hooks";
import {
Dialog,
DialogContent,
Expand Down Expand Up @@ -32,6 +33,7 @@ import { AccountContext } from "./App";
import routes from "./routes";
import session from "../app/Session";
import accountApi, { SubscriptionInterval } from "../app/AccountApi";
import prefs from "../app/Prefs";

const Feature = (props) => <FeatureItem feature>{props.children}</FeatureItem>;

Expand Down Expand Up @@ -63,6 +65,7 @@ const Banner = {
const UpgradeDialog = (props) => {
const theme = useTheme();
const { t, i18n } = useTranslation();
const dateTimeFormat = useLiveQuery(async () => prefs.dateTimeFormat());
const { account } = useContext(AccountContext); // May be undefined!
const [error, setError] = useState("");
const [tiers, setTiers] = useState(null);
Expand Down Expand Up @@ -233,7 +236,7 @@ const UpgradeDialog = (props) => {
<Trans
i18nKey="account_upgrade_dialog_cancel_warning"
values={{
date: formatShortDate(account?.billing?.paid_until || 0, i18n.language),
date: formatShortDate(account?.billing?.paid_until || 0, i18n.language, dateTimeFormat),
}}
/>
</Alert>
Expand Down