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