Run eslint autofixes

This commit is contained in:
nimbleghost 2023-05-24 09:03:28 +02:00
parent f558b4dbe9
commit 8319f1cf26
32 changed files with 394 additions and 435 deletions

View file

@ -1,3 +1,4 @@
import i18n from "i18next";
import { import {
accountBillingPortalUrl, accountBillingPortalUrl,
accountBillingSubscriptionUrl, accountBillingSubscriptionUrl,
@ -17,7 +18,6 @@ import {
} from "./utils"; } from "./utils";
import session from "./Session"; import session from "./Session";
import subscriptionManager from "./SubscriptionManager"; import subscriptionManager from "./SubscriptionManager";
import i18n from "i18next";
import prefs from "./Prefs"; import prefs from "./Prefs";
import routes from "../components/routes"; import routes from "../components/routes";
import { fetchOrThrow, UnauthorizedError } from "./errors"; import { fetchOrThrow, UnauthorizedError } from "./errors";
@ -66,13 +66,13 @@ class AccountApi {
async create(username, password) { async create(username, password) {
const url = accountUrl(config.base_url); const url = accountUrl(config.base_url);
const body = JSON.stringify({ const body = JSON.stringify({
username: username, username,
password: password, password,
}); });
console.log(`[AccountApi] Creating user account ${url}`); console.log(`[AccountApi] Creating user account ${url}`);
await fetchOrThrow(url, { await fetchOrThrow(url, {
method: "POST", method: "POST",
body: body, body,
}); });
} }
@ -97,7 +97,7 @@ class AccountApi {
method: "DELETE", method: "DELETE",
headers: withBearerAuth({}, session.token()), headers: withBearerAuth({}, session.token()),
body: JSON.stringify({ body: JSON.stringify({
password: password, password,
}), }),
}); });
} }
@ -118,7 +118,7 @@ class AccountApi {
async createToken(label, expires) { async createToken(label, expires) {
const url = accountTokenUrl(config.base_url); const url = accountTokenUrl(config.base_url);
const body = { const body = {
label: label, label,
expires: expires > 0 ? Math.floor(Date.now() / 1000) + expires : 0, expires: expires > 0 ? Math.floor(Date.now() / 1000) + expires : 0,
}; };
console.log(`[AccountApi] Creating user access token ${url}`); console.log(`[AccountApi] Creating user access token ${url}`);
@ -132,8 +132,8 @@ class AccountApi {
async updateToken(token, label, expires) { async updateToken(token, label, expires) {
const url = accountTokenUrl(config.base_url); const url = accountTokenUrl(config.base_url);
const body = { const body = {
token: token, token,
label: label, label,
}; };
if (expires > 0) { if (expires > 0) {
body.expires = Math.floor(Date.now() / 1000) + expires; body.expires = Math.floor(Date.now() / 1000) + expires;
@ -171,7 +171,7 @@ class AccountApi {
await fetchOrThrow(url, { await fetchOrThrow(url, {
method: "PATCH", method: "PATCH",
headers: withBearerAuth({}, session.token()), headers: withBearerAuth({}, session.token()),
body: body, body,
}); });
} }
@ -179,13 +179,13 @@ class AccountApi {
const url = accountSubscriptionUrl(config.base_url); const url = accountSubscriptionUrl(config.base_url);
const body = JSON.stringify({ const body = JSON.stringify({
base_url: baseUrl, base_url: baseUrl,
topic: topic, topic,
}); });
console.log(`[AccountApi] Adding user subscription ${url}: ${body}`); console.log(`[AccountApi] Adding user subscription ${url}: ${body}`);
const response = await fetchOrThrow(url, { const response = await fetchOrThrow(url, {
method: "POST", method: "POST",
headers: withBearerAuth({}, session.token()), headers: withBearerAuth({}, session.token()),
body: body, body,
}); });
const subscription = await response.json(); // May throw SyntaxError const subscription = await response.json(); // May throw SyntaxError
console.log(`[AccountApi] Subscription`, subscription); console.log(`[AccountApi] Subscription`, subscription);
@ -196,14 +196,14 @@ class AccountApi {
const url = accountSubscriptionUrl(config.base_url); const url = accountSubscriptionUrl(config.base_url);
const body = JSON.stringify({ const body = JSON.stringify({
base_url: baseUrl, base_url: baseUrl,
topic: topic, topic,
...payload, ...payload,
}); });
console.log(`[AccountApi] Updating user subscription ${url}: ${body}`); console.log(`[AccountApi] Updating user subscription ${url}: ${body}`);
const response = await fetchOrThrow(url, { const response = await fetchOrThrow(url, {
method: "PATCH", method: "PATCH",
headers: withBearerAuth({}, session.token()), headers: withBearerAuth({}, session.token()),
body: body, body,
}); });
const subscription = await response.json(); // May throw SyntaxError const subscription = await response.json(); // May throw SyntaxError
console.log(`[AccountApi] Subscription`, subscription); console.log(`[AccountApi] Subscription`, subscription);
@ -230,8 +230,8 @@ class AccountApi {
method: "POST", method: "POST",
headers: withBearerAuth({}, session.token()), headers: withBearerAuth({}, session.token()),
body: JSON.stringify({ body: JSON.stringify({
topic: topic, topic,
everyone: everyone, everyone,
}), }),
}); });
} }
@ -272,11 +272,11 @@ class AccountApi {
async upsertBillingSubscription(method, tier, interval) { async upsertBillingSubscription(method, tier, interval) {
const url = accountBillingSubscriptionUrl(config.base_url); const url = accountBillingSubscriptionUrl(config.base_url);
const response = await fetchOrThrow(url, { const response = await fetchOrThrow(url, {
method: method, method,
headers: withBearerAuth({}, session.token()), headers: withBearerAuth({}, session.token()),
body: JSON.stringify({ body: JSON.stringify({
tier: tier, tier,
interval: interval, interval,
}), }),
}); });
return await response.json(); // May throw SyntaxError return await response.json(); // May throw SyntaxError
@ -309,7 +309,7 @@ class AccountApi {
headers: withBearerAuth({}, session.token()), headers: withBearerAuth({}, session.token()),
body: JSON.stringify({ body: JSON.stringify({
number: phoneNumber, number: phoneNumber,
channel: channel, channel,
}), }),
}); });
} }
@ -322,7 +322,7 @@ class AccountApi {
headers: withBearerAuth({}, session.token()), headers: withBearerAuth({}, session.token()),
body: JSON.stringify({ body: JSON.stringify({
number: phoneNumber, number: phoneNumber,
code: code, code,
}), }),
}); });
} }

View file

@ -18,7 +18,7 @@ class Api {
const messages = []; const messages = [];
const headers = maybeWithAuth({}, user); const headers = maybeWithAuth({}, user);
console.log(`[Api] Polling ${url}`); 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); const message = JSON.parse(line);
if (message.id) { if (message.id) {
console.log(`[Api, ${shortUrl}] Received message ${line}`); console.log(`[Api, ${shortUrl}] Received message ${line}`);
@ -33,8 +33,8 @@ class Api {
console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`); console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`);
const headers = {}; const headers = {};
const body = { const body = {
topic: topic, topic,
message: message, message,
...options, ...options,
}; };
await fetchOrThrow(baseUrl, { await fetchOrThrow(baseUrl, {
@ -60,7 +60,7 @@ class Api {
publishXHR(url, body, headers, onProgress) { publishXHR(url, body, headers, onProgress) {
console.log(`[Api] Publishing message to ${url}`); console.log(`[Api] Publishing message to ${url}`);
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
const send = new Promise(function (resolve, reject) { const send = new Promise((resolve, reject) => {
xhr.open("PUT", url); xhr.open("PUT", url);
if (body.type) { if (body.type) {
xhr.overrideMimeType(body.type); xhr.overrideMimeType(body.type);
@ -106,7 +106,8 @@ class Api {
}); });
if (response.status >= 200 && response.status <= 299) { if (response.status >= 200 && response.status <= 299) {
return true; return true;
} else if (response.status === 401 || response.status === 403) { }
if (response.status === 401 || response.status === 403) {
// See server/server.go // See server/server.go
return false; return false;
} }

View file

@ -77,7 +77,7 @@ class Connection {
close() { close() {
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`);
const socket = this.ws; const socket = this.ws;
const retryTimeout = this.retryTimeout; const { retryTimeout } = this;
if (socket !== null) { if (socket !== null) {
socket.close(); socket.close();
} }
@ -110,6 +110,7 @@ class Connection {
export class ConnectionState { export class ConnectionState {
static Connected = "connected"; static Connected = "connected";
static Connecting = "connecting"; static Connecting = "connecting";
} }

View file

@ -55,12 +55,12 @@ class ConnectionManager {
// Create and add new connections // Create and add new connections
subscriptionsWithUsersAndConnectionId.forEach((subscription) => { subscriptionsWithUsersAndConnectionId.forEach((subscription) => {
const subscriptionId = subscription.id; const subscriptionId = subscription.id;
const connectionId = subscription.connectionId; const { connectionId } = subscription;
const added = !this.connections.get(connectionId); const added = !this.connections.get(connectionId);
if (added) { if (added) {
const baseUrl = subscription.baseUrl; const { baseUrl } = subscription;
const topic = subscription.topic; const { topic } = subscription;
const user = subscription.user; const { user } = subscription;
const since = subscription.last; const since = subscription.last;
const connection = new Connection( const connection = new Connection(
connectionId, connectionId,
@ -112,9 +112,8 @@ class ConnectionManager {
} }
} }
const makeConnectionId = async (subscription, user) => { const makeConnectionId = async (subscription, user) =>
return user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`); user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`);
};
const connectionManager = new ConnectionManager(); const connectionManager = new ConnectionManager();
export default connectionManager; export default connectionManager;

View file

@ -25,8 +25,8 @@ class SubscriptionManager {
} }
const subscription = { const subscription = {
id: topicUrl(baseUrl, topic), id: topicUrl(baseUrl, topic),
baseUrl: baseUrl, baseUrl,
topic: topic, topic,
mutedUntil: 0, mutedUntil: 0,
last: null, last: null,
internal: internal || false, internal: internal || false,
@ -39,14 +39,14 @@ class SubscriptionManager {
console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions); console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions);
// Add remote subscriptions // Add remote subscriptions
let remoteIds = []; // = topicUrl(baseUrl, topic) const remoteIds = []; // = topicUrl(baseUrl, topic)
for (let i = 0; i < remoteSubscriptions.length; i++) { for (let i = 0; i < remoteSubscriptions.length; i++) {
const remote = remoteSubscriptions[i]; const remote = remoteSubscriptions[i];
const local = await this.add(remote.base_url, remote.topic, false); 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; const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null;
await this.update(local.id, { await this.update(local.id, {
displayName: remote.display_name, // May be undefined displayName: remote.display_name, // May be undefined
reservation: reservation, // May be null! reservation, // May be null!
}); });
remoteIds.push(local.id); remoteIds.push(local.id);
} }
@ -63,12 +63,12 @@ class SubscriptionManager {
} }
async updateState(subscriptionId, state) { async updateState(subscriptionId, state) {
db.subscriptions.update(subscriptionId, { state: state }); db.subscriptions.update(subscriptionId, { state });
} }
async remove(subscriptionId) { async remove(subscriptionId) {
await db.subscriptions.delete(subscriptionId); await db.subscriptions.delete(subscriptionId);
await db.notifications.where({ subscriptionId: subscriptionId }).delete(); await db.notifications.where({ subscriptionId }).delete();
} }
async first() { async first() {
@ -140,7 +140,7 @@ class SubscriptionManager {
} }
async deleteNotifications(subscriptionId) { async deleteNotifications(subscriptionId) {
await db.notifications.where({ subscriptionId: subscriptionId }).delete(); await db.notifications.where({ subscriptionId }).delete();
} }
async markNotificationRead(notificationId) { async markNotificationRead(notificationId) {
@ -148,24 +148,24 @@ class SubscriptionManager {
} }
async markNotificationsRead(subscriptionId) { 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) { async setMutedUntil(subscriptionId, mutedUntil) {
await db.subscriptions.update(subscriptionId, { await db.subscriptions.update(subscriptionId, {
mutedUntil: mutedUntil, mutedUntil,
}); });
} }
async setDisplayName(subscriptionId, displayName) { async setDisplayName(subscriptionId, displayName) {
await db.subscriptions.update(subscriptionId, { await db.subscriptions.update(subscriptionId, {
displayName: displayName, displayName,
}); });
} }
async setReservation(subscriptionId, reservation) { async setReservation(subscriptionId, reservation) {
await db.subscriptions.update(subscriptionId, { await db.subscriptions.update(subscriptionId, {
reservation: reservation, reservation,
}); });
} }

View file

@ -1,4 +1,4 @@
const config = window.config; const { config } = window;
// The backend returns an empty base_url for the config struct, // The backend returns an empty base_url for the config struct,
// so the frontend (hey, that's us!) can use the current location. // so the frontend (hey, that's us!) can use the current location.

View file

@ -48,6 +48,7 @@ export class UnauthorizedError extends Error {
export class UserExistsError extends Error { export class UserExistsError extends Error {
static CODE = 40901; // errHTTPConflictUserExists static CODE = 40901; // errHTTPConflictUserExists
constructor() { constructor() {
super("Username already exists"); super("Username already exists");
} }
@ -55,6 +56,7 @@ export class UserExistsError extends Error {
export class TopicReservedError extends Error { export class TopicReservedError extends Error {
static CODE = 40902; // errHTTPConflictTopicReserved static CODE = 40902; // errHTTPConflictTopicReserved
constructor() { constructor() {
super("Topic already reserved"); super("Topic already reserved");
} }
@ -62,6 +64,7 @@ export class TopicReservedError extends Error {
export class AccountCreateLimitReachedError extends Error { export class AccountCreateLimitReachedError extends Error {
static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation
constructor() { constructor() {
super("Account creation limit reached"); super("Account creation limit reached");
} }
@ -69,6 +72,7 @@ export class AccountCreateLimitReachedError extends Error {
export class IncorrectPasswordError extends Error { export class IncorrectPasswordError extends Error {
static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation
constructor() { constructor() {
super("Password incorrect"); super("Password incorrect");
} }

View file

@ -1,3 +1,4 @@
import { Base64 } from "js-base64";
import { rawEmojis } from "./emojis"; import { rawEmojis } from "./emojis";
import beep from "../sounds/beep.mp3"; import beep from "../sounds/beep.mp3";
import juntos from "../sounds/juntos.mp3"; import juntos from "../sounds/juntos.mp3";
@ -7,7 +8,6 @@ import dadum from "../sounds/dadum.mp3";
import pop from "../sounds/pop.mp3"; import pop from "../sounds/pop.mp3";
import popSwoosh from "../sounds/pop-swoosh.mp3"; import popSwoosh from "../sounds/pop-swoosh.mp3";
import config from "./config"; import config from "./config";
import { Base64 } from "js-base64";
export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
export const topicUrlWs = (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 expandUrl = (url) => [`https://${url}`, `http://${url}`];
export const expandSecureUrl = (url) => `https://${url}`; export const expandSecureUrl = (url) => `https://${url}`;
export const validUrl = (url) => { export const validUrl = (url) => url.match(/^https?:\/\/.+/);
return url.match(/^https?:\/\/.+/);
};
export const validTopic = (topic) => { export const validTopic = (topic) => {
if (disallowedTopic(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! return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app!
}; };
export const disallowedTopic = (topic) => { export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic);
return config.disallowed_topics.includes(topic);
};
export const topicDisplayName = (subscription) => { export const topicDisplayName = (subscription) => {
if (subscription.displayName) { if (subscription.displayName) {
return subscription.displayName; return subscription.displayName;
} else if (subscription.baseUrl === config.base_url) { }
if (subscription.baseUrl === config.base_url) {
return subscription.topic; return subscription.topic;
} }
return topicShortUrl(subscription.baseUrl, subscription.topic); return topicShortUrl(subscription.baseUrl, subscription.topic);
@ -67,7 +64,7 @@ rawEmojis.forEach((emoji) => {
const toEmojis = (tags) => { const toEmojis = (tags) => {
if (!tags) return []; 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) => { export const formatTitleWithDefault = (m, fallback) => {
@ -81,33 +78,31 @@ export const formatTitle = (m) => {
const emojiList = toEmojis(m.tags); const emojiList = toEmojis(m.tags);
if (emojiList.length > 0) { if (emojiList.length > 0) {
return `${emojiList.join(" ")} ${m.title}`; return `${emojiList.join(" ")} ${m.title}`;
} else {
return m.title;
} }
return m.title;
}; };
export const formatMessage = (m) => { export const formatMessage = (m) => {
if (m.title) { if (m.title) {
return m.message; 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) => { export const unmatchedTags = (tags) => {
if (!tags) return []; if (!tags) return [];
else return tags.filter((tag) => !(tag in emojis)); return tags.filter((tag) => !(tag in emojis));
}; };
export const maybeWithAuth = (headers, user) => { export const maybeWithAuth = (headers, user) => {
if (user && user.password) { if (user && user.password) {
return withBasicAuth(headers, user.username, user.password); return withBasicAuth(headers, user.username, user.password);
} else if (user && user.token) { }
if (user && user.token) {
return withBearerAuth(headers, user.token); return withBearerAuth(headers, user.token);
} }
return headers; return headers;
@ -121,30 +116,22 @@ export const maybeWithBearerAuth = (headers, token) => {
}; };
export const withBasicAuth = (headers, username, password) => { export const withBasicAuth = (headers, username, password) => {
headers["Authorization"] = basicAuth(username, password); headers.Authorization = basicAuth(username, password);
return headers; return headers;
}; };
export const basicAuth = (username, password) => { export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`;
return `Basic ${encodeBase64(`${username}:${password}`)}`;
};
export const withBearerAuth = (headers, token) => { export const withBearerAuth = (headers, token) => {
headers["Authorization"] = bearerAuth(token); headers.Authorization = bearerAuth(token);
return headers; return headers;
}; };
export const bearerAuth = (token) => { export const bearerAuth = (token) => `Bearer ${token}`;
return `Bearer ${token}`;
};
export const encodeBase64 = (s) => { export const encodeBase64 = (s) => Base64.encode(s);
return Base64.encode(s);
};
export const encodeBase64Url = (s) => { export const encodeBase64Url = (s) => Base64.encodeURI(s);
return Base64.encodeURI(s);
};
export const maybeAppendActionErrors = (message, notification) => { export const maybeAppendActionErrors = (message, notification) => {
const actionErrors = (notification.actions ?? []) const actionErrors = (notification.actions ?? [])
@ -153,13 +140,13 @@ export const maybeAppendActionErrors = (message, notification) => {
.join("\n"); .join("\n");
if (actionErrors.length === 0) { if (actionErrors.length === 0) {
return message; return message;
} else {
return `${message}\n\n${actionErrors}`;
} }
return `${message}\n\n${actionErrors}`;
}; };
export const shuffle = (arr) => { export const shuffle = (arr) => {
let j, x; let j;
let x;
for (let index = arr.length - 1; index > 0; index--) { for (let index = arr.length - 1; index > 0; index--) {
j = Math.floor(Math.random() * (index + 1)); j = Math.floor(Math.random() * (index + 1));
x = arr[index]; x = arr[index];
@ -169,12 +156,11 @@ export const shuffle = (arr) => {
return arr; return arr;
}; };
export const splitNoEmpty = (s, delimiter) => { export const splitNoEmpty = (s, delimiter) =>
return s s
.split(delimiter) .split(delimiter)
.map((x) => x.trim()) .map((x) => x.trim())
.filter((x) => x !== ""); .filter((x) => x !== "");
};
/** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */ /** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */
export const hashCode = async (s) => { export const hashCode = async (s) => {
@ -182,21 +168,18 @@ export const hashCode = async (s) => {
for (let i = 0; i < s.length; i++) { for (let i = 0; i < s.length; i++) {
const char = s.charCodeAt(i); const char = s.charCodeAt(i);
hash = (hash << 5) - hash + char; hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer hash &= hash; // Convert to 32bit integer
} }
return hash; return hash;
}; };
export const formatShortDateTime = (timestamp) => { export const formatShortDateTime = (timestamp) =>
return new Intl.DateTimeFormat("default", { new Intl.DateTimeFormat("default", {
dateStyle: "short", dateStyle: "short",
timeStyle: "short", timeStyle: "short",
}).format(new Date(timestamp * 1000)); }).format(new Date(timestamp * 1000));
};
export const formatShortDate = (timestamp) => { export const formatShortDate = (timestamp) => new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(new Date(timestamp * 1000));
return new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(new Date(timestamp * 1000));
};
export const formatBytes = (bytes, decimals = 2) => { export const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return "0 bytes"; if (bytes === 0) return "0 bytes";
@ -204,13 +187,14 @@ export const formatBytes = (bytes, decimals = 2) => {
const dm = decimals < 0 ? 0 : decimals; const dm = decimals < 0 ? 0 : decimals;
const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); 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) => { export const formatNumber = (n) => {
if (n === 0) { if (n === 0) {
return n; return n;
} else if (n % 1000 === 0) { }
if (n % 1000 === 0) {
return `${n / 1000}k`; return `${n / 1000}k`;
} }
return n.toLocaleString(); return n.toLocaleString();
@ -267,7 +251,7 @@ export const playSound = async (id) => {
export async function* fetchLinesIterator(fileURL, headers) { export async function* fetchLinesIterator(fileURL, headers) {
const utf8Decoder = new TextDecoder("utf-8"); const utf8Decoder = new TextDecoder("utf-8");
const response = await fetch(fileURL, { const response = await fetch(fileURL, {
headers: headers, headers,
}); });
const reader = response.body.getReader(); const reader = response.body.getReader();
let { value: chunk, done: readerDone } = await reader.read(); let { value: chunk, done: readerDone } = await reader.read();
@ -277,12 +261,12 @@ export async function* fetchLinesIterator(fileURL, headers) {
let startIndex = 0; let startIndex = 0;
for (;;) { for (;;) {
let result = re.exec(chunk); const result = re.exec(chunk);
if (!result) { if (!result) {
if (readerDone) { if (readerDone) {
break; break;
} }
let remainder = chunk.substr(startIndex); const remainder = chunk.substr(startIndex);
({ value: chunk, done: readerDone } = await reader.read()); ({ value: chunk, done: readerDone } = await reader.read());
chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ""); chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : "");
startIndex = re.lastIndex = 0; startIndex = re.lastIndex = 0;

View file

@ -29,34 +29,34 @@ import Container from "@mui/material/Container";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import session from "../app/Session";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import theme from "./theme";
import Dialog from "@mui/material/Dialog"; import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import routes from "./routes";
import IconButton from "@mui/material/IconButton"; 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 InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import { Pref, PrefGroup } from "./Pref";
import db from "../app/db";
import i18n from "i18next"; import i18n from "i18next";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import UpgradeDialog from "./UpgradeDialog";
import CelebrationIcon from "@mui/icons-material/Celebration"; 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 CloseIcon from "@mui/icons-material/Close";
import { ContentCopy, Public } from "@mui/icons-material"; import { ContentCopy, Public } from "@mui/icons-material";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import DialogContentText from "@mui/material/DialogContentText"; 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 { IncorrectPasswordError, UnauthorizedError } from "../app/errors";
import { ProChip } from "./SubscriptionPopup"; import { ProChip } from "./SubscriptionPopup";
import AddIcon from "@mui/icons-material/Add"; import theme from "./theme";
import session from "../app/Session";
const Account = () => { const Account = () => {
if (!session.exists()) { if (!session.exists()) {
@ -561,9 +561,7 @@ const Stats = () => {
return <></>; return <></>;
} }
const normalize = (value, max) => { const normalize = (value, max) => Math.min((value / max) * 100, 100);
return Math.min((value / max) * 100, 100);
};
return ( return (
<Card sx={{ p: 3 }} aria-label={t("account_usage_title")}> <Card sx={{ p: 3 }} aria-label={t("account_usage_title")}>
@ -746,18 +744,16 @@ const Stats = () => {
); );
}; };
const InfoIcon = () => { const InfoIcon = () => (
return ( <InfoOutlinedIcon
<InfoOutlinedIcon sx={{
sx={{ verticalAlign: "middle",
verticalAlign: "middle", width: "18px",
width: "18px", marginLeft: "4px",
marginLeft: "4px", color: "gray",
color: "gray", }}
}} />
/> );
);
};
const Tokens = () => { const Tokens = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -814,7 +810,8 @@ const TokensTable = (props) => {
const tokens = (props.tokens || []).sort((a, b) => { const tokens = (props.tokens || []).sort((a, b) => {
if (a.token === session.token()) { if (a.token === session.token()) {
return -1; return -1;
} else if (b.token === session.token()) { }
if (b.token === session.token()) {
return 1; return 1;
} }
return a.token.localeCompare(b.token); return a.token.localeCompare(b.token);

View file

@ -1,5 +1,4 @@
import AppBar from "@mui/material/AppBar"; import AppBar from "@mui/material/AppBar";
import Navigation from "./Navigation";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
@ -7,23 +6,24 @@ import Typography from "@mui/material/Typography";
import * as React from "react"; import * as React from "react";
import { useState } from "react"; import { useState } from "react";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { topicDisplayName } from "../app/utils";
import db from "../app/db";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import MoreVertIcon from "@mui/icons-material/MoreVert"; import MoreVertIcon from "@mui/icons-material/MoreVert";
import NotificationsIcon from "@mui/icons-material/Notifications"; import NotificationsIcon from "@mui/icons-material/Notifications";
import NotificationsOffIcon from "@mui/icons-material/NotificationsOff"; 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 { useTranslation } from "react-i18next";
import session from "../app/Session";
import AccountCircleIcon from "@mui/icons-material/AccountCircle"; import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import { Logout, Person, Settings } from "@mui/icons-material"; import { Logout, Person, Settings } from "@mui/icons-material";
import ListItemIcon from "@mui/material/ListItemIcon"; 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 accountApi from "../app/AccountApi";
import PopupMenu from "./PopupMenu"; import PopupMenu from "./PopupMenu";
import { SubscriptionPopup } from "./SubscriptionPopup"; import { SubscriptionPopup } from "./SubscriptionPopup";
@ -86,7 +86,7 @@ const ActionBar = (props) => {
const SettingsIcons = (props) => { const SettingsIcons = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const subscription = props.subscription; const { subscription } = props;
const handleToggleMute = async () => { const handleToggleMute = async () => {
const mutedUntil = subscription.mutedUntil ? 0 : 1; // Make this a timestamp in the future const mutedUntil = subscription.mutedUntil ? 0 : 1; // Make this a timestamp in the future

View file

@ -4,16 +4,17 @@ import Box from "@mui/material/Box";
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline"; import CssBaseline from "@mui/material/CssBaseline";
import Toolbar from "@mui/material/Toolbar"; 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 { AllSubscriptions, SingleSubscription } from "./Notifications";
import theme from "./theme"; import theme from "./theme";
import Navigation from "./Navigation"; import Navigation from "./Navigation";
import ActionBar from "./ActionBar"; import ActionBar from "./ActionBar";
import notifier from "../app/Notifier"; import notifier from "../app/Notifier";
import Preferences from "./Preferences"; import Preferences from "./Preferences";
import { useLiveQuery } from "dexie-react-hooks";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom";
import { expandUrl } from "../app/utils"; import { expandUrl } from "../app/utils";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes"; import routes from "./routes";
@ -21,7 +22,6 @@ import { useAccountListener, useBackgroundProcesses, useConnectionListeners } fr
import PublishDialog from "./PublishDialog"; import PublishDialog from "./PublishDialog";
import Messaging from "./Messaging"; import Messaging from "./Messaging";
import "./i18n"; // Translations! import "./i18n"; // Translations!
import { Backdrop, CircularProgress } from "@mui/material";
import Login from "./Login"; import Login from "./Login";
import Signup from "./Signup"; import Signup from "./Signup";
import Account from "./Account"; import Account from "./Account";
@ -66,12 +66,11 @@ const Layout = () => {
const subscriptions = useLiveQuery(() => subscriptionManager.all()); const subscriptions = useLiveQuery(() => subscriptionManager.all());
const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal); const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal);
const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0;
const [selected] = (subscriptionsWithoutInternal || []).filter((s) => { const [selected] = (subscriptionsWithoutInternal || []).filter(
return ( (s) =>
(params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) || (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) ||
(config.base_url === s.baseUrl && params.topic === s.topic) (config.base_url === s.baseUrl && params.topic === s.topic)
); );
});
useConnectionListeners(account, subscriptions, users); useConnectionListeners(account, subscriptions, users);
useAccountListener(setAccount); useAccountListener(setAccount);
@ -95,7 +94,7 @@ const Layout = () => {
<Outlet <Outlet
context={{ context={{
subscriptions: subscriptionsWithoutInternal, subscriptions: subscriptionsWithoutInternal,
selected: selected, selected,
}} }}
/> />
</Main> </Main>
@ -104,30 +103,28 @@ const Layout = () => {
); );
}; };
const Main = (props) => { const Main = (props) => (
return ( <Box
<Box id="main"
id="main" component="main"
component="main" sx={{
sx={{ display: "flex",
display: "flex", flexGrow: 1,
flexGrow: 1, flexDirection: "column",
flexDirection: "column", padding: 3,
padding: 3, width: { sm: `calc(100% - ${Navigation.width}px)` },
width: { sm: `calc(100% - ${Navigation.width}px)` }, height: "100vh",
height: "100vh", overflow: "auto",
overflow: "auto", backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]),
backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), }}
}} >
> {props.children}
{props.children} </Box>
</Box> );
);
};
const Loader = () => ( const Loader = () => (
<Backdrop <Backdrop
open={true} open
sx={{ sx={{
zIndex: 100000, zIndex: 100000,
backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]),

View file

@ -1,16 +1,17 @@
import * as React from "react"; import * as React from "react";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { useTranslation } from "react-i18next";
import fileDocument from "../img/file-document.svg"; import fileDocument from "../img/file-document.svg";
import fileImage from "../img/file-image.svg"; import fileImage from "../img/file-image.svg";
import fileVideo from "../img/file-video.svg"; import fileVideo from "../img/file-video.svg";
import fileAudio from "../img/file-audio.svg"; import fileAudio from "../img/file-audio.svg";
import fileApp from "../img/file-app.svg"; import fileApp from "../img/file-app.svg";
import { useTranslation } from "react-i18next";
const AttachmentIcon = (props) => { const AttachmentIcon = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const type = props.type; const { type } = props;
let imageFile, imageLabel; let imageFile;
let imageLabel;
if (!type) { if (!type) {
imageFile = fileDocument; imageFile = fileDocument;
imageLabel = t("notifications_attachment_file_image"); imageLabel = t("notifications_attachment_file_image");

View file

@ -3,23 +3,21 @@ import { Avatar } from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import logo from "../img/ntfy-filled.svg"; import logo from "../img/ntfy-filled.svg";
const AvatarBox = (props) => { const AvatarBox = (props) => (
return ( <Box
<Box sx={{
sx={{ display: "flex",
display: "flex", flexGrow: 1,
flexGrow: 1, justifyContent: "center",
justifyContent: "center", flexDirection: "column",
flexDirection: "column", alignContent: "center",
alignContent: "center", alignItems: "center",
alignItems: "center", height: "100vh",
height: "100vh", }}
}} >
> <Avatar sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }} src={logo} variant="rounded" />
<Avatar sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }} src={logo} variant="rounded" /> {props.children}
{props.children} </Box>
</Box> );
);
};
export default AvatarBox; export default AvatarBox;

View file

@ -3,31 +3,29 @@ import Box from "@mui/material/Box";
import DialogContentText from "@mui/material/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
const DialogFooter = (props) => { const DialogFooter = (props) => (
return ( <Box
<Box sx={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
paddingLeft: "24px",
paddingBottom: "8px",
}}
>
<DialogContentText
component="div"
aria-live="polite"
sx={{ sx={{
display: "flex", margin: "0px",
flexDirection: "row", paddingTop: "12px",
justifyContent: "space-between", paddingBottom: "4px",
paddingLeft: "24px",
paddingBottom: "8px",
}} }}
> >
<DialogContentText {props.status}
component="div" </DialogContentText>
aria-live="polite" <DialogActions sx={{ paddingRight: 2 }}>{props.children}</DialogActions>
sx={{ </Box>
margin: "0px", );
paddingTop: "12px",
paddingBottom: "4px",
}}
>
{props.status}
</DialogContentText>
<DialogActions sx={{ paddingRight: 2 }}>{props.children}</DialogActions>
</Box>
);
};
export default DialogFooter; export default DialogFooter;

View file

@ -1,15 +1,15 @@
import * as React from "react"; import * as React from "react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { rawEmojis } from "../app/emojis";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { ClickAwayListener, Fade, InputAdornment, styled } from "@mui/material"; import { ClickAwayListener, Fade, InputAdornment, styled } from "@mui/material";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import { Close } from "@mui/icons-material"; import { Close } from "@mui/icons-material";
import Popper from "@mui/material/Popper"; import Popper from "@mui/material/Popper";
import { splitNoEmpty } from "../app/utils";
import { useTranslation } from "react-i18next"; 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) // 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; const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome;
if (supportedEmoji) { if (supportedEmoji) {
const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`; 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); emojisByCategory[emoji.category].push(emojiWithSearchBase);
} }
} catch (e) { } catch (e) {
@ -133,7 +133,7 @@ const Category = (props) => {
}; };
const Emoji = (props) => { const Emoji = (props) => {
const emoji = props.emoji; const { emoji } = props;
const matches = emojiMatches(emoji, props.search); const matches = emojiMatches(emoji, props.search);
const title = `${emoji.description} (${emoji.aliases[0]})`; const title = `${emoji.description} (${emoji.aliases[0]})`;
return ( return (

View file

@ -46,9 +46,9 @@ class ErrorBoundaryImpl extends React.Component {
// Fetch additional info and a better stack trace // Fetch additional info and a better stack trace
StackTrace.fromError(error).then((stack) => { StackTrace.fromError(error).then((stack) => {
console.error("[ErrorBoundary] Stacktrace fetched", stack); console.error("[ErrorBoundary] Stacktrace fetched", stack);
const niceStack = const niceStack = `${error.toString()}\n${stack
`${error.toString()}\n` + .map((el) => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`)
stack.map((el) => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n"); .join("\n")}`;
this.setState({ niceStack }); this.setState({ niceStack });
}); });
} }
@ -73,9 +73,8 @@ class ErrorBoundaryImpl extends React.Component {
if (this.state.error) { if (this.state.error) {
if (this.state.unsupportedIndexedDB) { if (this.state.unsupportedIndexedDB) {
return this.renderUnsupportedIndexedDB(); return this.renderUnsupportedIndexedDB();
} else {
return this.renderError();
} }
return this.renderError();
} }
return this.props.children; return this.props.children;
} }

View file

@ -5,15 +5,15 @@ import WarningAmberIcon from "@mui/icons-material/WarningAmber";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import routes from "./routes";
import session from "../app/Session";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import AvatarBox from "./AvatarBox";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import accountApi from "../app/AccountApi";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import { InputAdornment } from "@mui/material"; import { InputAdornment } from "@mui/material";
import { Visibility, VisibilityOff } from "@mui/icons-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"; import { UnauthorizedError } from "../app/errors";
const Login = () => { const Login = () => {

View file

@ -1,21 +1,21 @@
import * as React from "react"; import * as React from "react";
import { useState } from "react"; import { useState } from "react";
import Navigation from "./Navigation";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import SendIcon from "@mui/icons-material/Send"; import SendIcon from "@mui/icons-material/Send";
import api from "../app/Api";
import PublishDialog from "./PublishDialog";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import { Portal, Snackbar } from "@mui/material"; import { Portal, Snackbar } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import PublishDialog from "./PublishDialog";
import api from "../app/Api";
import Navigation from "./Navigation";
const Messaging = (props) => { const Messaging = (props) => {
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const dialogOpenMode = props.dialogOpenMode; const { dialogOpenMode } = props;
const subscription = props.selected; const subscription = props.selected;
const handleOpenDialogClick = () => { const handleOpenDialogClick = () => {
@ -39,7 +39,7 @@ const Messaging = (props) => {
topic={subscription?.topic ?? ""} topic={subscription?.topic ?? ""}
message={message} message={message}
onClose={handleDialogClose} 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)} onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)}
/> />
</> </>
@ -48,7 +48,7 @@ const Messaging = (props) => {
const MessageBar = (props) => { const MessageBar = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const subscription = props.subscription; const { subscription } = props;
const [snackOpen, setSnackOpen] = useState(false); const [snackOpen, setSnackOpen] = useState(false);
const handleSendClick = async () => { const handleSendClick = async () => {
try { try {

View file

@ -11,28 +11,28 @@ import Divider from "@mui/material/Divider";
import List from "@mui/material/List"; import List from "@mui/material/List";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import SubscribeDialog from "./SubscribeDialog";
import { Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Portal, Tooltip } from "@mui/material"; import { Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Portal, Tooltip } from "@mui/material";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography"; 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 { openUrl, topicDisplayName, topicUrl } from "../app/utils";
import routes from "./routes"; import routes from "./routes";
import { ConnectionState } from "../app/Connection"; import { ConnectionState } from "../app/Connection";
import { useLocation, useNavigate } from "react-router-dom";
import subscriptionManager from "../app/SubscriptionManager"; 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 notifier from "../app/Notifier";
import config from "../app/config"; import config from "../app/config";
import ArticleIcon from "@mui/icons-material/Article";
import { Trans, useTranslation } from "react-i18next";
import session from "../app/Session"; import session from "../app/Session";
import accountApi, { Permission, Role } from "../app/AccountApi"; import accountApi, { Permission, Role } from "../app/AccountApi";
import CelebrationIcon from "@mui/icons-material/Celebration";
import UpgradeDialog from "./UpgradeDialog"; import UpgradeDialog from "./UpgradeDialog";
import { AccountContext } from "./App"; import { AccountContext } from "./App";
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
import IconButton from "@mui/material/IconButton";
import { SubscriptionPopup } from "./SubscriptionPopup"; import { SubscriptionPopup } from "./SubscriptionPopup";
const navWidth = 280; const navWidth = 280;
@ -237,9 +237,7 @@ const UpgradeBanner = () => {
const SubscriptionList = (props) => { const SubscriptionList = (props) => {
const sortedSubscriptions = props.subscriptions const sortedSubscriptions = props.subscriptions
.filter((s) => !s.internal) .filter((s) => !s.internal)
.sort((a, b) => { .sort((a, b) => (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1));
return topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1;
});
return ( return (
<> <>
{sortedSubscriptions.map((subscription) => ( {sortedSubscriptions.map((subscription) => (
@ -258,7 +256,7 @@ const SubscriptionItem = (props) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [menuAnchorEl, setMenuAnchorEl] = useState(null); const [menuAnchorEl, setMenuAnchorEl] = useState(null);
const subscription = props.subscription; const { subscription } = props;
const iconBadge = subscription.new <= 99 ? subscription.new : "99+"; const iconBadge = subscription.new <= 99 ? subscription.new : "99+";
const displayName = topicDisplayName(subscription); const displayName = topicDisplayName(subscription);
const ariaLabel = subscription.state === ConnectionState.Connecting ? `${displayName} (${t("nav_button_connecting")})` : displayName; const ariaLabel = subscription.state === ConnectionState.Connecting ? `${displayName} (${t("nav_button_connecting")})` : displayName;

View file

@ -4,6 +4,15 @@ import Card from "@mui/material/Card";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import * as React from "react"; import * as React from "react";
import { useEffect, useState } 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 { import {
formatBytes, formatBytes,
formatMessage, formatMessage,
@ -15,23 +24,14 @@ import {
topicShortUrl, topicShortUrl,
unmatchedTags, unmatchedTags,
} from "../app/utils"; } 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 { 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 subscriptionManager from "../app/SubscriptionManager";
import InfiniteScroll from "react-infinite-scroll-component";
import priority1 from "../img/priority-1.svg"; import priority1 from "../img/priority-1.svg";
import priority2 from "../img/priority-2.svg"; import priority2 from "../img/priority-2.svg";
import priority4 from "../img/priority-4.svg"; import priority4 from "../img/priority-4.svg";
import priority5 from "../img/priority-5.svg"; import priority5 from "../img/priority-5.svg";
import logoOutline from "../img/ntfy-outline.svg"; import logoOutline from "../img/ntfy-outline.svg";
import AttachmentIcon from "./AttachmentIcon"; import AttachmentIcon from "./AttachmentIcon";
import { Trans, useTranslation } from "react-i18next";
import { useOutletContext } from "react-router-dom";
import { useAutoSubscribe } from "./hooks"; import { useAutoSubscribe } from "./hooks";
export const AllSubscriptions = () => { export const AllSubscriptions = () => {
@ -52,46 +52,50 @@ export const SingleSubscription = () => {
}; };
const AllSubscriptionsList = (props) => { const AllSubscriptionsList = (props) => {
const subscriptions = props.subscriptions; const { subscriptions } = props;
const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []);
if (notifications === null || notifications === undefined) { if (notifications === null || notifications === undefined) {
return <Loading />; return <Loading />;
} else if (subscriptions.length === 0) { }
if (subscriptions.length === 0) {
return <NoSubscriptions />; return <NoSubscriptions />;
} else if (notifications.length === 0) { }
if (notifications.length === 0) {
return <NoNotificationsWithoutSubscription subscriptions={subscriptions} />; return <NoNotificationsWithoutSubscription subscriptions={subscriptions} />;
} }
return <NotificationList key="all" notifications={notifications} messageBar={false} />; return <NotificationList key="all" notifications={notifications} messageBar={false} />;
}; };
const SingleSubscriptionList = (props) => { const SingleSubscriptionList = (props) => {
const subscription = props.subscription; const { subscription } = props;
const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]);
if (notifications === null || notifications === undefined) { if (notifications === null || notifications === undefined) {
return <Loading />; return <Loading />;
} else if (notifications.length === 0) { }
if (notifications.length === 0) {
return <NoNotifications subscription={subscription} />; return <NoNotifications subscription={subscription} />;
} }
return <NotificationList id={subscription.id} notifications={notifications} messageBar={true} />; return <NotificationList id={subscription.id} notifications={notifications} messageBar />;
}; };
const NotificationList = (props) => { const NotificationList = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const pageSize = 20; const pageSize = 20;
const notifications = props.notifications; const { notifications } = props;
const [snackOpen, setSnackOpen] = useState(false); const [snackOpen, setSnackOpen] = useState(false);
const [maxCount, setMaxCount] = useState(pageSize); const [maxCount, setMaxCount] = useState(pageSize);
const count = Math.min(notifications.length, maxCount); const count = Math.min(notifications.length, maxCount);
useEffect(() => { useEffect(
return () => { () => () => {
setMaxCount(pageSize); setMaxCount(pageSize);
const main = document.getElementById("main"); const main = document.getElementById("main");
if (main) { if (main) {
main.scrollTo(0, 0); main.scrollTo(0, 0);
} }
}; },
}, [props.id]); [props.id]
);
return ( return (
<InfiniteScroll <InfiniteScroll
@ -129,8 +133,8 @@ const NotificationList = (props) => {
const NotificationItem = (props) => { const NotificationItem = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const notification = props.notification; const { notification } = props;
const attachment = notification.attachment; const { attachment } = notification;
const date = formatShortDateTime(notification.time); const date = formatShortDateTime(notification.time);
const otherTags = unmatchedTags(notification.tags); const otherTags = unmatchedTags(notification.tags);
const tags = otherTags.length > 0 ? otherTags.join(", ") : null; const tags = otherTags.length > 0 ? otherTags.join(", ") : null;
@ -272,7 +276,7 @@ const priorityFiles = {
const Attachment = (props) => { const Attachment = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const attachment = props.attachment; const { attachment } = props;
const expired = attachment.expires && attachment.expires < Date.now() / 1000; const expired = attachment.expires && attachment.expires < Date.now() / 1000;
const expires = 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/"); const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/");
@ -402,20 +406,18 @@ const Image = (props) => {
); );
}; };
const UserActions = (props) => { const UserActions = (props) => (
return ( <>
<> {props.notification.actions.map((action) => (
{props.notification.actions.map((action) => ( <UserAction key={action.id} notification={props.notification} action={action} />
<UserAction key={action.id} notification={props.notification} action={action} /> ))}
))} </>
</> );
);
};
const UserAction = (props) => { const UserAction = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const notification = props.notification; const { notification } = props;
const action = props.action; const { action } = props;
if (action.action === "broadcast") { if (action.action === "broadcast") {
return ( return (
<Tooltip title={t("notifications_actions_not_supported")}> <Tooltip title={t("notifications_actions_not_supported")}>
@ -426,7 +428,8 @@ const UserAction = (props) => {
</span> </span>
</Tooltip> </Tooltip>
); );
} else if (action.action === "view") { }
if (action.action === "view") {
return ( return (
<Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}> <Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
<Button <Button
@ -439,20 +442,21 @@ const UserAction = (props) => {
</Button> </Button>
</Tooltip> </Tooltip>
); );
} else if (action.action === "http") { }
if (action.action === "http") {
const method = action.method ?? "POST"; const method = action.method ?? "POST";
const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
return ( return (
<Tooltip <Tooltip
title={t("notifications_actions_http_request_title", { title={t("notifications_actions_http_request_title", {
method: method, method,
url: action.url, url: action.url,
})} })}
> >
<Button <Button
onClick={() => performHttpAction(notification, action)} onClick={() => performHttpAction(notification, action)}
aria-label={t("notifications_actions_http_request_title", { aria-label={t("notifications_actions_http_request_title", {
method: method, method,
url: action.url, url: action.url,
})} })}
> >
@ -493,7 +497,7 @@ const updateActionStatus = (notification, action, progress, error) => {
if (a.id !== action.id) { if (a.id !== action.id) {
return a; return a;
} }
return { ...a, progress: progress, error: error }; return { ...a, progress, error };
}); });
subscriptionManager.updateNotification(notification); subscriptionManager.updateNotification(notification);
}; };
@ -574,17 +578,15 @@ const NoSubscriptions = () => {
); );
}; };
const ForMoreDetails = () => { const ForMoreDetails = () => (
return ( <Trans
<Trans i18nKey="notifications_more_details"
i18nKey="notifications_more_details" components={{
components={{ websiteLink: <Link href="https://ntfy.sh" target="_blank" rel="noopener" />,
websiteLink: <Link href="https://ntfy.sh" target="_blank" rel="noopener" />, docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener" />,
docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener" />, }}
}} />
/> );
);
};
const Loading = () => { const Loading = () => {
const { t } = useTranslation(); const { t } = useTranslation();

View file

@ -37,8 +37,8 @@ const PopupMenu = (props) => {
}, },
}, },
}} }}
transformOrigin={{ horizontal: horizontal, vertical: "top" }} transformOrigin={{ horizontal, vertical: "top" }}
anchorOrigin={{ horizontal: horizontal, vertical: "bottom" }} anchorOrigin={{ horizontal, vertical: "bottom" }}
> >
{props.children} {props.children}
</Menu> </Menu>

View file

@ -1,8 +1,6 @@
import * as React from "react"; import * as React from "react";
export const PrefGroup = (props) => { export const PrefGroup = (props) => <div role="table">{props.children}</div>;
return <div role="table">{props.children}</div>;
};
export const Pref = (props) => { export const Pref = (props) => {
const justifyContent = props.alignTop ? "normal" : "center"; const justifyContent = props.alignTop ? "normal" : "center";
@ -24,7 +22,7 @@ export const Pref = (props) => {
flex: "1 0 40%", flex: "1 0 40%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: justifyContent, justifyContent,
paddingRight: "30px", paddingRight: "30px",
}} }}
> >
@ -44,7 +42,7 @@ export const Pref = (props) => {
flex: "1 0 calc(60% - 50px)", flex: "1 0 calc(60% - 50px)",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: justifyContent, justifyContent,
}} }}
> >
{props.children} {props.children}

View file

@ -17,8 +17,6 @@ import {
useMediaQuery, useMediaQuery,
} from "@mui/material"; } from "@mui/material";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import prefs from "../app/Prefs";
import { Paragraph } from "./styles";
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
@ -29,39 +27,39 @@ import MenuItem from "@mui/material/MenuItem";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import theme from "./theme";
import Dialog from "@mui/material/Dialog"; import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import { useTranslation } from "react-i18next";
import { Info } from "@mui/icons-material";
import { useOutletContext } from "react-router-dom";
import theme from "./theme";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
import { playSound, shuffle, sounds, validUrl } from "../app/utils"; import { playSound, shuffle, sounds, validUrl } from "../app/utils";
import { useTranslation } from "react-i18next";
import session from "../app/Session"; import session from "../app/Session";
import routes from "./routes"; import routes from "./routes";
import accountApi, { Permission, Role } from "../app/AccountApi"; import accountApi, { Permission, Role } from "../app/AccountApi";
import { Pref, PrefGroup } from "./Pref"; import { Pref, PrefGroup } from "./Pref";
import { Info } from "@mui/icons-material";
import { AccountContext } from "./App"; import { AccountContext } from "./App";
import { useOutletContext } from "react-router-dom"; import { Paragraph } from "./styles";
import prefs 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";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import { subscribeTopic } from "./SubscribeDialog"; import { subscribeTopic } from "./SubscribeDialog";
const Preferences = () => { const Preferences = () => (
return ( <Container maxWidth="md" sx={{ marginTop: 3, marginBottom: 3 }}>
<Container maxWidth="md" sx={{ marginTop: 3, marginBottom: 3 }}> <Stack spacing={3}>
<Stack spacing={3}> <Notifications />
<Notifications /> <Reservations />
<Reservations /> <Users />
<Users /> <Appearance />
<Appearance /> </Stack>
</Stack> </Container>
</Container> );
);
};
const Notifications = () => { const Notifications = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -107,7 +105,7 @@ const Sound = () => {
<div style={{ display: "flex", width: "100%" }}> <div style={{ display: "flex", width: "100%" }}>
<FormControl fullWidth variant="standard" sx={{ margin: 1 }}> <FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
<Select value={sound} onChange={handleChange} aria-labelledby={labelId}> <Select value={sound} onChange={handleChange} aria-labelledby={labelId}>
<MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem> <MenuItem value="none">{t("prefs_notifications_sound_no_sound")}</MenuItem>
{Object.entries(sounds).map((s) => ( {Object.entries(sounds).map((s) => (
<MenuItem key={s[0]} value={s[0]}> <MenuItem key={s[0]} value={s[0]}>
{s[1].label} {s[1].label}
@ -245,7 +243,7 @@ const Users = () => {
</Typography> </Typography>
<Paragraph> <Paragraph>
{t("prefs_users_description")} {t("prefs_users_description")}
{session.exists() && <>{" " + t("prefs_users_description_no_sync")}</>} {session.exists() && <>{` ${t("prefs_users_description_no_sync")}`}</>}
</Paragraph> </Paragraph>
{users?.length > 0 && <UserTable users={users} />} {users?.length > 0 && <UserTable users={users} />}
</CardContent> </CardContent>
@ -371,9 +369,9 @@ const UserDialog = (props) => {
})(); })();
const handleSubmit = async () => { const handleSubmit = async () => {
props.onSubmit({ props.onSubmit({
baseUrl: baseUrl, baseUrl,
username: username, username,
password: password, password,
}); });
}; };
useEffect(() => { useEffect(() => {
@ -479,7 +477,7 @@ const Language = () => {
const showFlags = !navigator.userAgent.includes("Windows"); const showFlags = !navigator.userAgent.includes("Windows");
let title = t("prefs_appearance_language_title"); let title = t("prefs_appearance_language_title");
if (showFlags) { if (showFlags) {
title += " " + randomFlags.join(" "); title += ` ${randomFlags.join(" ")}`;
} }
const handleChange = async (ev) => { const handleChange = async (ev) => {

View file

@ -1,13 +1,7 @@
import * as React from "react"; import * as React from "react";
import { useContext, useEffect, useRef, useState } from "react"; import { useContext, useEffect, useRef, useState } from "react";
import theme from "./theme";
import { Checkbox, Chip, FormControl, FormControlLabel, InputLabel, Link, Select, Tooltip, useMediaQuery } from "@mui/material"; import { Checkbox, Chip, FormControl, FormControlLabel, InputLabel, Link, Select, Tooltip, useMediaQuery } from "@mui/material";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import priority1 from "../img/priority-1.svg";
import priority2 from "../img/priority-2.svg";
import priority3 from "../img/priority-3.svg";
import priority4 from "../img/priority-4.svg";
import priority5 from "../img/priority-5.svg";
import Dialog from "@mui/material/Dialog"; import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
@ -17,14 +11,20 @@ import IconButton from "@mui/material/IconButton";
import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon"; import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon";
import { Close } from "@mui/icons-material"; import { Close } from "@mui/icons-material";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import { formatBytes, maybeWithAuth, topicShortUrl, topicUrl, validTopic, validUrl } from "../app/utils";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { Trans, useTranslation } from "react-i18next";
import priority1 from "../img/priority-1.svg";
import priority2 from "../img/priority-2.svg";
import priority3 from "../img/priority-3.svg";
import priority4 from "../img/priority-4.svg";
import priority5 from "../img/priority-5.svg";
import { formatBytes, maybeWithAuth, topicShortUrl, topicUrl, validTopic, validUrl } from "../app/utils";
import AttachmentIcon from "./AttachmentIcon"; import AttachmentIcon from "./AttachmentIcon";
import DialogFooter from "./DialogFooter"; import DialogFooter from "./DialogFooter";
import api from "../app/Api"; import api from "../app/Api";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
import EmojiPicker from "./EmojiPicker"; import EmojiPicker from "./EmojiPicker";
import { Trans, useTranslation } from "react-i18next"; import theme from "./theme";
import session from "../app/Session"; import session from "../app/Session";
import routes from "./routes"; import routes from "./routes";
import accountApi from "../app/AccountApi"; import accountApi from "../app/AccountApi";
@ -137,7 +137,7 @@ const PublishDialog = (props) => {
if (attachFile && message.trim()) { if (attachFile && message.trim()) {
url.searchParams.append("message", message.replaceAll("\n", "\\n").trim()); url.searchParams.append("message", message.replaceAll("\n", "\\n").trim());
} }
const body = attachFile ? attachFile : message; const body = attachFile || message;
try { try {
const user = await userManager.get(baseUrl); const user = await userManager.get(baseUrl);
const headers = maybeWithAuth({}, user); const headers = maybeWithAuth({}, user);
@ -183,13 +183,15 @@ const PublishDialog = (props) => {
remainingBytes: formatBytes(remainingBytes), remainingBytes: formatBytes(remainingBytes),
}) })
); );
} else if (fileSizeLimitReached) { }
if (fileSizeLimitReached) {
return setAttachFileError( return setAttachFileError(
t("publish_dialog_attachment_limits_file_reached", { t("publish_dialog_attachment_limits_file_reached", {
fileSizeLimit: formatBytes(fileSizeLimit), fileSizeLimit: formatBytes(fileSizeLimit),
}) })
); );
} else if (quotaReached) { }
if (quotaReached) {
return setAttachFileError( return setAttachFileError(
t("publish_dialog_attachment_limits_quota_reached", { t("publish_dialog_attachment_limits_quota_reached", {
remainingBytes: formatBytes(remainingBytes), remainingBytes: formatBytes(remainingBytes),
@ -377,7 +379,7 @@ const PublishDialog = (props) => {
key={`priorityMenuItem${priority}`} key={`priorityMenuItem${priority}`}
value={priority} value={priority}
aria-label={t("notifications_priority_x", { aria-label={t("notifications_priority_x", {
priority: priority, priority,
})} })}
> >
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
@ -385,7 +387,7 @@ const PublishDialog = (props) => {
src={priorities[priority].file} src={priorities[priority].file}
style={{ marginRight: "8px" }} style={{ marginRight: "8px" }}
alt={t("notifications_priority_x", { alt={t("notifications_priority_x", {
priority: priority, priority,
})} })}
/> />
<div>{priorities[priority].label}</div> <div>{priorities[priority].label}</div>
@ -533,7 +535,7 @@ const PublishDialog = (props) => {
/> />
</ClosableRow> </ClosableRow>
)} )}
<input type="file" ref={attachFileInput} onChange={handleAttachFileChanged} style={{ display: "none" }} aria-hidden={true} /> <input type="file" ref={attachFileInput} onChange={handleAttachFileChanged} style={{ display: "none" }} aria-hidden />
{showAttachFile && ( {showAttachFile && (
<AttachmentBox <AttachmentBox
file={attachFile} file={attachFile}
@ -707,13 +709,11 @@ const PublishDialog = (props) => {
); );
}; };
const Row = (props) => { const Row = (props) => (
return ( <div style={{ display: "flex" }} role="row">
<div style={{ display: "flex" }} role="row"> {props.children}
{props.children} </div>
</div> );
);
};
const ClosableRow = (props) => { const ClosableRow = (props) => {
const closable = props.hasOwnProperty("closable") ? props.closable : true; const closable = props.hasOwnProperty("closable") ? props.closable : true;
@ -748,7 +748,7 @@ const DialogIconButton = (props) => {
const AttachmentBox = (props) => { const AttachmentBox = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const file = props.file; const { file } = props;
return ( return (
<> <>
<Typography variant="body1" sx={{ marginTop: 2 }}> <Typography variant="body1" sx={{ marginTop: 2 }}>
@ -811,13 +811,7 @@ const ExpandingTextField = (props) => {
}, [props.value]); }, [props.value]);
return ( return (
<> <>
<Typography <Typography ref={invisibleFieldRef} component="span" variant={props.variant} aria-hidden sx={{ position: "absolute", left: "-200%" }}>
ref={invisibleFieldRef}
component="span"
variant={props.variant}
aria-hidden={true}
sx={{ position: "absolute", left: "-200%" }}
>
{props.value} {props.value}
</Typography> </Typography>
<TextField <TextField

View file

@ -7,18 +7,18 @@ import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
import { Alert, FormControl, Select, useMediaQuery } from "@mui/material"; import { Alert, FormControl, Select, useMediaQuery } from "@mui/material";
import theme from "./theme";
import { validTopic } from "../app/utils";
import DialogFooter from "./DialogFooter";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import session from "../app/Session";
import routes from "./routes";
import accountApi, { Permission } from "../app/AccountApi";
import ReserveTopicSelect from "./ReserveTopicSelect";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import { Check, DeleteForever } from "@mui/icons-material"; import { Check, DeleteForever } from "@mui/icons-material";
import theme from "./theme";
import { validTopic } from "../app/utils";
import DialogFooter from "./DialogFooter";
import session from "../app/Session";
import routes from "./routes";
import accountApi, { Permission } from "../app/AccountApi";
import ReserveTopicSelect from "./ReserveTopicSelect";
import { TopicReservedError, UnauthorizedError } from "../app/errors"; import { TopicReservedError, UnauthorizedError } from "../app/errors";
export const ReserveAddDialog = (props) => { export const ReserveAddDialog = (props) => {
@ -164,7 +164,7 @@ export const ReserveDeleteDialog = (props) => {
</ListItemIcon> </ListItemIcon>
<ListItemText primary={t("reservation_delete_dialog_action_keep_title")} /> <ListItemText primary={t("reservation_delete_dialog_action_keep_title")} />
</MenuItem> </MenuItem>
<MenuItem value={true}> <MenuItem value>
<ListItemIcon> <ListItemIcon>
<DeleteForever /> <DeleteForever />
</ListItemIcon> </ListItemIcon>

View file

@ -2,21 +2,13 @@ import * as React from "react";
import { Lock, Public } from "@mui/icons-material"; import { Lock, Public } from "@mui/icons-material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
export const PermissionReadWrite = React.forwardRef((props, ref) => { export const PermissionReadWrite = React.forwardRef((props, ref) => <PermissionInternal icon={Public} ref={ref} {...props} />);
return <PermissionInternal icon={Public} ref={ref} {...props} />;
});
export const PermissionDenyAll = React.forwardRef((props, ref) => { export const PermissionDenyAll = React.forwardRef((props, ref) => <PermissionInternal icon={Lock} ref={ref} {...props} />);
return <PermissionInternal icon={Lock} ref={ref} {...props} />;
});
export const PermissionRead = React.forwardRef((props, ref) => { export const PermissionRead = React.forwardRef((props, ref) => <PermissionInternal icon={Public} text="R" ref={ref} {...props} />);
return <PermissionInternal icon={Public} text="R" ref={ref} {...props} />;
});
export const PermissionWrite = React.forwardRef((props, ref) => { export const PermissionWrite = React.forwardRef((props, ref) => <PermissionInternal icon={Public} text="W" ref={ref} {...props} />);
return <PermissionInternal icon={Public} text="W" ref={ref} {...props} />;
});
const PermissionInternal = React.forwardRef((props, ref) => { const PermissionInternal = React.forwardRef((props, ref) => {
const size = props.size ?? "medium"; const size = props.size ?? "medium";

View file

@ -3,17 +3,17 @@ import { useState } from "react";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import routes from "./routes";
import session from "../app/Session";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import AvatarBox from "./AvatarBox";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import WarningAmberIcon from "@mui/icons-material/WarningAmber";
import accountApi from "../app/AccountApi";
import { InputAdornment } from "@mui/material"; import { InputAdornment } from "@mui/material";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import { Visibility, VisibilityOff } from "@mui/icons-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 { AccountCreateLimitReachedError, UserExistsError } from "../app/errors"; import { AccountCreateLimitReachedError, UserExistsError } from "../app/errors";
const Signup = () => { const Signup = () => {

View file

@ -7,6 +7,7 @@ import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
import { Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery } from "@mui/material"; import { Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery } from "@mui/material";
import { useTranslation } from "react-i18next";
import theme from "./theme"; import theme from "./theme";
import api from "../app/Api"; import api from "../app/Api";
import { randomAlphanumericString, topicUrl, validTopic, validUrl } from "../app/utils"; import { randomAlphanumericString, topicUrl, validTopic, validUrl } from "../app/utils";
@ -14,7 +15,6 @@ import userManager from "../app/UserManager";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import poller from "../app/Poller"; import poller from "../app/Poller";
import DialogFooter from "./DialogFooter"; import DialogFooter from "./DialogFooter";
import { useTranslation } from "react-i18next";
import session from "../app/Session"; import session from "../app/Session";
import routes from "./routes"; import routes from "./routes";
import accountApi, { Permission, Role } from "../app/AccountApi"; import accountApi, { Permission, Role } from "../app/AccountApi";
@ -33,7 +33,7 @@ const SubscribeDialog = (props) => {
const handleSuccess = async () => { const handleSuccess = async () => {
console.log(`[SubscribeDialog] Subscribing to topic ${topic}`); console.log(`[SubscribeDialog] Subscribing to topic ${topic}`);
const actualBaseUrl = baseUrl ? baseUrl : config.base_url; const actualBaseUrl = baseUrl || config.base_url;
const subscription = await subscribeTopic(actualBaseUrl, topic); const subscription = await subscribeTopic(actualBaseUrl, topic);
poller.pollInBackground(subscription); // Dangle! poller.pollInBackground(subscription); // Dangle!
props.onSuccess(subscription); props.onSuccess(subscription);
@ -66,7 +66,7 @@ const SubscribePage = (props) => {
const [anotherServerVisible, setAnotherServerVisible] = useState(false); const [anotherServerVisible, setAnotherServerVisible] = useState(false);
const [everyone, setEveryone] = useState(Permission.DENY_ALL); const [everyone, setEveryone] = useState(Permission.DENY_ALL);
const baseUrl = anotherServerVisible ? props.baseUrl : config.base_url; const baseUrl = anotherServerVisible ? props.baseUrl : config.base_url;
const topic = props.topic; const { topic } = props;
const existingTopicUrls = props.subscriptions.map((s) => topicUrl(s.baseUrl, s.topic)); const existingTopicUrls = props.subscriptions.map((s) => topicUrl(s.baseUrl, s.topic));
const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map((s) => s.baseUrl)])).filter( const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map((s) => s.baseUrl)])).filter(
(s) => s !== config.base_url (s) => s !== config.base_url
@ -86,14 +86,13 @@ const SubscribePage = (props) => {
if (user) { if (user) {
setError( setError(
t("subscribe_dialog_error_user_not_authorized", { t("subscribe_dialog_error_user_not_authorized", {
username: username, username,
}) })
); );
return; return;
} else {
props.onNeedsLogin();
return;
} }
props.onNeedsLogin();
return;
} }
// Reserve topic (if requested) // Reserve topic (if requested)
@ -125,10 +124,9 @@ const SubscribePage = (props) => {
if (anotherServerVisible) { if (anotherServerVisible) {
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic)); const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl; return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
} else {
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.base_url, topic));
return validTopic(topic) && !isExistingTopicUrl;
} }
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.base_url, topic));
return validTopic(topic) && !isExistingTopicUrl;
})(); })();
const updateBaseUrl = (ev, newVal) => { const updateBaseUrl = (ev, newVal) => {
@ -242,14 +240,14 @@ const LoginPage = (props) => {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const baseUrl = props.baseUrl ? props.baseUrl : config.base_url; const baseUrl = props.baseUrl ? props.baseUrl : config.base_url;
const topic = props.topic; const { topic } = props;
const handleLogin = async () => { const handleLogin = async () => {
const user = { baseUrl, username, password }; const user = { baseUrl, username, password };
const success = await api.topicAuth(baseUrl, topic, user); const success = await api.topicAuth(baseUrl, topic, user);
if (!success) { if (!success) {
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
setError(t("subscribe_dialog_error_user_not_authorized", { username: username })); setError(t("subscribe_dialog_error_user_not_authorized", { username }));
return; return;
} }
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);

View file

@ -7,20 +7,20 @@ import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
import { Chip, InputAdornment, Portal, Snackbar, useMediaQuery } from "@mui/material"; import { Chip, InputAdornment, Portal, Snackbar, useMediaQuery } from "@mui/material";
import theme from "./theme";
import subscriptionManager from "../app/SubscriptionManager";
import DialogFooter from "./DialogFooter";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import accountApi, { Role } from "../app/AccountApi";
import session from "../app/Session";
import routes from "./routes";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import PopupMenu from "./PopupMenu";
import { formatShortDateTime, shuffle } from "../app/utils";
import api from "../app/Api";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import { Clear } from "@mui/icons-material"; import { Clear } from "@mui/icons-material";
import theme from "./theme";
import subscriptionManager from "../app/SubscriptionManager";
import DialogFooter from "./DialogFooter";
import accountApi, { Role } from "../app/AccountApi";
import session from "../app/Session";
import routes from "./routes";
import PopupMenu from "./PopupMenu";
import { formatShortDateTime, shuffle } from "../app/utils";
import api from "../app/Api";
import { AccountContext } from "./App"; import { AccountContext } from "./App";
import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs"; import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs";
import { UnauthorizedError } from "../app/errors"; import { UnauthorizedError } from "../app/errors";
@ -34,7 +34,7 @@ export const SubscriptionPopup = (props) => {
const [reserveEditDialogOpen, setReserveEditDialogOpen] = useState(false); const [reserveEditDialogOpen, setReserveEditDialogOpen] = useState(false);
const [reserveDeleteDialogOpen, setReserveDeleteDialogOpen] = useState(false); const [reserveDeleteDialogOpen, setReserveDeleteDialogOpen] = useState(false);
const [showPublishError, setShowPublishError] = useState(false); const [showPublishError, setShowPublishError] = useState(false);
const subscription = props.subscription; const { subscription } = props;
const placement = props.placement ?? "left"; const placement = props.placement ?? "left";
const reservations = account?.reservations || []; const reservations = account?.reservations || [];
@ -64,8 +64,8 @@ export const SubscriptionPopup = (props) => {
}; };
const handleSendTestMessage = async () => { const handleSendTestMessage = async () => {
const baseUrl = props.subscription.baseUrl; const { baseUrl } = props.subscription;
const topic = props.subscription.topic; const { topic } = props.subscription;
const tags = shuffle([ const tags = shuffle([
"grinning", "grinning",
"octopus", "octopus",
@ -110,9 +110,9 @@ export const SubscriptionPopup = (props) => {
])[0]; ])[0];
try { try {
await api.publish(baseUrl, topic, message, { await api.publish(baseUrl, topic, message, {
title: title, title,
priority: priority, priority,
tags: tags, tags,
}); });
} catch (e) { } catch (e) {
console.log(`[SubscriptionPopup] Error publishing message`, e); console.log(`[SubscriptionPopup] Error publishing message`, e);
@ -201,7 +201,7 @@ export const SubscriptionPopup = (props) => {
const DisplayNameDialog = (props) => { const DisplayNameDialog = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const subscription = props.subscription; const { subscription } = props;
const [error, setError] = useState(""); const [error, setError] = useState("");
const [displayName, setDisplayName] = useState(subscription.displayName ?? ""); const [displayName, setDisplayName] = useState(subscription.displayName ?? "");
const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
@ -265,9 +265,11 @@ export const ReserveLimitChip = () => {
const { account } = useContext(AccountContext); const { account } = useContext(AccountContext);
if (account?.role === Role.ADMIN || account?.stats.reservations_remaining > 0) { if (account?.role === Role.ADMIN || account?.stats.reservations_remaining > 0) {
return <></>; return <></>;
} else if (config.enable_payments) { }
if (config.enable_payments) {
return account?.limits.reservations > 0 ? <LimitReachedChip /> : <ProChip />; return account?.limits.reservations > 0 ? <LimitReachedChip /> : <ProChip />;
} else if (account) { }
if (account) {
return <LimitReachedChip />; return <LimitReachedChip />;
} }
return <></>; return <></>;
@ -294,7 +296,7 @@ export const ProChip = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Chip <Chip
label={"ntfy Pro"} label="ntfy Pro"
variant="outlined" variant="outlined"
color="primary" color="primary"
sx={{ sx={{

View file

@ -4,15 +4,9 @@ import Dialog from "@mui/material/Dialog";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
import { Alert, CardActionArea, CardContent, Chip, Link, ListItem, Switch, useMediaQuery } from "@mui/material"; import { Alert, CardActionArea, CardContent, Chip, Link, ListItem, Switch, useMediaQuery } from "@mui/material";
import theme from "./theme";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import accountApi, { SubscriptionInterval } from "../app/AccountApi";
import session from "../app/Session";
import routes from "./routes";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { AccountContext } from "./App";
import { formatBytes, formatNumber, formatPrice, formatShortDate } from "../app/utils";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import List from "@mui/material/List"; import List from "@mui/material/List";
import { Check, Close } from "@mui/icons-material"; import { Check, Close } from "@mui/icons-material";
@ -20,9 +14,15 @@ import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { UnauthorizedError } from "../app/errors";
import DialogContentText from "@mui/material/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import { UnauthorizedError } from "../app/errors";
import { formatBytes, formatNumber, formatPrice, formatShortDate } from "../app/utils";
import { AccountContext } from "./App";
import routes from "./routes";
import session from "../app/Session";
import accountApi, { SubscriptionInterval } from "../app/AccountApi";
import theme from "./theme";
const UpgradeDialog = (props) => { const UpgradeDialog = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -52,7 +52,9 @@ const UpgradeDialog = (props) => {
const currentTierCode = currentTier?.code; // May be undefined const currentTierCode = currentTier?.code; // May be undefined
// Figure out buttons, labels and the submit action // Figure out buttons, labels and the submit action
let submitAction, submitButtonLabel, banner; let submitAction;
let submitButtonLabel;
let banner;
if (!account) { if (!account) {
submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup"); submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup");
submitAction = Action.REDIRECT_SIGNUP; submitAction = Action.REDIRECT_SIGNUP;
@ -112,8 +114,8 @@ const UpgradeDialog = (props) => {
}; };
// Figure out discount // Figure out discount
let discount = 0, let discount = 0;
upto = false; let upto = false;
if (newTier?.prices) { if (newTier?.prices) {
discount = Math.round(((newTier.prices.month * 12) / newTier.prices.year - 1) * 100); discount = Math.round(((newTier.prices.month * 12) / newTier.prices.year - 1) * 100);
} else { } else {
@ -157,8 +159,8 @@ const UpgradeDialog = (props) => {
<Chip <Chip
label={ label={
upto upto
? t("account_upgrade_dialog_interval_yearly_discount_save_up_to", { discount: discount }) ? t("account_upgrade_dialog_interval_yearly_discount_save_up_to", { discount })
: t("account_upgrade_dialog_interval_yearly_discount_save", { discount: discount }) : t("account_upgrade_dialog_interval_yearly_discount_save", { discount })
} }
color="primary" color="primary"
size="small" size="small"
@ -269,9 +271,11 @@ const UpgradeDialog = (props) => {
const TierCard = (props) => { const TierCard = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const tier = props.tier; const { tier } = props;
let cardStyle, labelStyle, labelText; let cardStyle;
let labelStyle;
let labelText;
if (props.selected) { if (props.selected) {
cardStyle = { background: "#eee", border: "3px solid #338574" }; cardStyle = { background: "#eee", border: "3px solid #338574" };
labelStyle = { background: "#338574", color: "white" }; labelStyle = { background: "#338574", color: "white" };
@ -392,25 +396,19 @@ const TierCard = (props) => {
); );
}; };
const Feature = (props) => { const Feature = (props) => <FeatureItem feature>{props.children}</FeatureItem>;
return <FeatureItem feature={true}>{props.children}</FeatureItem>;
};
const NoFeature = (props) => { const NoFeature = (props) => <FeatureItem feature={false}>{props.children}</FeatureItem>;
return <FeatureItem feature={false}>{props.children}</FeatureItem>;
};
const FeatureItem = (props) => { const FeatureItem = (props) => (
return ( <ListItem disableGutters sx={{ m: 0, p: 0 }}>
<ListItem disableGutters sx={{ m: 0, p: 0 }}> <ListItemIcon sx={{ minWidth: "24px" }}>
<ListItemIcon sx={{ minWidth: "24px" }}> {props.feature && <Check fontSize="small" sx={{ color: "#338574" }} />}
{props.feature && <Check fontSize="small" sx={{ color: "#338574" }} />} {!props.feature && <Close fontSize="small" sx={{ color: "gray" }} />}
{!props.feature && <Close fontSize="small" sx={{ color: "gray" }} />} </ListItemIcon>
</ListItemIcon> <ListItemText sx={{ mt: "2px", mb: "2px" }} primary={<Typography variant="body1">{props.children}</Typography>} />
<ListItemText sx={{ mt: "2px", mb: "2px" }} primary={<Typography variant="body1">{props.children}</Typography>} /> </ListItem>
</ListItem> );
);
};
const Action = { const Action = {
REDIRECT_SIGNUP: 1, REDIRECT_SIGNUP: 1,

View file

@ -61,7 +61,7 @@ export const useConnectionListeners = (account, subscriptions, users) => {
}; };
}, },
// We have to disable dep checking for "navigate". This is fine, it never changes. // We have to disable dep checking for "navigate". This is fine, it never changes.
// eslint-disable-next-line
[] []
); );

View file

@ -1,7 +1,7 @@
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import theme from "./theme";
import Container from "@mui/material/Container"; import Container from "@mui/material/Container";
import { Backdrop, styled } from "@mui/material"; import { Backdrop, styled } from "@mui/material";
import theme from "./theme";
export const Paragraph = styled(Typography)({ export const Paragraph = styled(Typography)({
paddingTop: 8, paddingTop: 8,