Make manual eslint fixes
These are safe fixes, more complicated fixes can be done separately (just disabled those errors for now). - Reorder declarations to fix `no-use-before-define` - Rename parameters for `no-shadow` - Remove unused parameters, functions, imports - Switch from `++` and `—` to `+= 1` and `-= 1` for `no-unary` - Use object spreading instead of parameter reassignment in auth utils - Use `window.location` instead of `location` global - Use inline JSX strings instead of unescaped values -
This commit is contained in:
parent
8319f1cf26
commit
59011c8a32
20 changed files with 369 additions and 351 deletions
|
@ -261,12 +261,12 @@ class AccountApi {
|
||||||
|
|
||||||
async createBillingSubscription(tier, interval) {
|
async createBillingSubscription(tier, interval) {
|
||||||
console.log(`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`);
|
console.log(`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`);
|
||||||
return await this.upsertBillingSubscription("POST", tier, interval);
|
return this.upsertBillingSubscription("POST", tier, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateBillingSubscription(tier, interval) {
|
async updateBillingSubscription(tier, interval) {
|
||||||
console.log(`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`);
|
console.log(`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`);
|
||||||
return await this.upsertBillingSubscription("PUT", tier, interval);
|
return this.upsertBillingSubscription("PUT", tier, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertBillingSubscription(method, tier, interval) {
|
async upsertBillingSubscription(method, tier, interval) {
|
||||||
|
@ -279,7 +279,7 @@ class AccountApi {
|
||||||
interval,
|
interval,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
return await response.json(); // May throw SyntaxError
|
return response.json(); // May throw SyntaxError
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteBillingSubscription() {
|
async deleteBillingSubscription() {
|
||||||
|
@ -298,7 +298,7 @@ class AccountApi {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: withBearerAuth({}, session.token()),
|
headers: withBearerAuth({}, session.token()),
|
||||||
});
|
});
|
||||||
return await response.json(); // May throw SyntaxError
|
return response.json(); // May throw SyntaxError
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyPhoneNumber(phoneNumber, channel) {
|
async verifyPhoneNumber(phoneNumber, channel) {
|
||||||
|
@ -327,7 +327,7 @@ class AccountApi {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deletePhoneNumber(phoneNumber, code) {
|
async deletePhoneNumber(phoneNumber) {
|
||||||
const url = accountPhoneUrl(config.base_url);
|
const url = accountPhoneUrl(config.base_url);
|
||||||
console.log(`[AccountApi] Deleting phone number ${url}`);
|
console.log(`[AccountApi] Deleting phone number ${url}`);
|
||||||
await fetchOrThrow(url, {
|
await fetchOrThrow(url, {
|
||||||
|
@ -369,6 +369,7 @@ class AccountApi {
|
||||||
if (e instanceof UnauthorizedError) {
|
if (e instanceof UnauthorizedError) {
|
||||||
session.resetAndRedirect(routes.login);
|
session.resetAndRedirect(routes.login);
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
import { basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs } from "./utils";
|
import { basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs } from "./utils";
|
||||||
|
|
||||||
const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
|
const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
|
||||||
|
|
||||||
|
export class ConnectionState {
|
||||||
|
static Connected = "connected";
|
||||||
|
|
||||||
|
static Connecting = "connecting";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A connection contains a single WebSocket connection for one topic. It handles its connection
|
* A connection contains a single WebSocket connection for one topic. It handles its connection
|
||||||
* status itself, including reconnect attempts and backoff.
|
* status itself, including reconnect attempts and backoff.
|
||||||
|
@ -63,7 +70,7 @@ class Connection {
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
} else {
|
} else {
|
||||||
const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length - 1)];
|
const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length - 1)];
|
||||||
this.retryCount++;
|
this.retryCount += 1;
|
||||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`);
|
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`);
|
||||||
this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000);
|
this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000);
|
||||||
this.onStateChanged(this.subscriptionId, ConnectionState.Connecting);
|
this.onStateChanged(this.subscriptionId, ConnectionState.Connecting);
|
||||||
|
@ -108,10 +115,4 @@ class Connection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConnectionState {
|
|
||||||
static Connected = "connected";
|
|
||||||
|
|
||||||
static Connecting = "connecting";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Connection;
|
export default Connection;
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import Connection from "./Connection";
|
import Connection from "./Connection";
|
||||||
import { hashCode } from "./utils";
|
import { hashCode } from "./utils";
|
||||||
|
|
||||||
|
const makeConnectionId = async (subscription, user) =>
|
||||||
|
user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The connection manager keeps track of active connections (WebSocket connections, see Connection).
|
* The connection manager keeps track of active connections (WebSocket connections, see Connection).
|
||||||
*
|
*
|
||||||
|
@ -69,8 +72,8 @@ class ConnectionManager {
|
||||||
topic,
|
topic,
|
||||||
user,
|
user,
|
||||||
since,
|
since,
|
||||||
(subscriptionId, notification) => this.notificationReceived(subscriptionId, notification),
|
(subId, notification) => this.notificationReceived(subId, notification),
|
||||||
(subscriptionId, state) => this.stateChanged(subscriptionId, state)
|
(subId, state) => this.stateChanged(subId, state)
|
||||||
);
|
);
|
||||||
this.connections.set(connectionId, connection);
|
this.connections.set(connectionId, connection);
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -112,8 +115,5 @@ class ConnectionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeConnectionId = async (subscription, user) =>
|
|
||||||
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;
|
||||||
|
|
|
@ -29,7 +29,7 @@ class Notifier {
|
||||||
icon: logo,
|
icon: logo,
|
||||||
});
|
});
|
||||||
if (notification.click) {
|
if (notification.click) {
|
||||||
n.onclick = (e) => openUrl(notification.click);
|
n.onclick = () => openUrl(notification.click);
|
||||||
} else {
|
} else {
|
||||||
n.onclick = () => onClickFallback(subscription);
|
n.onclick = () => onClickFallback(subscription);
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ class Notifier {
|
||||||
* is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification
|
* is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification
|
||||||
*/
|
*/
|
||||||
contextSupported() {
|
contextSupported() {
|
||||||
return location.protocol === "https:" || location.hostname.match("^127.") || location.hostname === "localhost";
|
return window.location.protocol === "https:" || window.location.hostname.match("^127.") || window.location.hostname === "localhost";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,8 @@ class Poller {
|
||||||
const subscriptions = await subscriptionManager.all();
|
const subscriptions = await subscriptionManager.all();
|
||||||
for (const s of subscriptions) {
|
for (const s of subscriptions) {
|
||||||
try {
|
try {
|
||||||
|
// TODO(eslint): Switch to Promise.all
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await this.poll(s);
|
await this.poll(s);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[Poller] Error polling ${s.id}`, e);
|
console.log(`[Poller] Error polling ${s.id}`, e);
|
||||||
|
|
|
@ -7,6 +7,7 @@ class SubscriptionManager {
|
||||||
const subscriptions = await db.subscriptions.toArray();
|
const subscriptions = await db.subscriptions.toArray();
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
subscriptions.map(async (s) => {
|
subscriptions.map(async (s) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
s.new = await db.notifications.where({ subscriptionId: s.id, new: 1 }).count();
|
s.new = await db.notifications.where({ subscriptionId: s.id, new: 1 }).count();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -14,7 +15,7 @@ class SubscriptionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(subscriptionId) {
|
async get(subscriptionId) {
|
||||||
return await db.subscriptions.get(subscriptionId);
|
return db.subscriptions.get(subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async add(baseUrl, topic, internal) {
|
async add(baseUrl, topic, internal) {
|
||||||
|
@ -40,10 +41,14 @@ class SubscriptionManager {
|
||||||
|
|
||||||
// Add remote subscriptions
|
// Add remote subscriptions
|
||||||
const 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 += 1) {
|
||||||
const remote = remoteSubscriptions[i];
|
const remote = remoteSubscriptions[i];
|
||||||
|
// TODO(eslint): Switch to Promise.all
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
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;
|
||||||
|
// TODO(eslint): Switch to Promise.all
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
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, // May be null!
|
reservation, // May be null!
|
||||||
|
@ -53,10 +58,12 @@ class SubscriptionManager {
|
||||||
|
|
||||||
// Remove local subscriptions that do not exist remotely
|
// Remove local subscriptions that do not exist remotely
|
||||||
const localSubscriptions = await db.subscriptions.toArray();
|
const localSubscriptions = await db.subscriptions.toArray();
|
||||||
for (let i = 0; i < localSubscriptions.length; i++) {
|
for (let i = 0; i < localSubscriptions.length; i += 1) {
|
||||||
const local = localSubscriptions[i];
|
const local = localSubscriptions[i];
|
||||||
const remoteExists = remoteIds.includes(local.id);
|
const remoteExists = remoteIds.includes(local.id);
|
||||||
if (!local.internal && !remoteExists) {
|
if (!local.internal && !remoteExists) {
|
||||||
|
// TODO(eslint): Switch to Promise.all
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await this.remove(local.id);
|
await this.remove(local.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,6 +108,7 @@ class SubscriptionManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
notification.new = 1; // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
|
notification.new = 1; // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
|
||||||
await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab
|
await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab
|
||||||
await db.subscriptions.update(subscriptionId, {
|
await db.subscriptions.update(subscriptionId, {
|
||||||
|
|
|
@ -1,37 +1,6 @@
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
// This is a subset of, and the counterpart to errors.go
|
// This is a subset of, and the counterpart to errors.go
|
||||||
|
|
||||||
export const fetchOrThrow = async (url, options) => {
|
|
||||||
const response = await fetch(url, options);
|
|
||||||
if (response.status !== 200) {
|
|
||||||
await throwAppError(response);
|
|
||||||
}
|
|
||||||
return response; // Promise!
|
|
||||||
};
|
|
||||||
|
|
||||||
export const throwAppError = async (response) => {
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
|
||||||
console.log(`[Error] HTTP ${response.status}`, response);
|
|
||||||
throw new UnauthorizedError();
|
|
||||||
}
|
|
||||||
const error = await maybeToJson(response);
|
|
||||||
if (error?.code) {
|
|
||||||
console.log(`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${error.error || ""}`, response);
|
|
||||||
if (error.code === UserExistsError.CODE) {
|
|
||||||
throw new UserExistsError();
|
|
||||||
} else if (error.code === TopicReservedError.CODE) {
|
|
||||||
throw new TopicReservedError();
|
|
||||||
} else if (error.code === AccountCreateLimitReachedError.CODE) {
|
|
||||||
throw new AccountCreateLimitReachedError();
|
|
||||||
} else if (error.code === IncorrectPasswordError.CODE) {
|
|
||||||
throw new IncorrectPasswordError();
|
|
||||||
} else if (error?.error) {
|
|
||||||
throw new Error(`Error ${error.code}: ${error.error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response);
|
|
||||||
throw new Error(`Unexpected response ${response.status}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const maybeToJson = async (response) => {
|
const maybeToJson = async (response) => {
|
||||||
try {
|
try {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
|
@ -77,3 +46,35 @@ export class IncorrectPasswordError extends Error {
|
||||||
super("Password incorrect");
|
super("Password incorrect");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const throwAppError = async (response) => {
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
console.log(`[Error] HTTP ${response.status}`, response);
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
const error = await maybeToJson(response);
|
||||||
|
if (error?.code) {
|
||||||
|
console.log(`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${error.error || ""}`, response);
|
||||||
|
if (error.code === UserExistsError.CODE) {
|
||||||
|
throw new UserExistsError();
|
||||||
|
} else if (error.code === TopicReservedError.CODE) {
|
||||||
|
throw new TopicReservedError();
|
||||||
|
} else if (error.code === AccountCreateLimitReachedError.CODE) {
|
||||||
|
throw new AccountCreateLimitReachedError();
|
||||||
|
} else if (error.code === IncorrectPasswordError.CODE) {
|
||||||
|
throw new IncorrectPasswordError();
|
||||||
|
} else if (error?.error) {
|
||||||
|
throw new Error(`Error ${error.code}: ${error.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response);
|
||||||
|
throw new Error(`Unexpected response ${response.status}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchOrThrow = async (url, options) => {
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
await throwAppError(response);
|
||||||
|
}
|
||||||
|
return response; // Promise!
|
||||||
|
};
|
||||||
|
|
|
@ -9,6 +9,10 @@ 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";
|
||||||
|
|
||||||
|
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
||||||
|
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
||||||
|
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
|
||||||
|
export const expandSecureUrl = (url) => `https://${url}`;
|
||||||
export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
|
export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
|
||||||
export const topicUrlWs = (baseUrl, topic) =>
|
export const topicUrlWs = (baseUrl, topic) =>
|
||||||
`${topicUrl(baseUrl, topic)}/ws`.replaceAll("https://", "wss://").replaceAll("http://", "ws://");
|
`${topicUrl(baseUrl, topic)}/ws`.replaceAll("https://", "wss://").replaceAll("http://", "ws://");
|
||||||
|
@ -28,13 +32,11 @@ export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account
|
||||||
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
|
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
|
||||||
export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
|
export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
|
||||||
export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`;
|
export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`;
|
||||||
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
|
||||||
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
|
||||||
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
|
|
||||||
export const expandSecureUrl = (url) => `https://${url}`;
|
|
||||||
|
|
||||||
export const validUrl = (url) => url.match(/^https?:\/\/.+/);
|
export const validUrl = (url) => url.match(/^https?:\/\/.+/);
|
||||||
|
|
||||||
|
export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic);
|
||||||
|
|
||||||
export const validTopic = (topic) => {
|
export const validTopic = (topic) => {
|
||||||
if (disallowedTopic(topic)) {
|
if (disallowedTopic(topic)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -42,8 +44,6 @@ 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) => 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;
|
||||||
|
@ -67,13 +67,6 @@ const toEmojis = (tags) => {
|
||||||
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) => {
|
|
||||||
if (m.title) {
|
|
||||||
return formatTitle(m);
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatTitle = (m) => {
|
export const formatTitle = (m) => {
|
||||||
const emojiList = toEmojis(m.tags);
|
const emojiList = toEmojis(m.tags);
|
||||||
if (emojiList.length > 0) {
|
if (emojiList.length > 0) {
|
||||||
|
@ -82,6 +75,13 @@ export const formatTitle = (m) => {
|
||||||
return m.title;
|
return m.title;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatTitleWithDefault = (m, fallback) => {
|
||||||
|
if (m.title) {
|
||||||
|
return formatTitle(m);
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
export const formatMessage = (m) => {
|
export const formatMessage = (m) => {
|
||||||
if (m.title) {
|
if (m.title) {
|
||||||
return m.message;
|
return m.message;
|
||||||
|
@ -98,6 +98,25 @@ export const unmatchedTags = (tags) => {
|
||||||
return tags.filter((tag) => !(tag in emojis));
|
return tags.filter((tag) => !(tag in emojis));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const encodeBase64 = (s) => Base64.encode(s);
|
||||||
|
|
||||||
|
export const encodeBase64Url = (s) => Base64.encodeURI(s);
|
||||||
|
|
||||||
|
export const bearerAuth = (token) => `Bearer ${token}`;
|
||||||
|
|
||||||
|
export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`;
|
||||||
|
|
||||||
|
export const withBearerAuth = (headers, token) => ({ ...headers, Authorization: bearerAuth(token) });
|
||||||
|
|
||||||
|
export const maybeWithBearerAuth = (headers, token) => {
|
||||||
|
if (token) {
|
||||||
|
return withBearerAuth(headers, token);
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withBasicAuth = (headers, username, password) => ({ ...headers, Authorization: basicAuth(username, password) });
|
||||||
|
|
||||||
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);
|
||||||
|
@ -108,31 +127,6 @@ export const maybeWithAuth = (headers, user) => {
|
||||||
return headers;
|
return headers;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const maybeWithBearerAuth = (headers, token) => {
|
|
||||||
if (token) {
|
|
||||||
return withBearerAuth(headers, token);
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const withBasicAuth = (headers, username, password) => {
|
|
||||||
headers.Authorization = basicAuth(username, password);
|
|
||||||
return headers;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`;
|
|
||||||
|
|
||||||
export const withBearerAuth = (headers, token) => {
|
|
||||||
headers.Authorization = bearerAuth(token);
|
|
||||||
return headers;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const bearerAuth = (token) => `Bearer ${token}`;
|
|
||||||
|
|
||||||
export const encodeBase64 = (s) => Base64.encode(s);
|
|
||||||
|
|
||||||
export const encodeBase64Url = (s) => Base64.encodeURI(s);
|
|
||||||
|
|
||||||
export const maybeAppendActionErrors = (message, notification) => {
|
export const maybeAppendActionErrors = (message, notification) => {
|
||||||
const actionErrors = (notification.actions ?? [])
|
const actionErrors = (notification.actions ?? [])
|
||||||
.map((action) => action.error)
|
.map((action) => action.error)
|
||||||
|
@ -147,10 +141,12 @@ export const maybeAppendActionErrors = (message, notification) => {
|
||||||
export const shuffle = (arr) => {
|
export const shuffle = (arr) => {
|
||||||
let j;
|
let j;
|
||||||
let x;
|
let x;
|
||||||
for (let index = arr.length - 1; index > 0; index--) {
|
for (let index = arr.length - 1; index > 0; index -= 1) {
|
||||||
j = Math.floor(Math.random() * (index + 1));
|
j = Math.floor(Math.random() * (index + 1));
|
||||||
x = arr[index];
|
x = arr[index];
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
arr[index] = arr[j];
|
arr[index] = arr[j];
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
arr[j] = x;
|
arr[j] = x;
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
|
@ -165,9 +161,11 @@ export const splitNoEmpty = (s, delimiter) =>
|
||||||
/** 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) => {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < s.length; i++) {
|
for (let i = 0; i < s.length; i += 1) {
|
||||||
const char = s.charCodeAt(i);
|
const char = s.charCodeAt(i);
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
hash = (hash << 5) - hash + char;
|
hash = (hash << 5) - hash + char;
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
hash &= hash; // Convert to 32bit integer
|
hash &= hash; // Convert to 32bit integer
|
||||||
}
|
}
|
||||||
return hash;
|
return hash;
|
||||||
|
@ -248,6 +246,7 @@ export const playSound = async (id) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
|
// From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
|
||||||
|
// eslint-disable-next-line func-style
|
||||||
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, {
|
||||||
|
@ -267,9 +266,12 @@ export async function* fetchLinesIterator(fileURL, headers) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const remainder = chunk.substr(startIndex);
|
const remainder = chunk.substr(startIndex);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
({ 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 = 0;
|
||||||
|
re.lastIndex = 0;
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
yield chunk.substring(startIndex, result.index);
|
yield chunk.substring(startIndex, result.index);
|
||||||
|
@ -283,7 +285,8 @@ export async function* fetchLinesIterator(fileURL, headers) {
|
||||||
export const randomAlphanumericString = (len) => {
|
export const randomAlphanumericString = (len) => {
|
||||||
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
let id = "";
|
let id = "";
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
id += alphabet[(Math.random() * alphabet.length) | 0];
|
id += alphabet[(Math.random() * alphabet.length) | 0];
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
|
|
|
@ -439,23 +439,6 @@ const AddPhoneNumberDialog = (props) => {
|
||||||
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
|
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
|
||||||
const handleDialogSubmit = async () => {
|
|
||||||
if (!verificationCodeSent) {
|
|
||||||
await verifyPhone();
|
|
||||||
} else {
|
|
||||||
await checkVerifyPhone();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
if (verificationCodeSent) {
|
|
||||||
setVerificationCodeSent(false);
|
|
||||||
setCode("");
|
|
||||||
} else {
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyPhone = async () => {
|
const verifyPhone = async () => {
|
||||||
try {
|
try {
|
||||||
setSending(true);
|
setSending(true);
|
||||||
|
@ -490,6 +473,23 @@ const AddPhoneNumberDialog = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDialogSubmit = async () => {
|
||||||
|
if (!verificationCodeSent) {
|
||||||
|
await verifyPhone();
|
||||||
|
} else {
|
||||||
|
await checkVerifyPhone();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (verificationCodeSent) {
|
||||||
|
setVerificationCodeSent(false);
|
||||||
|
setCode("");
|
||||||
|
} else {
|
||||||
|
props.onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
|
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
|
||||||
<DialogTitle>{t("account_basics_phone_numbers_dialog_title")}</DialogTitle>
|
<DialogTitle>{t("account_basics_phone_numbers_dialog_title")}</DialogTitle>
|
||||||
|
@ -771,10 +771,6 @@ const Tokens = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDialogSubmit = async (user) => {
|
|
||||||
setDialogOpen(false);
|
|
||||||
//
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{ padding: 1 }} aria-label={t("prefs_users_title")}>
|
<Card sx={{ padding: 1 }} aria-label={t("prefs_users_title")}>
|
||||||
<CardContent sx={{ paddingBottom: 1 }}>
|
<CardContent sx={{ paddingBottom: 1 }}>
|
||||||
|
@ -998,7 +994,6 @@ const TokenDialog = (props) => {
|
||||||
|
|
||||||
const TokenDeleteDialog = (props) => {
|
const TokenDeleteDialog = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -1008,8 +1003,6 @@ const TokenDeleteDialog = (props) => {
|
||||||
console.log(`[Account] Error deleting token`, e);
|
console.log(`[Account] Error deleting token`, e);
|
||||||
if (e instanceof UnauthorizedError) {
|
if (e instanceof UnauthorizedError) {
|
||||||
session.resetAndRedirect(routes.login);
|
session.resetAndRedirect(routes.login);
|
||||||
} else {
|
|
||||||
setError(e.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { createContext, Suspense, useContext, useEffect, useState } from "react";
|
import { createContext, Suspense, useContext, useEffect, useState, useMemo } from "react";
|
||||||
import Box from "@mui/material/Box";
|
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";
|
||||||
|
@ -30,11 +30,14 @@ export const AccountContext = createContext(null);
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [account, setAccount] = useState(null);
|
const [account, setAccount] = useState(null);
|
||||||
|
|
||||||
|
const contextValue = useMemo(() => ({ account, setAccount }), [account, setAccount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Loader />}>
|
<Suspense fallback={<Loader />}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<AccountContext.Provider value={{ account, setAccount }}>
|
<AccountContext.Provider value={contextValue}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
@ -56,6 +59,10 @@ const App = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateTitle = (newNotificationsCount) => {
|
||||||
|
document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy";
|
||||||
|
};
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { account, setAccount } = useContext(AccountContext);
|
const { account, setAccount } = useContext(AccountContext);
|
||||||
|
@ -115,7 +122,7 @@ const Main = (props) => (
|
||||||
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: ({ palette }) => (palette.mode === "light" ? palette.grey[100] : palette.grey[900]),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -127,15 +134,11 @@ const Loader = () => (
|
||||||
open
|
open
|
||||||
sx={{
|
sx={{
|
||||||
zIndex: 100000,
|
zIndex: 100000,
|
||||||
backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]),
|
backgroundColor: ({ palette }) => (palette.mode === "light" ? palette.grey[100] : palette.grey[900]),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress color="success" disableShrink />
|
<CircularProgress color="success" disableShrink />
|
||||||
</Backdrop>
|
</Backdrop>
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateTitle = (newNotificationsCount) => {
|
|
||||||
document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy";
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -79,8 +79,6 @@ const EmojiPicker = (props) => {
|
||||||
inputProps={{
|
inputProps={{
|
||||||
role: "searchbox",
|
role: "searchbox",
|
||||||
"aria-label": t("emoji_picker_search_placeholder"),
|
"aria-label": t("emoji_picker_search_placeholder"),
|
||||||
}}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end" sx={{ display: search ? "" : "none" }}>
|
<InputAdornment position="end" sx={{ display: search ? "" : "none" }}>
|
||||||
<IconButton size="small" onClick={handleSearchClear} edge="end" aria-label={t("emoji_picker_search_clear")}>
|
<IconButton size="small" onClick={handleSearchClear} edge="end" aria-label={t("emoji_picker_search_clear")}>
|
||||||
|
@ -132,6 +130,18 @@ const Category = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const emojiMatches = (emoji, words) => {
|
||||||
|
if (words.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (const word of words) {
|
||||||
|
if (emoji.searchBase.indexOf(word) === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const Emoji = (props) => {
|
const Emoji = (props) => {
|
||||||
const { emoji } = props;
|
const { emoji } = props;
|
||||||
const matches = emojiMatches(emoji, props.search);
|
const matches = emojiMatches(emoji, props.search);
|
||||||
|
@ -158,16 +168,4 @@ const EmojiDiv = styled("div")({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojiMatches = (emoji, words) => {
|
|
||||||
if (words.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (const word of words) {
|
|
||||||
if (emoji.searchBase.indexOf(word) === -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmojiPicker;
|
export default EmojiPicker;
|
||||||
|
|
|
@ -69,16 +69,6 @@ class ErrorBoundaryImpl extends React.Component {
|
||||||
navigator.clipboard.writeText(stack);
|
navigator.clipboard.writeText(stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.error) {
|
|
||||||
if (this.state.unsupportedIndexedDB) {
|
|
||||||
return this.renderUnsupportedIndexedDB();
|
|
||||||
}
|
|
||||||
return this.renderError();
|
|
||||||
}
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderUnsupportedIndexedDB() {
|
renderUnsupportedIndexedDB() {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
return (
|
return (
|
||||||
|
@ -130,6 +120,16 @@ class ErrorBoundaryImpl extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.error) {
|
||||||
|
if (this.state.unsupportedIndexedDB) {
|
||||||
|
return this.renderUnsupportedIndexedDB();
|
||||||
|
}
|
||||||
|
return this.renderError();
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorBoundary = withTranslation()(ErrorBoundaryImpl); // Adds props.t
|
const ErrorBoundary = withTranslation()(ErrorBoundaryImpl); // Adds props.t
|
||||||
|
|
|
@ -85,6 +85,10 @@ const NavList = (props) => {
|
||||||
setSubscribeDialogKey((prev) => prev + 1);
|
setSubscribeDialogKey((prev) => prev + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRequestNotificationPermission = () => {
|
||||||
|
notifier.maybeRequestPermission((granted) => props.onNotificationGranted(granted));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubscribeSubmit = (subscription) => {
|
const handleSubscribeSubmit = (subscription) => {
|
||||||
console.log(`[Navigation] New subscription: ${subscription.id}`, subscription);
|
console.log(`[Navigation] New subscription: ${subscription.id}`, subscription);
|
||||||
handleSubscribeReset();
|
handleSubscribeReset();
|
||||||
|
@ -92,10 +96,6 @@ const NavList = (props) => {
|
||||||
handleRequestNotificationPermission();
|
handleRequestNotificationPermission();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRequestNotificationPermission = () => {
|
|
||||||
notifier.maybeRequestPermission((granted) => props.onNotificationGranted(granted));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAccountClick = () => {
|
const handleAccountClick = () => {
|
||||||
accountApi.sync(); // Dangle!
|
accountApi.sync(); // Dangle!
|
||||||
navigate(routes.account);
|
navigate(routes.account);
|
||||||
|
|
|
@ -34,6 +34,13 @@ import logoOutline from "../img/ntfy-outline.svg";
|
||||||
import AttachmentIcon from "./AttachmentIcon";
|
import AttachmentIcon from "./AttachmentIcon";
|
||||||
import { useAutoSubscribe } from "./hooks";
|
import { useAutoSubscribe } from "./hooks";
|
||||||
|
|
||||||
|
const priorityFiles = {
|
||||||
|
1: priority1,
|
||||||
|
2: priority2,
|
||||||
|
4: priority4,
|
||||||
|
5: priority5,
|
||||||
|
};
|
||||||
|
|
||||||
export const AllSubscriptions = () => {
|
export const AllSubscriptions = () => {
|
||||||
const { subscriptions } = useOutletContext();
|
const { subscriptions } = useOutletContext();
|
||||||
if (!subscriptions) {
|
if (!subscriptions) {
|
||||||
|
@ -131,6 +138,25 @@ const NotificationList = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace links with <Link/> components; this is a combination of the genius function
|
||||||
|
* in [1] and the regex in [2].
|
||||||
|
*
|
||||||
|
* [1] https://github.com/facebook/react/issues/3386#issuecomment-78605760
|
||||||
|
* [2] https://github.com/bryanwoods/autolink-js/blob/master/autolink.js#L9
|
||||||
|
*/
|
||||||
|
const autolink = (s) => {
|
||||||
|
const parts = s.split(/(\bhttps?:\/\/[-A-Z0-9+\u0026\u2019@#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026@#/%=~()_|]\b)/gi);
|
||||||
|
for (let i = 1; i < parts.length; i += 2) {
|
||||||
|
parts[i] = (
|
||||||
|
<Link key={i} href={parts[i]} underline="hover" target="_blank" rel="noreferrer,noopener">
|
||||||
|
{shortUrl(parts[i])}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <>{parts}</>;
|
||||||
|
};
|
||||||
|
|
||||||
const NotificationItem = (props) => {
|
const NotificationItem = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { notification } = props;
|
const { notification } = props;
|
||||||
|
@ -248,32 +274,6 @@ const NotificationItem = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace links with <Link/> components; this is a combination of the genius function
|
|
||||||
* in [1] and the regex in [2].
|
|
||||||
*
|
|
||||||
* [1] https://github.com/facebook/react/issues/3386#issuecomment-78605760
|
|
||||||
* [2] https://github.com/bryanwoods/autolink-js/blob/master/autolink.js#L9
|
|
||||||
*/
|
|
||||||
const autolink = (s) => {
|
|
||||||
const parts = s.split(/(\bhttps?:\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|]\b)/gi);
|
|
||||||
for (let i = 1; i < parts.length; i += 2) {
|
|
||||||
parts[i] = (
|
|
||||||
<Link key={i} href={parts[i]} underline="hover" target="_blank" rel="noreferrer,noopener">
|
|
||||||
{shortUrl(parts[i])}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <>{parts}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const priorityFiles = {
|
|
||||||
1: priority1,
|
|
||||||
2: priority2,
|
|
||||||
4: priority4,
|
|
||||||
5: priority5,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Attachment = (props) => {
|
const Attachment = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { attachment } = props;
|
const { attachment } = props;
|
||||||
|
@ -414,6 +414,52 @@ const UserActions = (props) => (
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ACTION_PROGRESS_ONGOING = 1;
|
||||||
|
const ACTION_PROGRESS_SUCCESS = 2;
|
||||||
|
const ACTION_PROGRESS_FAILED = 3;
|
||||||
|
|
||||||
|
const ACTION_LABEL_SUFFIX = {
|
||||||
|
[ACTION_PROGRESS_ONGOING]: " …",
|
||||||
|
[ACTION_PROGRESS_SUCCESS]: " ✔",
|
||||||
|
[ACTION_PROGRESS_FAILED]: " ❌",
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateActionStatus = (notification, action, progress, error) => {
|
||||||
|
// TODO(eslint): Fix by spreading? Does the code depend on the change, though?
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
notification.actions = notification.actions.map((a) => {
|
||||||
|
if (a.id !== action.id) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return { ...a, progress, error };
|
||||||
|
});
|
||||||
|
subscriptionManager.updateNotification(notification);
|
||||||
|
};
|
||||||
|
|
||||||
|
const performHttpAction = async (notification, action) => {
|
||||||
|
console.log(`[Notifications] Performing HTTP user action`, action);
|
||||||
|
try {
|
||||||
|
updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null);
|
||||||
|
const response = await fetch(action.url, {
|
||||||
|
method: action.method ?? "POST",
|
||||||
|
headers: action.headers ?? {},
|
||||||
|
// This must not null-coalesce to a non nullish value. Otherwise, the fetch API
|
||||||
|
// will reject it for "having a body"
|
||||||
|
body: action.body,
|
||||||
|
});
|
||||||
|
console.log(`[Notifications] HTTP user action response`, response);
|
||||||
|
const success = response.status >= 200 && response.status <= 299;
|
||||||
|
if (success) {
|
||||||
|
updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
|
||||||
|
} else {
|
||||||
|
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[Notifications] HTTP action failed`, e);
|
||||||
|
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const UserAction = (props) => {
|
const UserAction = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { notification } = props;
|
const { notification } = props;
|
||||||
|
@ -468,53 +514,9 @@ const UserAction = (props) => {
|
||||||
return null; // Others
|
return null; // Others
|
||||||
};
|
};
|
||||||
|
|
||||||
const performHttpAction = async (notification, action) => {
|
|
||||||
console.log(`[Notifications] Performing HTTP user action`, action);
|
|
||||||
try {
|
|
||||||
updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null);
|
|
||||||
const response = await fetch(action.url, {
|
|
||||||
method: action.method ?? "POST",
|
|
||||||
headers: action.headers ?? {},
|
|
||||||
// This must not null-coalesce to a non nullish value. Otherwise, the fetch API
|
|
||||||
// will reject it for "having a body"
|
|
||||||
body: action.body,
|
|
||||||
});
|
|
||||||
console.log(`[Notifications] HTTP user action response`, response);
|
|
||||||
const success = response.status >= 200 && response.status <= 299;
|
|
||||||
if (success) {
|
|
||||||
updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
|
|
||||||
} else {
|
|
||||||
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[Notifications] HTTP action failed`, e);
|
|
||||||
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateActionStatus = (notification, action, progress, error) => {
|
|
||||||
notification.actions = notification.actions.map((a) => {
|
|
||||||
if (a.id !== action.id) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return { ...a, progress, error };
|
|
||||||
});
|
|
||||||
subscriptionManager.updateNotification(notification);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ACTION_PROGRESS_ONGOING = 1;
|
|
||||||
const ACTION_PROGRESS_SUCCESS = 2;
|
|
||||||
const ACTION_PROGRESS_FAILED = 3;
|
|
||||||
|
|
||||||
const ACTION_LABEL_SUFFIX = {
|
|
||||||
[ACTION_PROGRESS_ONGOING]: " …",
|
|
||||||
[ACTION_PROGRESS_SUCCESS]: " ✔",
|
|
||||||
[ACTION_PROGRESS_FAILED]: " ❌",
|
|
||||||
};
|
|
||||||
|
|
||||||
const NoNotifications = (props) => {
|
const NoNotifications = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
|
const topicShortUrlResolved = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
|
||||||
return (
|
return (
|
||||||
<VerticallyCenteredContainer maxWidth="xs">
|
<VerticallyCenteredContainer maxWidth="xs">
|
||||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||||
|
@ -525,7 +527,10 @@ const NoNotifications = (props) => {
|
||||||
<Paragraph>{t("notifications_none_for_topic_description")}</Paragraph>
|
<Paragraph>{t("notifications_none_for_topic_description")}</Paragraph>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
{t("notifications_example")}:<br />
|
{t("notifications_example")}:<br />
|
||||||
<tt>$ curl -d "Hi" {shortUrl}</tt>
|
<tt>
|
||||||
|
{'$ curl -d "Hi" '}
|
||||||
|
{topicShortUrlResolved}
|
||||||
|
</tt>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
<ForMoreDetails />
|
<ForMoreDetails />
|
||||||
|
@ -537,7 +542,7 @@ const NoNotifications = (props) => {
|
||||||
const NoNotificationsWithoutSubscription = (props) => {
|
const NoNotificationsWithoutSubscription = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const subscription = props.subscriptions[0];
|
const subscription = props.subscriptions[0];
|
||||||
const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic);
|
const topicShortUrlResolved = topicShortUrl(subscription.baseUrl, subscription.topic);
|
||||||
return (
|
return (
|
||||||
<VerticallyCenteredContainer maxWidth="xs">
|
<VerticallyCenteredContainer maxWidth="xs">
|
||||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||||
|
@ -548,7 +553,10 @@ const NoNotificationsWithoutSubscription = (props) => {
|
||||||
<Paragraph>{t("notifications_none_for_any_description")}</Paragraph>
|
<Paragraph>{t("notifications_none_for_any_description")}</Paragraph>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
{t("notifications_example")}:<br />
|
{t("notifications_example")}:<br />
|
||||||
<tt>$ curl -d "Hi" {shortUrl}</tt>
|
<tt>
|
||||||
|
{'$ curl -d "Hi" '}
|
||||||
|
{topicShortUrlResolved}
|
||||||
|
</tt>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
<ForMoreDetails />
|
<ForMoreDetails />
|
||||||
|
|
|
@ -47,9 +47,22 @@ 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 { subscribeTopic } from "./SubscribeDialog";
|
import { subscribeTopic } from "./SubscribeDialog";
|
||||||
|
|
||||||
|
const maybeUpdateAccountSettings = async (payload) => {
|
||||||
|
if (!session.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await accountApi.updateSettings(payload);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[Preferences] Error updating account settings`, e);
|
||||||
|
if (e instanceof UnauthorizedError) {
|
||||||
|
session.resetAndRedirect(routes.login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Preferences = () => (
|
const Preferences = () => (
|
||||||
<Container maxWidth="md" sx={{ marginTop: 3, marginBottom: 3 }}>
|
<Container maxWidth="md" sx={{ marginTop: 3, marginBottom: 3 }}>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
|
@ -181,10 +194,12 @@ const DeleteAfter = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (deleteAfter === null || deleteAfter === undefined) {
|
if (deleteAfter === null || deleteAfter === undefined) {
|
||||||
// !deleteAfter will not work with "0"
|
// !deleteAfter will not work with "0"
|
||||||
return null; // While loading
|
return null; // While loading
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = (() => {
|
const description = (() => {
|
||||||
switch (deleteAfter) {
|
switch (deleteAfter) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -197,8 +212,11 @@ const DeleteAfter = () => {
|
||||||
return t("prefs_notifications_delete_after_one_week_description");
|
return t("prefs_notifications_delete_after_one_week_description");
|
||||||
case 2592000:
|
case 2592000:
|
||||||
return t("prefs_notifications_delete_after_one_month_description");
|
return t("prefs_notifications_delete_after_one_month_description");
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pref labelId={labelId} title={t("prefs_notifications_delete_after_title")} description={description}>
|
<Pref labelId={labelId} title={t("prefs_notifications_delete_after_title")} description={description}>
|
||||||
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
||||||
|
@ -674,18 +692,4 @@ const ReservationsTable = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const maybeUpdateAccountSettings = async (payload) => {
|
|
||||||
if (!session.exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await accountApi.updateSettings(payload);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[Preferences] Error updating account settings`, e);
|
|
||||||
if (e instanceof UnauthorizedError) {
|
|
||||||
session.resetAndRedirect(routes.login);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Preferences;
|
export default Preferences;
|
||||||
|
|
|
@ -171,34 +171,33 @@ const PublishDialog = (props) => {
|
||||||
|
|
||||||
const checkAttachmentLimits = async (file) => {
|
const checkAttachmentLimits = async (file) => {
|
||||||
try {
|
try {
|
||||||
const account = await accountApi.get();
|
const apiAccount = await accountApi.get();
|
||||||
const fileSizeLimit = account.limits.attachment_file_size ?? 0;
|
const fileSizeLimit = apiAccount.limits.attachment_file_size ?? 0;
|
||||||
const remainingBytes = account.stats.attachment_total_size_remaining;
|
const remainingBytes = apiAccount.stats.attachment_total_size_remaining;
|
||||||
const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit;
|
const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit;
|
||||||
const quotaReached = remainingBytes > 0 && file.size > remainingBytes;
|
const quotaReached = remainingBytes > 0 && file.size > remainingBytes;
|
||||||
if (fileSizeLimitReached && quotaReached) {
|
if (fileSizeLimitReached && quotaReached) {
|
||||||
return setAttachFileError(
|
setAttachFileError(
|
||||||
t("publish_dialog_attachment_limits_file_and_quota_reached", {
|
t("publish_dialog_attachment_limits_file_and_quota_reached", {
|
||||||
fileSizeLimit: formatBytes(fileSizeLimit),
|
fileSizeLimit: formatBytes(fileSizeLimit),
|
||||||
remainingBytes: formatBytes(remainingBytes),
|
remainingBytes: formatBytes(remainingBytes),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
} else if (fileSizeLimitReached) {
|
||||||
if (fileSizeLimitReached) {
|
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) {
|
setAttachFileError(
|
||||||
return setAttachFileError(
|
|
||||||
t("publish_dialog_attachment_limits_quota_reached", {
|
t("publish_dialog_attachment_limits_quota_reached", {
|
||||||
remainingBytes: formatBytes(remainingBytes),
|
remainingBytes: formatBytes(remainingBytes),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
setAttachFileError("");
|
||||||
}
|
}
|
||||||
setAttachFileError("");
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[PublishDialog] Retrieving attachment limits failed`, e);
|
console.log(`[PublishDialog] Retrieving attachment limits failed`, e);
|
||||||
if (e instanceof UnauthorizedError) {
|
if (e instanceof UnauthorizedError) {
|
||||||
|
@ -213,6 +212,13 @@ const PublishDialog = (props) => {
|
||||||
attachFileInput.current.click();
|
attachFileInput.current.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateAttachFile = async (file) => {
|
||||||
|
setAttachFile(file);
|
||||||
|
setFilename(file.name);
|
||||||
|
props.onResetOpenMode();
|
||||||
|
await checkAttachmentLimits(file);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAttachFileChanged = async (ev) => {
|
const handleAttachFileChanged = async (ev) => {
|
||||||
await updateAttachFile(ev.target.files[0]);
|
await updateAttachFile(ev.target.files[0]);
|
||||||
};
|
};
|
||||||
|
@ -223,13 +229,6 @@ const PublishDialog = (props) => {
|
||||||
await updateAttachFile(ev.dataTransfer.files[0]);
|
await updateAttachFile(ev.dataTransfer.files[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAttachFile = async (file) => {
|
|
||||||
setAttachFile(file);
|
|
||||||
setFilename(file.name);
|
|
||||||
props.onResetOpenMode();
|
|
||||||
await checkAttachmentLimits(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAttachFileDragLeave = () => {
|
const handleAttachFileDragLeave = () => {
|
||||||
setDropZone(false);
|
setDropZone(false);
|
||||||
if (props.openMode === PublishDialog.OPEN_MODE_DRAG) {
|
if (props.openMode === PublishDialog.OPEN_MODE_DRAG) {
|
||||||
|
@ -242,7 +241,7 @@ const PublishDialog = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmojiPick = (emoji) => {
|
const handleEmojiPick = (emoji) => {
|
||||||
setTags((tags) => (tags.trim() ? `${tags.trim()}, ${emoji}` : emoji));
|
setTags((prevTags) => (prevTags.trim() ? `${prevTags.trim()}, ${emoji}` : emoji));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmojiClose = () => {
|
const handleEmojiClose = () => {
|
||||||
|
@ -374,23 +373,23 @@ const PublishDialog = (props) => {
|
||||||
"aria-label": t("publish_dialog_priority_label"),
|
"aria-label": t("publish_dialog_priority_label"),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[5, 4, 3, 2, 1].map((priority) => (
|
{[5, 4, 3, 2, 1].map((priorityMenuItem) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={`priorityMenuItem${priority}`}
|
key={`priorityMenuItem${priorityMenuItem}`}
|
||||||
value={priority}
|
value={priorityMenuItem}
|
||||||
aria-label={t("notifications_priority_x", {
|
aria-label={t("notifications_priority_x", {
|
||||||
priority,
|
priority: priorityMenuItem,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<img
|
<img
|
||||||
src={priorities[priority].file}
|
src={priorities[priorityMenuItem].file}
|
||||||
style={{ marginRight: "8px" }}
|
style={{ marginRight: "8px" }}
|
||||||
alt={t("notifications_priority_x", {
|
alt={t("notifications_priority_x", {
|
||||||
priority,
|
priority: priorityMenuItem,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<div>{priorities[priority].label}</div>
|
<div>{priorities[priorityMenuItem].label}</div>
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
@ -469,6 +468,8 @@ const PublishDialog = (props) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{account?.phone_numbers?.map((phoneNumber, i) => (
|
{account?.phone_numbers?.map((phoneNumber, i) => (
|
||||||
|
// TODO(eslint): Possibly just use the phone number as a key?
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<MenuItem key={`phoneNumberMenuItem${i}`} value={phoneNumber} aria-label={phoneNumber}>
|
<MenuItem key={`phoneNumberMenuItem${i}`} value={phoneNumber} aria-label={phoneNumber}>
|
||||||
{t("publish_dialog_call_item", { number: phoneNumber })}
|
{t("publish_dialog_call_item", { number: phoneNumber })}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -716,7 +717,7 @@ const Row = (props) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const ClosableRow = (props) => {
|
const ClosableRow = (props) => {
|
||||||
const closable = props.hasOwnProperty("closable") ? props.closable : true;
|
const closable = props.closable !== undefined ? props.closable : true;
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -823,10 +824,7 @@ const ExpandingTextField = (props) => {
|
||||||
variant="standard"
|
variant="standard"
|
||||||
sx={{ width: `${textWidth}px`, borderBottom: "none" }}
|
sx={{ width: `${textWidth}px`, borderBottom: "none" }}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
style: { fontSize: theme.typography[props.variant].fontSize },
|
style: { fontSize: theme.typography[props.variant].fontSize, paddingBottom: 0, paddingTop: 0 },
|
||||||
}}
|
|
||||||
inputProps={{
|
|
||||||
style: { paddingBottom: 0, paddingTop: 0 },
|
|
||||||
"aria-label": props.placeholder,
|
"aria-label": props.placeholder,
|
||||||
}}
|
}}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
|
@ -840,6 +838,7 @@ const DropArea = (props) => {
|
||||||
// This is where we could disallow certain files to be dragged in.
|
// This is where we could disallow certain files to be dragged in.
|
||||||
// For now we allow all files.
|
// For now we allow all files.
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
ev.dataTransfer.dropEffect = "copy";
|
ev.dataTransfer.dropEffect = "copy";
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,6 +25,21 @@ import { ReserveLimitChip } from "./SubscriptionPopup";
|
||||||
|
|
||||||
const publicBaseUrl = "https://ntfy.sh";
|
const publicBaseUrl = "https://ntfy.sh";
|
||||||
|
|
||||||
|
export const subscribeTopic = async (baseUrl, topic) => {
|
||||||
|
const subscription = await subscriptionManager.add(baseUrl, topic);
|
||||||
|
if (session.exists()) {
|
||||||
|
try {
|
||||||
|
await accountApi.addSubscription(baseUrl, topic);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
|
||||||
|
if (e instanceof UnauthorizedError) {
|
||||||
|
session.resetAndRedirect(routes.login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return subscription;
|
||||||
|
};
|
||||||
|
|
||||||
const SubscribeDialog = (props) => {
|
const SubscribeDialog = (props) => {
|
||||||
const [baseUrl, setBaseUrl] = useState("");
|
const [baseUrl, setBaseUrl] = useState("");
|
||||||
const [topic, setTopic] = useState("");
|
const [topic, setTopic] = useState("");
|
||||||
|
@ -296,19 +311,4 @@ const LoginPage = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const subscribeTopic = async (baseUrl, topic) => {
|
|
||||||
const subscription = await subscriptionManager.add(baseUrl, topic);
|
|
||||||
if (session.exists()) {
|
|
||||||
try {
|
|
||||||
await accountApi.addSubscription(baseUrl, topic);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
|
|
||||||
if (e instanceof UnauthorizedError) {
|
|
||||||
session.resetAndRedirect(routes.login);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return subscription;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SubscribeDialog;
|
export default SubscribeDialog;
|
||||||
|
|
|
@ -241,8 +241,6 @@ const DisplayNameDialog = (props) => {
|
||||||
inputProps={{
|
inputProps={{
|
||||||
maxLength: 64,
|
maxLength: 64,
|
||||||
"aria-label": t("display_name_dialog_placeholder"),
|
"aria-label": t("display_name_dialog_placeholder"),
|
||||||
}}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<IconButton onClick={() => setDisplayName("")} edge="end">
|
<IconButton onClick={() => setDisplayName("")} edge="end">
|
||||||
|
@ -292,20 +290,17 @@ const LimitReachedChip = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProChip = () => {
|
export const ProChip = () => (
|
||||||
const { t } = useTranslation();
|
<Chip
|
||||||
return (
|
label="ntfy Pro"
|
||||||
<Chip
|
variant="outlined"
|
||||||
label="ntfy Pro"
|
color="primary"
|
||||||
variant="outlined"
|
sx={{
|
||||||
color="primary"
|
opacity: 0.8,
|
||||||
sx={{
|
fontWeight: "bold",
|
||||||
opacity: 0.8,
|
borderWidth: "2px",
|
||||||
fontWeight: "bold",
|
height: "24px",
|
||||||
borderWidth: "2px",
|
marginLeft: "5px",
|
||||||
height: "24px",
|
}}
|
||||||
marginLeft: "5px",
|
/>
|
||||||
}}
|
);
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -24,6 +24,33 @@ import session from "../app/Session";
|
||||||
import accountApi, { SubscriptionInterval } from "../app/AccountApi";
|
import accountApi, { SubscriptionInterval } from "../app/AccountApi";
|
||||||
import theme from "./theme";
|
import theme from "./theme";
|
||||||
|
|
||||||
|
const Feature = (props) => <FeatureItem feature>{props.children}</FeatureItem>;
|
||||||
|
|
||||||
|
const NoFeature = (props) => <FeatureItem feature={false}>{props.children}</FeatureItem>;
|
||||||
|
|
||||||
|
const FeatureItem = (props) => (
|
||||||
|
<ListItem disableGutters sx={{ m: 0, p: 0 }}>
|
||||||
|
<ListItemIcon sx={{ minWidth: "24px" }}>
|
||||||
|
{props.feature && <Check fontSize="small" sx={{ color: "#338574" }} />}
|
||||||
|
{!props.feature && <Close fontSize="small" sx={{ color: "gray" }} />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText sx={{ mt: "2px", mb: "2px" }} primary={<Typography variant="body1">{props.children}</Typography>} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Action = {
|
||||||
|
REDIRECT_SIGNUP: 1,
|
||||||
|
CREATE_SUBSCRIPTION: 2,
|
||||||
|
UPDATE_SUBSCRIPTION: 3,
|
||||||
|
CANCEL_SUBSCRIPTION: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Banner = {
|
||||||
|
CANCEL_WARNING: 1,
|
||||||
|
PRORATION_INFO: 2,
|
||||||
|
RESERVATIONS_WARNING: 3,
|
||||||
|
};
|
||||||
|
|
||||||
const UpgradeDialog = (props) => {
|
const UpgradeDialog = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { account } = useContext(AccountContext); // May be undefined!
|
const { account } = useContext(AccountContext); // May be undefined!
|
||||||
|
@ -120,12 +147,12 @@ const UpgradeDialog = (props) => {
|
||||||
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 {
|
||||||
let n = 0;
|
let n = 0;
|
||||||
for (const t of tiers) {
|
for (const tier of tiers) {
|
||||||
if (t.prices) {
|
if (tier.prices) {
|
||||||
const tierDiscount = Math.round(((t.prices.month * 12) / t.prices.year - 1) * 100);
|
const tierDiscount = Math.round(((tier.prices.month * 12) / tier.prices.year - 1) * 100);
|
||||||
if (tierDiscount > discount) {
|
if (tierDiscount > discount) {
|
||||||
discount = tierDiscount;
|
discount = tierDiscount;
|
||||||
n++;
|
n += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,7 +237,7 @@ const UpgradeDialog = (props) => {
|
||||||
<Alert severity="warning" sx={{ fontSize: "1rem" }}>
|
<Alert severity="warning" sx={{ fontSize: "1rem" }}>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="account_upgrade_dialog_reservations_warning"
|
i18nKey="account_upgrade_dialog_reservations_warning"
|
||||||
count={account?.reservations.length - newTier?.limits.reservations}
|
count={(account?.reservations.length ?? 0) - (newTier?.limits.reservations ?? 0)}
|
||||||
components={{
|
components={{
|
||||||
Link: <NavLink to={routes.settings} />,
|
Link: <NavLink to={routes.settings} />,
|
||||||
}}
|
}}
|
||||||
|
@ -396,31 +423,4 @@ const TierCard = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Feature = (props) => <FeatureItem feature>{props.children}</FeatureItem>;
|
|
||||||
|
|
||||||
const NoFeature = (props) => <FeatureItem feature={false}>{props.children}</FeatureItem>;
|
|
||||||
|
|
||||||
const FeatureItem = (props) => (
|
|
||||||
<ListItem disableGutters sx={{ m: 0, p: 0 }}>
|
|
||||||
<ListItemIcon sx={{ minWidth: "24px" }}>
|
|
||||||
{props.feature && <Check fontSize="small" sx={{ color: "#338574" }} />}
|
|
||||||
{!props.feature && <Close fontSize="small" sx={{ color: "gray" }} />}
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText sx={{ mt: "2px", mb: "2px" }} primary={<Typography variant="body1">{props.children}</Typography>} />
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Action = {
|
|
||||||
REDIRECT_SIGNUP: 1,
|
|
||||||
CREATE_SUBSCRIPTION: 2,
|
|
||||||
UPDATE_SUBSCRIPTION: 3,
|
|
||||||
CANCEL_SUBSCRIPTION: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Banner = {
|
|
||||||
CANCEL_WARNING: 1,
|
|
||||||
PRORATION_INFO: 2,
|
|
||||||
RESERVATIONS_WARNING: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UpgradeDialog;
|
export default UpgradeDialog;
|
||||||
|
|
|
@ -22,15 +22,6 @@ export const useConnectionListeners = (account, subscriptions, users) => {
|
||||||
// Register listeners for incoming messages, and connection state changes
|
// Register listeners for incoming messages, and connection state changes
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
const handleMessage = async (subscriptionId, message) => {
|
|
||||||
const subscription = await subscriptionManager.get(subscriptionId);
|
|
||||||
if (subscription.internal) {
|
|
||||||
await handleInternalMessage(message);
|
|
||||||
} else {
|
|
||||||
await handleNotification(subscriptionId, message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInternalMessage = async (message) => {
|
const handleInternalMessage = async (message) => {
|
||||||
console.log(`[ConnectionListener] Received message on sync topic`, message.message);
|
console.log(`[ConnectionListener] Received message on sync topic`, message.message);
|
||||||
try {
|
try {
|
||||||
|
@ -53,8 +44,19 @@ export const useConnectionListeners = (account, subscriptions, users) => {
|
||||||
await notifier.notify(subscriptionId, notification, defaultClickAction);
|
await notifier.notify(subscriptionId, notification, defaultClickAction);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMessage = async (subscriptionId, message) => {
|
||||||
|
const subscription = await subscriptionManager.get(subscriptionId);
|
||||||
|
if (subscription.internal) {
|
||||||
|
await handleInternalMessage(message);
|
||||||
|
} else {
|
||||||
|
await handleNotification(subscriptionId, message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
connectionManager.registerStateListener(subscriptionManager.updateState);
|
connectionManager.registerStateListener(subscriptionManager.updateState);
|
||||||
connectionManager.registerMessageListener(handleMessage);
|
connectionManager.registerMessageListener(handleMessage);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
connectionManager.resetStateListener();
|
connectionManager.resetStateListener();
|
||||||
connectionManager.resetMessageListener();
|
connectionManager.resetMessageListener();
|
||||||
|
|
Loading…
Reference in a new issue