diff --git a/web/src/app/AccountApi.js b/web/src/app/AccountApi.js index 9af220a0..d3d5d4b6 100644 --- a/web/src/app/AccountApi.js +++ b/web/src/app/AccountApi.js @@ -1,3 +1,4 @@ +import i18n from "i18next"; import { accountBillingPortalUrl, accountBillingSubscriptionUrl, @@ -17,7 +18,6 @@ import { } from "./utils"; import session from "./Session"; import subscriptionManager from "./SubscriptionManager"; -import i18n from "i18next"; import prefs from "./Prefs"; import routes from "../components/routes"; import { fetchOrThrow, UnauthorizedError } from "./errors"; @@ -66,13 +66,13 @@ class AccountApi { async create(username, password) { const url = accountUrl(config.base_url); const body = JSON.stringify({ - username: username, - password: password, + username, + password, }); console.log(`[AccountApi] Creating user account ${url}`); await fetchOrThrow(url, { method: "POST", - body: body, + body, }); } @@ -97,7 +97,7 @@ class AccountApi { method: "DELETE", headers: withBearerAuth({}, session.token()), body: JSON.stringify({ - password: password, + password, }), }); } @@ -118,7 +118,7 @@ class AccountApi { async createToken(label, expires) { const url = accountTokenUrl(config.base_url); const body = { - label: label, + label, expires: expires > 0 ? Math.floor(Date.now() / 1000) + expires : 0, }; console.log(`[AccountApi] Creating user access token ${url}`); @@ -132,8 +132,8 @@ class AccountApi { async updateToken(token, label, expires) { const url = accountTokenUrl(config.base_url); const body = { - token: token, - label: label, + token, + label, }; if (expires > 0) { body.expires = Math.floor(Date.now() / 1000) + expires; @@ -171,7 +171,7 @@ class AccountApi { await fetchOrThrow(url, { method: "PATCH", headers: withBearerAuth({}, session.token()), - body: body, + body, }); } @@ -179,13 +179,13 @@ class AccountApi { const url = accountSubscriptionUrl(config.base_url); const body = JSON.stringify({ base_url: baseUrl, - topic: topic, + topic, }); console.log(`[AccountApi] Adding user subscription ${url}: ${body}`); const response = await fetchOrThrow(url, { method: "POST", headers: withBearerAuth({}, session.token()), - body: body, + body, }); const subscription = await response.json(); // May throw SyntaxError console.log(`[AccountApi] Subscription`, subscription); @@ -196,14 +196,14 @@ class AccountApi { const url = accountSubscriptionUrl(config.base_url); const body = JSON.stringify({ base_url: baseUrl, - topic: topic, + topic, ...payload, }); console.log(`[AccountApi] Updating user subscription ${url}: ${body}`); const response = await fetchOrThrow(url, { method: "PATCH", headers: withBearerAuth({}, session.token()), - body: body, + body, }); const subscription = await response.json(); // May throw SyntaxError console.log(`[AccountApi] Subscription`, subscription); @@ -230,8 +230,8 @@ class AccountApi { method: "POST", headers: withBearerAuth({}, session.token()), body: JSON.stringify({ - topic: topic, - everyone: everyone, + topic, + everyone, }), }); } @@ -272,11 +272,11 @@ class AccountApi { async upsertBillingSubscription(method, tier, interval) { const url = accountBillingSubscriptionUrl(config.base_url); const response = await fetchOrThrow(url, { - method: method, + method, headers: withBearerAuth({}, session.token()), body: JSON.stringify({ - tier: tier, - interval: interval, + tier, + interval, }), }); return await response.json(); // May throw SyntaxError @@ -309,7 +309,7 @@ class AccountApi { headers: withBearerAuth({}, session.token()), body: JSON.stringify({ number: phoneNumber, - channel: channel, + channel, }), }); } @@ -322,7 +322,7 @@ class AccountApi { headers: withBearerAuth({}, session.token()), body: JSON.stringify({ number: phoneNumber, - code: code, + code, }), }); } diff --git a/web/src/app/Api.js b/web/src/app/Api.js index b956e0bd..ba1cbe61 100644 --- a/web/src/app/Api.js +++ b/web/src/app/Api.js @@ -18,7 +18,7 @@ class Api { const messages = []; const headers = maybeWithAuth({}, user); console.log(`[Api] Polling ${url}`); - for await (let line of fetchLinesIterator(url, headers)) { + for await (const line of fetchLinesIterator(url, headers)) { const message = JSON.parse(line); if (message.id) { console.log(`[Api, ${shortUrl}] Received message ${line}`); @@ -33,8 +33,8 @@ class Api { console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`); const headers = {}; const body = { - topic: topic, - message: message, + topic, + message, ...options, }; await fetchOrThrow(baseUrl, { @@ -60,7 +60,7 @@ class Api { publishXHR(url, body, headers, onProgress) { console.log(`[Api] Publishing message to ${url}`); const xhr = new XMLHttpRequest(); - const send = new Promise(function (resolve, reject) { + const send = new Promise((resolve, reject) => { xhr.open("PUT", url); if (body.type) { xhr.overrideMimeType(body.type); @@ -106,7 +106,8 @@ class Api { }); if (response.status >= 200 && response.status <= 299) { return true; - } else if (response.status === 401 || response.status === 403) { + } + if (response.status === 401 || response.status === 403) { // See server/server.go return false; } diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js index 7b25467c..dd3cf63d 100644 --- a/web/src/app/Connection.js +++ b/web/src/app/Connection.js @@ -77,7 +77,7 @@ class Connection { close() { console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`); const socket = this.ws; - const retryTimeout = this.retryTimeout; + const { retryTimeout } = this; if (socket !== null) { socket.close(); } @@ -110,6 +110,7 @@ class Connection { export class ConnectionState { static Connected = "connected"; + static Connecting = "connecting"; } diff --git a/web/src/app/ConnectionManager.js b/web/src/app/ConnectionManager.js index f50ed531..f6316aa2 100644 --- a/web/src/app/ConnectionManager.js +++ b/web/src/app/ConnectionManager.js @@ -55,12 +55,12 @@ class ConnectionManager { // Create and add new connections subscriptionsWithUsersAndConnectionId.forEach((subscription) => { const subscriptionId = subscription.id; - const connectionId = subscription.connectionId; + const { connectionId } = subscription; const added = !this.connections.get(connectionId); if (added) { - const baseUrl = subscription.baseUrl; - const topic = subscription.topic; - const user = subscription.user; + const { baseUrl } = subscription; + const { topic } = subscription; + const { user } = subscription; const since = subscription.last; const connection = new Connection( connectionId, @@ -112,9 +112,8 @@ class ConnectionManager { } } -const makeConnectionId = async (subscription, user) => { - return user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`); -}; +const makeConnectionId = async (subscription, user) => + user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`); const connectionManager = new ConnectionManager(); export default connectionManager; diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index a539362c..aeec3fc9 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -25,8 +25,8 @@ class SubscriptionManager { } const subscription = { id: topicUrl(baseUrl, topic), - baseUrl: baseUrl, - topic: topic, + baseUrl, + topic, mutedUntil: 0, last: null, internal: internal || false, @@ -39,14 +39,14 @@ class SubscriptionManager { console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions); // Add remote subscriptions - let remoteIds = []; // = topicUrl(baseUrl, topic) + const remoteIds = []; // = topicUrl(baseUrl, topic) for (let i = 0; i < remoteSubscriptions.length; i++) { const remote = remoteSubscriptions[i]; const local = await this.add(remote.base_url, remote.topic, false); const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null; await this.update(local.id, { displayName: remote.display_name, // May be undefined - reservation: reservation, // May be null! + reservation, // May be null! }); remoteIds.push(local.id); } @@ -63,12 +63,12 @@ class SubscriptionManager { } async updateState(subscriptionId, state) { - db.subscriptions.update(subscriptionId, { state: state }); + db.subscriptions.update(subscriptionId, { state }); } async remove(subscriptionId) { await db.subscriptions.delete(subscriptionId); - await db.notifications.where({ subscriptionId: subscriptionId }).delete(); + await db.notifications.where({ subscriptionId }).delete(); } async first() { @@ -140,7 +140,7 @@ class SubscriptionManager { } async deleteNotifications(subscriptionId) { - await db.notifications.where({ subscriptionId: subscriptionId }).delete(); + await db.notifications.where({ subscriptionId }).delete(); } async markNotificationRead(notificationId) { @@ -148,24 +148,24 @@ class SubscriptionManager { } async markNotificationsRead(subscriptionId) { - await db.notifications.where({ subscriptionId: subscriptionId, new: 1 }).modify({ new: 0 }); + await db.notifications.where({ subscriptionId, new: 1 }).modify({ new: 0 }); } async setMutedUntil(subscriptionId, mutedUntil) { await db.subscriptions.update(subscriptionId, { - mutedUntil: mutedUntil, + mutedUntil, }); } async setDisplayName(subscriptionId, displayName) { await db.subscriptions.update(subscriptionId, { - displayName: displayName, + displayName, }); } async setReservation(subscriptionId, reservation) { await db.subscriptions.update(subscriptionId, { - reservation: reservation, + reservation, }); } diff --git a/web/src/app/config.js b/web/src/app/config.js index 15225f5b..24e86f3a 100644 --- a/web/src/app/config.js +++ b/web/src/app/config.js @@ -1,4 +1,4 @@ -const config = window.config; +const { config } = window; // The backend returns an empty base_url for the config struct, // so the frontend (hey, that's us!) can use the current location. diff --git a/web/src/app/errors.js b/web/src/app/errors.js index e31949d2..0d443757 100644 --- a/web/src/app/errors.js +++ b/web/src/app/errors.js @@ -48,6 +48,7 @@ export class UnauthorizedError extends Error { export class UserExistsError extends Error { static CODE = 40901; // errHTTPConflictUserExists + constructor() { super("Username already exists"); } @@ -55,6 +56,7 @@ export class UserExistsError extends Error { export class TopicReservedError extends Error { static CODE = 40902; // errHTTPConflictTopicReserved + constructor() { super("Topic already reserved"); } @@ -62,6 +64,7 @@ export class TopicReservedError extends Error { export class AccountCreateLimitReachedError extends Error { static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation + constructor() { super("Account creation limit reached"); } @@ -69,6 +72,7 @@ export class AccountCreateLimitReachedError extends Error { export class IncorrectPasswordError extends Error { static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation + constructor() { super("Password incorrect"); } diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 88e3684b..e8c98ec7 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -1,3 +1,4 @@ +import { Base64 } from "js-base64"; import { rawEmojis } from "./emojis"; import beep from "../sounds/beep.mp3"; import juntos from "../sounds/juntos.mp3"; @@ -7,7 +8,6 @@ import dadum from "../sounds/dadum.mp3"; import pop from "../sounds/pop.mp3"; import popSwoosh from "../sounds/pop-swoosh.mp3"; import config from "./config"; -import { Base64 } from "js-base64"; export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; export const topicUrlWs = (baseUrl, topic) => @@ -33,9 +33,7 @@ export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; export const expandSecureUrl = (url) => `https://${url}`; -export const validUrl = (url) => { - return url.match(/^https?:\/\/.+/); -}; +export const validUrl = (url) => url.match(/^https?:\/\/.+/); export const validTopic = (topic) => { if (disallowedTopic(topic)) { @@ -44,14 +42,13 @@ export const validTopic = (topic) => { return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app! }; -export const disallowedTopic = (topic) => { - return config.disallowed_topics.includes(topic); -}; +export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic); export const topicDisplayName = (subscription) => { if (subscription.displayName) { return subscription.displayName; - } else if (subscription.baseUrl === config.base_url) { + } + if (subscription.baseUrl === config.base_url) { return subscription.topic; } return topicShortUrl(subscription.baseUrl, subscription.topic); @@ -67,7 +64,7 @@ rawEmojis.forEach((emoji) => { const toEmojis = (tags) => { if (!tags) return []; - else return tags.filter((tag) => tag in emojis).map((tag) => emojis[tag]); + return tags.filter((tag) => tag in emojis).map((tag) => emojis[tag]); }; export const formatTitleWithDefault = (m, fallback) => { @@ -81,33 +78,31 @@ export const formatTitle = (m) => { const emojiList = toEmojis(m.tags); if (emojiList.length > 0) { return `${emojiList.join(" ")} ${m.title}`; - } else { - return m.title; } + return m.title; }; export const formatMessage = (m) => { if (m.title) { return m.message; - } else { - const emojiList = toEmojis(m.tags); - if (emojiList.length > 0) { - return `${emojiList.join(" ")} ${m.message}`; - } else { - return m.message; - } } + const emojiList = toEmojis(m.tags); + if (emojiList.length > 0) { + return `${emojiList.join(" ")} ${m.message}`; + } + return m.message; }; export const unmatchedTags = (tags) => { if (!tags) return []; - else return tags.filter((tag) => !(tag in emojis)); + return tags.filter((tag) => !(tag in emojis)); }; export const maybeWithAuth = (headers, user) => { if (user && user.password) { return withBasicAuth(headers, user.username, user.password); - } else if (user && user.token) { + } + if (user && user.token) { return withBearerAuth(headers, user.token); } return headers; @@ -121,30 +116,22 @@ export const maybeWithBearerAuth = (headers, token) => { }; export const withBasicAuth = (headers, username, password) => { - headers["Authorization"] = basicAuth(username, password); + headers.Authorization = basicAuth(username, password); return headers; }; -export const basicAuth = (username, password) => { - return `Basic ${encodeBase64(`${username}:${password}`)}`; -}; +export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`; export const withBearerAuth = (headers, token) => { - headers["Authorization"] = bearerAuth(token); + headers.Authorization = bearerAuth(token); return headers; }; -export const bearerAuth = (token) => { - return `Bearer ${token}`; -}; +export const bearerAuth = (token) => `Bearer ${token}`; -export const encodeBase64 = (s) => { - return Base64.encode(s); -}; +export const encodeBase64 = (s) => Base64.encode(s); -export const encodeBase64Url = (s) => { - return Base64.encodeURI(s); -}; +export const encodeBase64Url = (s) => Base64.encodeURI(s); export const maybeAppendActionErrors = (message, notification) => { const actionErrors = (notification.actions ?? []) @@ -153,13 +140,13 @@ export const maybeAppendActionErrors = (message, notification) => { .join("\n"); if (actionErrors.length === 0) { return message; - } else { - return `${message}\n\n${actionErrors}`; } + return `${message}\n\n${actionErrors}`; }; export const shuffle = (arr) => { - let j, x; + let j; + let x; for (let index = arr.length - 1; index > 0; index--) { j = Math.floor(Math.random() * (index + 1)); x = arr[index]; @@ -169,12 +156,11 @@ export const shuffle = (arr) => { return arr; }; -export const splitNoEmpty = (s, delimiter) => { - return s +export const splitNoEmpty = (s, delimiter) => + s .split(delimiter) .map((x) => x.trim()) .filter((x) => x !== ""); -}; /** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */ export const hashCode = async (s) => { @@ -182,21 +168,18 @@ export const hashCode = async (s) => { for (let i = 0; i < s.length; i++) { const char = s.charCodeAt(i); hash = (hash << 5) - hash + char; - hash = hash & hash; // Convert to 32bit integer + hash &= hash; // Convert to 32bit integer } return hash; }; -export const formatShortDateTime = (timestamp) => { - return new Intl.DateTimeFormat("default", { +export const formatShortDateTime = (timestamp) => + new Intl.DateTimeFormat("default", { dateStyle: "short", timeStyle: "short", }).format(new Date(timestamp * 1000)); -}; -export const formatShortDate = (timestamp) => { - return new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(new Date(timestamp * 1000)); -}; +export const formatShortDate = (timestamp) => new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(new Date(timestamp * 1000)); export const formatBytes = (bytes, decimals = 2) => { if (bytes === 0) return "0 bytes"; @@ -204,13 +187,14 @@ export const formatBytes = (bytes, decimals = 2) => { const dm = decimals < 0 ? 0 : decimals; const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; + return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; }; export const formatNumber = (n) => { if (n === 0) { return n; - } else if (n % 1000 === 0) { + } + if (n % 1000 === 0) { return `${n / 1000}k`; } return n.toLocaleString(); @@ -267,7 +251,7 @@ export const playSound = async (id) => { export async function* fetchLinesIterator(fileURL, headers) { const utf8Decoder = new TextDecoder("utf-8"); const response = await fetch(fileURL, { - headers: headers, + headers, }); const reader = response.body.getReader(); let { value: chunk, done: readerDone } = await reader.read(); @@ -277,12 +261,12 @@ export async function* fetchLinesIterator(fileURL, headers) { let startIndex = 0; for (;;) { - let result = re.exec(chunk); + const result = re.exec(chunk); if (!result) { if (readerDone) { break; } - let remainder = chunk.substr(startIndex); + const remainder = chunk.substr(startIndex); ({ value: chunk, done: readerDone } = await reader.read()); chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ""); startIndex = re.lastIndex = 0; diff --git a/web/src/components/Account.jsx b/web/src/components/Account.jsx index 5cb68c13..d6f74843 100644 --- a/web/src/components/Account.jsx +++ b/web/src/components/Account.jsx @@ -29,34 +29,34 @@ import Container from "@mui/material/Container"; import Card from "@mui/material/Card"; import Button from "@mui/material/Button"; import { Trans, useTranslation } from "react-i18next"; -import session from "../app/Session"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; -import theme from "./theme"; import Dialog from "@mui/material/Dialog"; import DialogTitle from "@mui/material/DialogTitle"; import DialogContent from "@mui/material/DialogContent"; import TextField from "@mui/material/TextField"; -import routes from "./routes"; import IconButton from "@mui/material/IconButton"; -import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; -import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; -import { Pref, PrefGroup } from "./Pref"; -import db from "../app/db"; import i18n from "i18next"; import humanizeDuration from "humanize-duration"; -import UpgradeDialog from "./UpgradeDialog"; import CelebrationIcon from "@mui/icons-material/Celebration"; -import { AccountContext } from "./App"; -import DialogFooter from "./DialogFooter"; -import { Paragraph } from "./styles"; import CloseIcon from "@mui/icons-material/Close"; import { ContentCopy, Public } from "@mui/icons-material"; import MenuItem from "@mui/material/MenuItem"; import DialogContentText from "@mui/material/DialogContentText"; +import AddIcon from "@mui/icons-material/Add"; +import routes from "./routes"; +import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; +import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi"; +import { Pref, PrefGroup } from "./Pref"; +import db from "../app/db"; +import UpgradeDialog from "./UpgradeDialog"; +import { AccountContext } from "./App"; +import DialogFooter from "./DialogFooter"; +import { Paragraph } from "./styles"; import { IncorrectPasswordError, UnauthorizedError } from "../app/errors"; import { ProChip } from "./SubscriptionPopup"; -import AddIcon from "@mui/icons-material/Add"; +import theme from "./theme"; +import session from "../app/Session"; const Account = () => { if (!session.exists()) { @@ -561,9 +561,7 @@ const Stats = () => { return <>; } - const normalize = (value, max) => { - return Math.min((value / max) * 100, 100); - }; + const normalize = (value, max) => Math.min((value / max) * 100, 100); return ( @@ -746,18 +744,16 @@ const Stats = () => { ); }; -const InfoIcon = () => { - return ( - - ); -}; +const InfoIcon = () => ( + +); const Tokens = () => { const { t } = useTranslation(); @@ -814,7 +810,8 @@ const TokensTable = (props) => { const tokens = (props.tokens || []).sort((a, b) => { if (a.token === session.token()) { return -1; - } else if (b.token === session.token()) { + } + if (b.token === session.token()) { return 1; } return a.token.localeCompare(b.token); diff --git a/web/src/components/ActionBar.jsx b/web/src/components/ActionBar.jsx index 24aef720..c9853df8 100644 --- a/web/src/components/ActionBar.jsx +++ b/web/src/components/ActionBar.jsx @@ -1,5 +1,4 @@ import AppBar from "@mui/material/AppBar"; -import Navigation from "./Navigation"; import Toolbar from "@mui/material/Toolbar"; import IconButton from "@mui/material/IconButton"; import MenuIcon from "@mui/icons-material/Menu"; @@ -7,23 +6,24 @@ import Typography from "@mui/material/Typography"; import * as React from "react"; import { useState } from "react"; import Box from "@mui/material/Box"; -import { topicDisplayName } from "../app/utils"; -import db from "../app/db"; import { useLocation, useNavigate } from "react-router-dom"; import MenuItem from "@mui/material/MenuItem"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import NotificationsIcon from "@mui/icons-material/Notifications"; import NotificationsOffIcon from "@mui/icons-material/NotificationsOff"; -import routes from "./routes"; -import subscriptionManager from "../app/SubscriptionManager"; -import logo from "../img/ntfy.svg"; import { useTranslation } from "react-i18next"; -import session from "../app/Session"; import AccountCircleIcon from "@mui/icons-material/AccountCircle"; import Button from "@mui/material/Button"; import Divider from "@mui/material/Divider"; import { Logout, Person, Settings } from "@mui/icons-material"; import ListItemIcon from "@mui/material/ListItemIcon"; +import session from "../app/Session"; +import logo from "../img/ntfy.svg"; +import subscriptionManager from "../app/SubscriptionManager"; +import routes from "./routes"; +import db from "../app/db"; +import { topicDisplayName } from "../app/utils"; +import Navigation from "./Navigation"; import accountApi from "../app/AccountApi"; import PopupMenu from "./PopupMenu"; import { SubscriptionPopup } from "./SubscriptionPopup"; @@ -86,7 +86,7 @@ const ActionBar = (props) => { const SettingsIcons = (props) => { const { t } = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); - const subscription = props.subscription; + const { subscription } = props; const handleToggleMute = async () => { const mutedUntil = subscription.mutedUntil ? 0 : 1; // Make this a timestamp in the future diff --git a/web/src/components/App.jsx b/web/src/components/App.jsx index 50f2ad65..661f6eb7 100644 --- a/web/src/components/App.jsx +++ b/web/src/components/App.jsx @@ -4,16 +4,17 @@ import Box from "@mui/material/Box"; import { ThemeProvider } from "@mui/material/styles"; import CssBaseline from "@mui/material/CssBaseline"; import Toolbar from "@mui/material/Toolbar"; +import { useLiveQuery } from "dexie-react-hooks"; +import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; +import { Backdrop, CircularProgress } from "@mui/material"; import { AllSubscriptions, SingleSubscription } from "./Notifications"; import theme from "./theme"; import Navigation from "./Navigation"; import ActionBar from "./ActionBar"; import notifier from "../app/Notifier"; import Preferences from "./Preferences"; -import { useLiveQuery } from "dexie-react-hooks"; import subscriptionManager from "../app/SubscriptionManager"; import userManager from "../app/UserManager"; -import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; import { expandUrl } from "../app/utils"; import ErrorBoundary from "./ErrorBoundary"; import routes from "./routes"; @@ -21,7 +22,6 @@ import { useAccountListener, useBackgroundProcesses, useConnectionListeners } fr import PublishDialog from "./PublishDialog"; import Messaging from "./Messaging"; import "./i18n"; // Translations! -import { Backdrop, CircularProgress } from "@mui/material"; import Login from "./Login"; import Signup from "./Signup"; import Account from "./Account"; @@ -66,12 +66,11 @@ const Layout = () => { const subscriptions = useLiveQuery(() => subscriptionManager.all()); const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal); const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; - const [selected] = (subscriptionsWithoutInternal || []).filter((s) => { - return ( + const [selected] = (subscriptionsWithoutInternal || []).filter( + (s) => (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) || (config.base_url === s.baseUrl && params.topic === s.topic) - ); - }); + ); useConnectionListeners(account, subscriptions, users); useAccountListener(setAccount); @@ -95,7 +94,7 @@ const Layout = () => { @@ -104,30 +103,28 @@ const Layout = () => { ); }; -const Main = (props) => { - return ( - (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), - }} - > - {props.children} - - ); -}; +const Main = (props) => ( + (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), + }} + > + {props.children} + +); const Loader = () => ( (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), diff --git a/web/src/components/AttachmentIcon.jsx b/web/src/components/AttachmentIcon.jsx index 9939b3b3..4d4e428a 100644 --- a/web/src/components/AttachmentIcon.jsx +++ b/web/src/components/AttachmentIcon.jsx @@ -1,16 +1,17 @@ import * as React from "react"; import Box from "@mui/material/Box"; +import { useTranslation } from "react-i18next"; import fileDocument from "../img/file-document.svg"; import fileImage from "../img/file-image.svg"; import fileVideo from "../img/file-video.svg"; import fileAudio from "../img/file-audio.svg"; import fileApp from "../img/file-app.svg"; -import { useTranslation } from "react-i18next"; const AttachmentIcon = (props) => { const { t } = useTranslation(); - const type = props.type; - let imageFile, imageLabel; + const { type } = props; + let imageFile; + let imageLabel; if (!type) { imageFile = fileDocument; imageLabel = t("notifications_attachment_file_image"); diff --git a/web/src/components/AvatarBox.jsx b/web/src/components/AvatarBox.jsx index 506ae630..470fcae8 100644 --- a/web/src/components/AvatarBox.jsx +++ b/web/src/components/AvatarBox.jsx @@ -3,23 +3,21 @@ import { Avatar } from "@mui/material"; import Box from "@mui/material/Box"; import logo from "../img/ntfy-filled.svg"; -const AvatarBox = (props) => { - return ( - - - {props.children} - - ); -}; +const AvatarBox = (props) => ( + + + {props.children} + +); export default AvatarBox; diff --git a/web/src/components/DialogFooter.jsx b/web/src/components/DialogFooter.jsx index 5a2bd7aa..2ddd7fb9 100644 --- a/web/src/components/DialogFooter.jsx +++ b/web/src/components/DialogFooter.jsx @@ -3,31 +3,29 @@ import Box from "@mui/material/Box"; import DialogContentText from "@mui/material/DialogContentText"; import DialogActions from "@mui/material/DialogActions"; -const DialogFooter = (props) => { - return ( - ( + + - - {props.status} - - {props.children} - - ); -}; + {props.status} + + {props.children} + +); export default DialogFooter; diff --git a/web/src/components/EmojiPicker.jsx b/web/src/components/EmojiPicker.jsx index 04cc5c72..6aa8e3c5 100644 --- a/web/src/components/EmojiPicker.jsx +++ b/web/src/components/EmojiPicker.jsx @@ -1,15 +1,15 @@ import * as React from "react"; import { useRef, useState } from "react"; import Typography from "@mui/material/Typography"; -import { rawEmojis } from "../app/emojis"; import Box from "@mui/material/Box"; import TextField from "@mui/material/TextField"; import { ClickAwayListener, Fade, InputAdornment, styled } from "@mui/material"; import IconButton from "@mui/material/IconButton"; import { Close } from "@mui/icons-material"; import Popper from "@mui/material/Popper"; -import { splitNoEmpty } from "../app/utils"; import { useTranslation } from "react-i18next"; +import { splitNoEmpty } from "../app/utils"; +import { rawEmojis } from "../app/emojis"; // Create emoji list by category and create a search base (string with all search words) // @@ -28,7 +28,7 @@ rawEmojis.forEach((emoji) => { const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; if (supportedEmoji) { const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`; - const emojiWithSearchBase = { ...emoji, searchBase: searchBase }; + const emojiWithSearchBase = { ...emoji, searchBase }; emojisByCategory[emoji.category].push(emojiWithSearchBase); } } catch (e) { @@ -133,7 +133,7 @@ const Category = (props) => { }; const Emoji = (props) => { - const emoji = props.emoji; + const { emoji } = props; const matches = emojiMatches(emoji, props.search); const title = `${emoji.description} (${emoji.aliases[0]})`; return ( diff --git a/web/src/components/ErrorBoundary.jsx b/web/src/components/ErrorBoundary.jsx index 21ee6a92..a8e67626 100644 --- a/web/src/components/ErrorBoundary.jsx +++ b/web/src/components/ErrorBoundary.jsx @@ -46,9 +46,9 @@ class ErrorBoundaryImpl extends React.Component { // Fetch additional info and a better stack trace StackTrace.fromError(error).then((stack) => { console.error("[ErrorBoundary] Stacktrace fetched", stack); - const niceStack = - `${error.toString()}\n` + - stack.map((el) => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n"); + const niceStack = `${error.toString()}\n${stack + .map((el) => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`) + .join("\n")}`; this.setState({ niceStack }); }); } @@ -73,9 +73,8 @@ class ErrorBoundaryImpl extends React.Component { if (this.state.error) { if (this.state.unsupportedIndexedDB) { return this.renderUnsupportedIndexedDB(); - } else { - return this.renderError(); } + return this.renderError(); } return this.props.children; } diff --git a/web/src/components/Login.jsx b/web/src/components/Login.jsx index ce4f3b50..57cf16ed 100644 --- a/web/src/components/Login.jsx +++ b/web/src/components/Login.jsx @@ -5,15 +5,15 @@ import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import TextField from "@mui/material/TextField"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; -import routes from "./routes"; -import session from "../app/Session"; import { NavLink } from "react-router-dom"; -import AvatarBox from "./AvatarBox"; import { useTranslation } from "react-i18next"; -import accountApi from "../app/AccountApi"; import IconButton from "@mui/material/IconButton"; import { InputAdornment } from "@mui/material"; import { Visibility, VisibilityOff } from "@mui/icons-material"; +import accountApi from "../app/AccountApi"; +import AvatarBox from "./AvatarBox"; +import session from "../app/Session"; +import routes from "./routes"; import { UnauthorizedError } from "../app/errors"; const Login = () => { diff --git a/web/src/components/Messaging.jsx b/web/src/components/Messaging.jsx index b6ed952b..cf91bbb1 100644 --- a/web/src/components/Messaging.jsx +++ b/web/src/components/Messaging.jsx @@ -1,21 +1,21 @@ import * as React from "react"; import { useState } from "react"; -import Navigation from "./Navigation"; import Paper from "@mui/material/Paper"; import IconButton from "@mui/material/IconButton"; import TextField from "@mui/material/TextField"; import SendIcon from "@mui/icons-material/Send"; -import api from "../app/Api"; -import PublishDialog from "./PublishDialog"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import { Portal, Snackbar } from "@mui/material"; import { useTranslation } from "react-i18next"; +import PublishDialog from "./PublishDialog"; +import api from "../app/Api"; +import Navigation from "./Navigation"; const Messaging = (props) => { const [message, setMessage] = useState(""); const [dialogKey, setDialogKey] = useState(0); - const dialogOpenMode = props.dialogOpenMode; + const { dialogOpenMode } = props; const subscription = props.selected; const handleOpenDialogClick = () => { @@ -39,7 +39,7 @@ const Messaging = (props) => { topic={subscription?.topic ?? ""} message={message} onClose={handleDialogClose} - onDragEnter={() => props.onDialogOpenModeChange((prev) => (prev ? prev : PublishDialog.OPEN_MODE_DRAG))} // Only update if not already open + onDragEnter={() => props.onDialogOpenModeChange((prev) => prev || PublishDialog.OPEN_MODE_DRAG)} // Only update if not already open onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)} /> @@ -48,7 +48,7 @@ const Messaging = (props) => { const MessageBar = (props) => { const { t } = useTranslation(); - const subscription = props.subscription; + const { subscription } = props; const [snackOpen, setSnackOpen] = useState(false); const handleSendClick = async () => { try { diff --git a/web/src/components/Navigation.jsx b/web/src/components/Navigation.jsx index 1eeb3e83..81353627 100644 --- a/web/src/components/Navigation.jsx +++ b/web/src/components/Navigation.jsx @@ -11,28 +11,28 @@ import Divider from "@mui/material/Divider"; import List from "@mui/material/List"; import SettingsIcon from "@mui/icons-material/Settings"; import AddIcon from "@mui/icons-material/Add"; -import SubscribeDialog from "./SubscribeDialog"; import { Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Portal, Tooltip } from "@mui/material"; import Button from "@mui/material/Button"; import Typography from "@mui/material/Typography"; +import { useLocation, useNavigate } from "react-router-dom"; +import { ChatBubble, MoreVert, NotificationsOffOutlined, Send } from "@mui/icons-material"; +import Box from "@mui/material/Box"; +import ArticleIcon from "@mui/icons-material/Article"; +import { Trans, useTranslation } from "react-i18next"; +import CelebrationIcon from "@mui/icons-material/Celebration"; +import IconButton from "@mui/material/IconButton"; +import SubscribeDialog from "./SubscribeDialog"; import { openUrl, topicDisplayName, topicUrl } from "../app/utils"; import routes from "./routes"; import { ConnectionState } from "../app/Connection"; -import { useLocation, useNavigate } from "react-router-dom"; import subscriptionManager from "../app/SubscriptionManager"; -import { ChatBubble, MoreVert, NotificationsOffOutlined, Send } from "@mui/icons-material"; -import Box from "@mui/material/Box"; import notifier from "../app/Notifier"; import config from "../app/config"; -import ArticleIcon from "@mui/icons-material/Article"; -import { Trans, useTranslation } from "react-i18next"; import session from "../app/Session"; import accountApi, { Permission, Role } from "../app/AccountApi"; -import CelebrationIcon from "@mui/icons-material/Celebration"; import UpgradeDialog from "./UpgradeDialog"; import { AccountContext } from "./App"; import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; -import IconButton from "@mui/material/IconButton"; import { SubscriptionPopup } from "./SubscriptionPopup"; const navWidth = 280; @@ -237,9 +237,7 @@ const UpgradeBanner = () => { const SubscriptionList = (props) => { const sortedSubscriptions = props.subscriptions .filter((s) => !s.internal) - .sort((a, b) => { - return topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1; - }); + .sort((a, b) => (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1)); return ( <> {sortedSubscriptions.map((subscription) => ( @@ -258,7 +256,7 @@ const SubscriptionItem = (props) => { const navigate = useNavigate(); const [menuAnchorEl, setMenuAnchorEl] = useState(null); - const subscription = props.subscription; + const { subscription } = props; const iconBadge = subscription.new <= 99 ? subscription.new : "99+"; const displayName = topicDisplayName(subscription); const ariaLabel = subscription.state === ConnectionState.Connecting ? `${displayName} (${t("nav_button_connecting")})` : displayName; diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index 35fd080b..5b611fb9 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -4,6 +4,15 @@ import Card from "@mui/material/Card"; import Typography from "@mui/material/Typography"; import * as React from "react"; import { useEffect, useState } from "react"; +import IconButton from "@mui/material/IconButton"; +import CheckIcon from "@mui/icons-material/Check"; +import CloseIcon from "@mui/icons-material/Close"; +import { useLiveQuery } from "dexie-react-hooks"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import InfiniteScroll from "react-infinite-scroll-component"; +import { Trans, useTranslation } from "react-i18next"; +import { useOutletContext } from "react-router-dom"; import { formatBytes, formatMessage, @@ -15,23 +24,14 @@ import { topicShortUrl, unmatchedTags, } from "../app/utils"; -import IconButton from "@mui/material/IconButton"; -import CheckIcon from "@mui/icons-material/Check"; -import CloseIcon from "@mui/icons-material/Close"; import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles"; -import { useLiveQuery } from "dexie-react-hooks"; -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; import subscriptionManager from "../app/SubscriptionManager"; -import InfiniteScroll from "react-infinite-scroll-component"; import priority1 from "../img/priority-1.svg"; import priority2 from "../img/priority-2.svg"; import priority4 from "../img/priority-4.svg"; import priority5 from "../img/priority-5.svg"; import logoOutline from "../img/ntfy-outline.svg"; import AttachmentIcon from "./AttachmentIcon"; -import { Trans, useTranslation } from "react-i18next"; -import { useOutletContext } from "react-router-dom"; import { useAutoSubscribe } from "./hooks"; export const AllSubscriptions = () => { @@ -52,46 +52,50 @@ export const SingleSubscription = () => { }; const AllSubscriptionsList = (props) => { - const subscriptions = props.subscriptions; + const { subscriptions } = props; const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); if (notifications === null || notifications === undefined) { return ; - } else if (subscriptions.length === 0) { + } + if (subscriptions.length === 0) { return ; - } else if (notifications.length === 0) { + } + if (notifications.length === 0) { return ; } return ; }; const SingleSubscriptionList = (props) => { - const subscription = props.subscription; + const { subscription } = props; const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); if (notifications === null || notifications === undefined) { return ; - } else if (notifications.length === 0) { + } + if (notifications.length === 0) { return ; } - return ; + return ; }; const NotificationList = (props) => { const { t } = useTranslation(); const pageSize = 20; - const notifications = props.notifications; + const { notifications } = props; const [snackOpen, setSnackOpen] = useState(false); const [maxCount, setMaxCount] = useState(pageSize); const count = Math.min(notifications.length, maxCount); - useEffect(() => { - return () => { + useEffect( + () => () => { setMaxCount(pageSize); const main = document.getElementById("main"); if (main) { main.scrollTo(0, 0); } - }; - }, [props.id]); + }, + [props.id] + ); return ( { const NotificationItem = (props) => { const { t } = useTranslation(); - const notification = props.notification; - const attachment = notification.attachment; + const { notification } = props; + const { attachment } = notification; const date = formatShortDateTime(notification.time); const otherTags = unmatchedTags(notification.tags); const tags = otherTags.length > 0 ? otherTags.join(", ") : null; @@ -272,7 +276,7 @@ const priorityFiles = { const Attachment = (props) => { const { t } = useTranslation(); - const attachment = props.attachment; + const { attachment } = props; const expired = attachment.expires && attachment.expires < Date.now() / 1000; const expires = attachment.expires && attachment.expires > Date.now() / 1000; const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/"); @@ -402,20 +406,18 @@ const Image = (props) => { ); }; -const UserActions = (props) => { - return ( - <> - {props.notification.actions.map((action) => ( - - ))} - - ); -}; +const UserActions = (props) => ( + <> + {props.notification.actions.map((action) => ( + + ))} + +); const UserAction = (props) => { const { t } = useTranslation(); - const notification = props.notification; - const action = props.action; + const { notification } = props; + const { action } = props; if (action.action === "broadcast") { return ( @@ -426,7 +428,8 @@ const UserAction = (props) => { ); - } else if (action.action === "view") { + } + if (action.action === "view") { return ( ); - } else if (action.action === "http") { + } + if (action.action === "http") { const method = action.method ?? "POST"; const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); return (