parent
e9f170a197
commit
64ac111d55
|
@ -7,15 +7,22 @@ Released June 28, 2023
|
||||||
|
|
||||||
With this release, the ntfy web app now contains a **[progressive web app](https://docs.ntfy.sh/subscribe/pwa/) (PWA)
|
With this release, the ntfy web app now contains a **[progressive web app](https://docs.ntfy.sh/subscribe/pwa/) (PWA)
|
||||||
with Web Push support**, which means you'll be able to **install the ntfy web app on your desktop or phone** similar
|
with Web Push support**, which means you'll be able to **install the ntfy web app on your desktop or phone** similar
|
||||||
to a native app (__even on iOS!__ 🥳), and get basic push notification support (without any battery drain).
|
to a native app (__even on iOS!__ 🥳). Installing the PWA gives ntfy web its own launcher, a standalone window,
|
||||||
|
push notifications, and an app badge with the unread notification count.
|
||||||
|
|
||||||
Installing the PWA gives ntfy web its own launcher (e.g. shortcut on Windows, app on macOS, launcher shortcut on Linux,
|
On top of that, this release also brings **dark mode** 🧛🌙 to the web app.
|
||||||
home screen icon on iOS, and launcher icon on Android), a standalone window, push notifications, and an app badge with
|
|
||||||
the unread notification count.
|
🙏 A huge thanks for this release goes to [@nimbleghost](https://github.com/nimbleghost), for basically implementing the
|
||||||
|
Web Push / PWA and dark mode feature by himself. I'm really grateful for your contributions.
|
||||||
|
|
||||||
|
❤️ If you like ntfy, **please consider sponsoring us** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
||||||
|
and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app) (20% off
|
||||||
|
if you use promo code `MYTOPIC`). ntfy will always remain open source.
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
* The web app now supports Web Push, and is installable as a [progressive web app (PWA)](https://docs.ntfy.sh/subscribe/pwa/) on Chrome, Edge, Android, and iOS ([#751](https://github.com/binwiederhier/ntfy/pull/751), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
* The web app now supports Web Push, and is installable as a [progressive web app (PWA)](https://docs.ntfy.sh/subscribe/pwa/) on Chrome, Edge, Android, and iOS ([#751](https://github.com/binwiederhier/ntfy/pull/751), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
* Support for dark mode in the web app ([#206](https://github.com/binwiederhier/ntfy/issues/206), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
|
||||||
**Bug fixes:**
|
**Bug fixes:**
|
||||||
|
|
||||||
|
|
|
@ -338,10 +338,6 @@
|
||||||
"prefs_notifications_web_push_disabled_description": "Notification are received when the web app is running (via WebSocket)",
|
"prefs_notifications_web_push_disabled_description": "Notification are received when the web app is running (via WebSocket)",
|
||||||
"prefs_notifications_web_push_enabled": "Enabled for {{server}}",
|
"prefs_notifications_web_push_enabled": "Enabled for {{server}}",
|
||||||
"prefs_notifications_web_push_disabled": "Disabled",
|
"prefs_notifications_web_push_disabled": "Disabled",
|
||||||
"prefs_ui_mode_title": "UI Mode",
|
|
||||||
"prefs_ui_mode_system": "System (default)",
|
|
||||||
"prefs_ui_mode_dark": "Dark",
|
|
||||||
"prefs_ui_mode_light": "Light",
|
|
||||||
"prefs_users_title": "Manage users",
|
"prefs_users_title": "Manage users",
|
||||||
"prefs_users_description": "Add/remove users for your protected topics here. Please note that username and password are stored in the browser's local storage.",
|
"prefs_users_description": "Add/remove users for your protected topics here. Please note that username and password are stored in the browser's local storage.",
|
||||||
"prefs_users_description_no_sync": "Users and passwords are not synchronized to your account.",
|
"prefs_users_description_no_sync": "Users and passwords are not synchronized to your account.",
|
||||||
|
@ -359,6 +355,10 @@
|
||||||
"prefs_users_dialog_password_label": "Password",
|
"prefs_users_dialog_password_label": "Password",
|
||||||
"prefs_appearance_title": "Appearance",
|
"prefs_appearance_title": "Appearance",
|
||||||
"prefs_appearance_language_title": "Language",
|
"prefs_appearance_language_title": "Language",
|
||||||
|
"prefs_appearance_theme_title": "Theme",
|
||||||
|
"prefs_appearance_theme_system": "System (default)",
|
||||||
|
"prefs_appearance_theme_dark": "Dark mode",
|
||||||
|
"prefs_appearance_theme_light": "Light mode",
|
||||||
"prefs_reservations_title": "Reserved topics",
|
"prefs_reservations_title": "Reserved topics",
|
||||||
"prefs_reservations_description": "You can reserve topic names for personal use here. Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.",
|
"prefs_reservations_description": "You can reserve topic names for personal use here. Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.",
|
||||||
"prefs_reservations_limit_reached": "You reached your reserved topics limit.",
|
"prefs_reservations_limit_reached": "You reached your reserved topics limit.",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import db from "./db";
|
import db from "./db";
|
||||||
|
|
||||||
export const UI_MODE = {
|
export const THEME = {
|
||||||
DARK: "dark",
|
DARK: "dark",
|
||||||
LIGHT: "light",
|
LIGHT: "light",
|
||||||
SYSTEM: "system",
|
SYSTEM: "system",
|
||||||
|
@ -47,13 +47,13 @@ class Prefs {
|
||||||
await this.db.prefs.put({ key: "webPushEnabled", value: enabled });
|
await this.db.prefs.put({ key: "webPushEnabled", value: enabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
async uiMode() {
|
async theme() {
|
||||||
const uiMode = await this.db.prefs.get("uiMode");
|
const theme = await this.db.prefs.get("theme");
|
||||||
return uiMode?.value ?? UI_MODE.SYSTEM;
|
return theme?.value ?? THEME.SYSTEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUIMode(mode) {
|
async setTheme(mode) {
|
||||||
await this.db.prefs.put({ key: "uiMode", value: mode });
|
await this.db.prefs.put({ key: "theme", value: mode });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,19 +22,19 @@ import Login from "./Login";
|
||||||
import Signup from "./Signup";
|
import Signup from "./Signup";
|
||||||
import Account from "./Account";
|
import Account from "./Account";
|
||||||
import "../app/i18n"; // Translations!
|
import "../app/i18n"; // Translations!
|
||||||
import prefs, { UI_MODE } from "../app/Prefs";
|
import prefs, { THEME } from "../app/Prefs";
|
||||||
|
|
||||||
export const AccountContext = createContext(null);
|
export const AccountContext = createContext(null);
|
||||||
|
|
||||||
const darkModeEnabled = (prefersDarkMode, uiModePreference) => {
|
const darkModeEnabled = (prefersDarkMode, themePreference) => {
|
||||||
switch (uiModePreference) {
|
switch (themePreference) {
|
||||||
case UI_MODE.DARK:
|
case THEME.DARK:
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case UI_MODE.LIGHT:
|
case THEME.LIGHT:
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case UI_MODE.SYSTEM:
|
case THEME.SYSTEM:
|
||||||
default:
|
default:
|
||||||
return prefersDarkMode;
|
return prefersDarkMode;
|
||||||
}
|
}
|
||||||
|
@ -43,20 +43,17 @@ const darkModeEnabled = (prefersDarkMode, uiModePreference) => {
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [account, setAccount] = useState(null);
|
const [account, setAccount] = useState(null);
|
||||||
const accountMemo = useMemo(() => ({ account, setAccount }), [account, setAccount]);
|
const accountMemo = useMemo(() => ({ account, setAccount }), [account, setAccount]);
|
||||||
|
|
||||||
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
||||||
|
const themePreference = useLiveQuery(() => prefs.theme());
|
||||||
const uiModePreference = useLiveQuery(() => prefs.uiMode());
|
|
||||||
|
|
||||||
const theme = React.useMemo(
|
const theme = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
createTheme({
|
createTheme({
|
||||||
...themeOptions,
|
...themeOptions,
|
||||||
palette: {
|
palette: {
|
||||||
...(darkModeEnabled(prefersDarkMode, uiModePreference) ? darkPalette : lightPalette),
|
...(darkModeEnabled(prefersDarkMode, themePreference) ? darkPalette : lightPalette),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[prefersDarkMode, uiModePreference]
|
[prefersDarkMode, themePreference]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -43,7 +43,7 @@ import accountApi, { Permission, Role } from "../app/AccountApi";
|
||||||
import { Pref, PrefGroup } from "./Pref";
|
import { Pref, PrefGroup } from "./Pref";
|
||||||
import { AccountContext } from "./App";
|
import { AccountContext } from "./App";
|
||||||
import { Paragraph } from "./styles";
|
import { Paragraph } from "./styles";
|
||||||
import prefs, { UI_MODE } from "../app/Prefs";
|
import prefs, { THEME } from "../app/Prefs";
|
||||||
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
|
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
|
||||||
import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs";
|
import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs";
|
||||||
import { UnauthorizedError } from "../app/errors";
|
import { UnauthorizedError } from "../app/errors";
|
||||||
|
@ -86,7 +86,6 @@ const Notifications = () => {
|
||||||
{t("prefs_notifications_title")}
|
{t("prefs_notifications_title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<PrefGroup>
|
<PrefGroup>
|
||||||
<UIMode />
|
|
||||||
<Sound />
|
<Sound />
|
||||||
<MinPriority />
|
<MinPriority />
|
||||||
<DeleteAfter />
|
<DeleteAfter />
|
||||||
|
@ -238,21 +237,21 @@ const DeleteAfter = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UIMode = () => {
|
const Theme = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const labelId = "prefUIMode";
|
const labelId = "prefTheme";
|
||||||
const enabled = useLiveQuery(async () => prefs.uiMode());
|
const enabled = useLiveQuery(async () => prefs.theme());
|
||||||
const handleChange = async (ev) => {
|
const handleChange = async (ev) => {
|
||||||
await prefs.setUIMode(ev.target.value);
|
await prefs.setTheme(ev.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pref labelId={labelId} title={t("prefs_ui_mode_title")}>
|
<Pref labelId={labelId} title={t("prefs_appearance_theme_title")}>
|
||||||
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
||||||
<Select value={enabled ?? false} onChange={handleChange} aria-labelledby={labelId}>
|
<Select value={enabled ?? false} onChange={handleChange} aria-labelledby={labelId}>
|
||||||
<MenuItem value={UI_MODE.SYSTEM}>{t("prefs_ui_mode_system")}</MenuItem>
|
<MenuItem value={THEME.SYSTEM}>{t("prefs_appearance_theme_system")}</MenuItem>
|
||||||
<MenuItem value={UI_MODE.DARK}>{t("prefs_ui_mode_dark")}</MenuItem>
|
<MenuItem value={THEME.DARK}>{t("prefs_appearance_theme_dark")}</MenuItem>
|
||||||
<MenuItem value={UI_MODE.LIGHT}>{t("prefs_ui_mode_light")}</MenuItem>
|
<MenuItem value={THEME.LIGHT}>{t("prefs_appearance_theme_light")}</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Pref>
|
</Pref>
|
||||||
|
@ -511,6 +510,7 @@ const Appearance = () => {
|
||||||
{t("prefs_appearance_title")}
|
{t("prefs_appearance_title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<PrefGroup>
|
<PrefGroup>
|
||||||
|
<Theme />
|
||||||
<Language />
|
<Language />
|
||||||
</PrefGroup>
|
</PrefGroup>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
Loading…
Reference in New Issue