mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-12-14 11:47:33 +00:00
Line width
This commit is contained in:
parent
2e27f58963
commit
ca5d736a71
33 changed files with 521 additions and 2033 deletions
|
@ -1,2 +1,2 @@
|
|||
build/
|
||||
public/static/langs/
|
||||
public/static/langs/
|
||||
|
|
|
@ -43,5 +43,8 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 160
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,15 +15,5 @@ var config = {
|
|||
enable_emails: true,
|
||||
enable_calls: true,
|
||||
billing_contact: "",
|
||||
disallowed_topics: [
|
||||
"docs",
|
||||
"static",
|
||||
"file",
|
||||
"app",
|
||||
"account",
|
||||
"settings",
|
||||
"signup",
|
||||
"login",
|
||||
"v1",
|
||||
],
|
||||
disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"],
|
||||
};
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
<title>ntfy web</title>
|
||||
|
||||
<!-- Mobile view -->
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
|
||||
|
@ -18,11 +15,7 @@
|
|||
<meta name="apple-mobile-web-app-status-bar-style" content="#317f6f" />
|
||||
|
||||
<!-- Favicon, see favicon.io -->
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href="%PUBLIC_URL%/static/images/favicon.ico"
|
||||
/>
|
||||
<link rel="icon" type="image/png" href="%PUBLIC_URL%/static/images/favicon.ico" />
|
||||
|
||||
<!-- Previews in Google, Slack, WhatsApp, etc. -->
|
||||
<meta property="og:type" content="website" />
|
||||
|
@ -40,23 +33,13 @@
|
|||
<meta name="robots" content="noindex, nofollow" />
|
||||
|
||||
<!-- Style overrides & fonts -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="%PUBLIC_URL%/static/css/app.css"
|
||||
type="text/css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="%PUBLIC_URL%/static/css/fonts.css"
|
||||
type="text/css"
|
||||
/>
|
||||
<link rel="stylesheet" href="%PUBLIC_URL%/static/css/app.css" type="text/css" />
|
||||
<link rel="stylesheet" href="%PUBLIC_URL%/static/css/fonts.css" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
ntfy web requires JavaScript, but you can also use the
|
||||
<a href="https://ntfy.sh/docs/subscribe/cli/">CLI</a> or
|
||||
<a href="https://ntfy.sh/docs/subscribe/phone/">Android/iOS app</a> to
|
||||
subscribe.
|
||||
<a href="https://ntfy.sh/docs/subscribe/cli/">CLI</a> or <a href="https://ntfy.sh/docs/subscribe/phone/">Android/iOS app</a> to subscribe.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<script src="%PUBLIC_URL%/config.js"></script>
|
||||
|
|
|
@ -56,9 +56,7 @@ class AccountApi {
|
|||
|
||||
async logout() {
|
||||
const url = accountTokenUrl(config.base_url);
|
||||
console.log(
|
||||
`[AccountApi] Logging out from ${url} using token ${session.token()}`
|
||||
);
|
||||
console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`);
|
||||
await fetchOrThrow(url, {
|
||||
method: "DELETE",
|
||||
headers: withBearerAuth({}, session.token()),
|
||||
|
@ -227,9 +225,7 @@ class AccountApi {
|
|||
|
||||
async upsertReservation(topic, everyone) {
|
||||
const url = accountReservationUrl(config.base_url);
|
||||
console.log(
|
||||
`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`
|
||||
);
|
||||
console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`);
|
||||
await fetchOrThrow(url, {
|
||||
method: "POST",
|
||||
headers: withBearerAuth({}, session.token()),
|
||||
|
@ -264,16 +260,12 @@ class AccountApi {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -324,9 +316,7 @@ class AccountApi {
|
|||
|
||||
async addPhoneNumber(phoneNumber, code) {
|
||||
const url = accountPhoneUrl(config.base_url);
|
||||
console.log(
|
||||
`[AccountApi] Adding phone number with verification code ${url}`
|
||||
);
|
||||
console.log(`[AccountApi] Adding phone number with verification code ${url}`);
|
||||
await fetchOrThrow(url, {
|
||||
method: "PUT",
|
||||
headers: withBearerAuth({}, session.token()),
|
||||
|
@ -371,10 +361,7 @@ class AccountApi {
|
|||
}
|
||||
}
|
||||
if (account.subscriptions) {
|
||||
await subscriptionManager.syncFromRemote(
|
||||
account.subscriptions,
|
||||
account.reservations
|
||||
);
|
||||
await subscriptionManager.syncFromRemote(account.subscriptions, account.reservations);
|
||||
}
|
||||
return account;
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
fetchLinesIterator,
|
||||
maybeWithAuth,
|
||||
topicShortUrl,
|
||||
topicUrl,
|
||||
topicUrlAuth,
|
||||
topicUrlJsonPoll,
|
||||
topicUrlJsonPollWithSince,
|
||||
} from "./utils";
|
||||
import { fetchLinesIterator, maybeWithAuth, topicShortUrl, topicUrl, topicUrlAuth, topicUrlJsonPoll, topicUrlJsonPollWithSince } from "./utils";
|
||||
import userManager from "./UserManager";
|
||||
import { fetchOrThrow } from "./errors";
|
||||
|
||||
|
@ -14,9 +6,7 @@ class Api {
|
|||
async poll(baseUrl, topic, since) {
|
||||
const user = await userManager.get(baseUrl);
|
||||
const shortUrl = topicShortUrl(baseUrl, topic);
|
||||
const url = since
|
||||
? topicUrlJsonPollWithSince(baseUrl, topic, since)
|
||||
: topicUrlJsonPoll(baseUrl, topic);
|
||||
const url = since ? topicUrlJsonPollWithSince(baseUrl, topic, since) : topicUrlJsonPoll(baseUrl, topic);
|
||||
const messages = [];
|
||||
const headers = maybeWithAuth({}, user);
|
||||
console.log(`[Api] Polling ${url}`);
|
||||
|
@ -73,17 +63,11 @@ class Api {
|
|||
xhr.upload.addEventListener("progress", onProgress);
|
||||
xhr.addEventListener("readystatechange", () => {
|
||||
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) {
|
||||
console.log(
|
||||
`[Api] Publish successful (HTTP ${xhr.status})`,
|
||||
xhr.response
|
||||
);
|
||||
console.log(`[Api] Publish successful (HTTP ${xhr.status})`, xhr.response);
|
||||
resolve(xhr.response);
|
||||
} else if (xhr.readyState === 4) {
|
||||
// Firefox bug; see description above!
|
||||
console.log(
|
||||
`[Api] Publish failed (HTTP ${xhr.status})`,
|
||||
xhr.responseText
|
||||
);
|
||||
console.log(`[Api] Publish failed (HTTP ${xhr.status})`, xhr.responseText);
|
||||
let errorText;
|
||||
try {
|
||||
const error = JSON.parse(xhr.responseText);
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
basicAuth,
|
||||
bearerAuth,
|
||||
encodeBase64Url,
|
||||
topicShortUrl,
|
||||
topicUrlWs,
|
||||
} from "./utils";
|
||||
import { basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs } from "./utils";
|
||||
|
||||
const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
|
||||
|
||||
|
@ -15,16 +9,7 @@ const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
|
|||
* Incoming messages and state changes are forwarded via listeners.
|
||||
*/
|
||||
class Connection {
|
||||
constructor(
|
||||
connectionId,
|
||||
subscriptionId,
|
||||
baseUrl,
|
||||
topic,
|
||||
user,
|
||||
since,
|
||||
onNotification,
|
||||
onStateChanged
|
||||
) {
|
||||
constructor(connectionId, subscriptionId, baseUrl, topic, user, since, onNotification, onStateChanged) {
|
||||
this.connectionId = connectionId;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.baseUrl = baseUrl;
|
||||
|
@ -44,78 +29,51 @@ class Connection {
|
|||
// we don't want to re-trigger the main view re-render potentially hundreds of times.
|
||||
|
||||
const wsUrl = this.wsUrl();
|
||||
console.log(
|
||||
`[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}`
|
||||
);
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}`);
|
||||
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
this.ws.onopen = (event) => {
|
||||
console.log(
|
||||
`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`,
|
||||
event
|
||||
);
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`, event);
|
||||
this.retryCount = 0;
|
||||
this.onStateChanged(this.subscriptionId, ConnectionState.Connected);
|
||||
};
|
||||
this.ws.onmessage = (event) => {
|
||||
console.log(
|
||||
`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`
|
||||
);
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.event === "open") {
|
||||
return;
|
||||
}
|
||||
const relevantAndValid =
|
||||
data.event === "message" &&
|
||||
"id" in data &&
|
||||
"time" in data &&
|
||||
"message" in data;
|
||||
const relevantAndValid = data.event === "message" && "id" in data && "time" in data && "message" in data;
|
||||
if (!relevantAndValid) {
|
||||
console.log(
|
||||
`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`
|
||||
);
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`);
|
||||
return;
|
||||
}
|
||||
this.since = data.id;
|
||||
this.onNotification(this.subscriptionId, data);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}`
|
||||
);
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}`);
|
||||
}
|
||||
};
|
||||
this.ws.onclose = (event) => {
|
||||
if (event.wasClean) {
|
||||
console.log(
|
||||
`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`
|
||||
);
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
|
||||
this.ws = null;
|
||||
} else {
|
||||
const retrySeconds =
|
||||
retryBackoffSeconds[
|
||||
Math.min(this.retryCount, retryBackoffSeconds.length - 1)
|
||||
];
|
||||
const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length - 1)];
|
||||
this.retryCount++;
|
||||
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.onStateChanged(this.subscriptionId, ConnectionState.Connecting);
|
||||
}
|
||||
};
|
||||
this.ws.onerror = (event) => {
|
||||
console.log(
|
||||
`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`,
|
||||
event
|
||||
);
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`, event);
|
||||
};
|
||||
}
|
||||
|
||||
close() {
|
||||
console.log(
|
||||
`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`
|
||||
);
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`);
|
||||
const socket = this.ws;
|
||||
const retryTimeout = this.retryTimeout;
|
||||
if (socket !== null) {
|
||||
|
|
|
@ -49,12 +49,8 @@ class ConnectionManager {
|
|||
return { ...s, user, connectionId };
|
||||
})
|
||||
);
|
||||
const targetIds = subscriptionsWithUsersAndConnectionId.map(
|
||||
(s) => s.connectionId
|
||||
);
|
||||
const deletedIds = Array.from(this.connections.keys()).filter(
|
||||
(id) => !targetIds.includes(id)
|
||||
);
|
||||
const targetIds = subscriptionsWithUsersAndConnectionId.map((s) => s.connectionId);
|
||||
const deletedIds = Array.from(this.connections.keys()).filter((id) => !targetIds.includes(id));
|
||||
|
||||
// Create and add new connections
|
||||
subscriptionsWithUsersAndConnectionId.forEach((subscription) => {
|
||||
|
@ -73,15 +69,12 @@ class ConnectionManager {
|
|||
topic,
|
||||
user,
|
||||
since,
|
||||
(subscriptionId, notification) =>
|
||||
this.notificationReceived(subscriptionId, notification),
|
||||
(subscriptionId, notification) => this.notificationReceived(subscriptionId, notification),
|
||||
(subscriptionId, state) => this.stateChanged(subscriptionId, state)
|
||||
);
|
||||
this.connections.set(connectionId, connection);
|
||||
console.log(
|
||||
`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${
|
||||
user ? user.username : "anonymous"
|
||||
})`
|
||||
`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${user ? user.username : "anonymous"})`
|
||||
);
|
||||
connection.start();
|
||||
}
|
||||
|
@ -101,10 +94,7 @@ class ConnectionManager {
|
|||
try {
|
||||
this.stateListener(subscriptionId, state);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[ConnectionManager] Error updating state of ${subscriptionId} to ${state}`,
|
||||
e
|
||||
);
|
||||
console.error(`[ConnectionManager] Error updating state of ${subscriptionId} to ${state}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,23 +104,14 @@ class ConnectionManager {
|
|||
try {
|
||||
this.messageListener(subscriptionId, notification);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[ConnectionManager] Error handling notification for ${subscriptionId}`,
|
||||
e
|
||||
);
|
||||
console.error(`[ConnectionManager] Error handling notification for ${subscriptionId}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const makeConnectionId = async (subscription, user) => {
|
||||
return user
|
||||
? hashCode(
|
||||
`${subscription.id}|${user.username}|${user.password ?? ""}|${
|
||||
user.token ?? ""
|
||||
}`
|
||||
)
|
||||
: hashCode(`${subscription.id}`);
|
||||
return user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`);
|
||||
};
|
||||
|
||||
const connectionManager = new ConnectionManager();
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
import {
|
||||
formatMessage,
|
||||
formatTitleWithDefault,
|
||||
openUrl,
|
||||
playSound,
|
||||
topicDisplayName,
|
||||
topicShortUrl,
|
||||
} from "./utils";
|
||||
import { formatMessage, formatTitleWithDefault, openUrl, playSound, topicDisplayName, topicShortUrl } from "./utils";
|
||||
import prefs from "./Prefs";
|
||||
import subscriptionManager from "./SubscriptionManager";
|
||||
import logo from "../img/ntfy.png";
|
||||
|
@ -30,9 +23,7 @@ class Notifier {
|
|||
const title = formatTitleWithDefault(notification, displayName);
|
||||
|
||||
// Show notification
|
||||
console.log(
|
||||
`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`
|
||||
);
|
||||
console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`);
|
||||
const n = new Notification(title, {
|
||||
body: message,
|
||||
icon: logo,
|
||||
|
@ -96,11 +87,7 @@ class Notifier {
|
|||
* is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification
|
||||
*/
|
||||
contextSupported() {
|
||||
return (
|
||||
location.protocol === "https:" ||
|
||||
location.hostname.match("^127.") ||
|
||||
location.hostname === "localhost"
|
||||
);
|
||||
return location.protocol === "https:" || location.hostname.match("^127.") || location.hostname === "localhost";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,18 +34,12 @@ class Poller {
|
|||
console.log(`[Poller] Polling ${subscription.id}`);
|
||||
|
||||
const since = subscription.last;
|
||||
const notifications = await api.poll(
|
||||
subscription.baseUrl,
|
||||
subscription.topic,
|
||||
since
|
||||
);
|
||||
const notifications = await api.poll(subscription.baseUrl, subscription.topic, since);
|
||||
if (!notifications || notifications.length === 0) {
|
||||
console.log(`[Poller] No new notifications found for ${subscription.id}`);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`
|
||||
);
|
||||
console.log(`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`);
|
||||
await subscriptionManager.addNotifications(subscription.id, notifications);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,15 +20,12 @@ class Pruner {
|
|||
|
||||
async prune() {
|
||||
const deleteAfterSeconds = await prefs.deleteAfter();
|
||||
const pruneThresholdTimestamp =
|
||||
Math.round(Date.now() / 1000) - deleteAfterSeconds;
|
||||
const pruneThresholdTimestamp = Math.round(Date.now() / 1000) - deleteAfterSeconds;
|
||||
if (deleteAfterSeconds === 0) {
|
||||
console.log(`[Pruner] Pruning is disabled. Skipping.`);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`[Pruner] Pruning notifications older than ${deleteAfterSeconds}s (timestamp ${pruneThresholdTimestamp})`
|
||||
);
|
||||
console.log(`[Pruner] Pruning notifications older than ${deleteAfterSeconds}s (timestamp ${pruneThresholdTimestamp})`);
|
||||
try {
|
||||
await subscriptionManager.pruneNotifications(pruneThresholdTimestamp);
|
||||
} catch (e) {
|
||||
|
|
|
@ -7,9 +7,7 @@ class SubscriptionManager {
|
|||
const subscriptions = await db.subscriptions.toArray();
|
||||
await Promise.all(
|
||||
subscriptions.map(async (s) => {
|
||||
s.new = await db.notifications
|
||||
.where({ subscriptionId: s.id, new: 1 })
|
||||
.count();
|
||||
s.new = await db.notifications.where({ subscriptionId: s.id, new: 1 }).count();
|
||||
})
|
||||
);
|
||||
return subscriptions;
|
||||
|
@ -38,20 +36,14 @@ class SubscriptionManager {
|
|||
}
|
||||
|
||||
async syncFromRemote(remoteSubscriptions, remoteReservations) {
|
||||
console.log(
|
||||
`[SubscriptionManager] Syncing subscriptions from remote`,
|
||||
remoteSubscriptions
|
||||
);
|
||||
console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions);
|
||||
|
||||
// Add remote subscriptions
|
||||
let remoteIds = []; // = topicUrl(baseUrl, topic)
|
||||
for (let i = 0; i < remoteSubscriptions.length; i++) {
|
||||
const remote = remoteSubscriptions[i];
|
||||
const local = await this.add(remote.base_url, remote.topic, false);
|
||||
const reservation =
|
||||
remoteReservations?.find(
|
||||
(r) => remote.base_url === config.base_url && remote.topic === r.topic
|
||||
) || null;
|
||||
const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null;
|
||||
await this.update(local.id, {
|
||||
displayName: remote.display_name, // May be undefined
|
||||
reservation: reservation, // May be null!
|
||||
|
@ -122,9 +114,7 @@ class SubscriptionManager {
|
|||
|
||||
/** Adds/replaces notifications, will not throw if they exist */
|
||||
async addNotifications(subscriptionId, notifications) {
|
||||
const notificationsWithSubscriptionId = notifications.map(
|
||||
(notification) => ({ ...notification, subscriptionId })
|
||||
);
|
||||
const notificationsWithSubscriptionId = notifications.map((notification) => ({ ...notification, subscriptionId }));
|
||||
const lastNotificationId = notifications.at(-1).id;
|
||||
await db.notifications.bulkPut(notificationsWithSubscriptionId);
|
||||
await db.subscriptions.update(subscriptionId, {
|
||||
|
@ -158,9 +148,7 @@ class SubscriptionManager {
|
|||
}
|
||||
|
||||
async markNotificationsRead(subscriptionId) {
|
||||
await db.notifications
|
||||
.where({ subscriptionId: subscriptionId, new: 1 })
|
||||
.modify({ new: 0 });
|
||||
await db.notifications.where({ subscriptionId: subscriptionId, new: 1 }).modify({ new: 0 });
|
||||
}
|
||||
|
||||
async setMutedUntil(subscriptionId, mutedUntil) {
|
||||
|
|
|
@ -15,12 +15,7 @@ export const throwAppError = async (response) => {
|
|||
}
|
||||
const error = await maybeToJson(response);
|
||||
if (error?.code) {
|
||||
console.log(
|
||||
`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${
|
||||
error.error || ""
|
||||
}`,
|
||||
response
|
||||
);
|
||||
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) {
|
||||
|
|
|
@ -10,37 +10,23 @@ import config from "./config";
|
|||
import { Base64 } from "js-base64";
|
||||
|
||||
export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
|
||||
export const topicUrlWs = (baseUrl, topic) =>
|
||||
`${topicUrl(baseUrl, topic)}/ws`
|
||||
.replaceAll("https://", "wss://")
|
||||
.replaceAll("http://", "ws://");
|
||||
export const topicUrlJson = (baseUrl, topic) =>
|
||||
`${topicUrl(baseUrl, topic)}/json`;
|
||||
export const topicUrlJsonPoll = (baseUrl, topic) =>
|
||||
`${topicUrlJson(baseUrl, topic)}?poll=1`;
|
||||
export const topicUrlJsonPollWithSince = (baseUrl, topic, since) =>
|
||||
`${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`;
|
||||
export const topicUrlAuth = (baseUrl, topic) =>
|
||||
`${topicUrl(baseUrl, topic)}/auth`;
|
||||
export const topicShortUrl = (baseUrl, topic) =>
|
||||
shortUrl(topicUrl(baseUrl, topic));
|
||||
export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws`.replaceAll("https://", "wss://").replaceAll("http://", "ws://");
|
||||
export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`;
|
||||
export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`;
|
||||
export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`;
|
||||
export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`;
|
||||
export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
|
||||
export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`;
|
||||
export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`;
|
||||
export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`;
|
||||
export const accountSettingsUrl = (baseUrl) => `${baseUrl}/v1/account/settings`;
|
||||
export const accountSubscriptionUrl = (baseUrl) =>
|
||||
`${baseUrl}/v1/account/subscription`;
|
||||
export const accountReservationUrl = (baseUrl) =>
|
||||
`${baseUrl}/v1/account/reservation`;
|
||||
export const accountReservationSingleUrl = (baseUrl, topic) =>
|
||||
`${baseUrl}/v1/account/reservation/${topic}`;
|
||||
export const accountBillingSubscriptionUrl = (baseUrl) =>
|
||||
`${baseUrl}/v1/account/billing/subscription`;
|
||||
export const accountBillingPortalUrl = (baseUrl) =>
|
||||
`${baseUrl}/v1/account/billing/portal`;
|
||||
export const accountSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/subscription`;
|
||||
export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reservation`;
|
||||
export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`;
|
||||
export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
|
||||
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
|
||||
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}`];
|
||||
|
@ -208,9 +194,7 @@ export const formatShortDateTime = (timestamp) => {
|
|||
};
|
||||
|
||||
export const formatShortDate = (timestamp) => {
|
||||
return 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) => {
|
||||
|
@ -312,8 +296,7 @@ export async function* fetchLinesIterator(fileURL, headers) {
|
|||
}
|
||||
|
||||
export const randomAlphanumericString = (len) => {
|
||||
const alphabet =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let id = "";
|
||||
for (let i = 0; i < len; i++) {
|
||||
id += alphabet[(Math.random() * alphabet.length) | 0];
|
||||
|
|
|
@ -38,18 +38,8 @@ import DialogContent from "@mui/material/DialogContent";
|
|||
import TextField from "@mui/material/TextField";
|
||||
import routes from "./routes";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import {
|
||||
formatBytes,
|
||||
formatShortDate,
|
||||
formatShortDateTime,
|
||||
openUrl,
|
||||
} from "../app/utils";
|
||||
import accountApi, {
|
||||
LimitBasis,
|
||||
Role,
|
||||
SubscriptionInterval,
|
||||
SubscriptionStatus,
|
||||
} from "../app/AccountApi";
|
||||
import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils";
|
||||
import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import { Pref, PrefGroup } from "./Pref";
|
||||
import db from "../app/db";
|
||||
|
@ -108,11 +98,7 @@ const Username = () => {
|
|||
const labelId = "prefUsername";
|
||||
|
||||
return (
|
||||
<Pref
|
||||
labelId={labelId}
|
||||
title={t("account_basics_username_title")}
|
||||
description={t("account_basics_username_description")}
|
||||
>
|
||||
<Pref labelId={labelId} title={t("account_basics_username_title")} description={t("account_basics_username_description")}>
|
||||
<div aria-labelledby={labelId}>
|
||||
{session.username()}
|
||||
{account?.role === Role.ADMIN ? (
|
||||
|
@ -146,30 +132,16 @@ const ChangePassword = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Pref
|
||||
labelId={labelId}
|
||||
title={t("account_basics_password_title")}
|
||||
description={t("account_basics_password_description")}
|
||||
>
|
||||
<Pref labelId={labelId} title={t("account_basics_password_title")} description={t("account_basics_password_description")}>
|
||||
<div aria-labelledby={labelId}>
|
||||
<Typography
|
||||
color="gray"
|
||||
sx={{ float: "left", fontSize: "0.7rem", lineHeight: "3.5" }}
|
||||
>
|
||||
<Typography color="gray" sx={{ float: "left", fontSize: "0.7rem", lineHeight: "3.5" }}>
|
||||
⬤⬤⬤⬤⬤⬤⬤⬤⬤⬤
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={handleDialogOpen}
|
||||
aria-label={t("account_basics_password_description")}
|
||||
>
|
||||
<IconButton onClick={handleDialogOpen} aria-label={t("account_basics_password_description")}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
<ChangePasswordDialog
|
||||
key={`changePasswordDialog${dialogKey}`}
|
||||
open={dialogOpen}
|
||||
onClose={handleDialogClose}
|
||||
/>
|
||||
<ChangePasswordDialog key={`changePasswordDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} />
|
||||
</Pref>
|
||||
);
|
||||
};
|
||||
|
@ -190,9 +162,7 @@ const ChangePasswordDialog = (props) => {
|
|||
} catch (e) {
|
||||
console.log(`[Account] Error changing password`, e);
|
||||
if (e instanceof IncorrectPasswordError) {
|
||||
setError(
|
||||
t("account_basics_password_dialog_current_password_incorrect")
|
||||
);
|
||||
setError(t("account_basics_password_dialog_current_password_incorrect"));
|
||||
} else if (e instanceof UnauthorizedError) {
|
||||
session.resetAndRedirect(routes.login);
|
||||
} else {
|
||||
|
@ -209,9 +179,7 @@ const ChangePasswordDialog = (props) => {
|
|||
margin="dense"
|
||||
id="current-password"
|
||||
label={t("account_basics_password_dialog_current_password_label")}
|
||||
aria-label={t(
|
||||
"account_basics_password_dialog_current_password_label"
|
||||
)}
|
||||
aria-label={t("account_basics_password_dialog_current_password_label")}
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(ev) => setCurrentPassword(ev.target.value)}
|
||||
|
@ -233,9 +201,7 @@ const ChangePasswordDialog = (props) => {
|
|||
margin="dense"
|
||||
id="confirm"
|
||||
label={t("account_basics_password_dialog_confirm_password_label")}
|
||||
aria-label={t(
|
||||
"account_basics_password_dialog_confirm_password_label"
|
||||
)}
|
||||
aria-label={t("account_basics_password_dialog_confirm_password_label")}
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(ev) => setConfirmPassword(ev.target.value)}
|
||||
|
@ -245,14 +211,7 @@ const ChangePasswordDialog = (props) => {
|
|||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
|
||||
<Button
|
||||
onClick={handleDialogSubmit}
|
||||
disabled={
|
||||
newPassword.length === 0 ||
|
||||
currentPassword.length === 0 ||
|
||||
newPassword !== confirmPassword
|
||||
}
|
||||
>
|
||||
<Button onClick={handleDialogSubmit} disabled={newPassword.length === 0 || currentPassword.length === 0 || newPassword !== confirmPassword}>
|
||||
{t("account_basics_password_dialog_button_submit")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
@ -299,9 +258,7 @@ const AccountType = () => {
|
|||
: t("account_basics_tier_admin_suffix_no_tier");
|
||||
accountType = `${t("account_basics_tier_admin")} ${tierSuffix}`;
|
||||
} else if (!account.tier) {
|
||||
accountType = config.enable_payments
|
||||
? t("account_basics_tier_free")
|
||||
: t("account_basics_tier_basic");
|
||||
accountType = config.enable_payments ? t("account_basics_tier_free") : t("account_basics_tier_basic");
|
||||
} else {
|
||||
accountType = account.tier.name;
|
||||
if (account.billing?.interval === SubscriptionInterval.MONTH) {
|
||||
|
@ -313,10 +270,7 @@ const AccountType = () => {
|
|||
|
||||
return (
|
||||
<Pref
|
||||
alignTop={
|
||||
account.billing?.status === SubscriptionStatus.PAST_DUE ||
|
||||
account.billing?.cancel_at > 0
|
||||
}
|
||||
alignTop={account.billing?.status === SubscriptionStatus.PAST_DUE || account.billing?.cancel_at > 0}
|
||||
title={t("account_basics_tier_title")}
|
||||
description={t("account_basics_tier_description")}
|
||||
>
|
||||
|
@ -333,49 +287,23 @@ const AccountType = () => {
|
|||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{config.enable_payments &&
|
||||
account.role === Role.USER &&
|
||||
!account.billing?.subscription && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<CelebrationIcon sx={{ color: "#55b86e" }} />}
|
||||
onClick={handleUpgradeClick}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
{t("account_basics_tier_upgrade_button")}
|
||||
</Button>
|
||||
)}
|
||||
{config.enable_payments &&
|
||||
account.role === Role.USER &&
|
||||
account.billing?.subscription && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleUpgradeClick}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
{t("account_basics_tier_change_button")}
|
||||
</Button>
|
||||
)}
|
||||
{config.enable_payments &&
|
||||
account.role === Role.USER &&
|
||||
account.billing?.customer && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleManageBilling}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
{t("account_basics_tier_manage_billing_button")}
|
||||
</Button>
|
||||
)}
|
||||
{config.enable_payments && account.role === Role.USER && !account.billing?.subscription && (
|
||||
<Button variant="outlined" size="small" startIcon={<CelebrationIcon sx={{ color: "#55b86e" }} />} onClick={handleUpgradeClick} sx={{ ml: 1 }}>
|
||||
{t("account_basics_tier_upgrade_button")}
|
||||
</Button>
|
||||
)}
|
||||
{config.enable_payments && account.role === Role.USER && account.billing?.subscription && (
|
||||
<Button variant="outlined" size="small" onClick={handleUpgradeClick} sx={{ ml: 1 }}>
|
||||
{t("account_basics_tier_change_button")}
|
||||
</Button>
|
||||
)}
|
||||
{config.enable_payments && account.role === Role.USER && account.billing?.customer && (
|
||||
<Button variant="outlined" size="small" onClick={handleManageBilling} sx={{ ml: 1 }}>
|
||||
{t("account_basics_tier_manage_billing_button")}
|
||||
</Button>
|
||||
)}
|
||||
{config.enable_payments && (
|
||||
<UpgradeDialog
|
||||
key={`upgradeDialogFromAccount${upgradeDialogKey}`}
|
||||
open={upgradeDialogOpen}
|
||||
onCancel={() => setUpgradeDialogOpen(false)}
|
||||
/>
|
||||
<UpgradeDialog key={`upgradeDialogFromAccount${upgradeDialogKey}`} open={upgradeDialogOpen} onCancel={() => setUpgradeDialogOpen(false)} />
|
||||
)}
|
||||
</div>
|
||||
{account.billing?.status === SubscriptionStatus.PAST_DUE && (
|
||||
|
@ -456,11 +384,7 @@ const PhoneNumbers = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Pref
|
||||
labelId={labelId}
|
||||
title={t("account_basics_phone_numbers_title")}
|
||||
description={t("account_basics_phone_numbers_description")}
|
||||
>
|
||||
<Pref labelId={labelId} title={t("account_basics_phone_numbers_title")} description={t("account_basics_phone_numbers_description")}>
|
||||
<div aria-labelledby={labelId}>
|
||||
{account?.phone_numbers?.map((phoneNumber) => (
|
||||
<Chip
|
||||
|
@ -474,18 +398,12 @@ const PhoneNumbers = () => {
|
|||
onDelete={() => handleDelete(phoneNumber)}
|
||||
/>
|
||||
))}
|
||||
{!account?.phone_numbers && (
|
||||
<em>{t("account_basics_phone_numbers_no_phone_numbers_yet")}</em>
|
||||
)}
|
||||
{!account?.phone_numbers && <em>{t("account_basics_phone_numbers_no_phone_numbers_yet")}</em>}
|
||||
<IconButton onClick={handleDialogOpen}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
<AddPhoneNumberDialog
|
||||
key={`addPhoneNumberDialog${dialogKey}`}
|
||||
open={dialogOpen}
|
||||
onClose={handleDialogClose}
|
||||
/>
|
||||
<AddPhoneNumberDialog key={`addPhoneNumberDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} />
|
||||
<Portal>
|
||||
<Snackbar
|
||||
open={snackOpen}
|
||||
|
@ -561,22 +479,16 @@ const AddPhoneNumberDialog = (props) => {
|
|||
|
||||
return (
|
||||
<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>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("account_basics_phone_numbers_dialog_description")}
|
||||
</DialogContentText>
|
||||
<DialogContentText>{t("account_basics_phone_numbers_dialog_description")}</DialogContentText>
|
||||
{!verificationCodeSent && (
|
||||
<div style={{ display: "flex" }}>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label={t("account_basics_phone_numbers_dialog_number_label")}
|
||||
aria-label={t("account_basics_phone_numbers_dialog_number_label")}
|
||||
placeholder={t(
|
||||
"account_basics_phone_numbers_dialog_number_placeholder"
|
||||
)}
|
||||
placeholder={t("account_basics_phone_numbers_dialog_number_placeholder")}
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
onChange={(ev) => setPhoneNumber(ev.target.value)}
|
||||
|
@ -585,28 +497,15 @@ const AddPhoneNumberDialog = (props) => {
|
|||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<FormControl sx={{ flexWrap: "nowrap" }}>
|
||||
<RadioGroup
|
||||
row
|
||||
sx={{ flexGrow: 1, marginTop: "8px", marginLeft: "5px" }}
|
||||
>
|
||||
<RadioGroup row sx={{ flexGrow: 1, marginTop: "8px", marginLeft: "5px" }}>
|
||||
<FormControlLabel
|
||||
value="sms"
|
||||
control={
|
||||
<Radio
|
||||
checked={channel === "sms"}
|
||||
onChange={(e) => setChannel(e.target.value)}
|
||||
/>
|
||||
}
|
||||
control={<Radio checked={channel === "sms"} onChange={(e) => setChannel(e.target.value)} />}
|
||||
label={t("account_basics_phone_numbers_dialog_channel_sms")}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="call"
|
||||
control={
|
||||
<Radio
|
||||
checked={channel === "call"}
|
||||
onChange={(e) => setChannel(e.target.value)}
|
||||
/>
|
||||
}
|
||||
control={<Radio checked={channel === "call"} onChange={(e) => setChannel(e.target.value)} />}
|
||||
label={t("account_basics_phone_numbers_dialog_channel_call")}
|
||||
sx={{ marginRight: 0 }}
|
||||
/>
|
||||
|
@ -619,9 +518,7 @@ const AddPhoneNumberDialog = (props) => {
|
|||
margin="dense"
|
||||
label={t("account_basics_phone_numbers_dialog_code_label")}
|
||||
aria-label={t("account_basics_phone_numbers_dialog_code_label")}
|
||||
placeholder={t(
|
||||
"account_basics_phone_numbers_dialog_code_placeholder"
|
||||
)}
|
||||
placeholder={t("account_basics_phone_numbers_dialog_code_placeholder")}
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(ev) => setCode(ev.target.value)}
|
||||
|
@ -632,21 +529,11 @@ const AddPhoneNumberDialog = (props) => {
|
|||
)}
|
||||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={handleCancel}>
|
||||
{verificationCodeSent ? t("common_back") : t("common_cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDialogSubmit}
|
||||
disabled={sending || !/^\+\d+$/.test(phoneNumber)}
|
||||
>
|
||||
{!verificationCodeSent &&
|
||||
channel === "sms" &&
|
||||
t("account_basics_phone_numbers_dialog_verify_button_sms")}
|
||||
{!verificationCodeSent &&
|
||||
channel === "call" &&
|
||||
t("account_basics_phone_numbers_dialog_verify_button_call")}
|
||||
{verificationCodeSent &&
|
||||
t("account_basics_phone_numbers_dialog_check_verification_button")}
|
||||
<Button onClick={handleCancel}>{verificationCodeSent ? t("common_back") : t("common_cancel")}</Button>
|
||||
<Button onClick={handleDialogSubmit} disabled={sending || !/^\+\d+$/.test(phoneNumber)}>
|
||||
{!verificationCodeSent && channel === "sms" && t("account_basics_phone_numbers_dialog_verify_button_sms")}
|
||||
{!verificationCodeSent && channel === "call" && t("account_basics_phone_numbers_dialog_verify_button_call")}
|
||||
{verificationCodeSent && t("account_basics_phone_numbers_dialog_check_verification_button")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
|
@ -687,14 +574,7 @@ const Stats = () => {
|
|||
</div>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={
|
||||
account.role === Role.USER && account.limits.reservations > 0
|
||||
? normalize(
|
||||
account.stats.reservations,
|
||||
account.limits.reservations
|
||||
)
|
||||
: 100
|
||||
}
|
||||
value={account.role === Role.USER && account.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100}
|
||||
/>
|
||||
</Pref>
|
||||
)}
|
||||
|
@ -722,14 +602,7 @@ const Stats = () => {
|
|||
: t("account_usage_unlimited")}
|
||||
</Typography>
|
||||
</div>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={
|
||||
account.role === Role.USER
|
||||
? normalize(account.stats.messages, account.limits.messages)
|
||||
: 100
|
||||
}
|
||||
/>
|
||||
<LinearProgress variant="determinate" value={account.role === Role.USER ? normalize(account.stats.messages, account.limits.messages) : 100} />
|
||||
</Pref>
|
||||
{config.enable_emails && (
|
||||
<Pref
|
||||
|
@ -756,64 +629,49 @@ const Stats = () => {
|
|||
: t("account_usage_unlimited")}
|
||||
</Typography>
|
||||
</div>
|
||||
<LinearProgress variant="determinate" value={account.role === Role.USER ? normalize(account.stats.emails, account.limits.emails) : 100} />
|
||||
</Pref>
|
||||
)}
|
||||
{config.enable_calls && (account.role === Role.ADMIN || account.limits.calls > 0) && (
|
||||
<Pref
|
||||
title={
|
||||
<>
|
||||
{t("account_usage_calls_title")}
|
||||
<Tooltip title={t("account_usage_limits_reset_daily")}>
|
||||
<span>
|
||||
<InfoIcon />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{ float: "left" }}>
|
||||
{account.stats.calls.toLocaleString()}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ float: "right" }}>
|
||||
{account.role === Role.USER
|
||||
? t("account_usage_of_limit", {
|
||||
limit: account.limits.calls.toLocaleString(),
|
||||
})
|
||||
: t("account_usage_unlimited")}
|
||||
</Typography>
|
||||
</div>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={
|
||||
account.role === Role.USER
|
||||
? normalize(account.stats.emails, account.limits.emails)
|
||||
: 100
|
||||
}
|
||||
value={account.role === Role.USER && account.limits.calls > 0 ? normalize(account.stats.calls, account.limits.calls) : 100}
|
||||
/>
|
||||
</Pref>
|
||||
)}
|
||||
{config.enable_calls &&
|
||||
(account.role === Role.ADMIN || account.limits.calls > 0) && (
|
||||
<Pref
|
||||
title={
|
||||
<>
|
||||
{t("account_usage_calls_title")}
|
||||
<Tooltip title={t("account_usage_limits_reset_daily")}>
|
||||
<span>
|
||||
<InfoIcon />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{ float: "left" }}>
|
||||
{account.stats.calls.toLocaleString()}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ float: "right" }}>
|
||||
{account.role === Role.USER
|
||||
? t("account_usage_of_limit", {
|
||||
limit: account.limits.calls.toLocaleString(),
|
||||
})
|
||||
: t("account_usage_unlimited")}
|
||||
</Typography>
|
||||
</div>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={
|
||||
account.role === Role.USER && account.limits.calls > 0
|
||||
? normalize(account.stats.calls, account.limits.calls)
|
||||
: 100
|
||||
}
|
||||
/>
|
||||
</Pref>
|
||||
)}
|
||||
<Pref
|
||||
alignTop
|
||||
title={t("account_usage_attachment_storage_title")}
|
||||
description={t("account_usage_attachment_storage_description", {
|
||||
filesize: formatBytes(account.limits.attachment_file_size),
|
||||
expiry: humanizeDuration(
|
||||
account.limits.attachment_expiry_duration * 1000,
|
||||
{
|
||||
language: i18n.resolvedLanguage,
|
||||
fallbacks: ["en"],
|
||||
}
|
||||
),
|
||||
expiry: humanizeDuration(account.limits.attachment_expiry_duration * 1000, {
|
||||
language: i18n.resolvedLanguage,
|
||||
fallbacks: ["en"],
|
||||
}),
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
|
@ -830,49 +688,36 @@ const Stats = () => {
|
|||
</div>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={
|
||||
account.role === Role.USER
|
||||
? normalize(
|
||||
account.stats.attachment_total_size,
|
||||
account.limits.attachment_total_size
|
||||
)
|
||||
: 100
|
||||
}
|
||||
value={account.role === Role.USER ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100}
|
||||
/>
|
||||
</Pref>
|
||||
{config.enable_reservations &&
|
||||
account.role === Role.USER &&
|
||||
account.limits.reservations === 0 && (
|
||||
<Pref
|
||||
title={
|
||||
<>
|
||||
{t("account_usage_reservations_title")}
|
||||
{config.enable_payments && <ProChip />}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<em>{t("account_usage_reservations_none")}</em>
|
||||
</Pref>
|
||||
)}
|
||||
{config.enable_calls &&
|
||||
account.role === Role.USER &&
|
||||
account.limits.calls === 0 && (
|
||||
<Pref
|
||||
title={
|
||||
<>
|
||||
{t("account_usage_calls_title")}
|
||||
{config.enable_payments && <ProChip />}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<em>{t("account_usage_calls_none")}</em>
|
||||
</Pref>
|
||||
)}
|
||||
{config.enable_reservations && account.role === Role.USER && account.limits.reservations === 0 && (
|
||||
<Pref
|
||||
title={
|
||||
<>
|
||||
{t("account_usage_reservations_title")}
|
||||
{config.enable_payments && <ProChip />}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<em>{t("account_usage_reservations_none")}</em>
|
||||
</Pref>
|
||||
)}
|
||||
{config.enable_calls && account.role === Role.USER && account.limits.calls === 0 && (
|
||||
<Pref
|
||||
title={
|
||||
<>
|
||||
{t("account_usage_calls_title")}
|
||||
{config.enable_payments && <ProChip />}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<em>{t("account_usage_calls_none")}</em>
|
||||
</Pref>
|
||||
)}
|
||||
</PrefGroup>
|
||||
{account.role === Role.USER && account.limits.basis === LimitBasis.IP && (
|
||||
<Typography variant="body1">
|
||||
{t("account_usage_basis_ip_description")}
|
||||
</Typography>
|
||||
<Typography variant="body1">{t("account_usage_basis_ip_description")}</Typography>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
@ -928,15 +773,9 @@ const Tokens = () => {
|
|||
{tokens?.length > 0 && <TokensTable tokens={tokens} />}
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button onClick={handleCreateClick}>
|
||||
{t("account_tokens_table_create_token_button")}
|
||||
</Button>
|
||||
<Button onClick={handleCreateClick}>{t("account_tokens_table_create_token_button")}</Button>
|
||||
</CardActions>
|
||||
<TokenDialog
|
||||
key={`tokenDialogCreate${dialogKey}`}
|
||||
open={dialogOpen}
|
||||
onClose={handleDialogClose}
|
||||
/>
|
||||
<TokenDialog key={`tokenDialogCreate${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
@ -984,9 +823,7 @@ const TokensTable = (props) => {
|
|||
<Table size="small" aria-label={t("account_tokens_title")}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ paddingLeft: 0 }}>
|
||||
{t("account_tokens_table_token_header")}
|
||||
</TableCell>
|
||||
<TableCell sx={{ paddingLeft: 0 }}>{t("account_tokens_table_token_header")}</TableCell>
|
||||
<TableCell>{t("account_tokens_table_label_header")}</TableCell>
|
||||
<TableCell>{t("account_tokens_table_expires_header")}</TableCell>
|
||||
<TableCell>{t("account_tokens_table_last_access_header")}</TableCell>
|
||||
|
@ -995,25 +832,12 @@ const TokensTable = (props) => {
|
|||
</TableHead>
|
||||
<TableBody>
|
||||
{tokens.map((token) => (
|
||||
<TableRow
|
||||
key={token.token}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{ paddingLeft: 0, whiteSpace: "nowrap" }}
|
||||
aria-label={t("account_tokens_table_token_header")}
|
||||
>
|
||||
<TableRow key={token.token} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
|
||||
<TableCell component="th" scope="row" sx={{ paddingLeft: 0, whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_token_header")}>
|
||||
<span>
|
||||
<span style={{ fontFamily: "Monospace", fontSize: "0.9rem" }}>
|
||||
{token.token.slice(0, 12)}
|
||||
</span>
|
||||
<span style={{ fontFamily: "Monospace", fontSize: "0.9rem" }}>{token.token.slice(0, 12)}</span>
|
||||
...
|
||||
<Tooltip
|
||||
title={t("common_copy_to_clipboard")}
|
||||
placement="right"
|
||||
>
|
||||
<Tooltip title={t("common_copy_to_clipboard")} placement="right">
|
||||
<IconButton onClick={() => handleCopy(token.token)}>
|
||||
<ContentCopy />
|
||||
</IconButton>
|
||||
|
@ -1021,25 +845,13 @@ const TokensTable = (props) => {
|
|||
</span>
|
||||
</TableCell>
|
||||
<TableCell aria-label={t("account_tokens_table_label_header")}>
|
||||
{token.token === session.token() && (
|
||||
<em>{t("account_tokens_table_current_session")}</em>
|
||||
)}
|
||||
{token.token === session.token() && <em>{t("account_tokens_table_current_session")}</em>}
|
||||
{token.token !== session.token() && (token.label || "-")}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ whiteSpace: "nowrap" }}
|
||||
aria-label={t("account_tokens_table_expires_header")}
|
||||
>
|
||||
{token.expires ? (
|
||||
formatShortDateTime(token.expires)
|
||||
) : (
|
||||
<em>{t("account_tokens_table_never_expires")}</em>
|
||||
)}
|
||||
<TableCell sx={{ whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_expires_header")}>
|
||||
{token.expires ? formatShortDateTime(token.expires) : <em>{t("account_tokens_table_never_expires")}</em>}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ whiteSpace: "nowrap" }}
|
||||
aria-label={t("account_tokens_table_last_access_header")}
|
||||
>
|
||||
<TableCell sx={{ whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_last_access_header")}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span>{formatShortDateTime(token.last_access)}</span>
|
||||
<Tooltip
|
||||
|
@ -1047,13 +859,7 @@ const TokensTable = (props) => {
|
|||
ip: token.last_origin,
|
||||
})}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
openUrl(
|
||||
`https://whatismyipaddress.com/ip/${token.last_origin}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconButton onClick={() => openUrl(`https://whatismyipaddress.com/ip/${token.last_origin}`)}>
|
||||
<Public />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
@ -1062,24 +868,16 @@ const TokensTable = (props) => {
|
|||
<TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
|
||||
{token.token !== session.token() && (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => handleEditClick(token)}
|
||||
aria-label={t("account_tokens_dialog_title_edit")}
|
||||
>
|
||||
<IconButton onClick={() => handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => handleDeleteClick(token)}
|
||||
aria-label={t("account_tokens_dialog_title_delete")}
|
||||
>
|
||||
<IconButton onClick={() => handleDeleteClick(token)} aria-label={t("account_tokens_dialog_title_delete")}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
{token.token === session.token() && (
|
||||
<Tooltip
|
||||
title={t("account_tokens_table_cannot_delete_or_edit")}
|
||||
>
|
||||
<Tooltip title={t("account_tokens_table_cannot_delete_or_edit")}>
|
||||
<span>
|
||||
<IconButton disabled>
|
||||
<EditIcon />
|
||||
|
@ -1095,24 +893,10 @@ const TokensTable = (props) => {
|
|||
))}
|
||||
</TableBody>
|
||||
<Portal>
|
||||
<Snackbar
|
||||
open={snackOpen}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setSnackOpen(false)}
|
||||
message={t("account_tokens_table_copied_to_clipboard")}
|
||||
/>
|
||||
<Snackbar open={snackOpen} autoHideDuration={3000} onClose={() => setSnackOpen(false)} message={t("account_tokens_table_copied_to_clipboard")} />
|
||||
</Portal>
|
||||
<TokenDialog
|
||||
key={`tokenDialogEdit${upsertDialogKey}`}
|
||||
open={upsertDialogOpen}
|
||||
token={selectedToken}
|
||||
onClose={handleDialogClose}
|
||||
/>
|
||||
<TokenDeleteDialog
|
||||
open={deleteDialogOpen}
|
||||
token={selectedToken}
|
||||
onClose={handleDialogClose}
|
||||
/>
|
||||
<TokenDialog key={`tokenDialogEdit${upsertDialogKey}`} open={upsertDialogOpen} token={selectedToken} onClose={handleDialogClose} />
|
||||
<TokenDeleteDialog open={deleteDialogOpen} token={selectedToken} onClose={handleDialogClose} />
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
@ -1144,18 +928,8 @@ const TokenDialog = (props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<DialogTitle>
|
||||
{editMode
|
||||
? t("account_tokens_dialog_title_edit")
|
||||
: t("account_tokens_dialog_title_create")}
|
||||
</DialogTitle>
|
||||
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||
<DialogTitle>{editMode ? t("account_tokens_dialog_title_edit") : t("account_tokens_dialog_title_create")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
margin="dense"
|
||||
|
@ -1169,52 +943,22 @@ const TokenDialog = (props) => {
|
|||
variant="standard"
|
||||
/>
|
||||
<FormControl fullWidth variant="standard" sx={{ mt: 1 }}>
|
||||
<Select
|
||||
value={expires}
|
||||
onChange={(ev) => setExpires(ev.target.value)}
|
||||
aria-label={t("account_tokens_dialog_expires_label")}
|
||||
>
|
||||
{editMode && (
|
||||
<MenuItem value={-1}>
|
||||
{t("account_tokens_dialog_expires_unchanged")}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem value={0}>
|
||||
{t("account_tokens_dialog_expires_never")}
|
||||
</MenuItem>
|
||||
<MenuItem value={21600}>
|
||||
{t("account_tokens_dialog_expires_x_hours", { hours: 6 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={43200}>
|
||||
{t("account_tokens_dialog_expires_x_hours", { hours: 12 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={259200}>
|
||||
{t("account_tokens_dialog_expires_x_days", { days: 3 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={604800}>
|
||||
{t("account_tokens_dialog_expires_x_days", { days: 7 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={2592000}>
|
||||
{t("account_tokens_dialog_expires_x_days", { days: 30 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={7776000}>
|
||||
{t("account_tokens_dialog_expires_x_days", { days: 90 })}
|
||||
</MenuItem>
|
||||
<MenuItem value={15552000}>
|
||||
{t("account_tokens_dialog_expires_x_days", { days: 180 })}
|
||||
</MenuItem>
|
||||
<Select value={expires} onChange={(ev) => setExpires(ev.target.value)} aria-label={t("account_tokens_dialog_expires_label")}>
|
||||
{editMode && <MenuItem value={-1}>{t("account_tokens_dialog_expires_unchanged")}</MenuItem>}
|
||||
<MenuItem value={0}>{t("account_tokens_dialog_expires_never")}</MenuItem>
|
||||
<MenuItem value={21600}>{t("account_tokens_dialog_expires_x_hours", { hours: 6 })}</MenuItem>
|
||||
<MenuItem value={43200}>{t("account_tokens_dialog_expires_x_hours", { hours: 12 })}</MenuItem>
|
||||
<MenuItem value={259200}>{t("account_tokens_dialog_expires_x_days", { days: 3 })}</MenuItem>
|
||||
<MenuItem value={604800}>{t("account_tokens_dialog_expires_x_days", { days: 7 })}</MenuItem>
|
||||
<MenuItem value={2592000}>{t("account_tokens_dialog_expires_x_days", { days: 30 })}</MenuItem>
|
||||
<MenuItem value={7776000}>{t("account_tokens_dialog_expires_x_days", { days: 90 })}</MenuItem>
|
||||
<MenuItem value={15552000}>{t("account_tokens_dialog_expires_x_days", { days: 180 })}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={props.onClose}>
|
||||
{t("account_tokens_dialog_button_cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
{editMode
|
||||
? t("account_tokens_dialog_button_update")
|
||||
: t("account_tokens_dialog_button_create")}
|
||||
</Button>
|
||||
<Button onClick={props.onClose}>{t("account_tokens_dialog_button_cancel")}</Button>
|
||||
<Button onClick={handleSubmit}>{editMode ? t("account_tokens_dialog_button_update") : t("account_tokens_dialog_button_create")}</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -1285,26 +1029,13 @@ const DeleteAccount = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Pref
|
||||
title={t("account_delete_title")}
|
||||
description={t("account_delete_description")}
|
||||
>
|
||||
<Pref title={t("account_delete_title")} description={t("account_delete_description")}>
|
||||
<div>
|
||||
<Button
|
||||
fullWidth={false}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<DeleteOutlineIcon />}
|
||||
onClick={handleDialogOpen}
|
||||
>
|
||||
<Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} onClick={handleDialogOpen}>
|
||||
{t("account_delete_title")}
|
||||
</Button>
|
||||
</div>
|
||||
<DeleteAccountDialog
|
||||
key={`deleteAccountDialog${dialogKey}`}
|
||||
open={dialogOpen}
|
||||
onClose={handleDialogClose}
|
||||
/>
|
||||
<DeleteAccountDialog key={`deleteAccountDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} />
|
||||
</Pref>
|
||||
);
|
||||
};
|
||||
|
@ -1325,9 +1056,7 @@ const DeleteAccountDialog = (props) => {
|
|||
} catch (e) {
|
||||
console.log(`[Account] Error deleting account`, e);
|
||||
if (e instanceof IncorrectPasswordError) {
|
||||
setError(
|
||||
t("account_basics_password_dialog_current_password_incorrect")
|
||||
);
|
||||
setError(t("account_basics_password_dialog_current_password_incorrect"));
|
||||
} else if (e instanceof UnauthorizedError) {
|
||||
session.resetAndRedirect(routes.login);
|
||||
} else {
|
||||
|
@ -1340,9 +1069,7 @@ const DeleteAccountDialog = (props) => {
|
|||
<Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}>
|
||||
<DialogTitle>{t("account_delete_title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body1">
|
||||
{t("account_delete_dialog_description")}
|
||||
</Typography>
|
||||
<Typography variant="body1">{t("account_delete_dialog_description")}</Typography>
|
||||
<TextField
|
||||
margin="dense"
|
||||
id="account-delete-confirm"
|
||||
|
@ -1361,14 +1088,8 @@ const DeleteAccountDialog = (props) => {
|
|||
)}
|
||||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={props.onClose}>
|
||||
{t("account_delete_dialog_button_cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
color="error"
|
||||
disabled={password.length === 0}
|
||||
>
|
||||
<Button onClick={props.onClose}>{t("account_delete_dialog_button_cancel")}</Button>
|
||||
<Button onClick={handleSubmit} color="error" disabled={password.length === 0}>
|
||||
{t("account_delete_dialog_button_submit")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
@ -51,8 +51,7 @@ const ActionBar = (props) => {
|
|||
<Toolbar
|
||||
sx={{
|
||||
pr: "24px",
|
||||
background:
|
||||
"linear-gradient(150deg, rgba(51,133,116,1) 0%, rgba(86,189,168,1) 100%)",
|
||||
background: "linear-gradient(150deg, rgba(51,133,116,1) 0%, rgba(86,189,168,1) 100%)",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
|
@ -77,12 +76,7 @@ const ActionBar = (props) => {
|
|||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{props.selected && (
|
||||
<SettingsIcons
|
||||
subscription={props.selected}
|
||||
onUnsubscribe={props.onUnsubscribe}
|
||||
/>
|
||||
)}
|
||||
{props.selected && <SettingsIcons subscription={props.selected} onUnsubscribe={props.onUnsubscribe} />}
|
||||
<ProfileIcon />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
@ -101,34 +95,13 @@ const SettingsIcons = (props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="large"
|
||||
edge="end"
|
||||
onClick={handleToggleMute}
|
||||
aria-label={t("action_bar_toggle_mute")}
|
||||
>
|
||||
{subscription.mutedUntil ? (
|
||||
<NotificationsOffIcon />
|
||||
) : (
|
||||
<NotificationsIcon />
|
||||
)}
|
||||
<IconButton color="inherit" size="large" edge="end" onClick={handleToggleMute} aria-label={t("action_bar_toggle_mute")}>
|
||||
{subscription.mutedUntil ? <NotificationsOffIcon /> : <NotificationsIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="large"
|
||||
edge="end"
|
||||
onClick={(ev) => setAnchorEl(ev.currentTarget)}
|
||||
aria-label={t("action_bar_toggle_action_menu")}
|
||||
>
|
||||
<IconButton color="inherit" size="large" edge="end" onClick={(ev) => setAnchorEl(ev.currentTarget)} aria-label={t("action_bar_toggle_action_menu")}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<SubscriptionPopup
|
||||
subscription={subscription}
|
||||
anchor={anchorEl}
|
||||
placement="right"
|
||||
onClose={() => setAnchorEl(null)}
|
||||
/>
|
||||
<SubscriptionPopup subscription={subscription} anchor={anchorEl} placement="right" onClose={() => setAnchorEl(null)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -159,43 +132,21 @@ const ProfileIcon = () => {
|
|||
return (
|
||||
<>
|
||||
{session.exists() && (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="large"
|
||||
edge="end"
|
||||
onClick={handleClick}
|
||||
aria-label={t("action_bar_profile_title")}
|
||||
>
|
||||
<IconButton color="inherit" size="large" edge="end" onClick={handleClick} aria-label={t("action_bar_profile_title")}>
|
||||
<AccountCircleIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
{!session.exists() && config.enable_login && (
|
||||
<Button
|
||||
color="inherit"
|
||||
variant="text"
|
||||
onClick={() => navigate(routes.login)}
|
||||
sx={{ m: 1 }}
|
||||
aria-label={t("action_bar_sign_in")}
|
||||
>
|
||||
<Button color="inherit" variant="text" onClick={() => navigate(routes.login)} sx={{ m: 1 }} aria-label={t("action_bar_sign_in")}>
|
||||
{t("action_bar_sign_in")}
|
||||
</Button>
|
||||
)}
|
||||
{!session.exists() && config.enable_signup && (
|
||||
<Button
|
||||
color="inherit"
|
||||
variant="outlined"
|
||||
onClick={() => navigate(routes.signup)}
|
||||
aria-label={t("action_bar_sign_up")}
|
||||
>
|
||||
<Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)} aria-label={t("action_bar_sign_up")}>
|
||||
{t("action_bar_sign_up")}
|
||||
</Button>
|
||||
)}
|
||||
<PopupMenu
|
||||
horizontal="right"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<PopupMenu horizontal="right" anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||
<MenuItem onClick={() => navigate(routes.account)}>
|
||||
<ListItemIcon>
|
||||
<Person />
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
createContext,
|
||||
Suspense,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createContext, Suspense, useContext, useEffect, useState } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
|
@ -19,21 +13,11 @@ import Preferences from "./Preferences";
|
|||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
import userManager from "../app/UserManager";
|
||||
import {
|
||||
BrowserRouter,
|
||||
Outlet,
|
||||
Route,
|
||||
Routes,
|
||||
useParams,
|
||||
} from "react-router-dom";
|
||||
import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom";
|
||||
import { expandUrl } from "../app/utils";
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
import routes from "./routes";
|
||||
import {
|
||||
useAccountListener,
|
||||
useBackgroundProcesses,
|
||||
useConnectionListeners,
|
||||
} from "./hooks";
|
||||
import { useAccountListener, useBackgroundProcesses, useConnectionListeners } from "./hooks";
|
||||
import PublishDialog from "./PublishDialog";
|
||||
import Messaging from "./Messaging";
|
||||
import "./i18n"; // Translations!
|
||||
|
@ -60,14 +44,8 @@ const App = () => {
|
|||
<Route path={routes.app} element={<AllSubscriptions />} />
|
||||
<Route path={routes.account} element={<Account />} />
|
||||
<Route path={routes.settings} element={<Preferences />} />
|
||||
<Route
|
||||
path={routes.subscription}
|
||||
element={<SingleSubscription />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.subscriptionExternal}
|
||||
element={<SingleSubscription />}
|
||||
/>
|
||||
<Route path={routes.subscription} element={<SingleSubscription />} />
|
||||
<Route path={routes.subscriptionExternal} element={<SingleSubscription />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
|
@ -82,22 +60,15 @@ const Layout = () => {
|
|||
const params = useParams();
|
||||
const { account, setAccount } = useContext(AccountContext);
|
||||
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
||||
const [notificationsGranted, setNotificationsGranted] = useState(
|
||||
notifier.granted()
|
||||
);
|
||||
const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted());
|
||||
const [sendDialogOpenMode, setSendDialogOpenMode] = useState("");
|
||||
const users = useLiveQuery(() => userManager.all());
|
||||
const subscriptions = useLiveQuery(() => subscriptionManager.all());
|
||||
const subscriptionsWithoutInternal = subscriptions?.filter(
|
||||
(s) => !s.internal
|
||||
);
|
||||
const newNotificationsCount =
|
||||
subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0;
|
||||
const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal);
|
||||
const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0;
|
||||
const [selected] = (subscriptionsWithoutInternal || []).filter((s) => {
|
||||
return (
|
||||
(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)
|
||||
);
|
||||
});
|
||||
|
@ -109,10 +80,7 @@ const Layout = () => {
|
|||
|
||||
return (
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<ActionBar
|
||||
selected={selected}
|
||||
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
||||
/>
|
||||
<ActionBar selected={selected} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} />
|
||||
<Navigation
|
||||
subscriptions={subscriptionsWithoutInternal}
|
||||
selectedSubscription={selected}
|
||||
|
@ -120,9 +88,7 @@ const Layout = () => {
|
|||
mobileDrawerOpen={mobileDrawerOpen}
|
||||
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
||||
onNotificationGranted={setNotificationsGranted}
|
||||
onPublishMessageClick={() =>
|
||||
setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)
|
||||
}
|
||||
onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)}
|
||||
/>
|
||||
<Main>
|
||||
<Toolbar />
|
||||
|
@ -133,11 +99,7 @@ const Layout = () => {
|
|||
}}
|
||||
/>
|
||||
</Main>
|
||||
<Messaging
|
||||
selected={selected}
|
||||
dialogOpenMode={sendDialogOpenMode}
|
||||
onDialogOpenModeChange={setSendDialogOpenMode}
|
||||
/>
|
||||
<Messaging selected={selected} dialogOpenMode={sendDialogOpenMode} onDialogOpenModeChange={setSendDialogOpenMode} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -155,10 +117,7 @@ const Main = (props) => {
|
|||
width: { sm: `calc(100% - ${Navigation.width}px)` },
|
||||
height: "100vh",
|
||||
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}
|
||||
|
@ -171,10 +130,7 @@ const Loader = () => (
|
|||
open={true}
|
||||
sx={{
|
||||
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]),
|
||||
}}
|
||||
>
|
||||
<CircularProgress color="success" disableShrink />
|
||||
|
@ -182,8 +138,7 @@ const Loader = () => (
|
|||
);
|
||||
|
||||
const updateTitle = (newNotificationsCount) => {
|
||||
document.title =
|
||||
newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy";
|
||||
document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy";
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -16,11 +16,7 @@ const AvatarBox = (props) => {
|
|||
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}
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -17,8 +17,7 @@ import { useTranslation } from "react-i18next";
|
|||
// This is a hack, but on Ubuntu 18.04, with Chrome 99, only Emoji <= 11 are supported.
|
||||
|
||||
const emojisByCategory = {};
|
||||
const isDesktopChrome =
|
||||
/Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent);
|
||||
const isDesktopChrome = /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent);
|
||||
const maxSupportedVersionForDesktopChrome = 11;
|
||||
rawEmojis.forEach((emoji) => {
|
||||
if (!emojisByCategory[emoji.category]) {
|
||||
|
@ -26,12 +25,9 @@ rawEmojis.forEach((emoji) => {
|
|||
}
|
||||
try {
|
||||
const unicodeVersion = parseFloat(emoji.unicode_version);
|
||||
const supportedEmoji =
|
||||
unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome;
|
||||
const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome;
|
||||
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 };
|
||||
emojisByCategory[emoji.category].push(emojiWithSearchBase);
|
||||
}
|
||||
|
@ -53,13 +49,7 @@ const EmojiPicker = (props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Popper
|
||||
open={open}
|
||||
anchorEl={props.anchorEl}
|
||||
placement="bottom-start"
|
||||
sx={{ zIndex: 10005 }}
|
||||
transition
|
||||
>
|
||||
<Popper open={open} anchorEl={props.anchorEl} placement="bottom-start" sx={{ zIndex: 10005 }} transition>
|
||||
{({ TransitionProps }) => (
|
||||
<ClickAwayListener onClickAway={props.onClose}>
|
||||
<Fade {...TransitionProps} timeout={350}>
|
||||
|
@ -92,16 +82,8 @@ const EmojiPicker = (props) => {
|
|||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment
|
||||
position="end"
|
||||
sx={{ display: search ? "" : "none" }}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleSearchClear}
|
||||
edge="end"
|
||||
aria-label={t("emoji_picker_search_clear")}
|
||||
>
|
||||
<InputAdornment position="end" sx={{ display: search ? "" : "none" }}>
|
||||
<IconButton size="small" onClick={handleSearchClear} edge="end" aria-label={t("emoji_picker_search_clear")}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
|
@ -117,13 +99,7 @@ const EmojiPicker = (props) => {
|
|||
}}
|
||||
>
|
||||
{Object.keys(emojisByCategory).map((category) => (
|
||||
<Category
|
||||
key={category}
|
||||
title={category}
|
||||
emojis={emojisByCategory[category]}
|
||||
search={searchFields}
|
||||
onPick={props.onEmojiPick}
|
||||
/>
|
||||
<Category key={category} title={category} emojis={emojisByCategory[category]} search={searchFields} onPick={props.onEmojiPick} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -144,12 +120,7 @@ const Category = (props) => {
|
|||
</Typography>
|
||||
)}
|
||||
{props.emojis.map((emoji) => (
|
||||
<Emoji
|
||||
key={emoji.aliases[0]}
|
||||
emoji={emoji}
|
||||
search={props.search}
|
||||
onClick={() => props.onPick(emoji.aliases[0])}
|
||||
/>
|
||||
<Emoji key={emoji.aliases[0]} emoji={emoji} search={props.search} onClick={() => props.onPick(emoji.aliases[0])} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -160,12 +131,7 @@ const Emoji = (props) => {
|
|||
const matches = emojiMatches(emoji, props.search);
|
||||
const title = `${emoji.description} (${emoji.aliases[0]})`;
|
||||
return (
|
||||
<EmojiDiv
|
||||
onClick={props.onClick}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
style={{ display: matches ? "" : "none" }}
|
||||
>
|
||||
<EmojiDiv onClick={props.onClick} title={title} aria-label={title} style={{ display: matches ? "" : "none" }}>
|
||||
{props.emoji.emoji}
|
||||
</EmojiDiv>
|
||||
);
|
||||
|
|
|
@ -22,9 +22,7 @@ class ErrorBoundaryImpl extends React.Component {
|
|||
// - https://github.com/dexie/Dexie.js/issues/312
|
||||
// - https://bugzilla.mozilla.org/show_bug.cgi?id=781982
|
||||
const isUnsupportedIndexedDB =
|
||||
error?.name === "InvalidStateError" ||
|
||||
(error?.name === "DatabaseClosedError" &&
|
||||
error?.message?.indexOf("InvalidStateError") !== -1);
|
||||
error?.name === "InvalidStateError" || (error?.name === "DatabaseClosedError" && error?.message?.indexOf("InvalidStateError") !== -1);
|
||||
|
||||
if (isUnsupportedIndexedDB) {
|
||||
this.handleUnsupportedIndexedDB();
|
||||
|
@ -48,14 +46,7 @@ class ErrorBoundaryImpl extends React.Component {
|
|||
// Fetch additional info and a better stack trace
|
||||
StackTrace.fromError(error).then((stack) => {
|
||||
console.error("[ErrorBoundary] Stacktrace fetched", stack);
|
||||
const niceStack =
|
||||
`${error.toString()}\n` +
|
||||
stack
|
||||
.map(
|
||||
(el) =>
|
||||
` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`
|
||||
)
|
||||
.join("\n");
|
||||
const niceStack = `${error.toString()}\n` + stack.map((el) => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n");
|
||||
this.setState({ niceStack });
|
||||
});
|
||||
}
|
||||
|
@ -96,9 +87,7 @@ class ErrorBoundaryImpl extends React.Component {
|
|||
<Trans
|
||||
i18nKey="error_boundary_unsupported_indexeddb_description"
|
||||
components={{
|
||||
githubLink: (
|
||||
<Link href="https://github.com/binwiederhier/ntfy/issues/208" />
|
||||
),
|
||||
githubLink: <Link href="https://github.com/binwiederhier/ntfy/issues/208" />,
|
||||
discordLink: <Link href="https://discord.gg/cT7ECsZj9w" />,
|
||||
matrixLink: <Link href="https://matrix.to/#/#ntfy:matrix.org" />,
|
||||
}}
|
||||
|
@ -117,9 +106,7 @@ class ErrorBoundaryImpl extends React.Component {
|
|||
<Trans
|
||||
i18nKey="error_boundary_description"
|
||||
components={{
|
||||
githubLink: (
|
||||
<Link href="https://github.com/binwiederhier/ntfy/issues" />
|
||||
),
|
||||
githubLink: <Link href="https://github.com/binwiederhier/ntfy/issues" />,
|
||||
discordLink: <Link href="https://discord.gg/cT7ECsZj9w" />,
|
||||
matrixLink: <Link href="https://matrix.to/#/#ntfy:matrix.org" />,
|
||||
}}
|
||||
|
@ -135,11 +122,7 @@ class ErrorBoundaryImpl extends React.Component {
|
|||
<pre>{this.state.niceStack}</pre>
|
||||
) : (
|
||||
<>
|
||||
<CircularProgress
|
||||
size="20px"
|
||||
sx={{ verticalAlign: "text-bottom" }}
|
||||
/>{" "}
|
||||
{t("error_boundary_gathering_info")}
|
||||
<CircularProgress size="20px" sx={{ verticalAlign: "text-bottom" }} /> {t("error_boundary_gathering_info")}
|
||||
</>
|
||||
)}
|
||||
<pre>{this.state.originalStack}</pre>
|
||||
|
|
|
@ -28,9 +28,7 @@ const Login = () => {
|
|||
const user = { username, password };
|
||||
try {
|
||||
const token = await accountApi.login(user);
|
||||
console.log(
|
||||
`[Login] User auth for user ${user.username} successful, token is ${token}`
|
||||
);
|
||||
console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`);
|
||||
session.store(user.username, token);
|
||||
window.location.href = routes.app;
|
||||
} catch (e) {
|
||||
|
@ -52,12 +50,7 @@ const Login = () => {
|
|||
return (
|
||||
<AvatarBox>
|
||||
<Typography sx={{ typography: "h6" }}>{t("login_title")}</Typography>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
sx={{ mt: 1, maxWidth: 400 }}
|
||||
>
|
||||
<Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1, maxWidth: 400 }}>
|
||||
<TextField
|
||||
margin="dense"
|
||||
required
|
||||
|
@ -95,13 +88,7 @@ const Login = () => {
|
|||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
disabled={username === "" || password === ""}
|
||||
sx={{ mt: 2, mb: 2 }}
|
||||
>
|
||||
<Button type="submit" fullWidth variant="contained" disabled={username === "" || password === ""} sx={{ mt: 2, mb: 2 }}>
|
||||
{t("login_form_button_submit")}
|
||||
</Button>
|
||||
{error && (
|
||||
|
|
|
@ -29,14 +29,7 @@ const Messaging = (props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{subscription && (
|
||||
<MessageBar
|
||||
subscription={subscription}
|
||||
message={message}
|
||||
onMessageChange={setMessage}
|
||||
onOpenDialogClick={handleOpenDialogClick}
|
||||
/>
|
||||
)}
|
||||
{subscription && <MessageBar subscription={subscription} message={message} onMessageChange={setMessage} onOpenDialogClick={handleOpenDialogClick} />}
|
||||
<PublishDialog
|
||||
key={`publishDialog${dialogKey}`} // Resets dialog when canceled/closed
|
||||
openMode={dialogOpenMode}
|
||||
|
@ -44,14 +37,8 @@ const Messaging = (props) => {
|
|||
topic={subscription?.topic ?? ""}
|
||||
message={message}
|
||||
onClose={handleDialogClose}
|
||||
onDragEnter={() =>
|
||||
props.onDialogOpenModeChange((prev) =>
|
||||
prev ? prev : PublishDialog.OPEN_MODE_DRAG
|
||||
)
|
||||
} // Only update if not already open
|
||||
onResetOpenMode={() =>
|
||||
props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)
|
||||
}
|
||||
onDragEnter={() => props.onDialogOpenModeChange((prev) => (prev ? prev : PublishDialog.OPEN_MODE_DRAG))} // Only update if not already open
|
||||
onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -63,11 +50,7 @@ const MessageBar = (props) => {
|
|||
const [snackOpen, setSnackOpen] = useState(false);
|
||||
const handleSendClick = async () => {
|
||||
try {
|
||||
await api.publish(
|
||||
subscription.baseUrl,
|
||||
subscription.topic,
|
||||
props.message
|
||||
);
|
||||
await api.publish(subscription.baseUrl, subscription.topic, props.message);
|
||||
} catch (e) {
|
||||
console.log(`[MessageBar] Error publishing message`, e);
|
||||
setSnackOpen(true);
|
||||
|
@ -84,19 +67,10 @@ const MessageBar = (props) => {
|
|||
right: 0,
|
||||
padding: 2,
|
||||
width: { xs: "100%", sm: `calc(100% - ${Navigation.width}px)` },
|
||||
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]),
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="large"
|
||||
edge="start"
|
||||
onClick={props.onOpenDialogClick}
|
||||
aria-label={t("message_bar_show_dialog")}
|
||||
>
|
||||
<IconButton color="inherit" size="large" edge="start" onClick={props.onOpenDialogClick} aria-label={t("message_bar_show_dialog")}>
|
||||
<KeyboardArrowUpIcon />
|
||||
</IconButton>
|
||||
<TextField
|
||||
|
@ -117,22 +91,11 @@ const MessageBar = (props) => {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="large"
|
||||
edge="end"
|
||||
onClick={handleSendClick}
|
||||
aria-label={t("message_bar_publish")}
|
||||
>
|
||||
<IconButton color="inherit" size="large" edge="end" onClick={handleSendClick} aria-label={t("message_bar_publish")}>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
<Portal>
|
||||
<Snackbar
|
||||
open={snackOpen}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setSnackOpen(false)}
|
||||
message={t("message_bar_error_publishing")}
|
||||
/>
|
||||
<Snackbar open={snackOpen} autoHideDuration={3000} onClose={() => setSnackOpen(false)} message={t("message_bar_error_publishing")} />
|
||||
</Portal>
|
||||
</Paper>
|
||||
);
|
||||
|
|
|
@ -12,16 +12,7 @@ import List from "@mui/material/List";
|
|||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import SubscribeDialog from "./SubscribeDialog";
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Badge,
|
||||
CircularProgress,
|
||||
Link,
|
||||
ListSubheader,
|
||||
Portal,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Portal, Tooltip } from "@mui/material";
|
||||
import Button from "@mui/material/Button";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { openUrl, topicDisplayName, topicUrl } from "../app/utils";
|
||||
|
@ -29,12 +20,7 @@ import routes from "./routes";
|
|||
import { ConnectionState } from "../app/Connection";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
import {
|
||||
ChatBubble,
|
||||
MoreVert,
|
||||
NotificationsOffOutlined,
|
||||
Send,
|
||||
} from "@mui/icons-material";
|
||||
import { ChatBubble, MoreVert, NotificationsOffOutlined, Send } from "@mui/icons-material";
|
||||
import Box from "@mui/material/Box";
|
||||
import notifier from "../app/Notifier";
|
||||
import config from "../app/config";
|
||||
|
@ -45,12 +31,7 @@ import accountApi, { Permission, Role } from "../app/AccountApi";
|
|||
import CelebrationIcon from "@mui/icons-material/Celebration";
|
||||
import UpgradeDialog from "./UpgradeDialog";
|
||||
import { AccountContext } from "./App";
|
||||
import {
|
||||
PermissionDenyAll,
|
||||
PermissionRead,
|
||||
PermissionReadWrite,
|
||||
PermissionWrite,
|
||||
} from "./ReserveIcons";
|
||||
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { SubscriptionPopup } from "./SubscriptionPopup";
|
||||
|
||||
|
@ -59,11 +40,7 @@ const navWidth = 280;
|
|||
const Navigation = (props) => {
|
||||
const navigationList = <NavList {...props} />;
|
||||
return (
|
||||
<Box
|
||||
component="nav"
|
||||
role="navigation"
|
||||
sx={{ width: { sm: Navigation.width }, flexShrink: { sm: 0 } }}
|
||||
>
|
||||
<Box component="nav" role="navigation" sx={{ width: { sm: Navigation.width }, flexShrink: { sm: 0 } }}>
|
||||
{/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */}
|
||||
<Drawer
|
||||
variant="temporary"
|
||||
|
@ -109,19 +86,14 @@ const NavList = (props) => {
|
|||
};
|
||||
|
||||
const handleSubscribeSubmit = (subscription) => {
|
||||
console.log(
|
||||
`[Navigation] New subscription: ${subscription.id}`,
|
||||
subscription
|
||||
);
|
||||
console.log(`[Navigation] New subscription: ${subscription.id}`, subscription);
|
||||
handleSubscribeReset();
|
||||
navigate(routes.forSubscription(subscription));
|
||||
handleRequestNotificationPermission();
|
||||
};
|
||||
|
||||
const handleRequestNotificationPermission = () => {
|
||||
notifier.maybeRequestPermission((granted) =>
|
||||
props.onNotificationGranted(granted)
|
||||
);
|
||||
notifier.maybeRequestPermission((granted) => props.onNotificationGranted(granted));
|
||||
};
|
||||
|
||||
const handleAccountClick = () => {
|
||||
|
@ -134,39 +106,19 @@ const NavList = (props) => {
|
|||
const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid;
|
||||
const showSubscriptionsList = props.subscriptions?.length > 0;
|
||||
const showNotificationBrowserNotSupportedBox = !notifier.browserSupported();
|
||||
const showNotificationContextNotSupportedBox =
|
||||
notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
|
||||
const showNotificationGrantBox =
|
||||
notifier.supported() &&
|
||||
props.subscriptions?.length > 0 &&
|
||||
!props.notificationsGranted;
|
||||
const navListPadding =
|
||||
showNotificationGrantBox ||
|
||||
showNotificationBrowserNotSupportedBox ||
|
||||
showNotificationContextNotSupportedBox
|
||||
? "0"
|
||||
: "";
|
||||
const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
|
||||
const showNotificationGrantBox = notifier.supported() && props.subscriptions?.length > 0 && !props.notificationsGranted;
|
||||
const navListPadding = showNotificationGrantBox || showNotificationBrowserNotSupportedBox || showNotificationContextNotSupportedBox ? "0" : "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar sx={{ display: { xs: "none", sm: "block" } }} />
|
||||
<List component="nav" sx={{ paddingTop: navListPadding }}>
|
||||
{showNotificationBrowserNotSupportedBox && (
|
||||
<NotificationBrowserNotSupportedAlert />
|
||||
)}
|
||||
{showNotificationContextNotSupportedBox && (
|
||||
<NotificationContextNotSupportedAlert />
|
||||
)}
|
||||
{showNotificationGrantBox && (
|
||||
<NotificationGrantAlert
|
||||
onRequestPermissionClick={handleRequestNotificationPermission}
|
||||
/>
|
||||
)}
|
||||
{showNotificationBrowserNotSupportedBox && <NotificationBrowserNotSupportedAlert />}
|
||||
{showNotificationContextNotSupportedBox && <NotificationContextNotSupportedAlert />}
|
||||
{showNotificationGrantBox && <NotificationGrantAlert onRequestPermissionClick={handleRequestNotificationPermission} />}
|
||||
{!showSubscriptionsList && (
|
||||
<ListItemButton
|
||||
onClick={() => navigate(routes.app)}
|
||||
selected={location.pathname === config.app_root}
|
||||
>
|
||||
<ListItemButton onClick={() => navigate(routes.app)} selected={location.pathname === config.app_root}>
|
||||
<ListItemIcon>
|
||||
<ChatBubble />
|
||||
</ListItemIcon>
|
||||
|
@ -176,37 +128,25 @@ const NavList = (props) => {
|
|||
{showSubscriptionsList && (
|
||||
<>
|
||||
<ListSubheader>{t("nav_topics_title")}</ListSubheader>
|
||||
<ListItemButton
|
||||
onClick={() => navigate(routes.app)}
|
||||
selected={location.pathname === config.app_root}
|
||||
>
|
||||
<ListItemButton onClick={() => navigate(routes.app)} selected={location.pathname === config.app_root}>
|
||||
<ListItemIcon>
|
||||
<ChatBubble />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t("nav_button_all_notifications")} />
|
||||
</ListItemButton>
|
||||
<SubscriptionList
|
||||
subscriptions={props.subscriptions}
|
||||
selectedSubscription={props.selectedSubscription}
|
||||
/>
|
||||
<SubscriptionList subscriptions={props.subscriptions} selectedSubscription={props.selectedSubscription} />
|
||||
<Divider sx={{ my: 1 }} />
|
||||
</>
|
||||
)}
|
||||
{session.exists() && (
|
||||
<ListItemButton
|
||||
onClick={handleAccountClick}
|
||||
selected={location.pathname === routes.account}
|
||||
>
|
||||
<ListItemButton onClick={handleAccountClick} selected={location.pathname === routes.account}>
|
||||
<ListItemIcon>
|
||||
<Person />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t("nav_button_account")} />
|
||||
</ListItemButton>
|
||||
)}
|
||||
<ListItemButton
|
||||
onClick={() => navigate(routes.settings)}
|
||||
selected={location.pathname === routes.settings}
|
||||
>
|
||||
<ListItemButton onClick={() => navigate(routes.settings)} selected={location.pathname === routes.settings}>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
|
@ -260,8 +200,7 @@ const UpgradeBanner = () => {
|
|||
width: `${Navigation.width - 1}px`,
|
||||
bottom: 0,
|
||||
mt: "auto",
|
||||
background:
|
||||
"linear-gradient(150deg, rgba(196, 228, 221, 0.46) 0%, rgb(255, 255, 255) 100%)",
|
||||
background: "linear-gradient(150deg, rgba(196, 228, 221, 0.46) 0%, rgb(255, 255, 255) 100%)",
|
||||
}}
|
||||
>
|
||||
<Divider />
|
||||
|
@ -277,8 +216,7 @@ const UpgradeBanner = () => {
|
|||
style: {
|
||||
fontWeight: 500,
|
||||
fontSize: "1.1rem",
|
||||
background:
|
||||
"-webkit-linear-gradient(45deg, #09009f, #00ff95 80%)",
|
||||
background: "-webkit-linear-gradient(45deg, #09009f, #00ff95 80%)",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
},
|
||||
|
@ -290,11 +228,7 @@ const UpgradeBanner = () => {
|
|||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
<UpgradeDialog
|
||||
key={`upgradeDialog${dialogKey}`}
|
||||
open={dialogOpen}
|
||||
onCancel={() => setDialogOpen(false)}
|
||||
/>
|
||||
<UpgradeDialog key={`upgradeDialog${dialogKey}`} open={dialogOpen} onCancel={() => setDialogOpen(false)} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -303,9 +237,7 @@ const SubscriptionList = (props) => {
|
|||
const sortedSubscriptions = props.subscriptions
|
||||
.filter((s) => !s.internal)
|
||||
.sort((a, b) => {
|
||||
return topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic)
|
||||
? -1
|
||||
: 1;
|
||||
return topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1;
|
||||
});
|
||||
return (
|
||||
<>
|
||||
|
@ -313,10 +245,7 @@ const SubscriptionList = (props) => {
|
|||
<SubscriptionItem
|
||||
key={subscription.id}
|
||||
subscription={subscription}
|
||||
selected={
|
||||
props.selectedSubscription &&
|
||||
props.selectedSubscription.id === subscription.id
|
||||
}
|
||||
selected={props.selectedSubscription && props.selectedSubscription.id === subscription.id}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
@ -331,19 +260,12 @@ const SubscriptionItem = (props) => {
|
|||
const subscription = props.subscription;
|
||||
const iconBadge = subscription.new <= 99 ? subscription.new : "99+";
|
||||
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;
|
||||
const icon =
|
||||
subscription.state === ConnectionState.Connecting ? (
|
||||
<CircularProgress size="24px" />
|
||||
) : (
|
||||
<Badge
|
||||
badgeContent={iconBadge}
|
||||
invisible={subscription.new === 0}
|
||||
color="primary"
|
||||
>
|
||||
<Badge badgeContent={iconBadge} invisible={subscription.new === 0} color="primary">
|
||||
<ChatBubbleOutlineIcon />
|
||||
</Badge>
|
||||
);
|
||||
|
@ -355,12 +277,7 @@ const SubscriptionItem = (props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton
|
||||
onClick={handleClick}
|
||||
selected={props.selected}
|
||||
aria-label={ariaLabel}
|
||||
aria-live="polite"
|
||||
>
|
||||
<ListItemButton onClick={handleClick} selected={props.selected} aria-label={ariaLabel} aria-live="polite">
|
||||
<ListItemIcon>{icon}</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={displayName}
|
||||
|
@ -371,9 +288,7 @@ const SubscriptionItem = (props) => {
|
|||
{subscription.reservation?.everyone && (
|
||||
<ListItemIcon edge="end" sx={{ minWidth: "26px" }}>
|
||||
{subscription.reservation?.everyone === Permission.READ_WRITE && (
|
||||
<Tooltip
|
||||
title={t("prefs_reservations_table_everyone_read_write")}
|
||||
>
|
||||
<Tooltip title={t("prefs_reservations_table_everyone_read_write")}>
|
||||
<PermissionReadWrite size="small" />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
@ -383,9 +298,7 @@ const SubscriptionItem = (props) => {
|
|||
</Tooltip>
|
||||
)}
|
||||
{subscription.reservation?.everyone === Permission.WRITE_ONLY && (
|
||||
<Tooltip
|
||||
title={t("prefs_reservations_table_everyone_write_only")}
|
||||
>
|
||||
<Tooltip title={t("prefs_reservations_table_everyone_write_only")}>
|
||||
<PermissionWrite size="small" />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
@ -397,11 +310,7 @@ const SubscriptionItem = (props) => {
|
|||
</ListItemIcon>
|
||||
)}
|
||||
{subscription.mutedUntil > 0 && (
|
||||
<ListItemIcon
|
||||
edge="end"
|
||||
sx={{ minWidth: "26px" }}
|
||||
aria-label={t("nav_button_muted")}
|
||||
>
|
||||
<ListItemIcon edge="end" sx={{ minWidth: "26px" }} aria-label={t("nav_button_muted")}>
|
||||
<Tooltip title={t("nav_button_muted")}>
|
||||
<NotificationsOffOutlined />
|
||||
</Tooltip>
|
||||
|
@ -421,11 +330,7 @@ const SubscriptionItem = (props) => {
|
|||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
<Portal>
|
||||
<SubscriptionPopup
|
||||
subscription={subscription}
|
||||
anchor={menuAnchorEl}
|
||||
onClose={() => setMenuAnchorEl(null)}
|
||||
/>
|
||||
<SubscriptionPopup subscription={subscription} anchor={menuAnchorEl} onClose={() => setMenuAnchorEl(null)} />
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
|
@ -438,12 +343,7 @@ const NotificationGrantAlert = (props) => {
|
|||
<Alert severity="warning" sx={{ paddingTop: 2 }}>
|
||||
<AlertTitle>{t("alert_grant_title")}</AlertTitle>
|
||||
<Typography gutterBottom>{t("alert_grant_description")}</Typography>
|
||||
<Button
|
||||
sx={{ float: "right" }}
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={props.onRequestPermissionClick}
|
||||
>
|
||||
<Button sx={{ float: "right" }} color="inherit" size="small" onClick={props.onRequestPermissionClick}>
|
||||
{t("alert_grant_button")}
|
||||
</Button>
|
||||
</Alert>
|
||||
|
@ -458,9 +358,7 @@ const NotificationBrowserNotSupportedAlert = () => {
|
|||
<>
|
||||
<Alert severity="warning" sx={{ paddingTop: 2 }}>
|
||||
<AlertTitle>{t("alert_not_supported_title")}</AlertTitle>
|
||||
<Typography gutterBottom>
|
||||
{t("alert_not_supported_description")}
|
||||
</Typography>
|
||||
<Typography gutterBottom>{t("alert_not_supported_description")}</Typography>
|
||||
</Alert>
|
||||
<Divider />
|
||||
</>
|
||||
|
@ -477,13 +375,7 @@ const NotificationContextNotSupportedAlert = () => {
|
|||
<Trans
|
||||
i18nKey="alert_not_supported_context_description"
|
||||
components={{
|
||||
mdnLink: (
|
||||
<Link
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/API/notification"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
/>
|
||||
),
|
||||
mdnLink: <Link href="https://developer.mozilla.org/en-US/docs/Web/API/notification" target="_blank" rel="noopener" />,
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
|
|
|
@ -1,16 +1,5 @@
|
|||
import Container from "@mui/material/Container";
|
||||
import {
|
||||
ButtonBase,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Fade,
|
||||
Link,
|
||||
Modal,
|
||||
Snackbar,
|
||||
Stack,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { ButtonBase, CardActions, CardContent, CircularProgress, Fade, Link, Modal, Snackbar, Stack, Tooltip } from "@mui/material";
|
||||
import Card from "@mui/material/Card";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import * as React from "react";
|
||||
|
@ -29,11 +18,7 @@ import {
|
|||
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";
|
||||
|
@ -68,10 +53,7 @@ export const SingleSubscription = () => {
|
|||
|
||||
const AllSubscriptionsList = (props) => {
|
||||
const subscriptions = props.subscriptions;
|
||||
const notifications = useLiveQuery(
|
||||
() => subscriptionManager.getAllNotifications(),
|
||||
[]
|
||||
);
|
||||
const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []);
|
||||
if (notifications === null || notifications === undefined) {
|
||||
return <Loading />;
|
||||
} else if (subscriptions.length === 0) {
|
||||
|
@ -79,33 +61,18 @@ const AllSubscriptionsList = (props) => {
|
|||
} else if (notifications.length === 0) {
|
||||
return <NoNotificationsWithoutSubscription subscriptions={subscriptions} />;
|
||||
}
|
||||
return (
|
||||
<NotificationList
|
||||
key="all"
|
||||
notifications={notifications}
|
||||
messageBar={false}
|
||||
/>
|
||||
);
|
||||
return <NotificationList key="all" notifications={notifications} messageBar={false} />;
|
||||
};
|
||||
|
||||
const SingleSubscriptionList = (props) => {
|
||||
const subscription = props.subscription;
|
||||
const notifications = useLiveQuery(
|
||||
() => subscriptionManager.getNotifications(subscription.id),
|
||||
[subscription]
|
||||
);
|
||||
const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]);
|
||||
if (notifications === null || notifications === undefined) {
|
||||
return <Loading />;
|
||||
} else if (notifications.length === 0) {
|
||||
return <NoNotifications subscription={subscription} />;
|
||||
}
|
||||
return (
|
||||
<NotificationList
|
||||
id={subscription.id}
|
||||
notifications={notifications}
|
||||
messageBar={true}
|
||||
/>
|
||||
);
|
||||
return <NotificationList id={subscription.id} notifications={notifications} messageBar={true} />;
|
||||
};
|
||||
|
||||
const NotificationList = (props) => {
|
||||
|
@ -146,18 +113,9 @@ const NotificationList = (props) => {
|
|||
>
|
||||
<Stack spacing={3}>
|
||||
{notifications.slice(0, count).map((notification) => (
|
||||
<NotificationItem
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
onShowSnack={() => setSnackOpen(true)}
|
||||
/>
|
||||
<NotificationItem key={notification.id} notification={notification} onShowSnack={() => setSnackOpen(true)} />
|
||||
))}
|
||||
<Snackbar
|
||||
open={snackOpen}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setSnackOpen(false)}
|
||||
message={t("notifications_copied_to_clipboard")}
|
||||
/>
|
||||
<Snackbar open={snackOpen} autoHideDuration={3000} onClose={() => setSnackOpen(false)} message={t("notifications_copied_to_clipboard")} />
|
||||
</Stack>
|
||||
</Container>
|
||||
</InfiniteScroll>
|
||||
|
@ -176,45 +134,29 @@ const NotificationItem = (props) => {
|
|||
await subscriptionManager.deleteNotification(notification.id);
|
||||
};
|
||||
const handleMarkRead = async () => {
|
||||
console.log(
|
||||
`[Notifications] Marking notification ${notification.id} as read`
|
||||
);
|
||||
console.log(`[Notifications] Marking notification ${notification.id} as read`);
|
||||
await subscriptionManager.markNotificationRead(notification.id);
|
||||
};
|
||||
const handleCopy = (s) => {
|
||||
navigator.clipboard.writeText(s);
|
||||
props.onShowSnack();
|
||||
};
|
||||
const expired =
|
||||
attachment && attachment.expires && attachment.expires < Date.now() / 1000;
|
||||
const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000;
|
||||
const hasAttachmentActions = attachment && !expired;
|
||||
const hasClickAction = notification.click;
|
||||
const hasUserActions =
|
||||
notification.actions && notification.actions.length > 0;
|
||||
const hasUserActions = notification.actions && notification.actions.length > 0;
|
||||
const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
|
||||
return (
|
||||
<Card
|
||||
sx={{ minWidth: 275, padding: 1 }}
|
||||
role="listitem"
|
||||
aria-label={t("notifications_list_item")}
|
||||
>
|
||||
<Card sx={{ minWidth: 275, padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}>
|
||||
<CardContent>
|
||||
<Tooltip title={t("notifications_delete")} enterDelay={500}>
|
||||
<IconButton
|
||||
onClick={handleDelete}
|
||||
sx={{ float: "right", marginRight: -1, marginTop: -1 }}
|
||||
aria-label={t("notifications_delete")}
|
||||
>
|
||||
<IconButton onClick={handleDelete} sx={{ float: "right", marginRight: -1, marginTop: -1 }} aria-label={t("notifications_delete")}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{notification.new === 1 && (
|
||||
<Tooltip title={t("notifications_mark_read")} enterDelay={500}>
|
||||
<IconButton
|
||||
onClick={handleMarkRead}
|
||||
sx={{ float: "right", marginRight: -0.5, marginTop: -1 }}
|
||||
aria-label={t("notifications_mark_read")}
|
||||
>
|
||||
<IconButton onClick={handleMarkRead} sx={{ float: "right", marginRight: -0.5, marginTop: -1 }} aria-label={t("notifications_mark_read")}>
|
||||
<CheckIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
@ -247,9 +189,7 @@ const NotificationItem = (props) => {
|
|||
</Typography>
|
||||
)}
|
||||
<Typography variant="body1" sx={{ whiteSpace: "pre-line" }}>
|
||||
{autolink(
|
||||
maybeAppendActionErrors(formatMessage(notification), notification)
|
||||
)}
|
||||
{autolink(maybeAppendActionErrors(formatMessage(notification), notification))}
|
||||
</Typography>
|
||||
{attachment && <Attachment attachment={attachment} />}
|
||||
{tags && (
|
||||
|
@ -263,36 +203,28 @@ const NotificationItem = (props) => {
|
|||
{hasAttachmentActions && (
|
||||
<>
|
||||
<Tooltip title={t("notifications_attachment_copy_url_title")}>
|
||||
<Button onClick={() => handleCopy(attachment.url)}>
|
||||
{t("notifications_attachment_copy_url_button")}
|
||||
</Button>
|
||||
<Button onClick={() => handleCopy(attachment.url)}>{t("notifications_attachment_copy_url_button")}</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={t("notifications_attachment_open_title", {
|
||||
url: attachment.url,
|
||||
})}
|
||||
>
|
||||
<Button onClick={() => openUrl(attachment.url)}>
|
||||
{t("notifications_attachment_open_button")}
|
||||
</Button>
|
||||
<Button onClick={() => openUrl(attachment.url)}>{t("notifications_attachment_open_button")}</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
{hasClickAction && (
|
||||
<>
|
||||
<Tooltip title={t("notifications_click_copy_url_title")}>
|
||||
<Button onClick={() => handleCopy(notification.click)}>
|
||||
{t("notifications_click_copy_url_button")}
|
||||
</Button>
|
||||
<Button onClick={() => handleCopy(notification.click)}>{t("notifications_click_copy_url_button")}</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={t("notifications_actions_open_url_title", {
|
||||
url: notification.click,
|
||||
})}
|
||||
>
|
||||
<Button onClick={() => openUrl(notification.click)}>
|
||||
{t("notifications_click_open_button")}
|
||||
</Button>
|
||||
<Button onClick={() => openUrl(notification.click)}>{t("notifications_click_open_button")}</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
@ -311,18 +243,10 @@ const NotificationItem = (props) => {
|
|||
* [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
|
||||
);
|
||||
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"
|
||||
>
|
||||
<Link key={i} href={parts[i]} underline="hover" target="_blank" rel="noreferrer,noopener">
|
||||
{shortUrl(parts[i])}
|
||||
</Link>
|
||||
);
|
||||
|
@ -342,8 +266,7 @@ const Attachment = (props) => {
|
|||
const attachment = props.attachment;
|
||||
const expired = attachment.expires && attachment.expires < Date.now() / 1000;
|
||||
const expires = attachment.expires && attachment.expires > Date.now() / 1000;
|
||||
const displayableImage =
|
||||
!expired && attachment.type && attachment.type.startsWith("image/");
|
||||
const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/");
|
||||
|
||||
// Unexpired image
|
||||
if (displayableImage) {
|
||||
|
@ -386,10 +309,7 @@ const Attachment = (props) => {
|
|||
}}
|
||||
>
|
||||
<AttachmentIcon type={attachment.type} />
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ marginLeft: 1, textAlign: "left", color: "text.primary" }}
|
||||
>
|
||||
<Typography variant="body2" sx={{ marginLeft: 1, textAlign: "left", color: "text.primary" }}>
|
||||
<b>{attachment.name}</b>
|
||||
{maybeInfoText}
|
||||
</Typography>
|
||||
|
@ -420,10 +340,7 @@ const Attachment = (props) => {
|
|||
}}
|
||||
>
|
||||
<AttachmentIcon type={attachment.type} />
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ marginLeft: 1, textAlign: "left", color: "text.primary" }}
|
||||
>
|
||||
<Typography variant="body2" sx={{ marginLeft: 1, textAlign: "left", color: "text.primary" }}>
|
||||
<b>{attachment.name}</b>
|
||||
{maybeInfoText}
|
||||
</Typography>
|
||||
|
@ -453,11 +370,7 @@ const Image = (props) => {
|
|||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
BackdropComponent={LightboxBackdrop}
|
||||
>
|
||||
<Modal open={open} onClose={() => setOpen(false)} BackdropComponent={LightboxBackdrop}>
|
||||
<Fade in={open}>
|
||||
<Box
|
||||
component="img"
|
||||
|
@ -484,11 +397,7 @@ const UserActions = (props) => {
|
|||
return (
|
||||
<>
|
||||
{props.notification.actions.map((action) => (
|
||||
<UserAction
|
||||
key={action.id}
|
||||
notification={props.notification}
|
||||
action={action}
|
||||
/>
|
||||
<UserAction key={action.id} notification={props.notification} action={action} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -502,10 +411,7 @@ const UserAction = (props) => {
|
|||
return (
|
||||
<Tooltip title={t("notifications_actions_not_supported")}>
|
||||
<span>
|
||||
<Button
|
||||
disabled
|
||||
aria-label={t("notifications_actions_not_supported")}
|
||||
>
|
||||
<Button disabled aria-label={t("notifications_actions_not_supported")}>
|
||||
{action.label}
|
||||
</Button>
|
||||
</span>
|
||||
|
@ -513,9 +419,7 @@ const UserAction = (props) => {
|
|||
);
|
||||
} else if (action.action === "view") {
|
||||
return (
|
||||
<Tooltip
|
||||
title={t("notifications_actions_open_url_title", { url: action.url })}
|
||||
>
|
||||
<Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
|
||||
<Button
|
||||
onClick={() => openUrl(action.url)}
|
||||
aria-label={t("notifications_actions_open_url_title", {
|
||||
|
@ -528,8 +432,7 @@ const UserAction = (props) => {
|
|||
);
|
||||
} else if (action.action === "http") {
|
||||
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 (
|
||||
<Tooltip
|
||||
title={t("notifications_actions_http_request_title", {
|
||||
|
@ -568,21 +471,11 @@ const performHttpAction = async (notification, action) => {
|
|||
if (success) {
|
||||
updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
|
||||
} else {
|
||||
updateActionStatus(
|
||||
notification,
|
||||
action,
|
||||
ACTION_PROGRESS_FAILED,
|
||||
`${action.label}: Unexpected response HTTP ${response.status}`
|
||||
);
|
||||
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.`
|
||||
);
|
||||
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -608,19 +501,11 @@ const ACTION_LABEL_SUFFIX = {
|
|||
|
||||
const NoNotifications = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const shortUrl = topicShortUrl(
|
||||
props.subscription.baseUrl,
|
||||
props.subscription.topic
|
||||
);
|
||||
const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
|
||||
return (
|
||||
<VerticallyCenteredContainer maxWidth="xs">
|
||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||
<img
|
||||
src={logoOutline}
|
||||
height="64"
|
||||
width="64"
|
||||
alt={t("action_bar_logo_alt")}
|
||||
/>
|
||||
<img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} />
|
||||
<br />
|
||||
{t("notifications_none_for_topic_title")}
|
||||
</Typography>
|
||||
|
@ -643,12 +528,7 @@ const NoNotificationsWithoutSubscription = (props) => {
|
|||
return (
|
||||
<VerticallyCenteredContainer maxWidth="xs">
|
||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||
<img
|
||||
src={logoOutline}
|
||||
height="64"
|
||||
width="64"
|
||||
alt={t("action_bar_logo_alt")}
|
||||
/>
|
||||
<img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} />
|
||||
<br />
|
||||
{t("notifications_none_for_any_title")}
|
||||
</Typography>
|
||||
|
@ -669,12 +549,7 @@ const NoSubscriptions = () => {
|
|||
return (
|
||||
<VerticallyCenteredContainer maxWidth="xs">
|
||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||
<img
|
||||
src={logoOutline}
|
||||
height="64"
|
||||
width="64"
|
||||
alt={t("action_bar_logo_alt")}
|
||||
/>
|
||||
<img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} />
|
||||
<br />
|
||||
{t("notifications_no_subscriptions_title")}
|
||||
</Typography>
|
||||
|
@ -695,12 +570,8 @@ const ForMoreDetails = () => {
|
|||
<Trans
|
||||
i18nKey="notifications_more_details"
|
||||
components={{
|
||||
websiteLink: (
|
||||
<Link href="https://ntfy.sh" target="_blank" rel="noopener" />
|
||||
),
|
||||
docsLink: (
|
||||
<Link href="https://ntfy.sh/docs" 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" />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -710,12 +581,7 @@ const Loading = () => {
|
|||
const { t } = useTranslation();
|
||||
return (
|
||||
<VerticallyCenteredContainer>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color="text.secondary"
|
||||
align="center"
|
||||
sx={{ paddingBottom: 1 }}
|
||||
>
|
||||
<Typography variant="h5" color="text.secondary" align="center" sx={{ paddingBottom: 1 }}>
|
||||
<CircularProgress disableShrink sx={{ marginBottom: 1 }} />
|
||||
<br />
|
||||
{t("notifications_loading")}
|
||||
|
|
|
@ -44,17 +44,8 @@ import { Pref, PrefGroup } from "./Pref";
|
|||
import { Info } from "@mui/icons-material";
|
||||
import { AccountContext } from "./App";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import {
|
||||
PermissionDenyAll,
|
||||
PermissionRead,
|
||||
PermissionReadWrite,
|
||||
PermissionWrite,
|
||||
} from "./ReserveIcons";
|
||||
import {
|
||||
ReserveAddDialog,
|
||||
ReserveDeleteDialog,
|
||||
ReserveEditDialog,
|
||||
} from "./ReserveDialogs";
|
||||
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
|
||||
import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs";
|
||||
import { UnauthorizedError } from "../app/errors";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
import { subscribeTopic } from "./SubscribeDialog";
|
||||
|
@ -112,21 +103,11 @@ const Sound = () => {
|
|||
});
|
||||
}
|
||||
return (
|
||||
<Pref
|
||||
labelId={labelId}
|
||||
title={t("prefs_notifications_sound_title")}
|
||||
description={description}
|
||||
>
|
||||
<Pref labelId={labelId} title={t("prefs_notifications_sound_title")} description={description}>
|
||||
<div style={{ display: "flex", width: "100%" }}>
|
||||
<FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
|
||||
<Select
|
||||
value={sound}
|
||||
onChange={handleChange}
|
||||
aria-labelledby={labelId}
|
||||
>
|
||||
<MenuItem value={"none"}>
|
||||
{t("prefs_notifications_sound_no_sound")}
|
||||
</MenuItem>
|
||||
<Select value={sound} onChange={handleChange} aria-labelledby={labelId}>
|
||||
<MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem>
|
||||
{Object.entries(sounds).map((s) => (
|
||||
<MenuItem key={s[0]} value={s[0]}>
|
||||
{s[1].label}
|
||||
|
@ -134,11 +115,7 @@ const Sound = () => {
|
|||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<IconButton
|
||||
onClick={() => playSound(sound)}
|
||||
disabled={sound === "none"}
|
||||
aria-label={t("prefs_notifications_sound_play")}
|
||||
>
|
||||
<IconButton onClick={() => playSound(sound)} disabled={sound === "none"} aria-label={t("prefs_notifications_sound_play")}>
|
||||
<PlayArrowIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
@ -174,41 +151,20 @@ const MinPriority = () => {
|
|||
} else if (minPriority === 5) {
|
||||
description = t("prefs_notifications_min_priority_description_max");
|
||||
} else {
|
||||
description = t(
|
||||
"prefs_notifications_min_priority_description_x_or_higher",
|
||||
{
|
||||
number: minPriority,
|
||||
name: priorities[minPriority],
|
||||
}
|
||||
);
|
||||
description = t("prefs_notifications_min_priority_description_x_or_higher", {
|
||||
number: minPriority,
|
||||
name: priorities[minPriority],
|
||||
});
|
||||
}
|
||||
return (
|
||||
<Pref
|
||||
labelId={labelId}
|
||||
title={t("prefs_notifications_min_priority_title")}
|
||||
description={description}
|
||||
>
|
||||
<Pref labelId={labelId} title={t("prefs_notifications_min_priority_title")} description={description}>
|
||||
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
||||
<Select
|
||||
value={minPriority}
|
||||
onChange={handleChange}
|
||||
aria-labelledby={labelId}
|
||||
>
|
||||
<MenuItem value={1}>
|
||||
{t("prefs_notifications_min_priority_any")}
|
||||
</MenuItem>
|
||||
<MenuItem value={2}>
|
||||
{t("prefs_notifications_min_priority_low_and_higher")}
|
||||
</MenuItem>
|
||||
<MenuItem value={3}>
|
||||
{t("prefs_notifications_min_priority_default_and_higher")}
|
||||
</MenuItem>
|
||||
<MenuItem value={4}>
|
||||
{t("prefs_notifications_min_priority_high_and_higher")}
|
||||
</MenuItem>
|
||||
<MenuItem value={5}>
|
||||
{t("prefs_notifications_min_priority_max_only")}
|
||||
</MenuItem>
|
||||
<Select value={minPriority} onChange={handleChange} aria-labelledby={labelId}>
|
||||
<MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem>
|
||||
<MenuItem value={2}>{t("prefs_notifications_min_priority_low_and_higher")}</MenuItem>
|
||||
<MenuItem value={3}>{t("prefs_notifications_min_priority_default_and_higher")}</MenuItem>
|
||||
<MenuItem value={4}>{t("prefs_notifications_min_priority_high_and_higher")}</MenuItem>
|
||||
<MenuItem value={5}>{t("prefs_notifications_min_priority_max_only")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Pref>
|
||||
|
@ -246,32 +202,14 @@ const DeleteAfter = () => {
|
|||
}
|
||||
})();
|
||||
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 }}>
|
||||
<Select
|
||||
value={deleteAfter}
|
||||
onChange={handleChange}
|
||||
aria-labelledby={labelId}
|
||||
>
|
||||
<MenuItem value={0}>
|
||||
{t("prefs_notifications_delete_after_never")}
|
||||
</MenuItem>
|
||||
<MenuItem value={10800}>
|
||||
{t("prefs_notifications_delete_after_three_hours")}
|
||||
</MenuItem>
|
||||
<MenuItem value={86400}>
|
||||
{t("prefs_notifications_delete_after_one_day")}
|
||||
</MenuItem>
|
||||
<MenuItem value={604800}>
|
||||
{t("prefs_notifications_delete_after_one_week")}
|
||||
</MenuItem>
|
||||
<MenuItem value={2592000}>
|
||||
{t("prefs_notifications_delete_after_one_month")}
|
||||
</MenuItem>
|
||||
<Select value={deleteAfter} onChange={handleChange} aria-labelledby={labelId}>
|
||||
<MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem>
|
||||
<MenuItem value={10800}>{t("prefs_notifications_delete_after_three_hours")}</MenuItem>
|
||||
<MenuItem value={86400}>{t("prefs_notifications_delete_after_one_day")}</MenuItem>
|
||||
<MenuItem value={604800}>{t("prefs_notifications_delete_after_one_week")}</MenuItem>
|
||||
<MenuItem value={2592000}>{t("prefs_notifications_delete_after_one_month")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Pref>
|
||||
|
@ -294,9 +232,7 @@ const Users = () => {
|
|||
setDialogOpen(false);
|
||||
try {
|
||||
await userManager.save(user);
|
||||
console.debug(
|
||||
`[Preferences] User ${user.username} for ${user.baseUrl} added`
|
||||
);
|
||||
console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`);
|
||||
} catch (e) {
|
||||
console.log(`[Preferences] Error adding user.`, e);
|
||||
}
|
||||
|
@ -309,22 +245,13 @@ const Users = () => {
|
|||
</Typography>
|
||||
<Paragraph>
|
||||
{t("prefs_users_description")}
|
||||
{session.exists() && (
|
||||
<>{" " + t("prefs_users_description_no_sync")}</>
|
||||
)}
|
||||
{session.exists() && <>{" " + t("prefs_users_description_no_sync")}</>}
|
||||
</Paragraph>
|
||||
{users?.length > 0 && <UserTable users={users} />}
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button onClick={handleAddClick}>{t("prefs_users_add_button")}</Button>
|
||||
<UserDialog
|
||||
key={`userAddDialog${dialogKey}`}
|
||||
open={dialogOpen}
|
||||
user={null}
|
||||
users={users}
|
||||
onCancel={handleDialogCancel}
|
||||
onSubmit={handleDialogSubmit}
|
||||
/>
|
||||
<UserDialog key={`userAddDialog${dialogKey}`} open={dialogOpen} user={null} users={users} onCancel={handleDialogCancel} onSubmit={handleDialogSubmit} />
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
|
@ -350,9 +277,7 @@ const UserTable = (props) => {
|
|||
setDialogOpen(false);
|
||||
try {
|
||||
await userManager.save(user);
|
||||
console.debug(
|
||||
`[Preferences] User ${user.username} for ${user.baseUrl} updated`
|
||||
);
|
||||
console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`);
|
||||
} catch (e) {
|
||||
console.log(`[Preferences] Error updating user.`, e);
|
||||
}
|
||||
|
@ -361,9 +286,7 @@ const UserTable = (props) => {
|
|||
const handleDeleteClick = async (user) => {
|
||||
try {
|
||||
await userManager.delete(user.baseUrl);
|
||||
console.debug(
|
||||
`[Preferences] User ${user.username} for ${user.baseUrl} deleted`
|
||||
);
|
||||
console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`);
|
||||
} catch (e) {
|
||||
console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e);
|
||||
}
|
||||
|
@ -373,43 +296,25 @@ const UserTable = (props) => {
|
|||
<Table size="small" aria-label={t("prefs_users_table")}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ paddingLeft: 0 }}>
|
||||
{t("prefs_users_table_user_header")}
|
||||
</TableCell>
|
||||
<TableCell sx={{ paddingLeft: 0 }}>{t("prefs_users_table_user_header")}</TableCell>
|
||||
<TableCell>{t("prefs_users_table_base_url_header")}</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.users?.map((user) => (
|
||||
<TableRow
|
||||
key={user.baseUrl}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{ paddingLeft: 0 }}
|
||||
aria-label={t("prefs_users_table_user_header")}
|
||||
>
|
||||
<TableRow key={user.baseUrl} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
|
||||
<TableCell component="th" scope="row" sx={{ paddingLeft: 0 }} aria-label={t("prefs_users_table_user_header")}>
|
||||
{user.username}
|
||||
</TableCell>
|
||||
<TableCell aria-label={t("prefs_users_table_base_url_header")}>
|
||||
{user.baseUrl}
|
||||
</TableCell>
|
||||
<TableCell aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell>
|
||||
<TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
|
||||
{(!session.exists() || user.baseUrl !== config.base_url) && (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => handleEditClick(user)}
|
||||
aria-label={t("prefs_users_edit_button")}
|
||||
>
|
||||
<IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => handleDeleteClick(user)}
|
||||
aria-label={t("prefs_users_delete_button")}
|
||||
>
|
||||
<IconButton onClick={() => handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
|
@ -454,15 +359,8 @@ const UserDialog = (props) => {
|
|||
return username.length > 0 && password.length > 0;
|
||||
}
|
||||
const baseUrlValid = validUrl(baseUrl);
|
||||
const baseUrlExists = props.users
|
||||
?.map((user) => user.baseUrl)
|
||||
.includes(baseUrl);
|
||||
return (
|
||||
baseUrlValid &&
|
||||
!baseUrlExists &&
|
||||
username.length > 0 &&
|
||||
password.length > 0
|
||||
);
|
||||
const baseUrlExists = props.users?.map((user) => user.baseUrl).includes(baseUrl);
|
||||
return baseUrlValid && !baseUrlExists && username.length > 0 && password.length > 0;
|
||||
})();
|
||||
const handleSubmit = async () => {
|
||||
props.onSubmit({
|
||||
|
@ -480,11 +378,7 @@ const UserDialog = (props) => {
|
|||
}, [editMode, props.user]);
|
||||
return (
|
||||
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
|
||||
<DialogTitle>
|
||||
{editMode
|
||||
? t("prefs_users_dialog_title_edit")
|
||||
: t("prefs_users_dialog_title_add")}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{editMode ? t("prefs_users_dialog_title_edit") : t("prefs_users_dialog_title_add")}</DialogTitle>
|
||||
<DialogContent>
|
||||
{!editMode && (
|
||||
<TextField
|
||||
|
@ -555,26 +449,7 @@ const Language = () => {
|
|||
|
||||
// Country flags are displayed using emoji. Emoji rendering is handled by platform fonts.
|
||||
// Windows in particular does not yet play nicely with flag emoji so for now, hide flags on Windows.
|
||||
const randomFlags = shuffle([
|
||||
"🇬🇧",
|
||||
"🇺🇸",
|
||||
"🇪🇸",
|
||||
"🇫🇷",
|
||||
"🇧🇬",
|
||||
"🇨🇿",
|
||||
"🇩🇪",
|
||||
"🇵🇱",
|
||||
"🇺🇦",
|
||||
"🇨🇳",
|
||||
"🇮🇹",
|
||||
"🇭🇺",
|
||||
"🇧🇷",
|
||||
"🇳🇱",
|
||||
"🇮🇩",
|
||||
"🇯🇵",
|
||||
"🇷🇺",
|
||||
"🇹🇷",
|
||||
]).slice(0, 3);
|
||||
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
|
||||
const showFlags = !navigator.userAgent.includes("Windows");
|
||||
let title = t("prefs_appearance_language_title");
|
||||
if (showFlags) {
|
||||
|
@ -635,8 +510,7 @@ const Reservations = () => {
|
|||
return <></>;
|
||||
}
|
||||
const reservations = account.reservations || [];
|
||||
const limitReached =
|
||||
account.role === Role.USER && account.stats.reservations_remaining === 0;
|
||||
const limitReached = account.role === Role.USER && account.stats.reservations_remaining === 0;
|
||||
|
||||
const handleAddClick = () => {
|
||||
setDialogKey((prev) => prev + 1);
|
||||
|
@ -650,23 +524,14 @@ const Reservations = () => {
|
|||
{t("prefs_reservations_title")}
|
||||
</Typography>
|
||||
<Paragraph>{t("prefs_reservations_description")}</Paragraph>
|
||||
{reservations.length > 0 && (
|
||||
<ReservationsTable reservations={reservations} />
|
||||
)}
|
||||
{limitReached && (
|
||||
<Alert severity="info">{t("prefs_reservations_limit_reached")}</Alert>
|
||||
)}
|
||||
{reservations.length > 0 && <ReservationsTable reservations={reservations} />}
|
||||
{limitReached && <Alert severity="info">{t("prefs_reservations_limit_reached")}</Alert>}
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button onClick={handleAddClick} disabled={limitReached}>
|
||||
{t("prefs_reservations_add_button")}
|
||||
</Button>
|
||||
<ReserveAddDialog
|
||||
key={`reservationAddDialog${dialogKey}`}
|
||||
open={dialogOpen}
|
||||
reservations={reservations}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
/>
|
||||
<ReserveAddDialog key={`reservationAddDialog${dialogKey}`} open={dialogOpen} reservations={reservations} onClose={() => setDialogOpen(false)} />
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
|
@ -680,14 +545,7 @@ const ReservationsTable = (props) => {
|
|||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const { subscriptions } = useOutletContext();
|
||||
const localSubscriptions =
|
||||
subscriptions?.length > 0
|
||||
? Object.assign(
|
||||
{},
|
||||
...subscriptions
|
||||
.filter((s) => s.baseUrl === config.base_url)
|
||||
.map((s) => ({ [s.topic]: s }))
|
||||
)
|
||||
: {};
|
||||
subscriptions?.length > 0 ? Object.assign({}, ...subscriptions.filter((s) => s.baseUrl === config.base_url).map((s) => ({ [s.topic]: s }))) : {};
|
||||
|
||||
const handleEditClick = (reservation) => {
|
||||
setDialogKey((prev) => prev + 1);
|
||||
|
@ -709,70 +567,46 @@ const ReservationsTable = (props) => {
|
|||
<Table size="small" aria-label={t("prefs_reservations_table")}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ paddingLeft: 0 }}>
|
||||
{t("prefs_reservations_table_topic_header")}
|
||||
</TableCell>
|
||||
<TableCell sx={{ paddingLeft: 0 }}>{t("prefs_reservations_table_topic_header")}</TableCell>
|
||||
<TableCell>{t("prefs_reservations_table_access_header")}</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.reservations.map((reservation) => (
|
||||
<TableRow
|
||||
key={reservation.topic}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{ paddingLeft: 0 }}
|
||||
aria-label={t("prefs_reservations_table_topic_header")}
|
||||
>
|
||||
<TableRow key={reservation.topic} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
|
||||
<TableCell component="th" scope="row" sx={{ paddingLeft: 0 }} aria-label={t("prefs_reservations_table_topic_header")}>
|
||||
{reservation.topic}
|
||||
</TableCell>
|
||||
<TableCell aria-label={t("prefs_reservations_table_access_header")}>
|
||||
{reservation.everyone === Permission.READ_WRITE && (
|
||||
<>
|
||||
<PermissionReadWrite
|
||||
size="small"
|
||||
sx={{ verticalAlign: "bottom", mr: 1.5 }}
|
||||
/>
|
||||
<PermissionReadWrite size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }} />
|
||||
{t("prefs_reservations_table_everyone_read_write")}
|
||||
</>
|
||||
)}
|
||||
{reservation.everyone === Permission.READ_ONLY && (
|
||||
<>
|
||||
<PermissionRead
|
||||
size="small"
|
||||
sx={{ verticalAlign: "bottom", mr: 1.5 }}
|
||||
/>
|
||||
<PermissionRead size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }} />
|
||||
{t("prefs_reservations_table_everyone_read_only")}
|
||||
</>
|
||||
)}
|
||||
{reservation.everyone === Permission.WRITE_ONLY && (
|
||||
<>
|
||||
<PermissionWrite
|
||||
size="small"
|
||||
sx={{ verticalAlign: "bottom", mr: 1.5 }}
|
||||
/>
|
||||
<PermissionWrite size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }} />
|
||||
{t("prefs_reservations_table_everyone_write_only")}
|
||||
</>
|
||||
)}
|
||||
{reservation.everyone === Permission.DENY_ALL && (
|
||||
<>
|
||||
<PermissionDenyAll
|
||||
size="small"
|
||||
sx={{ verticalAlign: "bottom", mr: 1.5 }}
|
||||
/>
|
||||
<PermissionDenyAll size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }} />
|
||||
{t("prefs_reservations_table_everyone_deny_all")}
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
|
||||
{!localSubscriptions[reservation.topic] && (
|
||||
<Tooltip
|
||||
title={t("prefs_reservations_table_click_to_subscribe")}
|
||||
>
|
||||
<Tooltip title={t("prefs_reservations_table_click_to_subscribe")}>
|
||||
<Chip
|
||||
icon={<Info />}
|
||||
onClick={() => handleSubscribeClick(reservation)}
|
||||
|
@ -782,16 +616,10 @@ const ReservationsTable = (props) => {
|
|||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={() => handleEditClick(reservation)}
|
||||
aria-label={t("prefs_reservations_edit_button")}
|
||||
>
|
||||
<IconButton onClick={() => handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => handleDeleteClick(reservation)}
|
||||
aria-label={t("prefs_reservations_delete_button")}
|
||||
>
|
||||
<IconButton onClick={() => handleDeleteClick(reservation)} aria-label={t("prefs_reservations_delete_button")}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
|
|
|
@ -1,17 +1,7 @@
|
|||
import * as React 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 priority1 from "../img/priority-1.svg";
|
||||
import priority2 from "../img/priority-2.svg";
|
||||
|
@ -27,14 +17,7 @@ import IconButton from "@mui/material/IconButton";
|
|||
import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon";
|
||||
import { Close } from "@mui/icons-material";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {
|
||||
formatBytes,
|
||||
maybeWithAuth,
|
||||
topicShortUrl,
|
||||
topicUrl,
|
||||
validTopic,
|
||||
validUrl,
|
||||
} from "../app/utils";
|
||||
import { formatBytes, maybeWithAuth, topicShortUrl, topicUrl, validTopic, validUrl } from "../app/utils";
|
||||
import Box from "@mui/material/Box";
|
||||
import AttachmentIcon from "./AttachmentIcon";
|
||||
import DialogFooter from "./DialogFooter";
|
||||
|
@ -152,10 +135,7 @@ const PublishDialog = (props) => {
|
|||
url.searchParams.append("delay", delay.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;
|
||||
try {
|
||||
|
@ -184,11 +164,7 @@ const PublishDialog = (props) => {
|
|||
setActiveRequest(null);
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus(
|
||||
<Typography sx={{ color: "error.main", maxWidth: "400px" }}>
|
||||
{e}
|
||||
</Typography>
|
||||
);
|
||||
setStatus(<Typography sx={{ color: "error.main", maxWidth: "400px" }}>{e}</Typography>);
|
||||
setActiveRequest(null);
|
||||
}
|
||||
};
|
||||
|
@ -198,8 +174,7 @@ const PublishDialog = (props) => {
|
|||
const account = await accountApi.get();
|
||||
const fileSizeLimit = account.limits.attachment_file_size ?? 0;
|
||||
const remainingBytes = account.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;
|
||||
if (fileSizeLimitReached && quotaReached) {
|
||||
return setAttachFileError(
|
||||
|
@ -282,18 +257,8 @@ const PublishDialog = (props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{dropZone && (
|
||||
<DropArea
|
||||
onDrop={handleAttachFileDrop}
|
||||
onDragLeave={handleAttachFileDragLeave}
|
||||
/>
|
||||
)}
|
||||
<Dialog
|
||||
maxWidth="md"
|
||||
open={open}
|
||||
onClose={props.onCancel}
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
{dropZone && <DropArea onDrop={handleAttachFileDrop} onDragLeave={handleAttachFileDragLeave} />}
|
||||
<Dialog maxWidth="md" open={open} onClose={props.onCancel} fullScreen={fullScreen}>
|
||||
<DialogTitle>
|
||||
{baseUrl && topic
|
||||
? t("publish_dialog_title_topic", {
|
||||
|
@ -377,16 +342,8 @@ const PublishDialog = (props) => {
|
|||
}}
|
||||
/>
|
||||
<div style={{ display: "flex" }}>
|
||||
<EmojiPicker
|
||||
anchorEl={emojiPickerAnchorEl}
|
||||
onEmojiPick={handleEmojiPick}
|
||||
onClose={handleEmojiClose}
|
||||
/>
|
||||
<DialogIconButton
|
||||
disabled={disabled}
|
||||
onClick={handleEmojiClick}
|
||||
aria-label={t("publish_dialog_emoji_picker_show")}
|
||||
>
|
||||
<EmojiPicker anchorEl={emojiPickerAnchorEl} onEmojiPick={handleEmojiPick} onClose={handleEmojiClose} />
|
||||
<DialogIconButton disabled={disabled} onClick={handleEmojiClick} aria-label={t("publish_dialog_emoji_picker_show")}>
|
||||
<InsertEmoticonIcon />
|
||||
</DialogIconButton>
|
||||
<TextField
|
||||
|
@ -403,11 +360,7 @@ const PublishDialog = (props) => {
|
|||
"aria-label": t("publish_dialog_tags_label"),
|
||||
}}
|
||||
/>
|
||||
<FormControl
|
||||
variant="standard"
|
||||
margin="dense"
|
||||
sx={{ minWidth: 170, maxWidth: 300, flexGrow: 1 }}
|
||||
>
|
||||
<FormControl variant="standard" margin="dense" sx={{ minWidth: 170, maxWidth: 300, flexGrow: 1 }}>
|
||||
<InputLabel />
|
||||
<Select
|
||||
label={t("publish_dialog_priority_label")}
|
||||
|
@ -514,11 +467,7 @@ const PublishDialog = (props) => {
|
|||
}}
|
||||
>
|
||||
{account?.phone_numbers?.map((phoneNumber, i) => (
|
||||
<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 })}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
@ -584,13 +533,7 @@ const PublishDialog = (props) => {
|
|||
/>
|
||||
</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={true} />
|
||||
{showAttachFile && (
|
||||
<AttachmentBox
|
||||
file={attachFile}
|
||||
|
@ -712,11 +655,7 @@ const PublishDialog = (props) => {
|
|||
/>
|
||||
)}
|
||||
{account && !account?.phone_numbers && (
|
||||
<Tooltip
|
||||
title={t(
|
||||
"publish_dialog_chip_call_no_verified_numbers_tooltip"
|
||||
)}
|
||||
>
|
||||
<Tooltip title={t("publish_dialog_chip_call_no_verified_numbers_tooltip")}>
|
||||
<span>
|
||||
<Chip
|
||||
clickable
|
||||
|
@ -733,23 +672,13 @@ const PublishDialog = (props) => {
|
|||
<Trans
|
||||
i18nKey="publish_dialog_details_examples_description"
|
||||
components={{
|
||||
docsLink: (
|
||||
<Link
|
||||
href="https://ntfy.sh/docs"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
/>
|
||||
),
|
||||
docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener" />,
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogFooter status={status}>
|
||||
{activeRequest && (
|
||||
<Button onClick={() => activeRequest.abort()}>
|
||||
{t("publish_dialog_button_cancel_sending")}
|
||||
</Button>
|
||||
)}
|
||||
{activeRequest && <Button onClick={() => activeRequest.abort()}>{t("publish_dialog_button_cancel_sending")}</Button>}
|
||||
{!activeRequest && (
|
||||
<>
|
||||
<FormControlLabel
|
||||
|
@ -761,16 +690,12 @@ const PublishDialog = (props) => {
|
|||
checked={publishAnother}
|
||||
onChange={(ev) => setPublishAnother(ev.target.checked)}
|
||||
inputProps={{
|
||||
"aria-label": t(
|
||||
"publish_dialog_checkbox_publish_another"
|
||||
),
|
||||
"aria-label": t("publish_dialog_checkbox_publish_another"),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Button onClick={props.onClose}>
|
||||
{t("publish_dialog_button_cancel")}
|
||||
</Button>
|
||||
<Button onClick={props.onClose}>{t("publish_dialog_button_cancel")}</Button>
|
||||
<Button onClick={handleSubmit} disabled={!sendButtonEnabled}>
|
||||
{t("publish_dialog_button_send")}
|
||||
</Button>
|
||||
|
@ -796,12 +721,7 @@ const ClosableRow = (props) => {
|
|||
<Row>
|
||||
{props.children}
|
||||
{closable && (
|
||||
<DialogIconButton
|
||||
disabled={props.disabled}
|
||||
onClick={props.onClose}
|
||||
sx={{ marginLeft: "6px" }}
|
||||
aria-label={props.closeLabel}
|
||||
>
|
||||
<DialogIconButton disabled={props.disabled} onClick={props.onClose} sx={{ marginLeft: "6px" }} aria-label={props.closeLabel}>
|
||||
<Close />
|
||||
</DialogIconButton>
|
||||
)}
|
||||
|
@ -856,23 +776,14 @@ const AttachmentBox = (props) => {
|
|||
<Typography variant="body2" sx={{ color: "text.primary" }}>
|
||||
{formatBytes(file.size)}
|
||||
{props.error && (
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ color: "error.main" }}
|
||||
aria-live="polite"
|
||||
>
|
||||
<Typography component="span" sx={{ color: "error.main" }} aria-live="polite">
|
||||
{" "}
|
||||
({props.error})
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<DialogIconButton
|
||||
disabled={props.disabled}
|
||||
onClick={props.onClose}
|
||||
sx={{ marginLeft: "6px" }}
|
||||
aria-label={t("publish_dialog_attached_file_remove")}
|
||||
>
|
||||
<DialogIconButton disabled={props.disabled} onClick={props.onClose} sx={{ marginLeft: "6px" }} aria-label={t("publish_dialog_attached_file_remove")}>
|
||||
<Close />
|
||||
</DialogIconButton>
|
||||
</Box>
|
||||
|
@ -888,22 +799,14 @@ const ExpandingTextField = (props) => {
|
|||
if (!boundingRect) {
|
||||
return props.minWidth;
|
||||
}
|
||||
return boundingRect.width >= props.minWidth
|
||||
? Math.round(boundingRect.width)
|
||||
: props.minWidth;
|
||||
return boundingRect.width >= props.minWidth ? Math.round(boundingRect.width) : props.minWidth;
|
||||
};
|
||||
useEffect(() => {
|
||||
setTextWidth(determineTextWidth() + 5);
|
||||
}, [props.value]);
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
ref={invisibleFieldRef}
|
||||
component="span"
|
||||
variant={props.variant}
|
||||
aria-hidden={true}
|
||||
sx={{ position: "absolute", left: "-200%" }}
|
||||
>
|
||||
<Typography ref={invisibleFieldRef} component="span" variant={props.variant} aria-hidden={true} sx={{ position: "absolute", left: "-200%" }}>
|
||||
{props.value}
|
||||
</Typography>
|
||||
<TextField
|
||||
|
@ -983,9 +886,7 @@ const DropBox = () => {
|
|||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{t("publish_dialog_drop_file_here")}
|
||||
</Typography>
|
||||
<Typography variant="h5">{t("publish_dialog_drop_file_here")}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -28,16 +28,13 @@ export const ReserveAddDialog = (props) => {
|
|||
const [everyone, setEveryone] = useState(Permission.DENY_ALL);
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const allowTopicEdit = !props.topic;
|
||||
const alreadyReserved =
|
||||
props.reservations.filter((r) => r.topic === topic).length > 0;
|
||||
const alreadyReserved = props.reservations.filter((r) => r.topic === topic).length > 0;
|
||||
const submitButtonEnabled = validTopic(topic) && !alreadyReserved;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await accountApi.upsertReservation(topic, everyone);
|
||||
console.debug(
|
||||
`[ReserveAddDialog] Added reservation for topic ${topic}: ${everyone}`
|
||||
);
|
||||
console.debug(`[ReserveAddDialog] Added reservation for topic ${topic}: ${everyone}`);
|
||||
} catch (e) {
|
||||
console.log(`[ReserveAddDialog] Error adding topic reservation.`, e);
|
||||
if (e instanceof UnauthorizedError) {
|
||||
|
@ -54,18 +51,10 @@ export const ReserveAddDialog = (props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||
<DialogTitle>{t("prefs_reservations_dialog_title_add")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("prefs_reservations_dialog_description")}
|
||||
</DialogContentText>
|
||||
<DialogContentText>{t("prefs_reservations_dialog_description")}</DialogContentText>
|
||||
{allowTopicEdit && (
|
||||
<TextField
|
||||
autoFocus
|
||||
|
@ -80,11 +69,7 @@ export const ReserveAddDialog = (props) => {
|
|||
variant="standard"
|
||||
/>
|
||||
)}
|
||||
<ReserveTopicSelect
|
||||
value={everyone}
|
||||
onChange={setEveryone}
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
<ReserveTopicSelect value={everyone} onChange={setEveryone} sx={{ mt: 1 }} />
|
||||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
|
||||
|
@ -99,17 +84,13 @@ export const ReserveAddDialog = (props) => {
|
|||
export const ReserveEditDialog = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [error, setError] = useState("");
|
||||
const [everyone, setEveryone] = useState(
|
||||
props.reservation?.everyone || Permission.DENY_ALL
|
||||
);
|
||||
const [everyone, setEveryone] = useState(props.reservation?.everyone || Permission.DENY_ALL);
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await accountApi.upsertReservation(props.reservation.topic, everyone);
|
||||
console.debug(
|
||||
`[ReserveEditDialog] Updated reservation for topic ${t}: ${everyone}`
|
||||
);
|
||||
console.debug(`[ReserveEditDialog] Updated reservation for topic ${t}: ${everyone}`);
|
||||
} catch (e) {
|
||||
console.log(`[ReserveEditDialog] Error updating topic reservation.`, e);
|
||||
if (e instanceof UnauthorizedError) {
|
||||
|
@ -123,23 +104,11 @@ export const ReserveEditDialog = (props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||
<DialogTitle>{t("prefs_reservations_dialog_title_edit")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("prefs_reservations_dialog_description")}
|
||||
</DialogContentText>
|
||||
<ReserveTopicSelect
|
||||
value={everyone}
|
||||
onChange={setEveryone}
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
<DialogContentText>{t("prefs_reservations_dialog_description")}</DialogContentText>
|
||||
<ReserveTopicSelect value={everyone} onChange={setEveryone} sx={{ mt: 1 }} />
|
||||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
|
||||
|
@ -158,9 +127,7 @@ export const ReserveDeleteDialog = (props) => {
|
|||
const handleSubmit = async () => {
|
||||
try {
|
||||
await accountApi.deleteReservation(props.topic, deleteMessages);
|
||||
console.debug(
|
||||
`[ReserveDeleteDialog] Deleted reservation for topic ${props.topic}`
|
||||
);
|
||||
console.debug(`[ReserveDeleteDialog] Deleted reservation for topic ${props.topic}`);
|
||||
} catch (e) {
|
||||
console.log(`[ReserveDeleteDialog] Error deleting topic reservation.`, e);
|
||||
if (e instanceof UnauthorizedError) {
|
||||
|
@ -174,18 +141,10 @@ export const ReserveDeleteDialog = (props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||
<DialogTitle>{t("prefs_reservations_dialog_title_delete")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("reservation_delete_dialog_description")}
|
||||
</DialogContentText>
|
||||
<DialogContentText>{t("reservation_delete_dialog_description")}</DialogContentText>
|
||||
<FormControl fullWidth variant="standard">
|
||||
<Select
|
||||
value={deleteMessages}
|
||||
|
@ -203,17 +162,13 @@ export const ReserveDeleteDialog = (props) => {
|
|||
<ListItemIcon>
|
||||
<Check />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t("reservation_delete_dialog_action_keep_title")}
|
||||
/>
|
||||
<ListItemText primary={t("reservation_delete_dialog_action_keep_title")} />
|
||||
</MenuItem>
|
||||
<MenuItem value={true}>
|
||||
<ListItemIcon>
|
||||
<DeleteForever />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t("reservation_delete_dialog_action_delete_title")}
|
||||
/>
|
||||
<ListItemText primary={t("reservation_delete_dialog_action_delete_title")} />
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
|
|
@ -4,12 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||
import MenuItem from "@mui/material/MenuItem";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import {
|
||||
PermissionDenyAll,
|
||||
PermissionRead,
|
||||
PermissionReadWrite,
|
||||
PermissionWrite,
|
||||
} from "./ReserveIcons";
|
||||
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
|
||||
import { Permission } from "../app/AccountApi";
|
||||
|
||||
const ReserveTopicSelect = (props) => {
|
||||
|
@ -34,33 +29,25 @@ const ReserveTopicSelect = (props) => {
|
|||
<ListItemIcon>
|
||||
<PermissionDenyAll />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t("prefs_reservations_table_everyone_deny_all")}
|
||||
/>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")} />
|
||||
</MenuItem>
|
||||
<MenuItem value={Permission.READ_ONLY}>
|
||||
<ListItemIcon>
|
||||
<PermissionRead />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t("prefs_reservations_table_everyone_read_only")}
|
||||
/>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")} />
|
||||
</MenuItem>
|
||||
<MenuItem value={Permission.WRITE_ONLY}>
|
||||
<ListItemIcon>
|
||||
<PermissionWrite />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t("prefs_reservations_table_everyone_write_only")}
|
||||
/>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")} />
|
||||
</MenuItem>
|
||||
<MenuItem value={Permission.READ_WRITE}>
|
||||
<ListItemIcon>
|
||||
<PermissionReadWrite />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t("prefs_reservations_table_everyone_read_write")}
|
||||
/>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")} />
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
|
|
@ -31,9 +31,7 @@ const Signup = () => {
|
|||
try {
|
||||
await accountApi.create(user.username, user.password);
|
||||
const token = await accountApi.login(user);
|
||||
console.log(
|
||||
`[Signup] User signup for user ${user.username} successful, token is ${token}`
|
||||
);
|
||||
console.log(`[Signup] User signup for user ${user.username} successful, token is ${token}`);
|
||||
session.store(user.username, token);
|
||||
window.location.href = routes.app;
|
||||
} catch (e) {
|
||||
|
@ -51,9 +49,7 @@ const Signup = () => {
|
|||
if (!config.enable_signup) {
|
||||
return (
|
||||
<AvatarBox>
|
||||
<Typography sx={{ typography: "h6" }}>
|
||||
{t("signup_disabled")}
|
||||
</Typography>
|
||||
<Typography sx={{ typography: "h6" }}>{t("signup_disabled")}</Typography>
|
||||
</AvatarBox>
|
||||
);
|
||||
}
|
||||
|
@ -61,12 +57,7 @@ const Signup = () => {
|
|||
return (
|
||||
<AvatarBox>
|
||||
<Typography sx={{ typography: "h6" }}>{t("signup_title")}</Typography>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
sx={{ mt: 1, maxWidth: 400 }}
|
||||
>
|
||||
<Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1, maxWidth: 400 }}>
|
||||
<TextField
|
||||
margin="dense"
|
||||
required
|
||||
|
@ -130,13 +121,7 @@ const Signup = () => {
|
|||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
disabled={username === "" || password === "" || password !== confirm}
|
||||
sx={{ mt: 2, mb: 2 }}
|
||||
>
|
||||
<Button type="submit" fullWidth variant="contained" disabled={username === "" || password === "" || password !== confirm} sx={{ mt: 2, mb: 2 }}>
|
||||
{t("signup_form_button_submit")}
|
||||
</Button>
|
||||
{error && (
|
||||
|
|
|
@ -6,21 +6,10 @@ import Dialog from "@mui/material/Dialog";
|
|||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
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 theme from "./theme";
|
||||
import api from "../app/Api";
|
||||
import {
|
||||
randomAlphanumericString,
|
||||
topicUrl,
|
||||
validTopic,
|
||||
validUrl,
|
||||
} from "../app/utils";
|
||||
import { randomAlphanumericString, topicUrl, validTopic, validUrl } from "../app/utils";
|
||||
import userManager from "../app/UserManager";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
import poller from "../app/Poller";
|
||||
|
@ -64,14 +53,7 @@ const SubscribeDialog = (props) => {
|
|||
onSuccess={handleSuccess}
|
||||
/>
|
||||
)}
|
||||
{showLoginPage && (
|
||||
<LoginPage
|
||||
baseUrl={baseUrl}
|
||||
topic={topic}
|
||||
onBack={() => setShowLoginPage(false)}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
)}
|
||||
{showLoginPage && <LoginPage baseUrl={baseUrl} topic={topic} onBack={() => setShowLoginPage(false)} onSuccess={handleSuccess} />}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
@ -85,37 +67,20 @@ const SubscribePage = (props) => {
|
|||
const [everyone, setEveryone] = useState(Permission.DENY_ALL);
|
||||
const baseUrl = anotherServerVisible ? props.baseUrl : config.base_url;
|
||||
const topic = props.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((s) => s !== config.base_url);
|
||||
const showReserveTopicCheckbox =
|
||||
config.enable_reservations &&
|
||||
!anotherServerVisible &&
|
||||
(config.enable_payments || account);
|
||||
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((s) => s !== config.base_url);
|
||||
const showReserveTopicCheckbox = config.enable_reservations && !anotherServerVisible && (config.enable_payments || account);
|
||||
const reserveTopicEnabled =
|
||||
session.exists() &&
|
||||
(account?.role === Role.ADMIN ||
|
||||
(account?.role === Role.USER &&
|
||||
(account?.stats.reservations_remaining || 0) > 0));
|
||||
session.exists() && (account?.role === Role.ADMIN || (account?.role === Role.USER && (account?.stats.reservations_remaining || 0) > 0));
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
const user = await userManager.get(baseUrl); // May be undefined
|
||||
const username = user
|
||||
? user.username
|
||||
: t("subscribe_dialog_error_user_anonymous");
|
||||
const username = user ? user.username : t("subscribe_dialog_error_user_anonymous");
|
||||
|
||||
// Check read access to topic
|
||||
const success = await api.topicAuth(baseUrl, topic, user);
|
||||
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}`);
|
||||
if (user) {
|
||||
setError(
|
||||
t("subscribe_dialog_error_user_not_authorized", {
|
||||
|
@ -130,14 +95,8 @@ const SubscribePage = (props) => {
|
|||
}
|
||||
|
||||
// Reserve topic (if requested)
|
||||
if (
|
||||
session.exists() &&
|
||||
baseUrl === config.base_url &&
|
||||
reserveTopicVisible
|
||||
) {
|
||||
console.log(
|
||||
`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`
|
||||
);
|
||||
if (session.exists() && baseUrl === config.base_url && reserveTopicVisible) {
|
||||
console.log(`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`);
|
||||
try {
|
||||
await accountApi.upsertReservation(topic, everyone);
|
||||
} catch (e) {
|
||||
|
@ -151,12 +110,7 @@ const SubscribePage = (props) => {
|
|||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[SubscribeDialog] Successful login to ${topicUrl(
|
||||
baseUrl,
|
||||
topic
|
||||
)} for user ${username}`
|
||||
);
|
||||
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
|
||||
props.onSuccess();
|
||||
};
|
||||
|
||||
|
@ -167,14 +121,10 @@ const SubscribePage = (props) => {
|
|||
|
||||
const subscribeButtonEnabled = (() => {
|
||||
if (anotherServerVisible) {
|
||||
const isExistingTopicUrl = existingTopicUrls.includes(
|
||||
topicUrl(baseUrl, topic)
|
||||
);
|
||||
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
|
||||
return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
|
||||
} else {
|
||||
const isExistingTopicUrl = existingTopicUrls.includes(
|
||||
topicUrl(config.base_url, topic)
|
||||
);
|
||||
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.base_url, topic));
|
||||
return validTopic(topic) && !isExistingTopicUrl;
|
||||
}
|
||||
})();
|
||||
|
@ -191,9 +141,7 @@ const SubscribePage = (props) => {
|
|||
<>
|
||||
<DialogTitle>{t("subscribe_dialog_subscribe_title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("subscribe_dialog_subscribe_description")}
|
||||
</DialogContentText>
|
||||
<DialogContentText>{t("subscribe_dialog_subscribe_description")}</DialogContentText>
|
||||
<div style={{ display: "flex", paddingBottom: "8px" }} role="row">
|
||||
<TextField
|
||||
autoFocus
|
||||
|
@ -241,9 +189,7 @@ const SubscribePage = (props) => {
|
|||
</>
|
||||
}
|
||||
/>
|
||||
{reserveTopicVisible && (
|
||||
<ReserveTopicSelect value={everyone} onChange={setEveryone} />
|
||||
)}
|
||||
{reserveTopicVisible && <ReserveTopicSelect value={everyone} onChange={setEveryone} />}
|
||||
</FormGroup>
|
||||
)}
|
||||
{!reserveTopicVisible && (
|
||||
|
@ -253,9 +199,7 @@ const SubscribePage = (props) => {
|
|||
<Checkbox
|
||||
onChange={handleUseAnotherChanged}
|
||||
inputProps={{
|
||||
"aria-label": t(
|
||||
"subscribe_dialog_subscribe_use_another_label"
|
||||
),
|
||||
"aria-label": t("subscribe_dialog_subscribe_use_another_label"),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
@ -268,12 +212,7 @@ const SubscribePage = (props) => {
|
|||
inputValue={props.baseUrl}
|
||||
onInputChange={updateBaseUrl}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder={config.base_url}
|
||||
variant="standard"
|
||||
aria-label={t("subscribe_dialog_subscribe_base_url_label")}
|
||||
/>
|
||||
<TextField {...params} placeholder={config.base_url} variant="standard" aria-label={t("subscribe_dialog_subscribe_base_url_label")} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
@ -281,9 +220,7 @@ const SubscribePage = (props) => {
|
|||
)}
|
||||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={props.onCancel}>
|
||||
{t("subscribe_dialog_subscribe_button_cancel")}
|
||||
</Button>
|
||||
<Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button>
|
||||
<Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}>
|
||||
{t("subscribe_dialog_subscribe_button_subscribe")}
|
||||
</Button>
|
||||
|
@ -304,23 +241,11 @@ const LoginPage = (props) => {
|
|||
const user = { baseUrl, username, password };
|
||||
const success = await api.topicAuth(baseUrl, topic, user);
|
||||
if (!success) {
|
||||
console.log(
|
||||
`[SubscribeDialog] Login to ${topicUrl(
|
||||
baseUrl,
|
||||
topic
|
||||
)} failed for user ${username}`
|
||||
);
|
||||
setError(
|
||||
t("subscribe_dialog_error_user_not_authorized", { username: username })
|
||||
);
|
||||
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
|
||||
setError(t("subscribe_dialog_error_user_not_authorized", { username: username }));
|
||||
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}`);
|
||||
await userManager.save(user);
|
||||
props.onSuccess();
|
||||
};
|
||||
|
@ -329,9 +254,7 @@ const LoginPage = (props) => {
|
|||
<>
|
||||
<DialogTitle>{t("subscribe_dialog_login_title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("subscribe_dialog_login_description")}
|
||||
</DialogContentText>
|
||||
<DialogContentText>{t("subscribe_dialog_login_description")}</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
|
@ -362,9 +285,7 @@ const LoginPage = (props) => {
|
|||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={props.onBack}>{t("common_back")}</Button>
|
||||
<Button onClick={handleLogin}>
|
||||
{t("subscribe_dialog_login_button_login")}
|
||||
</Button>
|
||||
<Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -6,13 +6,7 @@ import Dialog from "@mui/material/Dialog";
|
|||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
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";
|
||||
|
@ -28,11 +22,7 @@ import { useNavigate } from "react-router-dom";
|
|||
import IconButton from "@mui/material/IconButton";
|
||||
import { Clear } from "@mui/icons-material";
|
||||
import { AccountContext } from "./App";
|
||||
import {
|
||||
ReserveAddDialog,
|
||||
ReserveDeleteDialog,
|
||||
ReserveEditDialog,
|
||||
} from "./ReserveDialogs";
|
||||
import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs";
|
||||
import { UnauthorizedError } from "../app/errors";
|
||||
|
||||
export const SubscriptionPopup = (props) => {
|
||||
|
@ -48,19 +38,11 @@ export const SubscriptionPopup = (props) => {
|
|||
const placement = props.placement ?? "left";
|
||||
const reservations = account?.reservations || [];
|
||||
|
||||
const showReservationAdd =
|
||||
config.enable_reservations &&
|
||||
!subscription?.reservation &&
|
||||
account?.stats.reservations_remaining > 0;
|
||||
const showReservationAdd = config.enable_reservations && !subscription?.reservation && account?.stats.reservations_remaining > 0;
|
||||
const showReservationAddDisabled =
|
||||
!showReservationAdd &&
|
||||
config.enable_reservations &&
|
||||
!subscription?.reservation &&
|
||||
(config.enable_payments || account?.stats.reservations_remaining === 0);
|
||||
const showReservationEdit =
|
||||
config.enable_reservations && !!subscription?.reservation;
|
||||
const showReservationDelete =
|
||||
config.enable_reservations && !!subscription?.reservation;
|
||||
!showReservationAdd && config.enable_reservations && !subscription?.reservation && (config.enable_payments || account?.stats.reservations_remaining === 0);
|
||||
const showReservationEdit = config.enable_reservations && !!subscription?.reservation;
|
||||
const showReservationDelete = config.enable_reservations && !!subscription?.reservation;
|
||||
|
||||
const handleChangeDisplayName = async () => {
|
||||
setDisplayNameDialogOpen(true);
|
||||
|
@ -115,14 +97,10 @@ export const SubscriptionPopup = (props) => {
|
|||
])[0];
|
||||
const nowSeconds = Math.round(Date.now() / 1000);
|
||||
const message = shuffle([
|
||||
`Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(
|
||||
nowSeconds
|
||||
)} right now. Is that early or late?`,
|
||||
`Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(nowSeconds)} right now. Is that early or late?`,
|
||||
`So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`,
|
||||
`It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`,
|
||||
`Alright then, it's ${formatShortDateTime(
|
||||
nowSeconds
|
||||
)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`,
|
||||
`Alright then, it's ${formatShortDateTime(nowSeconds)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`,
|
||||
`There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`,
|
||||
`I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`,
|
||||
`It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?`,
|
||||
|
@ -140,24 +118,16 @@ export const SubscriptionPopup = (props) => {
|
|||
};
|
||||
|
||||
const handleClearAll = async () => {
|
||||
console.log(
|
||||
`[SubscriptionPopup] Deleting all notifications from ${props.subscription.id}`
|
||||
);
|
||||
console.log(`[SubscriptionPopup] Deleting all notifications from ${props.subscription.id}`);
|
||||
await subscriptionManager.deleteNotifications(props.subscription.id);
|
||||
};
|
||||
|
||||
const handleUnsubscribe = async () => {
|
||||
console.log(
|
||||
`[SubscriptionPopup] Unsubscribing from ${props.subscription.id}`,
|
||||
props.subscription
|
||||
);
|
||||
console.log(`[SubscriptionPopup] Unsubscribing from ${props.subscription.id}`, props.subscription);
|
||||
await subscriptionManager.remove(props.subscription.id);
|
||||
if (session.exists() && !subscription.internal) {
|
||||
try {
|
||||
await accountApi.deleteSubscription(
|
||||
props.subscription.baseUrl,
|
||||
props.subscription.topic
|
||||
);
|
||||
await accountApi.deleteSubscription(props.subscription.baseUrl, props.subscription.topic);
|
||||
} catch (e) {
|
||||
console.log(`[SubscriptionPopup] Error unsubscribing`, e);
|
||||
if (e instanceof UnauthorizedError) {
|
||||
|
@ -175,67 +145,26 @@ export const SubscriptionPopup = (props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<PopupMenu
|
||||
horizontal={placement}
|
||||
anchorEl={props.anchor}
|
||||
open={!!props.anchor}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<MenuItem onClick={handleChangeDisplayName}>
|
||||
{t("action_bar_change_display_name")}
|
||||
</MenuItem>
|
||||
{showReservationAdd && (
|
||||
<MenuItem onClick={handleReserveAdd}>
|
||||
{t("action_bar_reservation_add")}
|
||||
</MenuItem>
|
||||
)}
|
||||
<PopupMenu horizontal={placement} anchorEl={props.anchor} open={!!props.anchor} onClose={props.onClose}>
|
||||
<MenuItem onClick={handleChangeDisplayName}>{t("action_bar_change_display_name")}</MenuItem>
|
||||
{showReservationAdd && <MenuItem onClick={handleReserveAdd}>{t("action_bar_reservation_add")}</MenuItem>}
|
||||
{showReservationAddDisabled && (
|
||||
<MenuItem sx={{ cursor: "default" }}>
|
||||
<span style={{ opacity: 0.3 }}>
|
||||
{t("action_bar_reservation_add")}
|
||||
</span>
|
||||
<span style={{ opacity: 0.3 }}>{t("action_bar_reservation_add")}</span>
|
||||
<ReserveLimitChip />
|
||||
</MenuItem>
|
||||
)}
|
||||
{showReservationEdit && (
|
||||
<MenuItem onClick={handleReserveEdit}>
|
||||
{t("action_bar_reservation_edit")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{showReservationDelete && (
|
||||
<MenuItem onClick={handleReserveDelete}>
|
||||
{t("action_bar_reservation_delete")}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={handleSendTestMessage}>
|
||||
{t("action_bar_send_test_notification")}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClearAll}>
|
||||
{t("action_bar_clear_notifications")}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleUnsubscribe}>
|
||||
{t("action_bar_unsubscribe")}
|
||||
</MenuItem>
|
||||
{showReservationEdit && <MenuItem onClick={handleReserveEdit}>{t("action_bar_reservation_edit")}</MenuItem>}
|
||||
{showReservationDelete && <MenuItem onClick={handleReserveDelete}>{t("action_bar_reservation_delete")}</MenuItem>}
|
||||
<MenuItem onClick={handleSendTestMessage}>{t("action_bar_send_test_notification")}</MenuItem>
|
||||
<MenuItem onClick={handleClearAll}>{t("action_bar_clear_notifications")}</MenuItem>
|
||||
<MenuItem onClick={handleUnsubscribe}>{t("action_bar_unsubscribe")}</MenuItem>
|
||||
</PopupMenu>
|
||||
<Portal>
|
||||
<Snackbar
|
||||
open={showPublishError}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setShowPublishError(false)}
|
||||
message={t("message_bar_error_publishing")}
|
||||
/>
|
||||
<DisplayNameDialog
|
||||
open={displayNameDialogOpen}
|
||||
subscription={subscription}
|
||||
onClose={() => setDisplayNameDialogOpen(false)}
|
||||
/>
|
||||
<Snackbar open={showPublishError} autoHideDuration={3000} onClose={() => setShowPublishError(false)} message={t("message_bar_error_publishing")} />
|
||||
<DisplayNameDialog open={displayNameDialogOpen} subscription={subscription} onClose={() => setDisplayNameDialogOpen(false)} />
|
||||
{showReservationAdd && (
|
||||
<ReserveAddDialog
|
||||
open={reserveAddDialogOpen}
|
||||
topic={subscription.topic}
|
||||
reservations={reservations}
|
||||
onClose={() => setReserveAddDialogOpen(false)}
|
||||
/>
|
||||
<ReserveAddDialog open={reserveAddDialogOpen} topic={subscription.topic} reservations={reservations} onClose={() => setReserveAddDialogOpen(false)} />
|
||||
)}
|
||||
{showReservationEdit && (
|
||||
<ReserveEditDialog
|
||||
|
@ -246,11 +175,7 @@ export const SubscriptionPopup = (props) => {
|
|||
/>
|
||||
)}
|
||||
{showReservationDelete && (
|
||||
<ReserveDeleteDialog
|
||||
open={reserveDeleteDialogOpen}
|
||||
topic={subscription.topic}
|
||||
onClose={() => setReserveDeleteDialogOpen(false)}
|
||||
/>
|
||||
<ReserveDeleteDialog open={reserveDeleteDialogOpen} topic={subscription.topic} onClose={() => setReserveDeleteDialogOpen(false)} />
|
||||
)}
|
||||
</Portal>
|
||||
</>
|
||||
|
@ -261,28 +186,17 @@ const DisplayNameDialog = (props) => {
|
|||
const { t } = useTranslation();
|
||||
const subscription = props.subscription;
|
||||
const [error, setError] = useState("");
|
||||
const [displayName, setDisplayName] = useState(
|
||||
subscription.displayName ?? ""
|
||||
);
|
||||
const [displayName, setDisplayName] = useState(subscription.displayName ?? "");
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const handleSave = async () => {
|
||||
await subscriptionManager.setDisplayName(subscription.id, displayName);
|
||||
if (session.exists() && !subscription.internal) {
|
||||
try {
|
||||
console.log(
|
||||
`[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}`
|
||||
);
|
||||
await accountApi.updateSubscription(
|
||||
subscription.baseUrl,
|
||||
subscription.topic,
|
||||
{ display_name: displayName }
|
||||
);
|
||||
console.log(`[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}`);
|
||||
await accountApi.updateSubscription(subscription.baseUrl, subscription.topic, { display_name: displayName });
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`[SubscriptionSettingsDialog] Error updating subscription`,
|
||||
e
|
||||
);
|
||||
console.log(`[SubscriptionSettingsDialog] Error updating subscription`, e);
|
||||
if (e instanceof UnauthorizedError) {
|
||||
session.resetAndRedirect(routes.login);
|
||||
} else {
|
||||
|
@ -295,18 +209,10 @@ const DisplayNameDialog = (props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||
<DialogTitle>{t("display_name_dialog_title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("display_name_dialog_description")}
|
||||
</DialogContentText>
|
||||
<DialogContentText>{t("display_name_dialog_description")}</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
placeholder={t("display_name_dialog_placeholder")}
|
||||
|
@ -340,17 +246,10 @@ const DisplayNameDialog = (props) => {
|
|||
|
||||
export const ReserveLimitChip = () => {
|
||||
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 <></>;
|
||||
} else if (config.enable_payments) {
|
||||
return account?.limits.reservations > 0 ? (
|
||||
<LimitReachedChip />
|
||||
) : (
|
||||
<ProChip />
|
||||
);
|
||||
return account?.limits.reservations > 0 ? <LimitReachedChip /> : <ProChip />;
|
||||
} else if (account) {
|
||||
return <LimitReachedChip />;
|
||||
}
|
||||
|
|
|
@ -3,16 +3,7 @@ import { useContext, useEffect, useState } from "react";
|
|||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
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 accountApi, { SubscriptionInterval } from "../app/AccountApi";
|
||||
|
@ -21,12 +12,7 @@ import routes from "./routes";
|
|||
import Card from "@mui/material/Card";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { AccountContext } from "./App";
|
||||
import {
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
formatPrice,
|
||||
formatShortDate,
|
||||
} from "../app/utils";
|
||||
import { formatBytes, formatNumber, formatPrice, formatShortDate } from "../app/utils";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import List from "@mui/material/List";
|
||||
import { Check, Close } from "@mui/icons-material";
|
||||
|
@ -43,9 +29,7 @@ const UpgradeDialog = (props) => {
|
|||
const { account } = useContext(AccountContext); // May be undefined!
|
||||
const [error, setError] = useState("");
|
||||
const [tiers, setTiers] = useState(null);
|
||||
const [interval, setInterval] = useState(
|
||||
account?.billing?.interval || SubscriptionInterval.YEAR
|
||||
);
|
||||
const [interval, setInterval] = useState(account?.billing?.interval || SubscriptionInterval.YEAR);
|
||||
const [newTierCode, setNewTierCode] = useState(account?.tier?.code); // May be undefined
|
||||
const [loading, setLoading] = useState(false);
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
@ -61,9 +45,7 @@ const UpgradeDialog = (props) => {
|
|||
return <></>;
|
||||
}
|
||||
|
||||
const tiersMap = Object.assign(
|
||||
...tiers.map((tier) => ({ [tier.code]: tier }))
|
||||
);
|
||||
const tiersMap = Object.assign(...tiers.map((tier) => ({ [tier.code]: tier })));
|
||||
const newTier = tiersMap[newTierCode]; // May be undefined
|
||||
const currentTier = account?.tier; // May be undefined
|
||||
const currentInterval = account?.billing?.interval; // May be undefined
|
||||
|
@ -75,10 +57,7 @@ const UpgradeDialog = (props) => {
|
|||
submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup");
|
||||
submitAction = Action.REDIRECT_SIGNUP;
|
||||
banner = null;
|
||||
} else if (
|
||||
currentTierCode === newTierCode &&
|
||||
(currentInterval === undefined || currentInterval === interval)
|
||||
) {
|
||||
} else if (currentTierCode === newTierCode && (currentInterval === undefined || currentInterval === interval)) {
|
||||
submitButtonLabel = t("account_upgrade_dialog_button_update_subscription");
|
||||
submitAction = null;
|
||||
banner = currentTierCode ? Banner.PRORATION_INFO : null;
|
||||
|
@ -99,10 +78,7 @@ const UpgradeDialog = (props) => {
|
|||
// Exceptional conditions
|
||||
if (loading) {
|
||||
submitAction = null;
|
||||
} else if (
|
||||
newTier?.code &&
|
||||
account?.reservations?.length > newTier?.limits?.reservations
|
||||
) {
|
||||
} else if (newTier?.code && account?.reservations?.length > newTier?.limits?.reservations) {
|
||||
submitAction = null;
|
||||
banner = Banner.RESERVATIONS_WARNING;
|
||||
}
|
||||
|
@ -115,10 +91,7 @@ const UpgradeDialog = (props) => {
|
|||
try {
|
||||
setLoading(true);
|
||||
if (submitAction === Action.CREATE_SUBSCRIPTION) {
|
||||
const response = await accountApi.createBillingSubscription(
|
||||
newTierCode,
|
||||
interval
|
||||
);
|
||||
const response = await accountApi.createBillingSubscription(newTierCode, interval);
|
||||
window.location.href = response.redirect_url;
|
||||
} else if (submitAction === Action.UPDATE_SUBSCRIPTION) {
|
||||
await accountApi.updateBillingSubscription(newTierCode, interval);
|
||||
|
@ -142,16 +115,12 @@ const UpgradeDialog = (props) => {
|
|||
let discount = 0,
|
||||
upto = false;
|
||||
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 {
|
||||
let n = 0;
|
||||
for (const t of tiers) {
|
||||
if (t.prices) {
|
||||
const tierDiscount = Math.round(
|
||||
((t.prices.month * 12) / t.prices.year - 1) * 100
|
||||
);
|
||||
const tierDiscount = Math.round(((t.prices.month * 12) / t.prices.year - 1) * 100);
|
||||
if (tierDiscount > discount) {
|
||||
discount = tierDiscount;
|
||||
n++;
|
||||
|
@ -162,12 +131,7 @@ const UpgradeDialog = (props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={props.onCancel}
|
||||
maxWidth="lg"
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<Dialog open={props.open} onClose={props.onCancel} maxWidth="lg" fullScreen={fullScreen}>
|
||||
<DialogTitle>
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
<div style={{ flexGrow: 1 }}>{t("account_upgrade_dialog_title")}</div>
|
||||
|
@ -184,13 +148,7 @@ const UpgradeDialog = (props) => {
|
|||
</Typography>
|
||||
<Switch
|
||||
checked={interval === SubscriptionInterval.YEAR}
|
||||
onChange={(ev) =>
|
||||
setInterval(
|
||||
ev.target.checked
|
||||
? SubscriptionInterval.YEAR
|
||||
: SubscriptionInterval.MONTH
|
||||
)
|
||||
}
|
||||
onChange={(ev) => setInterval(ev.target.checked ? SubscriptionInterval.YEAR : SubscriptionInterval.MONTH)}
|
||||
/>
|
||||
<Typography component="span" variant="subtitle1">
|
||||
{t("account_upgrade_dialog_interval_yearly")}
|
||||
|
@ -199,20 +157,12 @@ const UpgradeDialog = (props) => {
|
|||
<Chip
|
||||
label={
|
||||
upto
|
||||
? t(
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to",
|
||||
{ discount: discount }
|
||||
)
|
||||
: t(
|
||||
"account_upgrade_dialog_interval_yearly_discount_save",
|
||||
{ discount: discount }
|
||||
)
|
||||
? t("account_upgrade_dialog_interval_yearly_discount_save_up_to", { discount: discount })
|
||||
: t("account_upgrade_dialog_interval_yearly_discount_save", { discount: discount })
|
||||
}
|
||||
color="primary"
|
||||
size="small"
|
||||
variant={
|
||||
interval === SubscriptionInterval.YEAR ? "filled" : "outlined"
|
||||
}
|
||||
variant={interval === SubscriptionInterval.YEAR ? "filled" : "outlined"}
|
||||
sx={{ marginLeft: "5px" }}
|
||||
/>
|
||||
)}
|
||||
|
@ -258,9 +208,7 @@ const UpgradeDialog = (props) => {
|
|||
<Alert severity="warning" sx={{ fontSize: "1rem" }}>
|
||||
<Trans
|
||||
i18nKey="account_upgrade_dialog_reservations_warning"
|
||||
count={
|
||||
account?.reservations.length - newTier?.limits.reservations
|
||||
}
|
||||
count={account?.reservations.length - newTier?.limits.reservations}
|
||||
components={{
|
||||
Link: <NavLink to={routes.settings} />,
|
||||
}}
|
||||
|
@ -309,9 +257,7 @@ const UpgradeDialog = (props) => {
|
|||
{error}
|
||||
</DialogContentText>
|
||||
<DialogActions sx={{ paddingRight: 2 }}>
|
||||
<Button onClick={props.onCancel}>
|
||||
{t("account_upgrade_dialog_button_cancel")}
|
||||
</Button>
|
||||
<Button onClick={props.onCancel}>{t("account_upgrade_dialog_button_cancel")}</Button>
|
||||
<Button onClick={handleSubmit} disabled={!submitAction}>
|
||||
{submitButtonLabel}
|
||||
</Button>
|
||||
|
@ -382,16 +328,10 @@ const TierCard = (props) => {
|
|||
{tier.name || t("account_basics_tier_free")}
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="h4"
|
||||
sx={{ fontWeight: 500, marginRight: "3px" }}
|
||||
>
|
||||
<Typography component="span" variant="h4" sx={{ fontWeight: 500, marginRight: "3px" }}>
|
||||
{formatPrice(monthlyPrice)}
|
||||
</Typography>
|
||||
{monthlyPrice > 0 && (
|
||||
<>/ {t("account_upgrade_dialog_tier_price_per_month")}</>
|
||||
)}
|
||||
{monthlyPrice > 0 && <>/ {t("account_upgrade_dialog_tier_price_per_month")}</>}
|
||||
</div>
|
||||
<List dense>
|
||||
{tier.limits.reservations > 0 && (
|
||||
|
@ -423,21 +363,10 @@ const TierCard = (props) => {
|
|||
</Feature>
|
||||
)}
|
||||
<Feature>
|
||||
{t(
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size",
|
||||
{ filesize: formatBytes(tier.limits.attachment_file_size, 0) }
|
||||
)}
|
||||
{t("account_upgrade_dialog_tier_features_attachment_file_size", { filesize: formatBytes(tier.limits.attachment_file_size, 0) })}
|
||||
</Feature>
|
||||
{tier.limits.reservations === 0 && (
|
||||
<NoFeature>
|
||||
{t("account_upgrade_dialog_tier_features_no_reservations")}
|
||||
</NoFeature>
|
||||
)}
|
||||
{tier.limits.calls === 0 && (
|
||||
<NoFeature>
|
||||
{t("account_upgrade_dialog_tier_features_no_calls")}
|
||||
</NoFeature>
|
||||
)}
|
||||
{tier.limits.reservations === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_reservations")}</NoFeature>}
|
||||
{tier.limits.calls === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_calls")}</NoFeature>}
|
||||
</List>
|
||||
{tier.prices && props.interval === SubscriptionInterval.MONTH && (
|
||||
<Typography variant="body2" color="gray">
|
||||
|
@ -476,10 +405,7 @@ const FeatureItem = (props) => {
|
|||
{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>}
|
||||
/>
|
||||
<ListItemText sx={{ mt: "2px", mb: "2px" }} primary={<Typography variant="body1">{props.children}</Typography>} />
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -32,41 +32,25 @@ export const useConnectionListeners = (account, subscriptions, users) => {
|
|||
};
|
||||
|
||||
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 {
|
||||
const data = JSON.parse(message.message);
|
||||
if (data.event === "sync") {
|
||||
console.log(`[ConnectionListener] Triggering account sync`);
|
||||
await accountApi.sync();
|
||||
} else {
|
||||
console.log(
|
||||
`[ConnectionListener] Unknown message type. Doing nothing.`
|
||||
);
|
||||
console.log(`[ConnectionListener] Unknown message type. Doing nothing.`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`[ConnectionListener] Error parsing sync topic message`,
|
||||
e
|
||||
);
|
||||
console.log(`[ConnectionListener] Error parsing sync topic message`, e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotification = async (subscriptionId, notification) => {
|
||||
const added = await subscriptionManager.addNotification(
|
||||
subscriptionId,
|
||||
notification
|
||||
);
|
||||
const added = await subscriptionManager.addNotification(subscriptionId, notification);
|
||||
if (added) {
|
||||
const defaultClickAction = (subscription) =>
|
||||
navigate(routes.forSubscription(subscription));
|
||||
await notifier.notify(
|
||||
subscriptionId,
|
||||
notification,
|
||||
defaultClickAction
|
||||
);
|
||||
const defaultClickAction = (subscription) => navigate(routes.forSubscription(subscription));
|
||||
await notifier.notify(subscriptionId, notification, defaultClickAction);
|
||||
}
|
||||
};
|
||||
connectionManager.registerStateListener(subscriptionManager.updateState);
|
||||
|
@ -109,20 +93,12 @@ export const useAutoSubscribe = (subscriptions, selected) => {
|
|||
return;
|
||||
}
|
||||
setHasRun(true);
|
||||
const eligible =
|
||||
params.topic && !selected && !disallowedTopic(params.topic);
|
||||
const eligible = params.topic && !selected && !disallowedTopic(params.topic);
|
||||
if (eligible) {
|
||||
const baseUrl = params.baseUrl
|
||||
? expandSecureUrl(params.baseUrl)
|
||||
: config.base_url;
|
||||
console.log(
|
||||
`[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`
|
||||
);
|
||||
const baseUrl = params.baseUrl ? expandSecureUrl(params.baseUrl) : config.base_url;
|
||||
console.log(`[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
|
||||
(async () => {
|
||||
const subscription = await subscriptionManager.add(
|
||||
baseUrl,
|
||||
params.topic
|
||||
);
|
||||
const subscription = await subscriptionManager.add(baseUrl, params.topic);
|
||||
if (session.exists()) {
|
||||
try {
|
||||
await accountApi.addSubscription(baseUrl, params.topic);
|
||||
|
|
Loading…
Reference in a new issue