mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-05-15 07:35:49 -06:00
Merge 09a8b3e2f3 into 9a2b93f7b2
This commit is contained in:
commit
4715627668
8 changed files with 105 additions and 21 deletions
|
|
@ -377,6 +377,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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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, "");
|
||||
|
|
@ -142,14 +142,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";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import {
|
||||
Alert,
|
||||
CardActions,
|
||||
|
|
@ -56,6 +57,7 @@ import { Paragraph } from "./styles";
|
|||
import { EmailVerificationCodeInvalidError, IncorrectPasswordError, UnauthorizedError } from "../app/errors";
|
||||
import { ProChip } from "./SubscriptionPopup";
|
||||
import session from "../app/Session";
|
||||
import prefs from "../app/Prefs";
|
||||
|
||||
const Account = () => {
|
||||
if (!session.exists()) {
|
||||
|
|
@ -237,6 +239,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);
|
||||
|
|
@ -294,7 +297,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>
|
||||
|
|
@ -339,7 +342,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>
|
||||
)}
|
||||
|
|
@ -1006,6 +1009,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);
|
||||
|
|
@ -1079,11 +1083,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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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);
|
||||
|
|
@ -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}
|
||||
|
|
@ -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 () => {
|
||||
|
|
@ -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}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -528,6 +528,7 @@ const Appearance = () => {
|
|||
</Typography>
|
||||
<PrefGroup>
|
||||
<Theme />
|
||||
<DateTimeFormat />
|
||||
<Language />
|
||||
</PrefGroup>
|
||||
</Card>
|
||||
|
|
@ -619,6 +620,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);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
|
|
@ -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);
|
||||
|
|
@ -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.`,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -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>;
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue