Run eslint autofixes
This commit is contained in:
		
							parent
							
								
									f558b4dbe9
								
							
						
					
					
						commit
						8319f1cf26
					
				
					 32 changed files with 394 additions and 435 deletions
				
			
		|  | @ -1,3 +1,4 @@ | ||||||
|  | import i18n from "i18next"; | ||||||
| import { | import { | ||||||
|   accountBillingPortalUrl, |   accountBillingPortalUrl, | ||||||
|   accountBillingSubscriptionUrl, |   accountBillingSubscriptionUrl, | ||||||
|  | @ -17,7 +18,6 @@ import { | ||||||
| } from "./utils"; | } from "./utils"; | ||||||
| import session from "./Session"; | import session from "./Session"; | ||||||
| import subscriptionManager from "./SubscriptionManager"; | import subscriptionManager from "./SubscriptionManager"; | ||||||
| import i18n from "i18next"; |  | ||||||
| import prefs from "./Prefs"; | import prefs from "./Prefs"; | ||||||
| import routes from "../components/routes"; | import routes from "../components/routes"; | ||||||
| import { fetchOrThrow, UnauthorizedError } from "./errors"; | import { fetchOrThrow, UnauthorizedError } from "./errors"; | ||||||
|  | @ -66,13 +66,13 @@ class AccountApi { | ||||||
|   async create(username, password) { |   async create(username, password) { | ||||||
|     const url = accountUrl(config.base_url); |     const url = accountUrl(config.base_url); | ||||||
|     const body = JSON.stringify({ |     const body = JSON.stringify({ | ||||||
|       username: username, |       username, | ||||||
|       password: password, |       password, | ||||||
|     }); |     }); | ||||||
|     console.log(`[AccountApi] Creating user account ${url}`); |     console.log(`[AccountApi] Creating user account ${url}`); | ||||||
|     await fetchOrThrow(url, { |     await fetchOrThrow(url, { | ||||||
|       method: "POST", |       method: "POST", | ||||||
|       body: body, |       body, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -97,7 +97,7 @@ class AccountApi { | ||||||
|       method: "DELETE", |       method: "DELETE", | ||||||
|       headers: withBearerAuth({}, session.token()), |       headers: withBearerAuth({}, session.token()), | ||||||
|       body: JSON.stringify({ |       body: JSON.stringify({ | ||||||
|         password: password, |         password, | ||||||
|       }), |       }), | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | @ -118,7 +118,7 @@ class AccountApi { | ||||||
|   async createToken(label, expires) { |   async createToken(label, expires) { | ||||||
|     const url = accountTokenUrl(config.base_url); |     const url = accountTokenUrl(config.base_url); | ||||||
|     const body = { |     const body = { | ||||||
|       label: label, |       label, | ||||||
|       expires: expires > 0 ? Math.floor(Date.now() / 1000) + expires : 0, |       expires: expires > 0 ? Math.floor(Date.now() / 1000) + expires : 0, | ||||||
|     }; |     }; | ||||||
|     console.log(`[AccountApi] Creating user access token ${url}`); |     console.log(`[AccountApi] Creating user access token ${url}`); | ||||||
|  | @ -132,8 +132,8 @@ class AccountApi { | ||||||
|   async updateToken(token, label, expires) { |   async updateToken(token, label, expires) { | ||||||
|     const url = accountTokenUrl(config.base_url); |     const url = accountTokenUrl(config.base_url); | ||||||
|     const body = { |     const body = { | ||||||
|       token: token, |       token, | ||||||
|       label: label, |       label, | ||||||
|     }; |     }; | ||||||
|     if (expires > 0) { |     if (expires > 0) { | ||||||
|       body.expires = Math.floor(Date.now() / 1000) + expires; |       body.expires = Math.floor(Date.now() / 1000) + expires; | ||||||
|  | @ -171,7 +171,7 @@ class AccountApi { | ||||||
|     await fetchOrThrow(url, { |     await fetchOrThrow(url, { | ||||||
|       method: "PATCH", |       method: "PATCH", | ||||||
|       headers: withBearerAuth({}, session.token()), |       headers: withBearerAuth({}, session.token()), | ||||||
|       body: body, |       body, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -179,13 +179,13 @@ class AccountApi { | ||||||
|     const url = accountSubscriptionUrl(config.base_url); |     const url = accountSubscriptionUrl(config.base_url); | ||||||
|     const body = JSON.stringify({ |     const body = JSON.stringify({ | ||||||
|       base_url: baseUrl, |       base_url: baseUrl, | ||||||
|       topic: topic, |       topic, | ||||||
|     }); |     }); | ||||||
|     console.log(`[AccountApi] Adding user subscription ${url}: ${body}`); |     console.log(`[AccountApi] Adding user subscription ${url}: ${body}`); | ||||||
|     const response = await fetchOrThrow(url, { |     const response = await fetchOrThrow(url, { | ||||||
|       method: "POST", |       method: "POST", | ||||||
|       headers: withBearerAuth({}, session.token()), |       headers: withBearerAuth({}, session.token()), | ||||||
|       body: body, |       body, | ||||||
|     }); |     }); | ||||||
|     const subscription = await response.json(); // May throw SyntaxError
 |     const subscription = await response.json(); // May throw SyntaxError
 | ||||||
|     console.log(`[AccountApi] Subscription`, subscription); |     console.log(`[AccountApi] Subscription`, subscription); | ||||||
|  | @ -196,14 +196,14 @@ class AccountApi { | ||||||
|     const url = accountSubscriptionUrl(config.base_url); |     const url = accountSubscriptionUrl(config.base_url); | ||||||
|     const body = JSON.stringify({ |     const body = JSON.stringify({ | ||||||
|       base_url: baseUrl, |       base_url: baseUrl, | ||||||
|       topic: topic, |       topic, | ||||||
|       ...payload, |       ...payload, | ||||||
|     }); |     }); | ||||||
|     console.log(`[AccountApi] Updating user subscription ${url}: ${body}`); |     console.log(`[AccountApi] Updating user subscription ${url}: ${body}`); | ||||||
|     const response = await fetchOrThrow(url, { |     const response = await fetchOrThrow(url, { | ||||||
|       method: "PATCH", |       method: "PATCH", | ||||||
|       headers: withBearerAuth({}, session.token()), |       headers: withBearerAuth({}, session.token()), | ||||||
|       body: body, |       body, | ||||||
|     }); |     }); | ||||||
|     const subscription = await response.json(); // May throw SyntaxError
 |     const subscription = await response.json(); // May throw SyntaxError
 | ||||||
|     console.log(`[AccountApi] Subscription`, subscription); |     console.log(`[AccountApi] Subscription`, subscription); | ||||||
|  | @ -230,8 +230,8 @@ class AccountApi { | ||||||
|       method: "POST", |       method: "POST", | ||||||
|       headers: withBearerAuth({}, session.token()), |       headers: withBearerAuth({}, session.token()), | ||||||
|       body: JSON.stringify({ |       body: JSON.stringify({ | ||||||
|         topic: topic, |         topic, | ||||||
|         everyone: everyone, |         everyone, | ||||||
|       }), |       }), | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | @ -272,11 +272,11 @@ class AccountApi { | ||||||
|   async upsertBillingSubscription(method, tier, interval) { |   async upsertBillingSubscription(method, tier, interval) { | ||||||
|     const url = accountBillingSubscriptionUrl(config.base_url); |     const url = accountBillingSubscriptionUrl(config.base_url); | ||||||
|     const response = await fetchOrThrow(url, { |     const response = await fetchOrThrow(url, { | ||||||
|       method: method, |       method, | ||||||
|       headers: withBearerAuth({}, session.token()), |       headers: withBearerAuth({}, session.token()), | ||||||
|       body: JSON.stringify({ |       body: JSON.stringify({ | ||||||
|         tier: tier, |         tier, | ||||||
|         interval: interval, |         interval, | ||||||
|       }), |       }), | ||||||
|     }); |     }); | ||||||
|     return await response.json(); // May throw SyntaxError
 |     return await response.json(); // May throw SyntaxError
 | ||||||
|  | @ -309,7 +309,7 @@ class AccountApi { | ||||||
|       headers: withBearerAuth({}, session.token()), |       headers: withBearerAuth({}, session.token()), | ||||||
|       body: JSON.stringify({ |       body: JSON.stringify({ | ||||||
|         number: phoneNumber, |         number: phoneNumber, | ||||||
|         channel: channel, |         channel, | ||||||
|       }), |       }), | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | @ -322,7 +322,7 @@ class AccountApi { | ||||||
|       headers: withBearerAuth({}, session.token()), |       headers: withBearerAuth({}, session.token()), | ||||||
|       body: JSON.stringify({ |       body: JSON.stringify({ | ||||||
|         number: phoneNumber, |         number: phoneNumber, | ||||||
|         code: code, |         code, | ||||||
|       }), |       }), | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ class Api { | ||||||
|     const messages = []; |     const messages = []; | ||||||
|     const headers = maybeWithAuth({}, user); |     const headers = maybeWithAuth({}, user); | ||||||
|     console.log(`[Api] Polling ${url}`); |     console.log(`[Api] Polling ${url}`); | ||||||
|     for await (let line of fetchLinesIterator(url, headers)) { |     for await (const line of fetchLinesIterator(url, headers)) { | ||||||
|       const message = JSON.parse(line); |       const message = JSON.parse(line); | ||||||
|       if (message.id) { |       if (message.id) { | ||||||
|         console.log(`[Api, ${shortUrl}] Received message ${line}`); |         console.log(`[Api, ${shortUrl}] Received message ${line}`); | ||||||
|  | @ -33,8 +33,8 @@ class Api { | ||||||
|     console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`); |     console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`); | ||||||
|     const headers = {}; |     const headers = {}; | ||||||
|     const body = { |     const body = { | ||||||
|       topic: topic, |       topic, | ||||||
|       message: message, |       message, | ||||||
|       ...options, |       ...options, | ||||||
|     }; |     }; | ||||||
|     await fetchOrThrow(baseUrl, { |     await fetchOrThrow(baseUrl, { | ||||||
|  | @ -60,7 +60,7 @@ class Api { | ||||||
|   publishXHR(url, body, headers, onProgress) { |   publishXHR(url, body, headers, onProgress) { | ||||||
|     console.log(`[Api] Publishing message to ${url}`); |     console.log(`[Api] Publishing message to ${url}`); | ||||||
|     const xhr = new XMLHttpRequest(); |     const xhr = new XMLHttpRequest(); | ||||||
|     const send = new Promise(function (resolve, reject) { |     const send = new Promise((resolve, reject) => { | ||||||
|       xhr.open("PUT", url); |       xhr.open("PUT", url); | ||||||
|       if (body.type) { |       if (body.type) { | ||||||
|         xhr.overrideMimeType(body.type); |         xhr.overrideMimeType(body.type); | ||||||
|  | @ -106,7 +106,8 @@ class Api { | ||||||
|     }); |     }); | ||||||
|     if (response.status >= 200 && response.status <= 299) { |     if (response.status >= 200 && response.status <= 299) { | ||||||
|       return true; |       return true; | ||||||
|     } else if (response.status === 401 || response.status === 403) { |     } | ||||||
|  |     if (response.status === 401 || response.status === 403) { | ||||||
|       // See server/server.go
 |       // See server/server.go
 | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -77,7 +77,7 @@ class Connection { | ||||||
|   close() { |   close() { | ||||||
|     console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`); |     console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`); | ||||||
|     const socket = this.ws; |     const socket = this.ws; | ||||||
|     const retryTimeout = this.retryTimeout; |     const { retryTimeout } = this; | ||||||
|     if (socket !== null) { |     if (socket !== null) { | ||||||
|       socket.close(); |       socket.close(); | ||||||
|     } |     } | ||||||
|  | @ -110,6 +110,7 @@ class Connection { | ||||||
| 
 | 
 | ||||||
| export class ConnectionState { | export class ConnectionState { | ||||||
|   static Connected = "connected"; |   static Connected = "connected"; | ||||||
|  | 
 | ||||||
|   static Connecting = "connecting"; |   static Connecting = "connecting"; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -55,12 +55,12 @@ class ConnectionManager { | ||||||
|     // Create and add new connections
 |     // Create and add new connections
 | ||||||
|     subscriptionsWithUsersAndConnectionId.forEach((subscription) => { |     subscriptionsWithUsersAndConnectionId.forEach((subscription) => { | ||||||
|       const subscriptionId = subscription.id; |       const subscriptionId = subscription.id; | ||||||
|       const connectionId = subscription.connectionId; |       const { connectionId } = subscription; | ||||||
|       const added = !this.connections.get(connectionId); |       const added = !this.connections.get(connectionId); | ||||||
|       if (added) { |       if (added) { | ||||||
|         const baseUrl = subscription.baseUrl; |         const { baseUrl } = subscription; | ||||||
|         const topic = subscription.topic; |         const { topic } = subscription; | ||||||
|         const user = subscription.user; |         const { user } = subscription; | ||||||
|         const since = subscription.last; |         const since = subscription.last; | ||||||
|         const connection = new Connection( |         const connection = new Connection( | ||||||
|           connectionId, |           connectionId, | ||||||
|  | @ -112,9 +112,8 @@ class ConnectionManager { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const makeConnectionId = async (subscription, user) => { | const makeConnectionId = async (subscription, user) => | ||||||
|   return user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`); |   user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const connectionManager = new ConnectionManager(); | const connectionManager = new ConnectionManager(); | ||||||
| export default connectionManager; | export default connectionManager; | ||||||
|  |  | ||||||
|  | @ -25,8 +25,8 @@ class SubscriptionManager { | ||||||
|     } |     } | ||||||
|     const subscription = { |     const subscription = { | ||||||
|       id: topicUrl(baseUrl, topic), |       id: topicUrl(baseUrl, topic), | ||||||
|       baseUrl: baseUrl, |       baseUrl, | ||||||
|       topic: topic, |       topic, | ||||||
|       mutedUntil: 0, |       mutedUntil: 0, | ||||||
|       last: null, |       last: null, | ||||||
|       internal: internal || false, |       internal: internal || false, | ||||||
|  | @ -39,14 +39,14 @@ class SubscriptionManager { | ||||||
|     console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions); |     console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions); | ||||||
| 
 | 
 | ||||||
|     // Add remote subscriptions
 |     // Add remote subscriptions
 | ||||||
|     let remoteIds = []; // = topicUrl(baseUrl, topic)
 |     const remoteIds = []; // = topicUrl(baseUrl, topic)
 | ||||||
|     for (let i = 0; i < remoteSubscriptions.length; i++) { |     for (let i = 0; i < remoteSubscriptions.length; i++) { | ||||||
|       const remote = remoteSubscriptions[i]; |       const remote = remoteSubscriptions[i]; | ||||||
|       const local = await this.add(remote.base_url, remote.topic, false); |       const local = await this.add(remote.base_url, remote.topic, false); | ||||||
|       const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null; |       const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null; | ||||||
|       await this.update(local.id, { |       await this.update(local.id, { | ||||||
|         displayName: remote.display_name, // May be undefined
 |         displayName: remote.display_name, // May be undefined
 | ||||||
|         reservation: reservation, // May be null!
 |         reservation, // May be null!
 | ||||||
|       }); |       }); | ||||||
|       remoteIds.push(local.id); |       remoteIds.push(local.id); | ||||||
|     } |     } | ||||||
|  | @ -63,12 +63,12 @@ class SubscriptionManager { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async updateState(subscriptionId, state) { |   async updateState(subscriptionId, state) { | ||||||
|     db.subscriptions.update(subscriptionId, { state: state }); |     db.subscriptions.update(subscriptionId, { state }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async remove(subscriptionId) { |   async remove(subscriptionId) { | ||||||
|     await db.subscriptions.delete(subscriptionId); |     await db.subscriptions.delete(subscriptionId); | ||||||
|     await db.notifications.where({ subscriptionId: subscriptionId }).delete(); |     await db.notifications.where({ subscriptionId }).delete(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async first() { |   async first() { | ||||||
|  | @ -140,7 +140,7 @@ class SubscriptionManager { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async deleteNotifications(subscriptionId) { |   async deleteNotifications(subscriptionId) { | ||||||
|     await db.notifications.where({ subscriptionId: subscriptionId }).delete(); |     await db.notifications.where({ subscriptionId }).delete(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async markNotificationRead(notificationId) { |   async markNotificationRead(notificationId) { | ||||||
|  | @ -148,24 +148,24 @@ class SubscriptionManager { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async markNotificationsRead(subscriptionId) { |   async markNotificationsRead(subscriptionId) { | ||||||
|     await db.notifications.where({ subscriptionId: subscriptionId, new: 1 }).modify({ new: 0 }); |     await db.notifications.where({ subscriptionId, new: 1 }).modify({ new: 0 }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async setMutedUntil(subscriptionId, mutedUntil) { |   async setMutedUntil(subscriptionId, mutedUntil) { | ||||||
|     await db.subscriptions.update(subscriptionId, { |     await db.subscriptions.update(subscriptionId, { | ||||||
|       mutedUntil: mutedUntil, |       mutedUntil, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async setDisplayName(subscriptionId, displayName) { |   async setDisplayName(subscriptionId, displayName) { | ||||||
|     await db.subscriptions.update(subscriptionId, { |     await db.subscriptions.update(subscriptionId, { | ||||||
|       displayName: displayName, |       displayName, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async setReservation(subscriptionId, reservation) { |   async setReservation(subscriptionId, reservation) { | ||||||
|     await db.subscriptions.update(subscriptionId, { |     await db.subscriptions.update(subscriptionId, { | ||||||
|       reservation: reservation, |       reservation, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| const config = window.config; | const { config } = window; | ||||||
| 
 | 
 | ||||||
| // The backend returns an empty base_url for the config struct,
 | // The backend returns an empty base_url for the config struct,
 | ||||||
| // so the frontend (hey, that's us!) can use the current location.
 | // so the frontend (hey, that's us!) can use the current location.
 | ||||||
|  |  | ||||||
|  | @ -48,6 +48,7 @@ export class UnauthorizedError extends Error { | ||||||
| 
 | 
 | ||||||
| export class UserExistsError extends Error { | export class UserExistsError extends Error { | ||||||
|   static CODE = 40901; // errHTTPConflictUserExists
 |   static CODE = 40901; // errHTTPConflictUserExists
 | ||||||
|  | 
 | ||||||
|   constructor() { |   constructor() { | ||||||
|     super("Username already exists"); |     super("Username already exists"); | ||||||
|   } |   } | ||||||
|  | @ -55,6 +56,7 @@ export class UserExistsError extends Error { | ||||||
| 
 | 
 | ||||||
| export class TopicReservedError extends Error { | export class TopicReservedError extends Error { | ||||||
|   static CODE = 40902; // errHTTPConflictTopicReserved
 |   static CODE = 40902; // errHTTPConflictTopicReserved
 | ||||||
|  | 
 | ||||||
|   constructor() { |   constructor() { | ||||||
|     super("Topic already reserved"); |     super("Topic already reserved"); | ||||||
|   } |   } | ||||||
|  | @ -62,6 +64,7 @@ export class TopicReservedError extends Error { | ||||||
| 
 | 
 | ||||||
| export class AccountCreateLimitReachedError extends Error { | export class AccountCreateLimitReachedError extends Error { | ||||||
|   static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation
 |   static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation
 | ||||||
|  | 
 | ||||||
|   constructor() { |   constructor() { | ||||||
|     super("Account creation limit reached"); |     super("Account creation limit reached"); | ||||||
|   } |   } | ||||||
|  | @ -69,6 +72,7 @@ export class AccountCreateLimitReachedError extends Error { | ||||||
| 
 | 
 | ||||||
| export class IncorrectPasswordError extends Error { | export class IncorrectPasswordError extends Error { | ||||||
|   static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation
 |   static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation
 | ||||||
|  | 
 | ||||||
|   constructor() { |   constructor() { | ||||||
|     super("Password incorrect"); |     super("Password incorrect"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | import { Base64 } from "js-base64"; | ||||||
| import { rawEmojis } from "./emojis"; | import { rawEmojis } from "./emojis"; | ||||||
| import beep from "../sounds/beep.mp3"; | import beep from "../sounds/beep.mp3"; | ||||||
| import juntos from "../sounds/juntos.mp3"; | import juntos from "../sounds/juntos.mp3"; | ||||||
|  | @ -7,7 +8,6 @@ import dadum from "../sounds/dadum.mp3"; | ||||||
| import pop from "../sounds/pop.mp3"; | import pop from "../sounds/pop.mp3"; | ||||||
| import popSwoosh from "../sounds/pop-swoosh.mp3"; | import popSwoosh from "../sounds/pop-swoosh.mp3"; | ||||||
| import config from "./config"; | import config from "./config"; | ||||||
| import { Base64 } from "js-base64"; |  | ||||||
| 
 | 
 | ||||||
| export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; | export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; | ||||||
| export const topicUrlWs = (baseUrl, topic) => | export const topicUrlWs = (baseUrl, topic) => | ||||||
|  | @ -33,9 +33,7 @@ export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); | ||||||
| export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; | export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; | ||||||
| export const expandSecureUrl = (url) => `https://${url}`; | export const expandSecureUrl = (url) => `https://${url}`; | ||||||
| 
 | 
 | ||||||
| export const validUrl = (url) => { | export const validUrl = (url) => url.match(/^https?:\/\/.+/); | ||||||
|   return url.match(/^https?:\/\/.+/); |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const validTopic = (topic) => { | export const validTopic = (topic) => { | ||||||
|   if (disallowedTopic(topic)) { |   if (disallowedTopic(topic)) { | ||||||
|  | @ -44,14 +42,13 @@ export const validTopic = (topic) => { | ||||||
|   return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app!
 |   return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app!
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const disallowedTopic = (topic) => { | export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic); | ||||||
|   return config.disallowed_topics.includes(topic); |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const topicDisplayName = (subscription) => { | export const topicDisplayName = (subscription) => { | ||||||
|   if (subscription.displayName) { |   if (subscription.displayName) { | ||||||
|     return subscription.displayName; |     return subscription.displayName; | ||||||
|   } else if (subscription.baseUrl === config.base_url) { |   } | ||||||
|  |   if (subscription.baseUrl === config.base_url) { | ||||||
|     return subscription.topic; |     return subscription.topic; | ||||||
|   } |   } | ||||||
|   return topicShortUrl(subscription.baseUrl, subscription.topic); |   return topicShortUrl(subscription.baseUrl, subscription.topic); | ||||||
|  | @ -67,7 +64,7 @@ rawEmojis.forEach((emoji) => { | ||||||
| 
 | 
 | ||||||
| const toEmojis = (tags) => { | const toEmojis = (tags) => { | ||||||
|   if (!tags) return []; |   if (!tags) return []; | ||||||
|   else return tags.filter((tag) => tag in emojis).map((tag) => emojis[tag]); |   return tags.filter((tag) => tag in emojis).map((tag) => emojis[tag]); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const formatTitleWithDefault = (m, fallback) => { | export const formatTitleWithDefault = (m, fallback) => { | ||||||
|  | @ -81,33 +78,31 @@ export const formatTitle = (m) => { | ||||||
|   const emojiList = toEmojis(m.tags); |   const emojiList = toEmojis(m.tags); | ||||||
|   if (emojiList.length > 0) { |   if (emojiList.length > 0) { | ||||||
|     return `${emojiList.join(" ")} ${m.title}`; |     return `${emojiList.join(" ")} ${m.title}`; | ||||||
|   } else { |  | ||||||
|     return m.title; |  | ||||||
|   } |   } | ||||||
|  |   return m.title; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const formatMessage = (m) => { | export const formatMessage = (m) => { | ||||||
|   if (m.title) { |   if (m.title) { | ||||||
|     return m.message; |     return m.message; | ||||||
|   } else { |   } | ||||||
|   const emojiList = toEmojis(m.tags); |   const emojiList = toEmojis(m.tags); | ||||||
|   if (emojiList.length > 0) { |   if (emojiList.length > 0) { | ||||||
|     return `${emojiList.join(" ")} ${m.message}`; |     return `${emojiList.join(" ")} ${m.message}`; | ||||||
|     } else { |   } | ||||||
|   return m.message; |   return m.message; | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const unmatchedTags = (tags) => { | export const unmatchedTags = (tags) => { | ||||||
|   if (!tags) return []; |   if (!tags) return []; | ||||||
|   else return tags.filter((tag) => !(tag in emojis)); |   return tags.filter((tag) => !(tag in emojis)); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const maybeWithAuth = (headers, user) => { | export const maybeWithAuth = (headers, user) => { | ||||||
|   if (user && user.password) { |   if (user && user.password) { | ||||||
|     return withBasicAuth(headers, user.username, user.password); |     return withBasicAuth(headers, user.username, user.password); | ||||||
|   } else if (user && user.token) { |   } | ||||||
|  |   if (user && user.token) { | ||||||
|     return withBearerAuth(headers, user.token); |     return withBearerAuth(headers, user.token); | ||||||
|   } |   } | ||||||
|   return headers; |   return headers; | ||||||
|  | @ -121,30 +116,22 @@ export const maybeWithBearerAuth = (headers, token) => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const withBasicAuth = (headers, username, password) => { | export const withBasicAuth = (headers, username, password) => { | ||||||
|   headers["Authorization"] = basicAuth(username, password); |   headers.Authorization = basicAuth(username, password); | ||||||
|   return headers; |   return headers; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const basicAuth = (username, password) => { | export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`; | ||||||
|   return `Basic ${encodeBase64(`${username}:${password}`)}`; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const withBearerAuth = (headers, token) => { | export const withBearerAuth = (headers, token) => { | ||||||
|   headers["Authorization"] = bearerAuth(token); |   headers.Authorization = bearerAuth(token); | ||||||
|   return headers; |   return headers; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const bearerAuth = (token) => { | export const bearerAuth = (token) => `Bearer ${token}`; | ||||||
|   return `Bearer ${token}`; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const encodeBase64 = (s) => { | export const encodeBase64 = (s) => Base64.encode(s); | ||||||
|   return Base64.encode(s); |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const encodeBase64Url = (s) => { | export const encodeBase64Url = (s) => Base64.encodeURI(s); | ||||||
|   return Base64.encodeURI(s); |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const maybeAppendActionErrors = (message, notification) => { | export const maybeAppendActionErrors = (message, notification) => { | ||||||
|   const actionErrors = (notification.actions ?? []) |   const actionErrors = (notification.actions ?? []) | ||||||
|  | @ -153,13 +140,13 @@ export const maybeAppendActionErrors = (message, notification) => { | ||||||
|     .join("\n"); |     .join("\n"); | ||||||
|   if (actionErrors.length === 0) { |   if (actionErrors.length === 0) { | ||||||
|     return message; |     return message; | ||||||
|   } else { |  | ||||||
|     return `${message}\n\n${actionErrors}`; |  | ||||||
|   } |   } | ||||||
|  |   return `${message}\n\n${actionErrors}`; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const shuffle = (arr) => { | export const shuffle = (arr) => { | ||||||
|   let j, x; |   let j; | ||||||
|  |   let x; | ||||||
|   for (let index = arr.length - 1; index > 0; index--) { |   for (let index = arr.length - 1; index > 0; index--) { | ||||||
|     j = Math.floor(Math.random() * (index + 1)); |     j = Math.floor(Math.random() * (index + 1)); | ||||||
|     x = arr[index]; |     x = arr[index]; | ||||||
|  | @ -169,12 +156,11 @@ export const shuffle = (arr) => { | ||||||
|   return arr; |   return arr; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const splitNoEmpty = (s, delimiter) => { | export const splitNoEmpty = (s, delimiter) => | ||||||
|   return s |   s | ||||||
|     .split(delimiter) |     .split(delimiter) | ||||||
|     .map((x) => x.trim()) |     .map((x) => x.trim()) | ||||||
|     .filter((x) => x !== ""); |     .filter((x) => x !== ""); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| /** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */ | /** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */ | ||||||
| export const hashCode = async (s) => { | export const hashCode = async (s) => { | ||||||
|  | @ -182,21 +168,18 @@ export const hashCode = async (s) => { | ||||||
|   for (let i = 0; i < s.length; i++) { |   for (let i = 0; i < s.length; i++) { | ||||||
|     const char = s.charCodeAt(i); |     const char = s.charCodeAt(i); | ||||||
|     hash = (hash << 5) - hash + char; |     hash = (hash << 5) - hash + char; | ||||||
|     hash = hash & hash; // Convert to 32bit integer
 |     hash &= hash; // Convert to 32bit integer
 | ||||||
|   } |   } | ||||||
|   return hash; |   return hash; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const formatShortDateTime = (timestamp) => { | export const formatShortDateTime = (timestamp) => | ||||||
|   return new Intl.DateTimeFormat("default", { |   new Intl.DateTimeFormat("default", { | ||||||
|     dateStyle: "short", |     dateStyle: "short", | ||||||
|     timeStyle: "short", |     timeStyle: "short", | ||||||
|   }).format(new Date(timestamp * 1000)); |   }).format(new Date(timestamp * 1000)); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const formatShortDate = (timestamp) => { | export const formatShortDate = (timestamp) => new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(new Date(timestamp * 1000)); | ||||||
|   return new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(new Date(timestamp * 1000)); |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const formatBytes = (bytes, decimals = 2) => { | export const formatBytes = (bytes, decimals = 2) => { | ||||||
|   if (bytes === 0) return "0 bytes"; |   if (bytes === 0) return "0 bytes"; | ||||||
|  | @ -204,13 +187,14 @@ export const formatBytes = (bytes, decimals = 2) => { | ||||||
|   const dm = decimals < 0 ? 0 : decimals; |   const dm = decimals < 0 ? 0 : decimals; | ||||||
|   const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; |   const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; | ||||||
|   const i = Math.floor(Math.log(bytes) / Math.log(k)); |   const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||||
|   return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; |   return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const formatNumber = (n) => { | export const formatNumber = (n) => { | ||||||
|   if (n === 0) { |   if (n === 0) { | ||||||
|     return n; |     return n; | ||||||
|   } else if (n % 1000 === 0) { |   } | ||||||
|  |   if (n % 1000 === 0) { | ||||||
|     return `${n / 1000}k`; |     return `${n / 1000}k`; | ||||||
|   } |   } | ||||||
|   return n.toLocaleString(); |   return n.toLocaleString(); | ||||||
|  | @ -267,7 +251,7 @@ export const playSound = async (id) => { | ||||||
| export async function* fetchLinesIterator(fileURL, headers) { | export async function* fetchLinesIterator(fileURL, headers) { | ||||||
|   const utf8Decoder = new TextDecoder("utf-8"); |   const utf8Decoder = new TextDecoder("utf-8"); | ||||||
|   const response = await fetch(fileURL, { |   const response = await fetch(fileURL, { | ||||||
|     headers: headers, |     headers, | ||||||
|   }); |   }); | ||||||
|   const reader = response.body.getReader(); |   const reader = response.body.getReader(); | ||||||
|   let { value: chunk, done: readerDone } = await reader.read(); |   let { value: chunk, done: readerDone } = await reader.read(); | ||||||
|  | @ -277,12 +261,12 @@ export async function* fetchLinesIterator(fileURL, headers) { | ||||||
|   let startIndex = 0; |   let startIndex = 0; | ||||||
| 
 | 
 | ||||||
|   for (;;) { |   for (;;) { | ||||||
|     let result = re.exec(chunk); |     const result = re.exec(chunk); | ||||||
|     if (!result) { |     if (!result) { | ||||||
|       if (readerDone) { |       if (readerDone) { | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       let remainder = chunk.substr(startIndex); |       const remainder = chunk.substr(startIndex); | ||||||
|       ({ value: chunk, done: readerDone } = await reader.read()); |       ({ value: chunk, done: readerDone } = await reader.read()); | ||||||
|       chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ""); |       chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ""); | ||||||
|       startIndex = re.lastIndex = 0; |       startIndex = re.lastIndex = 0; | ||||||
|  |  | ||||||
|  | @ -29,34 +29,34 @@ import Container from "@mui/material/Container"; | ||||||
| import Card from "@mui/material/Card"; | import Card from "@mui/material/Card"; | ||||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||||
| import { Trans, useTranslation } from "react-i18next"; | import { Trans, useTranslation } from "react-i18next"; | ||||||
| import session from "../app/Session"; |  | ||||||
| import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; | import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; | ||||||
| import theme from "./theme"; |  | ||||||
| import Dialog from "@mui/material/Dialog"; | import Dialog from "@mui/material/Dialog"; | ||||||
| import DialogTitle from "@mui/material/DialogTitle"; | import DialogTitle from "@mui/material/DialogTitle"; | ||||||
| import DialogContent from "@mui/material/DialogContent"; | import DialogContent from "@mui/material/DialogContent"; | ||||||
| import TextField from "@mui/material/TextField"; | import TextField from "@mui/material/TextField"; | ||||||
| import routes from "./routes"; |  | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
| import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; |  | ||||||
| import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi"; |  | ||||||
| import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; | import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; | ||||||
| import { Pref, PrefGroup } from "./Pref"; |  | ||||||
| import db from "../app/db"; |  | ||||||
| import i18n from "i18next"; | import i18n from "i18next"; | ||||||
| import humanizeDuration from "humanize-duration"; | import humanizeDuration from "humanize-duration"; | ||||||
| import UpgradeDialog from "./UpgradeDialog"; |  | ||||||
| import CelebrationIcon from "@mui/icons-material/Celebration"; | import CelebrationIcon from "@mui/icons-material/Celebration"; | ||||||
| import { AccountContext } from "./App"; |  | ||||||
| import DialogFooter from "./DialogFooter"; |  | ||||||
| import { Paragraph } from "./styles"; |  | ||||||
| import CloseIcon from "@mui/icons-material/Close"; | import CloseIcon from "@mui/icons-material/Close"; | ||||||
| import { ContentCopy, Public } from "@mui/icons-material"; | import { ContentCopy, Public } from "@mui/icons-material"; | ||||||
| import MenuItem from "@mui/material/MenuItem"; | import MenuItem from "@mui/material/MenuItem"; | ||||||
| import DialogContentText from "@mui/material/DialogContentText"; | import DialogContentText from "@mui/material/DialogContentText"; | ||||||
|  | import AddIcon from "@mui/icons-material/Add"; | ||||||
|  | import routes from "./routes"; | ||||||
|  | import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; | ||||||
|  | import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi"; | ||||||
|  | import { Pref, PrefGroup } from "./Pref"; | ||||||
|  | import db from "../app/db"; | ||||||
|  | import UpgradeDialog from "./UpgradeDialog"; | ||||||
|  | import { AccountContext } from "./App"; | ||||||
|  | import DialogFooter from "./DialogFooter"; | ||||||
|  | import { Paragraph } from "./styles"; | ||||||
| import { IncorrectPasswordError, UnauthorizedError } from "../app/errors"; | import { IncorrectPasswordError, UnauthorizedError } from "../app/errors"; | ||||||
| import { ProChip } from "./SubscriptionPopup"; | import { ProChip } from "./SubscriptionPopup"; | ||||||
| import AddIcon from "@mui/icons-material/Add"; | import theme from "./theme"; | ||||||
|  | import session from "../app/Session"; | ||||||
| 
 | 
 | ||||||
| const Account = () => { | const Account = () => { | ||||||
|   if (!session.exists()) { |   if (!session.exists()) { | ||||||
|  | @ -561,9 +561,7 @@ const Stats = () => { | ||||||
|     return <></>; |     return <></>; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const normalize = (value, max) => { |   const normalize = (value, max) => Math.min((value / max) * 100, 100); | ||||||
|     return Math.min((value / max) * 100, 100); |  | ||||||
|   }; |  | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Card sx={{ p: 3 }} aria-label={t("account_usage_title")}> |     <Card sx={{ p: 3 }} aria-label={t("account_usage_title")}> | ||||||
|  | @ -746,8 +744,7 @@ const Stats = () => { | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const InfoIcon = () => { | const InfoIcon = () => ( | ||||||
|   return ( |  | ||||||
|   <InfoOutlinedIcon |   <InfoOutlinedIcon | ||||||
|     sx={{ |     sx={{ | ||||||
|       verticalAlign: "middle", |       verticalAlign: "middle", | ||||||
|  | @ -756,8 +753,7 @@ const InfoIcon = () => { | ||||||
|       color: "gray", |       color: "gray", | ||||||
|     }} |     }} | ||||||
|   /> |   /> | ||||||
|   ); | ); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const Tokens = () => { | const Tokens = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  | @ -814,7 +810,8 @@ const TokensTable = (props) => { | ||||||
|   const tokens = (props.tokens || []).sort((a, b) => { |   const tokens = (props.tokens || []).sort((a, b) => { | ||||||
|     if (a.token === session.token()) { |     if (a.token === session.token()) { | ||||||
|       return -1; |       return -1; | ||||||
|     } else if (b.token === session.token()) { |     } | ||||||
|  |     if (b.token === session.token()) { | ||||||
|       return 1; |       return 1; | ||||||
|     } |     } | ||||||
|     return a.token.localeCompare(b.token); |     return a.token.localeCompare(b.token); | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import AppBar from "@mui/material/AppBar"; | import AppBar from "@mui/material/AppBar"; | ||||||
| import Navigation from "./Navigation"; |  | ||||||
| import Toolbar from "@mui/material/Toolbar"; | import Toolbar from "@mui/material/Toolbar"; | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
| import MenuIcon from "@mui/icons-material/Menu"; | import MenuIcon from "@mui/icons-material/Menu"; | ||||||
|  | @ -7,23 +6,24 @@ import Typography from "@mui/material/Typography"; | ||||||
| import * as React from "react"; | import * as React from "react"; | ||||||
| import { useState } from "react"; | import { useState } from "react"; | ||||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||||
| import { topicDisplayName } from "../app/utils"; |  | ||||||
| import db from "../app/db"; |  | ||||||
| import { useLocation, useNavigate } from "react-router-dom"; | import { useLocation, useNavigate } from "react-router-dom"; | ||||||
| import MenuItem from "@mui/material/MenuItem"; | import MenuItem from "@mui/material/MenuItem"; | ||||||
| import MoreVertIcon from "@mui/icons-material/MoreVert"; | import MoreVertIcon from "@mui/icons-material/MoreVert"; | ||||||
| import NotificationsIcon from "@mui/icons-material/Notifications"; | import NotificationsIcon from "@mui/icons-material/Notifications"; | ||||||
| import NotificationsOffIcon from "@mui/icons-material/NotificationsOff"; | import NotificationsOffIcon from "@mui/icons-material/NotificationsOff"; | ||||||
| import routes from "./routes"; |  | ||||||
| import subscriptionManager from "../app/SubscriptionManager"; |  | ||||||
| import logo from "../img/ntfy.svg"; |  | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import session from "../app/Session"; |  | ||||||
| import AccountCircleIcon from "@mui/icons-material/AccountCircle"; | import AccountCircleIcon from "@mui/icons-material/AccountCircle"; | ||||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||||
| import Divider from "@mui/material/Divider"; | import Divider from "@mui/material/Divider"; | ||||||
| import { Logout, Person, Settings } from "@mui/icons-material"; | import { Logout, Person, Settings } from "@mui/icons-material"; | ||||||
| import ListItemIcon from "@mui/material/ListItemIcon"; | import ListItemIcon from "@mui/material/ListItemIcon"; | ||||||
|  | import session from "../app/Session"; | ||||||
|  | import logo from "../img/ntfy.svg"; | ||||||
|  | import subscriptionManager from "../app/SubscriptionManager"; | ||||||
|  | import routes from "./routes"; | ||||||
|  | import db from "../app/db"; | ||||||
|  | import { topicDisplayName } from "../app/utils"; | ||||||
|  | import Navigation from "./Navigation"; | ||||||
| import accountApi from "../app/AccountApi"; | import accountApi from "../app/AccountApi"; | ||||||
| import PopupMenu from "./PopupMenu"; | import PopupMenu from "./PopupMenu"; | ||||||
| import { SubscriptionPopup } from "./SubscriptionPopup"; | import { SubscriptionPopup } from "./SubscriptionPopup"; | ||||||
|  | @ -86,7 +86,7 @@ const ActionBar = (props) => { | ||||||
| const SettingsIcons = (props) => { | const SettingsIcons = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const [anchorEl, setAnchorEl] = useState(null); |   const [anchorEl, setAnchorEl] = useState(null); | ||||||
|   const subscription = props.subscription; |   const { subscription } = props; | ||||||
| 
 | 
 | ||||||
|   const handleToggleMute = async () => { |   const handleToggleMute = async () => { | ||||||
|     const mutedUntil = subscription.mutedUntil ? 0 : 1; // Make this a timestamp in the future |     const mutedUntil = subscription.mutedUntil ? 0 : 1; // Make this a timestamp in the future | ||||||
|  |  | ||||||
|  | @ -4,16 +4,17 @@ import Box from "@mui/material/Box"; | ||||||
| import { ThemeProvider } from "@mui/material/styles"; | import { ThemeProvider } from "@mui/material/styles"; | ||||||
| import CssBaseline from "@mui/material/CssBaseline"; | import CssBaseline from "@mui/material/CssBaseline"; | ||||||
| import Toolbar from "@mui/material/Toolbar"; | import Toolbar from "@mui/material/Toolbar"; | ||||||
|  | import { useLiveQuery } from "dexie-react-hooks"; | ||||||
|  | import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; | ||||||
|  | import { Backdrop, CircularProgress } from "@mui/material"; | ||||||
| import { AllSubscriptions, SingleSubscription } from "./Notifications"; | import { AllSubscriptions, SingleSubscription } from "./Notifications"; | ||||||
| import theme from "./theme"; | import theme from "./theme"; | ||||||
| import Navigation from "./Navigation"; | import Navigation from "./Navigation"; | ||||||
| import ActionBar from "./ActionBar"; | import ActionBar from "./ActionBar"; | ||||||
| import notifier from "../app/Notifier"; | import notifier from "../app/Notifier"; | ||||||
| import Preferences from "./Preferences"; | import Preferences from "./Preferences"; | ||||||
| import { useLiveQuery } from "dexie-react-hooks"; |  | ||||||
| import subscriptionManager from "../app/SubscriptionManager"; | import subscriptionManager from "../app/SubscriptionManager"; | ||||||
| import userManager from "../app/UserManager"; | import userManager from "../app/UserManager"; | ||||||
| import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; |  | ||||||
| import { expandUrl } from "../app/utils"; | import { expandUrl } from "../app/utils"; | ||||||
| import ErrorBoundary from "./ErrorBoundary"; | import ErrorBoundary from "./ErrorBoundary"; | ||||||
| import routes from "./routes"; | import routes from "./routes"; | ||||||
|  | @ -21,7 +22,6 @@ import { useAccountListener, useBackgroundProcesses, useConnectionListeners } fr | ||||||
| import PublishDialog from "./PublishDialog"; | import PublishDialog from "./PublishDialog"; | ||||||
| import Messaging from "./Messaging"; | import Messaging from "./Messaging"; | ||||||
| import "./i18n"; // Translations! | import "./i18n"; // Translations! | ||||||
| import { Backdrop, CircularProgress } from "@mui/material"; |  | ||||||
| import Login from "./Login"; | import Login from "./Login"; | ||||||
| import Signup from "./Signup"; | import Signup from "./Signup"; | ||||||
| import Account from "./Account"; | import Account from "./Account"; | ||||||
|  | @ -66,12 +66,11 @@ const Layout = () => { | ||||||
|   const subscriptions = useLiveQuery(() => subscriptionManager.all()); |   const subscriptions = useLiveQuery(() => subscriptionManager.all()); | ||||||
|   const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal); |   const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal); | ||||||
|   const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; |   const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; | ||||||
|   const [selected] = (subscriptionsWithoutInternal || []).filter((s) => { |   const [selected] = (subscriptionsWithoutInternal || []).filter( | ||||||
|     return ( |     (s) => | ||||||
|       (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) || |       (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) || | ||||||
|       (config.base_url === s.baseUrl && params.topic === s.topic) |       (config.base_url === s.baseUrl && params.topic === s.topic) | ||||||
|   ); |   ); | ||||||
|   }); |  | ||||||
| 
 | 
 | ||||||
|   useConnectionListeners(account, subscriptions, users); |   useConnectionListeners(account, subscriptions, users); | ||||||
|   useAccountListener(setAccount); |   useAccountListener(setAccount); | ||||||
|  | @ -95,7 +94,7 @@ const Layout = () => { | ||||||
|         <Outlet |         <Outlet | ||||||
|           context={{ |           context={{ | ||||||
|             subscriptions: subscriptionsWithoutInternal, |             subscriptions: subscriptionsWithoutInternal, | ||||||
|             selected: selected, |             selected, | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
|       </Main> |       </Main> | ||||||
|  | @ -104,8 +103,7 @@ const Layout = () => { | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const Main = (props) => { | const Main = (props) => ( | ||||||
|   return ( |  | ||||||
|   <Box |   <Box | ||||||
|     id="main" |     id="main" | ||||||
|     component="main" |     component="main" | ||||||
|  | @ -122,12 +120,11 @@ const Main = (props) => { | ||||||
|   > |   > | ||||||
|     {props.children} |     {props.children} | ||||||
|   </Box> |   </Box> | ||||||
|   ); | ); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const Loader = () => ( | const Loader = () => ( | ||||||
|   <Backdrop |   <Backdrop | ||||||
|     open={true} |     open | ||||||
|     sx={{ |     sx={{ | ||||||
|       zIndex: 100000, |       zIndex: 100000, | ||||||
|       backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), |       backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), | ||||||
|  |  | ||||||
|  | @ -1,16 +1,17 @@ | ||||||
| import * as React from "react"; | import * as React from "react"; | ||||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||||
|  | import { useTranslation } from "react-i18next"; | ||||||
| import fileDocument from "../img/file-document.svg"; | import fileDocument from "../img/file-document.svg"; | ||||||
| import fileImage from "../img/file-image.svg"; | import fileImage from "../img/file-image.svg"; | ||||||
| import fileVideo from "../img/file-video.svg"; | import fileVideo from "../img/file-video.svg"; | ||||||
| import fileAudio from "../img/file-audio.svg"; | import fileAudio from "../img/file-audio.svg"; | ||||||
| import fileApp from "../img/file-app.svg"; | import fileApp from "../img/file-app.svg"; | ||||||
| import { useTranslation } from "react-i18next"; |  | ||||||
| 
 | 
 | ||||||
| const AttachmentIcon = (props) => { | const AttachmentIcon = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const type = props.type; |   const { type } = props; | ||||||
|   let imageFile, imageLabel; |   let imageFile; | ||||||
|  |   let imageLabel; | ||||||
|   if (!type) { |   if (!type) { | ||||||
|     imageFile = fileDocument; |     imageFile = fileDocument; | ||||||
|     imageLabel = t("notifications_attachment_file_image"); |     imageLabel = t("notifications_attachment_file_image"); | ||||||
|  |  | ||||||
|  | @ -3,8 +3,7 @@ import { Avatar } from "@mui/material"; | ||||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||||
| import logo from "../img/ntfy-filled.svg"; | import logo from "../img/ntfy-filled.svg"; | ||||||
| 
 | 
 | ||||||
| const AvatarBox = (props) => { | const AvatarBox = (props) => ( | ||||||
|   return ( |  | ||||||
|   <Box |   <Box | ||||||
|     sx={{ |     sx={{ | ||||||
|       display: "flex", |       display: "flex", | ||||||
|  | @ -19,7 +18,6 @@ const AvatarBox = (props) => { | ||||||
|     <Avatar sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }} src={logo} variant="rounded" /> |     <Avatar sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }} src={logo} variant="rounded" /> | ||||||
|     {props.children} |     {props.children} | ||||||
|   </Box> |   </Box> | ||||||
|   ); | ); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export default AvatarBox; | export default AvatarBox; | ||||||
|  |  | ||||||
|  | @ -3,8 +3,7 @@ import Box from "@mui/material/Box"; | ||||||
| import DialogContentText from "@mui/material/DialogContentText"; | import DialogContentText from "@mui/material/DialogContentText"; | ||||||
| import DialogActions from "@mui/material/DialogActions"; | import DialogActions from "@mui/material/DialogActions"; | ||||||
| 
 | 
 | ||||||
| const DialogFooter = (props) => { | const DialogFooter = (props) => ( | ||||||
|   return ( |  | ||||||
|   <Box |   <Box | ||||||
|     sx={{ |     sx={{ | ||||||
|       display: "flex", |       display: "flex", | ||||||
|  | @ -27,7 +26,6 @@ const DialogFooter = (props) => { | ||||||
|     </DialogContentText> |     </DialogContentText> | ||||||
|     <DialogActions sx={{ paddingRight: 2 }}>{props.children}</DialogActions> |     <DialogActions sx={{ paddingRight: 2 }}>{props.children}</DialogActions> | ||||||
|   </Box> |   </Box> | ||||||
|   ); | ); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export default DialogFooter; | export default DialogFooter; | ||||||
|  |  | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| import * as React from "react"; | import * as React from "react"; | ||||||
| import { useRef, useState } from "react"; | import { useRef, useState } from "react"; | ||||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||||
| import { rawEmojis } from "../app/emojis"; |  | ||||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||||
| import TextField from "@mui/material/TextField"; | import TextField from "@mui/material/TextField"; | ||||||
| import { ClickAwayListener, Fade, InputAdornment, styled } from "@mui/material"; | import { ClickAwayListener, Fade, InputAdornment, styled } from "@mui/material"; | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
| import { Close } from "@mui/icons-material"; | import { Close } from "@mui/icons-material"; | ||||||
| import Popper from "@mui/material/Popper"; | import Popper from "@mui/material/Popper"; | ||||||
| import { splitNoEmpty } from "../app/utils"; |  | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
|  | import { splitNoEmpty } from "../app/utils"; | ||||||
|  | import { rawEmojis } from "../app/emojis"; | ||||||
| 
 | 
 | ||||||
| // Create emoji list by category and create a search base (string with all search words) | // Create emoji list by category and create a search base (string with all search words) | ||||||
| // | // | ||||||
|  | @ -28,7 +28,7 @@ rawEmojis.forEach((emoji) => { | ||||||
|     const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; |     const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; | ||||||
|     if (supportedEmoji) { |     if (supportedEmoji) { | ||||||
|       const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`; |       const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`; | ||||||
|       const emojiWithSearchBase = { ...emoji, searchBase: searchBase }; |       const emojiWithSearchBase = { ...emoji, searchBase }; | ||||||
|       emojisByCategory[emoji.category].push(emojiWithSearchBase); |       emojisByCategory[emoji.category].push(emojiWithSearchBase); | ||||||
|     } |     } | ||||||
|   } catch (e) { |   } catch (e) { | ||||||
|  | @ -133,7 +133,7 @@ const Category = (props) => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const Emoji = (props) => { | const Emoji = (props) => { | ||||||
|   const emoji = props.emoji; |   const { emoji } = props; | ||||||
|   const matches = emojiMatches(emoji, props.search); |   const matches = emojiMatches(emoji, props.search); | ||||||
|   const title = `${emoji.description} (${emoji.aliases[0]})`; |   const title = `${emoji.description} (${emoji.aliases[0]})`; | ||||||
|   return ( |   return ( | ||||||
|  |  | ||||||
|  | @ -46,9 +46,9 @@ class ErrorBoundaryImpl extends React.Component { | ||||||
|     // Fetch additional info and a better stack trace |     // Fetch additional info and a better stack trace | ||||||
|     StackTrace.fromError(error).then((stack) => { |     StackTrace.fromError(error).then((stack) => { | ||||||
|       console.error("[ErrorBoundary] Stacktrace fetched", stack); |       console.error("[ErrorBoundary] Stacktrace fetched", stack); | ||||||
|       const niceStack = |       const niceStack = `${error.toString()}\n${stack | ||||||
|         `${error.toString()}\n` + |         .map((el) => `  at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`) | ||||||
|         stack.map((el) => `  at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n"); |         .join("\n")}`; | ||||||
|       this.setState({ niceStack }); |       this.setState({ niceStack }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | @ -73,9 +73,8 @@ class ErrorBoundaryImpl extends React.Component { | ||||||
|     if (this.state.error) { |     if (this.state.error) { | ||||||
|       if (this.state.unsupportedIndexedDB) { |       if (this.state.unsupportedIndexedDB) { | ||||||
|         return this.renderUnsupportedIndexedDB(); |         return this.renderUnsupportedIndexedDB(); | ||||||
|       } else { |  | ||||||
|         return this.renderError(); |  | ||||||
|       } |       } | ||||||
|  |       return this.renderError(); | ||||||
|     } |     } | ||||||
|     return this.props.children; |     return this.props.children; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -5,15 +5,15 @@ import WarningAmberIcon from "@mui/icons-material/WarningAmber"; | ||||||
| import TextField from "@mui/material/TextField"; | import TextField from "@mui/material/TextField"; | ||||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||||
| import routes from "./routes"; |  | ||||||
| import session from "../app/Session"; |  | ||||||
| import { NavLink } from "react-router-dom"; | import { NavLink } from "react-router-dom"; | ||||||
| import AvatarBox from "./AvatarBox"; |  | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import accountApi from "../app/AccountApi"; |  | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
| import { InputAdornment } from "@mui/material"; | import { InputAdornment } from "@mui/material"; | ||||||
| import { Visibility, VisibilityOff } from "@mui/icons-material"; | import { Visibility, VisibilityOff } from "@mui/icons-material"; | ||||||
|  | import accountApi from "../app/AccountApi"; | ||||||
|  | import AvatarBox from "./AvatarBox"; | ||||||
|  | import session from "../app/Session"; | ||||||
|  | import routes from "./routes"; | ||||||
| import { UnauthorizedError } from "../app/errors"; | import { UnauthorizedError } from "../app/errors"; | ||||||
| 
 | 
 | ||||||
| const Login = () => { | const Login = () => { | ||||||
|  |  | ||||||
|  | @ -1,21 +1,21 @@ | ||||||
| import * as React from "react"; | import * as React from "react"; | ||||||
| import { useState } from "react"; | import { useState } from "react"; | ||||||
| import Navigation from "./Navigation"; |  | ||||||
| import Paper from "@mui/material/Paper"; | import Paper from "@mui/material/Paper"; | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
| import TextField from "@mui/material/TextField"; | import TextField from "@mui/material/TextField"; | ||||||
| import SendIcon from "@mui/icons-material/Send"; | import SendIcon from "@mui/icons-material/Send"; | ||||||
| import api from "../app/Api"; |  | ||||||
| import PublishDialog from "./PublishDialog"; |  | ||||||
| import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; | import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; | ||||||
| import { Portal, Snackbar } from "@mui/material"; | import { Portal, Snackbar } from "@mui/material"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
|  | import PublishDialog from "./PublishDialog"; | ||||||
|  | import api from "../app/Api"; | ||||||
|  | import Navigation from "./Navigation"; | ||||||
| 
 | 
 | ||||||
| const Messaging = (props) => { | const Messaging = (props) => { | ||||||
|   const [message, setMessage] = useState(""); |   const [message, setMessage] = useState(""); | ||||||
|   const [dialogKey, setDialogKey] = useState(0); |   const [dialogKey, setDialogKey] = useState(0); | ||||||
| 
 | 
 | ||||||
|   const dialogOpenMode = props.dialogOpenMode; |   const { dialogOpenMode } = props; | ||||||
|   const subscription = props.selected; |   const subscription = props.selected; | ||||||
| 
 | 
 | ||||||
|   const handleOpenDialogClick = () => { |   const handleOpenDialogClick = () => { | ||||||
|  | @ -39,7 +39,7 @@ const Messaging = (props) => { | ||||||
|         topic={subscription?.topic ?? ""} |         topic={subscription?.topic ?? ""} | ||||||
|         message={message} |         message={message} | ||||||
|         onClose={handleDialogClose} |         onClose={handleDialogClose} | ||||||
|         onDragEnter={() => props.onDialogOpenModeChange((prev) => (prev ? prev : PublishDialog.OPEN_MODE_DRAG))} // Only update if not already open |         onDragEnter={() => props.onDialogOpenModeChange((prev) => prev || PublishDialog.OPEN_MODE_DRAG)} // Only update if not already open | ||||||
|         onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)} |         onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)} | ||||||
|       /> |       /> | ||||||
|     </> |     </> | ||||||
|  | @ -48,7 +48,7 @@ const Messaging = (props) => { | ||||||
| 
 | 
 | ||||||
| const MessageBar = (props) => { | const MessageBar = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const subscription = props.subscription; |   const { subscription } = props; | ||||||
|   const [snackOpen, setSnackOpen] = useState(false); |   const [snackOpen, setSnackOpen] = useState(false); | ||||||
|   const handleSendClick = async () => { |   const handleSendClick = async () => { | ||||||
|     try { |     try { | ||||||
|  |  | ||||||
|  | @ -11,28 +11,28 @@ import Divider from "@mui/material/Divider"; | ||||||
| import List from "@mui/material/List"; | import List from "@mui/material/List"; | ||||||
| import SettingsIcon from "@mui/icons-material/Settings"; | import SettingsIcon from "@mui/icons-material/Settings"; | ||||||
| import AddIcon from "@mui/icons-material/Add"; | import AddIcon from "@mui/icons-material/Add"; | ||||||
| import SubscribeDialog from "./SubscribeDialog"; |  | ||||||
| import { Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Portal, Tooltip } from "@mui/material"; | import { Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Portal, Tooltip } from "@mui/material"; | ||||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||||
|  | import { useLocation, useNavigate } from "react-router-dom"; | ||||||
|  | import { ChatBubble, MoreVert, NotificationsOffOutlined, Send } from "@mui/icons-material"; | ||||||
|  | import Box from "@mui/material/Box"; | ||||||
|  | import ArticleIcon from "@mui/icons-material/Article"; | ||||||
|  | import { Trans, useTranslation } from "react-i18next"; | ||||||
|  | import CelebrationIcon from "@mui/icons-material/Celebration"; | ||||||
|  | import IconButton from "@mui/material/IconButton"; | ||||||
|  | import SubscribeDialog from "./SubscribeDialog"; | ||||||
| import { openUrl, topicDisplayName, topicUrl } from "../app/utils"; | import { openUrl, topicDisplayName, topicUrl } from "../app/utils"; | ||||||
| import routes from "./routes"; | import routes from "./routes"; | ||||||
| import { ConnectionState } from "../app/Connection"; | import { ConnectionState } from "../app/Connection"; | ||||||
| import { useLocation, useNavigate } from "react-router-dom"; |  | ||||||
| import subscriptionManager from "../app/SubscriptionManager"; | import subscriptionManager from "../app/SubscriptionManager"; | ||||||
| import { ChatBubble, MoreVert, NotificationsOffOutlined, Send } from "@mui/icons-material"; |  | ||||||
| import Box from "@mui/material/Box"; |  | ||||||
| import notifier from "../app/Notifier"; | import notifier from "../app/Notifier"; | ||||||
| import config from "../app/config"; | import config from "../app/config"; | ||||||
| import ArticleIcon from "@mui/icons-material/Article"; |  | ||||||
| import { Trans, useTranslation } from "react-i18next"; |  | ||||||
| import session from "../app/Session"; | import session from "../app/Session"; | ||||||
| import accountApi, { Permission, Role } from "../app/AccountApi"; | import accountApi, { Permission, Role } from "../app/AccountApi"; | ||||||
| import CelebrationIcon from "@mui/icons-material/Celebration"; |  | ||||||
| import UpgradeDialog from "./UpgradeDialog"; | import UpgradeDialog from "./UpgradeDialog"; | ||||||
| import { AccountContext } from "./App"; | import { AccountContext } from "./App"; | ||||||
| import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; | import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; | ||||||
| import IconButton from "@mui/material/IconButton"; |  | ||||||
| import { SubscriptionPopup } from "./SubscriptionPopup"; | import { SubscriptionPopup } from "./SubscriptionPopup"; | ||||||
| 
 | 
 | ||||||
| const navWidth = 280; | const navWidth = 280; | ||||||
|  | @ -237,9 +237,7 @@ const UpgradeBanner = () => { | ||||||
| const SubscriptionList = (props) => { | const SubscriptionList = (props) => { | ||||||
|   const sortedSubscriptions = props.subscriptions |   const sortedSubscriptions = props.subscriptions | ||||||
|     .filter((s) => !s.internal) |     .filter((s) => !s.internal) | ||||||
|     .sort((a, b) => { |     .sort((a, b) => (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1)); | ||||||
|       return topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1; |  | ||||||
|     }); |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       {sortedSubscriptions.map((subscription) => ( |       {sortedSubscriptions.map((subscription) => ( | ||||||
|  | @ -258,7 +256,7 @@ const SubscriptionItem = (props) => { | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const [menuAnchorEl, setMenuAnchorEl] = useState(null); |   const [menuAnchorEl, setMenuAnchorEl] = useState(null); | ||||||
| 
 | 
 | ||||||
|   const subscription = props.subscription; |   const { subscription } = props; | ||||||
|   const iconBadge = subscription.new <= 99 ? subscription.new : "99+"; |   const iconBadge = subscription.new <= 99 ? subscription.new : "99+"; | ||||||
|   const displayName = topicDisplayName(subscription); |   const displayName = topicDisplayName(subscription); | ||||||
|   const ariaLabel = subscription.state === ConnectionState.Connecting ? `${displayName} (${t("nav_button_connecting")})` : displayName; |   const ariaLabel = subscription.state === ConnectionState.Connecting ? `${displayName} (${t("nav_button_connecting")})` : displayName; | ||||||
|  |  | ||||||
|  | @ -4,6 +4,15 @@ import Card from "@mui/material/Card"; | ||||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||||
| import * as React from "react"; | import * as React from "react"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
|  | import IconButton from "@mui/material/IconButton"; | ||||||
|  | import CheckIcon from "@mui/icons-material/Check"; | ||||||
|  | import CloseIcon from "@mui/icons-material/Close"; | ||||||
|  | import { useLiveQuery } from "dexie-react-hooks"; | ||||||
|  | import Box from "@mui/material/Box"; | ||||||
|  | import Button from "@mui/material/Button"; | ||||||
|  | import InfiniteScroll from "react-infinite-scroll-component"; | ||||||
|  | import { Trans, useTranslation } from "react-i18next"; | ||||||
|  | import { useOutletContext } from "react-router-dom"; | ||||||
| import { | import { | ||||||
|   formatBytes, |   formatBytes, | ||||||
|   formatMessage, |   formatMessage, | ||||||
|  | @ -15,23 +24,14 @@ import { | ||||||
|   topicShortUrl, |   topicShortUrl, | ||||||
|   unmatchedTags, |   unmatchedTags, | ||||||
| } from "../app/utils"; | } from "../app/utils"; | ||||||
| import IconButton from "@mui/material/IconButton"; |  | ||||||
| import CheckIcon from "@mui/icons-material/Check"; |  | ||||||
| import CloseIcon from "@mui/icons-material/Close"; |  | ||||||
| import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles"; | import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles"; | ||||||
| import { useLiveQuery } from "dexie-react-hooks"; |  | ||||||
| import Box from "@mui/material/Box"; |  | ||||||
| import Button from "@mui/material/Button"; |  | ||||||
| import subscriptionManager from "../app/SubscriptionManager"; | import subscriptionManager from "../app/SubscriptionManager"; | ||||||
| import InfiniteScroll from "react-infinite-scroll-component"; |  | ||||||
| import priority1 from "../img/priority-1.svg"; | import priority1 from "../img/priority-1.svg"; | ||||||
| import priority2 from "../img/priority-2.svg"; | import priority2 from "../img/priority-2.svg"; | ||||||
| import priority4 from "../img/priority-4.svg"; | import priority4 from "../img/priority-4.svg"; | ||||||
| import priority5 from "../img/priority-5.svg"; | import priority5 from "../img/priority-5.svg"; | ||||||
| import logoOutline from "../img/ntfy-outline.svg"; | import logoOutline from "../img/ntfy-outline.svg"; | ||||||
| import AttachmentIcon from "./AttachmentIcon"; | import AttachmentIcon from "./AttachmentIcon"; | ||||||
| import { Trans, useTranslation } from "react-i18next"; |  | ||||||
| import { useOutletContext } from "react-router-dom"; |  | ||||||
| import { useAutoSubscribe } from "./hooks"; | import { useAutoSubscribe } from "./hooks"; | ||||||
| 
 | 
 | ||||||
| export const AllSubscriptions = () => { | export const AllSubscriptions = () => { | ||||||
|  | @ -52,46 +52,50 @@ export const SingleSubscription = () => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const AllSubscriptionsList = (props) => { | const AllSubscriptionsList = (props) => { | ||||||
|   const subscriptions = props.subscriptions; |   const { subscriptions } = props; | ||||||
|   const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); |   const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); | ||||||
|   if (notifications === null || notifications === undefined) { |   if (notifications === null || notifications === undefined) { | ||||||
|     return <Loading />; |     return <Loading />; | ||||||
|   } else if (subscriptions.length === 0) { |   } | ||||||
|  |   if (subscriptions.length === 0) { | ||||||
|     return <NoSubscriptions />; |     return <NoSubscriptions />; | ||||||
|   } else if (notifications.length === 0) { |   } | ||||||
|  |   if (notifications.length === 0) { | ||||||
|     return <NoNotificationsWithoutSubscription subscriptions={subscriptions} />; |     return <NoNotificationsWithoutSubscription subscriptions={subscriptions} />; | ||||||
|   } |   } | ||||||
|   return <NotificationList key="all" notifications={notifications} messageBar={false} />; |   return <NotificationList key="all" notifications={notifications} messageBar={false} />; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const SingleSubscriptionList = (props) => { | const SingleSubscriptionList = (props) => { | ||||||
|   const subscription = props.subscription; |   const { subscription } = props; | ||||||
|   const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); |   const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); | ||||||
|   if (notifications === null || notifications === undefined) { |   if (notifications === null || notifications === undefined) { | ||||||
|     return <Loading />; |     return <Loading />; | ||||||
|   } else if (notifications.length === 0) { |   } | ||||||
|  |   if (notifications.length === 0) { | ||||||
|     return <NoNotifications subscription={subscription} />; |     return <NoNotifications subscription={subscription} />; | ||||||
|   } |   } | ||||||
|   return <NotificationList id={subscription.id} notifications={notifications} messageBar={true} />; |   return <NotificationList id={subscription.id} notifications={notifications} messageBar />; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const NotificationList = (props) => { | const NotificationList = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const pageSize = 20; |   const pageSize = 20; | ||||||
|   const notifications = props.notifications; |   const { notifications } = props; | ||||||
|   const [snackOpen, setSnackOpen] = useState(false); |   const [snackOpen, setSnackOpen] = useState(false); | ||||||
|   const [maxCount, setMaxCount] = useState(pageSize); |   const [maxCount, setMaxCount] = useState(pageSize); | ||||||
|   const count = Math.min(notifications.length, maxCount); |   const count = Math.min(notifications.length, maxCount); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect( | ||||||
|     return () => { |     () => () => { | ||||||
|       setMaxCount(pageSize); |       setMaxCount(pageSize); | ||||||
|       const main = document.getElementById("main"); |       const main = document.getElementById("main"); | ||||||
|       if (main) { |       if (main) { | ||||||
|         main.scrollTo(0, 0); |         main.scrollTo(0, 0); | ||||||
|       } |       } | ||||||
|     }; |     }, | ||||||
|   }, [props.id]); |     [props.id] | ||||||
|  |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <InfiniteScroll |     <InfiniteScroll | ||||||
|  | @ -129,8 +133,8 @@ const NotificationList = (props) => { | ||||||
| 
 | 
 | ||||||
| const NotificationItem = (props) => { | const NotificationItem = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const notification = props.notification; |   const { notification } = props; | ||||||
|   const attachment = notification.attachment; |   const { attachment } = notification; | ||||||
|   const date = formatShortDateTime(notification.time); |   const date = formatShortDateTime(notification.time); | ||||||
|   const otherTags = unmatchedTags(notification.tags); |   const otherTags = unmatchedTags(notification.tags); | ||||||
|   const tags = otherTags.length > 0 ? otherTags.join(", ") : null; |   const tags = otherTags.length > 0 ? otherTags.join(", ") : null; | ||||||
|  | @ -272,7 +276,7 @@ const priorityFiles = { | ||||||
| 
 | 
 | ||||||
| const Attachment = (props) => { | const Attachment = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const attachment = props.attachment; |   const { attachment } = props; | ||||||
|   const expired = attachment.expires && attachment.expires < Date.now() / 1000; |   const expired = attachment.expires && attachment.expires < Date.now() / 1000; | ||||||
|   const expires = attachment.expires && attachment.expires > Date.now() / 1000; |   const expires = attachment.expires && attachment.expires > Date.now() / 1000; | ||||||
|   const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/"); |   const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/"); | ||||||
|  | @ -402,20 +406,18 @@ const Image = (props) => { | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const UserActions = (props) => { | const UserActions = (props) => ( | ||||||
|   return ( |  | ||||||
|   <> |   <> | ||||||
|     {props.notification.actions.map((action) => ( |     {props.notification.actions.map((action) => ( | ||||||
|       <UserAction key={action.id} notification={props.notification} action={action} /> |       <UserAction key={action.id} notification={props.notification} action={action} /> | ||||||
|     ))} |     ))} | ||||||
|   </> |   </> | ||||||
|   ); | ); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const UserAction = (props) => { | const UserAction = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const notification = props.notification; |   const { notification } = props; | ||||||
|   const action = props.action; |   const { action } = props; | ||||||
|   if (action.action === "broadcast") { |   if (action.action === "broadcast") { | ||||||
|     return ( |     return ( | ||||||
|       <Tooltip title={t("notifications_actions_not_supported")}> |       <Tooltip title={t("notifications_actions_not_supported")}> | ||||||
|  | @ -426,7 +428,8 @@ const UserAction = (props) => { | ||||||
|         </span> |         </span> | ||||||
|       </Tooltip> |       </Tooltip> | ||||||
|     ); |     ); | ||||||
|   } else if (action.action === "view") { |   } | ||||||
|  |   if (action.action === "view") { | ||||||
|     return ( |     return ( | ||||||
|       <Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}> |       <Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}> | ||||||
|         <Button |         <Button | ||||||
|  | @ -439,20 +442,21 @@ const UserAction = (props) => { | ||||||
|         </Button> |         </Button> | ||||||
|       </Tooltip> |       </Tooltip> | ||||||
|     ); |     ); | ||||||
|   } else if (action.action === "http") { |   } | ||||||
|  |   if (action.action === "http") { | ||||||
|     const method = action.method ?? "POST"; |     const method = action.method ?? "POST"; | ||||||
|     const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); |     const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); | ||||||
|     return ( |     return ( | ||||||
|       <Tooltip |       <Tooltip | ||||||
|         title={t("notifications_actions_http_request_title", { |         title={t("notifications_actions_http_request_title", { | ||||||
|           method: method, |           method, | ||||||
|           url: action.url, |           url: action.url, | ||||||
|         })} |         })} | ||||||
|       > |       > | ||||||
|         <Button |         <Button | ||||||
|           onClick={() => performHttpAction(notification, action)} |           onClick={() => performHttpAction(notification, action)} | ||||||
|           aria-label={t("notifications_actions_http_request_title", { |           aria-label={t("notifications_actions_http_request_title", { | ||||||
|             method: method, |             method, | ||||||
|             url: action.url, |             url: action.url, | ||||||
|           })} |           })} | ||||||
|         > |         > | ||||||
|  | @ -493,7 +497,7 @@ const updateActionStatus = (notification, action, progress, error) => { | ||||||
|     if (a.id !== action.id) { |     if (a.id !== action.id) { | ||||||
|       return a; |       return a; | ||||||
|     } |     } | ||||||
|     return { ...a, progress: progress, error: error }; |     return { ...a, progress, error }; | ||||||
|   }); |   }); | ||||||
|   subscriptionManager.updateNotification(notification); |   subscriptionManager.updateNotification(notification); | ||||||
| }; | }; | ||||||
|  | @ -574,8 +578,7 @@ const NoSubscriptions = () => { | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const ForMoreDetails = () => { | const ForMoreDetails = () => ( | ||||||
|   return ( |  | ||||||
|   <Trans |   <Trans | ||||||
|     i18nKey="notifications_more_details" |     i18nKey="notifications_more_details" | ||||||
|     components={{ |     components={{ | ||||||
|  | @ -583,8 +586,7 @@ const ForMoreDetails = () => { | ||||||
|       docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener" />, |       docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener" />, | ||||||
|     }} |     }} | ||||||
|   /> |   /> | ||||||
|   ); | ); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const Loading = () => { | const Loading = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |  | ||||||
|  | @ -37,8 +37,8 @@ const PopupMenu = (props) => { | ||||||
|           }, |           }, | ||||||
|         }, |         }, | ||||||
|       }} |       }} | ||||||
|       transformOrigin={{ horizontal: horizontal, vertical: "top" }} |       transformOrigin={{ horizontal, vertical: "top" }} | ||||||
|       anchorOrigin={{ horizontal: horizontal, vertical: "bottom" }} |       anchorOrigin={{ horizontal, vertical: "bottom" }} | ||||||
|     > |     > | ||||||
|       {props.children} |       {props.children} | ||||||
|     </Menu> |     </Menu> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,6 @@ | ||||||
| import * as React from "react"; | import * as React from "react"; | ||||||
| 
 | 
 | ||||||
| export const PrefGroup = (props) => { | export const PrefGroup = (props) => <div role="table">{props.children}</div>; | ||||||
|   return <div role="table">{props.children}</div>; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const Pref = (props) => { | export const Pref = (props) => { | ||||||
|   const justifyContent = props.alignTop ? "normal" : "center"; |   const justifyContent = props.alignTop ? "normal" : "center"; | ||||||
|  | @ -24,7 +22,7 @@ export const Pref = (props) => { | ||||||
|           flex: "1 0 40%", |           flex: "1 0 40%", | ||||||
|           display: "flex", |           display: "flex", | ||||||
|           flexDirection: "column", |           flexDirection: "column", | ||||||
|           justifyContent: justifyContent, |           justifyContent, | ||||||
|           paddingRight: "30px", |           paddingRight: "30px", | ||||||
|         }} |         }} | ||||||
|       > |       > | ||||||
|  | @ -44,7 +42,7 @@ export const Pref = (props) => { | ||||||
|           flex: "1 0 calc(60% - 50px)", |           flex: "1 0 calc(60% - 50px)", | ||||||
|           display: "flex", |           display: "flex", | ||||||
|           flexDirection: "column", |           flexDirection: "column", | ||||||
|           justifyContent: justifyContent, |           justifyContent, | ||||||
|         }} |         }} | ||||||
|       > |       > | ||||||
|         {props.children} |         {props.children} | ||||||
|  |  | ||||||
|  | @ -17,8 +17,6 @@ import { | ||||||
|   useMediaQuery, |   useMediaQuery, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||||
| import prefs from "../app/Prefs"; |  | ||||||
| import { Paragraph } from "./styles"; |  | ||||||
| import EditIcon from "@mui/icons-material/Edit"; | import EditIcon from "@mui/icons-material/Edit"; | ||||||
| import CloseIcon from "@mui/icons-material/Close"; | import CloseIcon from "@mui/icons-material/Close"; | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
|  | @ -29,29 +27,30 @@ import MenuItem from "@mui/material/MenuItem"; | ||||||
| import Card from "@mui/material/Card"; | import Card from "@mui/material/Card"; | ||||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||||
| import { useLiveQuery } from "dexie-react-hooks"; | import { useLiveQuery } from "dexie-react-hooks"; | ||||||
| import theme from "./theme"; |  | ||||||
| import Dialog from "@mui/material/Dialog"; | import Dialog from "@mui/material/Dialog"; | ||||||
| import DialogTitle from "@mui/material/DialogTitle"; | import DialogTitle from "@mui/material/DialogTitle"; | ||||||
| import DialogContent from "@mui/material/DialogContent"; | import DialogContent from "@mui/material/DialogContent"; | ||||||
| import DialogActions from "@mui/material/DialogActions"; | import DialogActions from "@mui/material/DialogActions"; | ||||||
|  | import { useTranslation } from "react-i18next"; | ||||||
|  | import { Info } from "@mui/icons-material"; | ||||||
|  | import { useOutletContext } from "react-router-dom"; | ||||||
|  | import theme from "./theme"; | ||||||
| import userManager from "../app/UserManager"; | import userManager from "../app/UserManager"; | ||||||
| import { playSound, shuffle, sounds, validUrl } from "../app/utils"; | import { playSound, shuffle, sounds, validUrl } from "../app/utils"; | ||||||
| import { useTranslation } from "react-i18next"; |  | ||||||
| import session from "../app/Session"; | import session from "../app/Session"; | ||||||
| import routes from "./routes"; | import routes from "./routes"; | ||||||
| import accountApi, { Permission, Role } from "../app/AccountApi"; | import accountApi, { Permission, Role } from "../app/AccountApi"; | ||||||
| import { Pref, PrefGroup } from "./Pref"; | import { Pref, PrefGroup } from "./Pref"; | ||||||
| import { Info } from "@mui/icons-material"; |  | ||||||
| import { AccountContext } from "./App"; | import { AccountContext } from "./App"; | ||||||
| import { useOutletContext } from "react-router-dom"; | import { Paragraph } from "./styles"; | ||||||
|  | import prefs from "../app/Prefs"; | ||||||
| import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; | import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; | ||||||
| import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs"; | import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs"; | ||||||
| import { UnauthorizedError } from "../app/errors"; | import { UnauthorizedError } from "../app/errors"; | ||||||
| import subscriptionManager from "../app/SubscriptionManager"; | import subscriptionManager from "../app/SubscriptionManager"; | ||||||
| import { subscribeTopic } from "./SubscribeDialog"; | import { subscribeTopic } from "./SubscribeDialog"; | ||||||
| 
 | 
 | ||||||
| const Preferences = () => { | const Preferences = () => ( | ||||||
|   return ( |  | ||||||
|   <Container maxWidth="md" sx={{ marginTop: 3, marginBottom: 3 }}> |   <Container maxWidth="md" sx={{ marginTop: 3, marginBottom: 3 }}> | ||||||
|     <Stack spacing={3}> |     <Stack spacing={3}> | ||||||
|       <Notifications /> |       <Notifications /> | ||||||
|  | @ -60,8 +59,7 @@ const Preferences = () => { | ||||||
|       <Appearance /> |       <Appearance /> | ||||||
|     </Stack> |     </Stack> | ||||||
|   </Container> |   </Container> | ||||||
|   ); | ); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const Notifications = () => { | const Notifications = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  | @ -107,7 +105,7 @@ const Sound = () => { | ||||||
|       <div style={{ display: "flex", width: "100%" }}> |       <div style={{ display: "flex", width: "100%" }}> | ||||||
|         <FormControl fullWidth variant="standard" sx={{ margin: 1 }}> |         <FormControl fullWidth variant="standard" sx={{ margin: 1 }}> | ||||||
|           <Select value={sound} onChange={handleChange} aria-labelledby={labelId}> |           <Select value={sound} onChange={handleChange} aria-labelledby={labelId}> | ||||||
|             <MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem> |             <MenuItem value="none">{t("prefs_notifications_sound_no_sound")}</MenuItem> | ||||||
|             {Object.entries(sounds).map((s) => ( |             {Object.entries(sounds).map((s) => ( | ||||||
|               <MenuItem key={s[0]} value={s[0]}> |               <MenuItem key={s[0]} value={s[0]}> | ||||||
|                 {s[1].label} |                 {s[1].label} | ||||||
|  | @ -245,7 +243,7 @@ const Users = () => { | ||||||
|         </Typography> |         </Typography> | ||||||
|         <Paragraph> |         <Paragraph> | ||||||
|           {t("prefs_users_description")} |           {t("prefs_users_description")} | ||||||
|           {session.exists() && <>{" " + t("prefs_users_description_no_sync")}</>} |           {session.exists() && <>{` ${t("prefs_users_description_no_sync")}`}</>} | ||||||
|         </Paragraph> |         </Paragraph> | ||||||
|         {users?.length > 0 && <UserTable users={users} />} |         {users?.length > 0 && <UserTable users={users} />} | ||||||
|       </CardContent> |       </CardContent> | ||||||
|  | @ -371,9 +369,9 @@ const UserDialog = (props) => { | ||||||
|   })(); |   })(); | ||||||
|   const handleSubmit = async () => { |   const handleSubmit = async () => { | ||||||
|     props.onSubmit({ |     props.onSubmit({ | ||||||
|       baseUrl: baseUrl, |       baseUrl, | ||||||
|       username: username, |       username, | ||||||
|       password: password, |       password, | ||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|  | @ -479,7 +477,7 @@ const Language = () => { | ||||||
|   const showFlags = !navigator.userAgent.includes("Windows"); |   const showFlags = !navigator.userAgent.includes("Windows"); | ||||||
|   let title = t("prefs_appearance_language_title"); |   let title = t("prefs_appearance_language_title"); | ||||||
|   if (showFlags) { |   if (showFlags) { | ||||||
|     title += " " + randomFlags.join(" "); |     title += ` ${randomFlags.join(" ")}`; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const handleChange = async (ev) => { |   const handleChange = async (ev) => { | ||||||
|  |  | ||||||
|  | @ -1,13 +1,7 @@ | ||||||
| import * as React from "react"; | import * as React from "react"; | ||||||
| import { useContext, useEffect, useRef, useState } from "react"; | import { useContext, useEffect, useRef, useState } from "react"; | ||||||
| import theme from "./theme"; |  | ||||||
| import { Checkbox, Chip, FormControl, FormControlLabel, InputLabel, Link, Select, Tooltip, useMediaQuery } from "@mui/material"; | import { Checkbox, Chip, FormControl, FormControlLabel, InputLabel, Link, Select, Tooltip, useMediaQuery } from "@mui/material"; | ||||||
| import TextField from "@mui/material/TextField"; | import TextField from "@mui/material/TextField"; | ||||||
| import priority1 from "../img/priority-1.svg"; |  | ||||||
| import priority2 from "../img/priority-2.svg"; |  | ||||||
| import priority3 from "../img/priority-3.svg"; |  | ||||||
| import priority4 from "../img/priority-4.svg"; |  | ||||||
| import priority5 from "../img/priority-5.svg"; |  | ||||||
| import Dialog from "@mui/material/Dialog"; | import Dialog from "@mui/material/Dialog"; | ||||||
| import DialogTitle from "@mui/material/DialogTitle"; | import DialogTitle from "@mui/material/DialogTitle"; | ||||||
| import DialogContent from "@mui/material/DialogContent"; | import DialogContent from "@mui/material/DialogContent"; | ||||||
|  | @ -17,14 +11,20 @@ import IconButton from "@mui/material/IconButton"; | ||||||
| import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon"; | import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon"; | ||||||
| import { Close } from "@mui/icons-material"; | import { Close } from "@mui/icons-material"; | ||||||
| import MenuItem from "@mui/material/MenuItem"; | import MenuItem from "@mui/material/MenuItem"; | ||||||
| import { formatBytes, maybeWithAuth, topicShortUrl, topicUrl, validTopic, validUrl } from "../app/utils"; |  | ||||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||||
|  | import { Trans, useTranslation } from "react-i18next"; | ||||||
|  | import priority1 from "../img/priority-1.svg"; | ||||||
|  | import priority2 from "../img/priority-2.svg"; | ||||||
|  | import priority3 from "../img/priority-3.svg"; | ||||||
|  | import priority4 from "../img/priority-4.svg"; | ||||||
|  | import priority5 from "../img/priority-5.svg"; | ||||||
|  | import { formatBytes, maybeWithAuth, topicShortUrl, topicUrl, validTopic, validUrl } from "../app/utils"; | ||||||
| import AttachmentIcon from "./AttachmentIcon"; | import AttachmentIcon from "./AttachmentIcon"; | ||||||
| import DialogFooter from "./DialogFooter"; | import DialogFooter from "./DialogFooter"; | ||||||
| import api from "../app/Api"; | import api from "../app/Api"; | ||||||
| import userManager from "../app/UserManager"; | import userManager from "../app/UserManager"; | ||||||
| import EmojiPicker from "./EmojiPicker"; | import EmojiPicker from "./EmojiPicker"; | ||||||
| import { Trans, useTranslation } from "react-i18next"; | import theme from "./theme"; | ||||||
| import session from "../app/Session"; | import session from "../app/Session"; | ||||||
| import routes from "./routes"; | import routes from "./routes"; | ||||||
| import accountApi from "../app/AccountApi"; | import accountApi from "../app/AccountApi"; | ||||||
|  | @ -137,7 +137,7 @@ const PublishDialog = (props) => { | ||||||
|     if (attachFile && message.trim()) { |     if (attachFile && message.trim()) { | ||||||
|       url.searchParams.append("message", message.replaceAll("\n", "\\n").trim()); |       url.searchParams.append("message", message.replaceAll("\n", "\\n").trim()); | ||||||
|     } |     } | ||||||
|     const body = attachFile ? attachFile : message; |     const body = attachFile || message; | ||||||
|     try { |     try { | ||||||
|       const user = await userManager.get(baseUrl); |       const user = await userManager.get(baseUrl); | ||||||
|       const headers = maybeWithAuth({}, user); |       const headers = maybeWithAuth({}, user); | ||||||
|  | @ -183,13 +183,15 @@ const PublishDialog = (props) => { | ||||||
|             remainingBytes: formatBytes(remainingBytes), |             remainingBytes: formatBytes(remainingBytes), | ||||||
|           }) |           }) | ||||||
|         ); |         ); | ||||||
|       } else if (fileSizeLimitReached) { |       } | ||||||
|  |       if (fileSizeLimitReached) { | ||||||
|         return setAttachFileError( |         return setAttachFileError( | ||||||
|           t("publish_dialog_attachment_limits_file_reached", { |           t("publish_dialog_attachment_limits_file_reached", { | ||||||
|             fileSizeLimit: formatBytes(fileSizeLimit), |             fileSizeLimit: formatBytes(fileSizeLimit), | ||||||
|           }) |           }) | ||||||
|         ); |         ); | ||||||
|       } else if (quotaReached) { |       } | ||||||
|  |       if (quotaReached) { | ||||||
|         return setAttachFileError( |         return setAttachFileError( | ||||||
|           t("publish_dialog_attachment_limits_quota_reached", { |           t("publish_dialog_attachment_limits_quota_reached", { | ||||||
|             remainingBytes: formatBytes(remainingBytes), |             remainingBytes: formatBytes(remainingBytes), | ||||||
|  | @ -377,7 +379,7 @@ const PublishDialog = (props) => { | ||||||
|                     key={`priorityMenuItem${priority}`} |                     key={`priorityMenuItem${priority}`} | ||||||
|                     value={priority} |                     value={priority} | ||||||
|                     aria-label={t("notifications_priority_x", { |                     aria-label={t("notifications_priority_x", { | ||||||
|                       priority: priority, |                       priority, | ||||||
|                     })} |                     })} | ||||||
|                   > |                   > | ||||||
|                     <div style={{ display: "flex", alignItems: "center" }}> |                     <div style={{ display: "flex", alignItems: "center" }}> | ||||||
|  | @ -385,7 +387,7 @@ const PublishDialog = (props) => { | ||||||
|                         src={priorities[priority].file} |                         src={priorities[priority].file} | ||||||
|                         style={{ marginRight: "8px" }} |                         style={{ marginRight: "8px" }} | ||||||
|                         alt={t("notifications_priority_x", { |                         alt={t("notifications_priority_x", { | ||||||
|                           priority: priority, |                           priority, | ||||||
|                         })} |                         })} | ||||||
|                       /> |                       /> | ||||||
|                       <div>{priorities[priority].label}</div> |                       <div>{priorities[priority].label}</div> | ||||||
|  | @ -533,7 +535,7 @@ const PublishDialog = (props) => { | ||||||
|               /> |               /> | ||||||
|             </ClosableRow> |             </ClosableRow> | ||||||
|           )} |           )} | ||||||
|           <input type="file" ref={attachFileInput} onChange={handleAttachFileChanged} style={{ display: "none" }} aria-hidden={true} /> |           <input type="file" ref={attachFileInput} onChange={handleAttachFileChanged} style={{ display: "none" }} aria-hidden /> | ||||||
|           {showAttachFile && ( |           {showAttachFile && ( | ||||||
|             <AttachmentBox |             <AttachmentBox | ||||||
|               file={attachFile} |               file={attachFile} | ||||||
|  | @ -707,13 +709,11 @@ const PublishDialog = (props) => { | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const Row = (props) => { | const Row = (props) => ( | ||||||
|   return ( |  | ||||||
|   <div style={{ display: "flex" }} role="row"> |   <div style={{ display: "flex" }} role="row"> | ||||||
|     {props.children} |     {props.children} | ||||||
|   </div> |   </div> | ||||||
|   ); | ); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const ClosableRow = (props) => { | const ClosableRow = (props) => { | ||||||
|   const closable = props.hasOwnProperty("closable") ? props.closable : true; |   const closable = props.hasOwnProperty("closable") ? props.closable : true; | ||||||
|  | @ -748,7 +748,7 @@ const DialogIconButton = (props) => { | ||||||
| 
 | 
 | ||||||
| const AttachmentBox = (props) => { | const AttachmentBox = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const file = props.file; |   const { file } = props; | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Typography variant="body1" sx={{ marginTop: 2 }}> |       <Typography variant="body1" sx={{ marginTop: 2 }}> | ||||||
|  | @ -811,13 +811,7 @@ const ExpandingTextField = (props) => { | ||||||
|   }, [props.value]); |   }, [props.value]); | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Typography |       <Typography ref={invisibleFieldRef} component="span" variant={props.variant} aria-hidden sx={{ position: "absolute", left: "-200%" }}> | ||||||
|         ref={invisibleFieldRef} |  | ||||||
|         component="span" |  | ||||||
|         variant={props.variant} |  | ||||||
|         aria-hidden={true} |  | ||||||
|         sx={{ position: "absolute", left: "-200%" }} |  | ||||||
|       > |  | ||||||
|         {props.value} |         {props.value} | ||||||
|       </Typography> |       </Typography> | ||||||
|       <TextField |       <TextField | ||||||
|  |  | ||||||
|  | @ -7,18 +7,18 @@ import DialogContent from "@mui/material/DialogContent"; | ||||||
| import DialogContentText from "@mui/material/DialogContentText"; | import DialogContentText from "@mui/material/DialogContentText"; | ||||||
| import DialogTitle from "@mui/material/DialogTitle"; | import DialogTitle from "@mui/material/DialogTitle"; | ||||||
| import { Alert, FormControl, Select, useMediaQuery } from "@mui/material"; | import { Alert, FormControl, Select, useMediaQuery } from "@mui/material"; | ||||||
| import theme from "./theme"; |  | ||||||
| import { validTopic } from "../app/utils"; |  | ||||||
| import DialogFooter from "./DialogFooter"; |  | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import session from "../app/Session"; |  | ||||||
| import routes from "./routes"; |  | ||||||
| import accountApi, { Permission } from "../app/AccountApi"; |  | ||||||
| import ReserveTopicSelect from "./ReserveTopicSelect"; |  | ||||||
| import MenuItem from "@mui/material/MenuItem"; | import MenuItem from "@mui/material/MenuItem"; | ||||||
| import ListItemIcon from "@mui/material/ListItemIcon"; | import ListItemIcon from "@mui/material/ListItemIcon"; | ||||||
| import ListItemText from "@mui/material/ListItemText"; | import ListItemText from "@mui/material/ListItemText"; | ||||||
| import { Check, DeleteForever } from "@mui/icons-material"; | import { Check, DeleteForever } from "@mui/icons-material"; | ||||||
|  | import theme from "./theme"; | ||||||
|  | import { validTopic } from "../app/utils"; | ||||||
|  | import DialogFooter from "./DialogFooter"; | ||||||
|  | import session from "../app/Session"; | ||||||
|  | import routes from "./routes"; | ||||||
|  | import accountApi, { Permission } from "../app/AccountApi"; | ||||||
|  | import ReserveTopicSelect from "./ReserveTopicSelect"; | ||||||
| import { TopicReservedError, UnauthorizedError } from "../app/errors"; | import { TopicReservedError, UnauthorizedError } from "../app/errors"; | ||||||
| 
 | 
 | ||||||
| export const ReserveAddDialog = (props) => { | export const ReserveAddDialog = (props) => { | ||||||
|  | @ -164,7 +164,7 @@ export const ReserveDeleteDialog = (props) => { | ||||||
|               </ListItemIcon> |               </ListItemIcon> | ||||||
|               <ListItemText primary={t("reservation_delete_dialog_action_keep_title")} /> |               <ListItemText primary={t("reservation_delete_dialog_action_keep_title")} /> | ||||||
|             </MenuItem> |             </MenuItem> | ||||||
|             <MenuItem value={true}> |             <MenuItem value> | ||||||
|               <ListItemIcon> |               <ListItemIcon> | ||||||
|                 <DeleteForever /> |                 <DeleteForever /> | ||||||
|               </ListItemIcon> |               </ListItemIcon> | ||||||
|  |  | ||||||
|  | @ -2,21 +2,13 @@ import * as React from "react"; | ||||||
| import { Lock, Public } from "@mui/icons-material"; | import { Lock, Public } from "@mui/icons-material"; | ||||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||||
| 
 | 
 | ||||||
| export const PermissionReadWrite = React.forwardRef((props, ref) => { | export const PermissionReadWrite = React.forwardRef((props, ref) => <PermissionInternal icon={Public} ref={ref} {...props} />); | ||||||
|   return <PermissionInternal icon={Public} ref={ref} {...props} />; |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| export const PermissionDenyAll = React.forwardRef((props, ref) => { | export const PermissionDenyAll = React.forwardRef((props, ref) => <PermissionInternal icon={Lock} ref={ref} {...props} />); | ||||||
|   return <PermissionInternal icon={Lock} ref={ref} {...props} />; |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| export const PermissionRead = React.forwardRef((props, ref) => { | export const PermissionRead = React.forwardRef((props, ref) => <PermissionInternal icon={Public} text="R" ref={ref} {...props} />); | ||||||
|   return <PermissionInternal icon={Public} text="R" ref={ref} {...props} />; |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| export const PermissionWrite = React.forwardRef((props, ref) => { | export const PermissionWrite = React.forwardRef((props, ref) => <PermissionInternal icon={Public} text="W" ref={ref} {...props} />); | ||||||
|   return <PermissionInternal icon={Public} text="W" ref={ref} {...props} />; |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| const PermissionInternal = React.forwardRef((props, ref) => { | const PermissionInternal = React.forwardRef((props, ref) => { | ||||||
|   const size = props.size ?? "medium"; |   const size = props.size ?? "medium"; | ||||||
|  |  | ||||||
|  | @ -3,17 +3,17 @@ import { useState } from "react"; | ||||||
| import TextField from "@mui/material/TextField"; | import TextField from "@mui/material/TextField"; | ||||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||||
| import routes from "./routes"; |  | ||||||
| import session from "../app/Session"; |  | ||||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||||
| import { NavLink } from "react-router-dom"; | import { NavLink } from "react-router-dom"; | ||||||
| import AvatarBox from "./AvatarBox"; |  | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import WarningAmberIcon from "@mui/icons-material/WarningAmber"; | import WarningAmberIcon from "@mui/icons-material/WarningAmber"; | ||||||
| import accountApi from "../app/AccountApi"; |  | ||||||
| import { InputAdornment } from "@mui/material"; | import { InputAdornment } from "@mui/material"; | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
| import { Visibility, VisibilityOff } from "@mui/icons-material"; | import { Visibility, VisibilityOff } from "@mui/icons-material"; | ||||||
|  | import accountApi from "../app/AccountApi"; | ||||||
|  | import AvatarBox from "./AvatarBox"; | ||||||
|  | import session from "../app/Session"; | ||||||
|  | import routes from "./routes"; | ||||||
| import { AccountCreateLimitReachedError, UserExistsError } from "../app/errors"; | import { AccountCreateLimitReachedError, UserExistsError } from "../app/errors"; | ||||||
| 
 | 
 | ||||||
| const Signup = () => { | const Signup = () => { | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import DialogContent from "@mui/material/DialogContent"; | ||||||
| import DialogContentText from "@mui/material/DialogContentText"; | import DialogContentText from "@mui/material/DialogContentText"; | ||||||
| import DialogTitle from "@mui/material/DialogTitle"; | import DialogTitle from "@mui/material/DialogTitle"; | ||||||
| import { Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery } from "@mui/material"; | import { Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery } from "@mui/material"; | ||||||
|  | import { useTranslation } from "react-i18next"; | ||||||
| import theme from "./theme"; | import theme from "./theme"; | ||||||
| import api from "../app/Api"; | import api from "../app/Api"; | ||||||
| import { randomAlphanumericString, topicUrl, validTopic, validUrl } from "../app/utils"; | import { randomAlphanumericString, topicUrl, validTopic, validUrl } from "../app/utils"; | ||||||
|  | @ -14,7 +15,6 @@ import userManager from "../app/UserManager"; | ||||||
| import subscriptionManager from "../app/SubscriptionManager"; | import subscriptionManager from "../app/SubscriptionManager"; | ||||||
| import poller from "../app/Poller"; | import poller from "../app/Poller"; | ||||||
| import DialogFooter from "./DialogFooter"; | import DialogFooter from "./DialogFooter"; | ||||||
| import { useTranslation } from "react-i18next"; |  | ||||||
| import session from "../app/Session"; | import session from "../app/Session"; | ||||||
| import routes from "./routes"; | import routes from "./routes"; | ||||||
| import accountApi, { Permission, Role } from "../app/AccountApi"; | import accountApi, { Permission, Role } from "../app/AccountApi"; | ||||||
|  | @ -33,7 +33,7 @@ const SubscribeDialog = (props) => { | ||||||
| 
 | 
 | ||||||
|   const handleSuccess = async () => { |   const handleSuccess = async () => { | ||||||
|     console.log(`[SubscribeDialog] Subscribing to topic ${topic}`); |     console.log(`[SubscribeDialog] Subscribing to topic ${topic}`); | ||||||
|     const actualBaseUrl = baseUrl ? baseUrl : config.base_url; |     const actualBaseUrl = baseUrl || config.base_url; | ||||||
|     const subscription = await subscribeTopic(actualBaseUrl, topic); |     const subscription = await subscribeTopic(actualBaseUrl, topic); | ||||||
|     poller.pollInBackground(subscription); // Dangle! |     poller.pollInBackground(subscription); // Dangle! | ||||||
|     props.onSuccess(subscription); |     props.onSuccess(subscription); | ||||||
|  | @ -66,7 +66,7 @@ const SubscribePage = (props) => { | ||||||
|   const [anotherServerVisible, setAnotherServerVisible] = useState(false); |   const [anotherServerVisible, setAnotherServerVisible] = useState(false); | ||||||
|   const [everyone, setEveryone] = useState(Permission.DENY_ALL); |   const [everyone, setEveryone] = useState(Permission.DENY_ALL); | ||||||
|   const baseUrl = anotherServerVisible ? props.baseUrl : config.base_url; |   const baseUrl = anotherServerVisible ? props.baseUrl : config.base_url; | ||||||
|   const topic = props.topic; |   const { topic } = props; | ||||||
|   const existingTopicUrls = props.subscriptions.map((s) => topicUrl(s.baseUrl, s.topic)); |   const existingTopicUrls = props.subscriptions.map((s) => topicUrl(s.baseUrl, s.topic)); | ||||||
|   const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map((s) => s.baseUrl)])).filter( |   const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map((s) => s.baseUrl)])).filter( | ||||||
|     (s) => s !== config.base_url |     (s) => s !== config.base_url | ||||||
|  | @ -86,15 +86,14 @@ const SubscribePage = (props) => { | ||||||
|       if (user) { |       if (user) { | ||||||
|         setError( |         setError( | ||||||
|           t("subscribe_dialog_error_user_not_authorized", { |           t("subscribe_dialog_error_user_not_authorized", { | ||||||
|             username: username, |             username, | ||||||
|           }) |           }) | ||||||
|         ); |         ); | ||||||
|         return; |         return; | ||||||
|       } else { |       } | ||||||
|       props.onNeedsLogin(); |       props.onNeedsLogin(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     // Reserve topic (if requested) |     // Reserve topic (if requested) | ||||||
|     if (session.exists() && baseUrl === config.base_url && reserveTopicVisible) { |     if (session.exists() && baseUrl === config.base_url && reserveTopicVisible) { | ||||||
|  | @ -125,10 +124,9 @@ const SubscribePage = (props) => { | ||||||
|     if (anotherServerVisible) { |     if (anotherServerVisible) { | ||||||
|       const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic)); |       const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic)); | ||||||
|       return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl; |       return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl; | ||||||
|     } else { |     } | ||||||
|     const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.base_url, topic)); |     const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.base_url, topic)); | ||||||
|     return validTopic(topic) && !isExistingTopicUrl; |     return validTopic(topic) && !isExistingTopicUrl; | ||||||
|     } |  | ||||||
|   })(); |   })(); | ||||||
| 
 | 
 | ||||||
|   const updateBaseUrl = (ev, newVal) => { |   const updateBaseUrl = (ev, newVal) => { | ||||||
|  | @ -242,14 +240,14 @@ const LoginPage = (props) => { | ||||||
|   const [password, setPassword] = useState(""); |   const [password, setPassword] = useState(""); | ||||||
|   const [error, setError] = useState(""); |   const [error, setError] = useState(""); | ||||||
|   const baseUrl = props.baseUrl ? props.baseUrl : config.base_url; |   const baseUrl = props.baseUrl ? props.baseUrl : config.base_url; | ||||||
|   const topic = props.topic; |   const { topic } = props; | ||||||
| 
 | 
 | ||||||
|   const handleLogin = async () => { |   const handleLogin = async () => { | ||||||
|     const user = { baseUrl, username, password }; |     const user = { baseUrl, username, password }; | ||||||
|     const success = await api.topicAuth(baseUrl, topic, user); |     const success = await api.topicAuth(baseUrl, topic, user); | ||||||
|     if (!success) { |     if (!success) { | ||||||
|       console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); |       console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); | ||||||
|       setError(t("subscribe_dialog_error_user_not_authorized", { username: username })); |       setError(t("subscribe_dialog_error_user_not_authorized", { username })); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); |     console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); | ||||||
|  |  | ||||||
|  | @ -7,20 +7,20 @@ import DialogContent from "@mui/material/DialogContent"; | ||||||
| import DialogContentText from "@mui/material/DialogContentText"; | import DialogContentText from "@mui/material/DialogContentText"; | ||||||
| import DialogTitle from "@mui/material/DialogTitle"; | import DialogTitle from "@mui/material/DialogTitle"; | ||||||
| import { Chip, InputAdornment, Portal, Snackbar, useMediaQuery } from "@mui/material"; | import { Chip, InputAdornment, Portal, Snackbar, useMediaQuery } from "@mui/material"; | ||||||
| import theme from "./theme"; |  | ||||||
| import subscriptionManager from "../app/SubscriptionManager"; |  | ||||||
| import DialogFooter from "./DialogFooter"; |  | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import accountApi, { Role } from "../app/AccountApi"; |  | ||||||
| import session from "../app/Session"; |  | ||||||
| import routes from "./routes"; |  | ||||||
| import MenuItem from "@mui/material/MenuItem"; | import MenuItem from "@mui/material/MenuItem"; | ||||||
| import PopupMenu from "./PopupMenu"; |  | ||||||
| import { formatShortDateTime, shuffle } from "../app/utils"; |  | ||||||
| import api from "../app/Api"; |  | ||||||
| import { useNavigate } from "react-router-dom"; | import { useNavigate } from "react-router-dom"; | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
| import { Clear } from "@mui/icons-material"; | import { Clear } from "@mui/icons-material"; | ||||||
|  | import theme from "./theme"; | ||||||
|  | import subscriptionManager from "../app/SubscriptionManager"; | ||||||
|  | import DialogFooter from "./DialogFooter"; | ||||||
|  | import accountApi, { Role } from "../app/AccountApi"; | ||||||
|  | import session from "../app/Session"; | ||||||
|  | import routes from "./routes"; | ||||||
|  | import PopupMenu from "./PopupMenu"; | ||||||
|  | import { formatShortDateTime, shuffle } from "../app/utils"; | ||||||
|  | import api from "../app/Api"; | ||||||
| import { AccountContext } from "./App"; | import { AccountContext } from "./App"; | ||||||
| import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs"; | import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs"; | ||||||
| import { UnauthorizedError } from "../app/errors"; | import { UnauthorizedError } from "../app/errors"; | ||||||
|  | @ -34,7 +34,7 @@ export const SubscriptionPopup = (props) => { | ||||||
|   const [reserveEditDialogOpen, setReserveEditDialogOpen] = useState(false); |   const [reserveEditDialogOpen, setReserveEditDialogOpen] = useState(false); | ||||||
|   const [reserveDeleteDialogOpen, setReserveDeleteDialogOpen] = useState(false); |   const [reserveDeleteDialogOpen, setReserveDeleteDialogOpen] = useState(false); | ||||||
|   const [showPublishError, setShowPublishError] = useState(false); |   const [showPublishError, setShowPublishError] = useState(false); | ||||||
|   const subscription = props.subscription; |   const { subscription } = props; | ||||||
|   const placement = props.placement ?? "left"; |   const placement = props.placement ?? "left"; | ||||||
|   const reservations = account?.reservations || []; |   const reservations = account?.reservations || []; | ||||||
| 
 | 
 | ||||||
|  | @ -64,8 +64,8 @@ export const SubscriptionPopup = (props) => { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleSendTestMessage = async () => { |   const handleSendTestMessage = async () => { | ||||||
|     const baseUrl = props.subscription.baseUrl; |     const { baseUrl } = props.subscription; | ||||||
|     const topic = props.subscription.topic; |     const { topic } = props.subscription; | ||||||
|     const tags = shuffle([ |     const tags = shuffle([ | ||||||
|       "grinning", |       "grinning", | ||||||
|       "octopus", |       "octopus", | ||||||
|  | @ -110,9 +110,9 @@ export const SubscriptionPopup = (props) => { | ||||||
|     ])[0]; |     ])[0]; | ||||||
|     try { |     try { | ||||||
|       await api.publish(baseUrl, topic, message, { |       await api.publish(baseUrl, topic, message, { | ||||||
|         title: title, |         title, | ||||||
|         priority: priority, |         priority, | ||||||
|         tags: tags, |         tags, | ||||||
|       }); |       }); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       console.log(`[SubscriptionPopup] Error publishing message`, e); |       console.log(`[SubscriptionPopup] Error publishing message`, e); | ||||||
|  | @ -201,7 +201,7 @@ export const SubscriptionPopup = (props) => { | ||||||
| 
 | 
 | ||||||
| const DisplayNameDialog = (props) => { | const DisplayNameDialog = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const subscription = props.subscription; |   const { subscription } = props; | ||||||
|   const [error, setError] = useState(""); |   const [error, setError] = useState(""); | ||||||
|   const [displayName, setDisplayName] = useState(subscription.displayName ?? ""); |   const [displayName, setDisplayName] = useState(subscription.displayName ?? ""); | ||||||
|   const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); |   const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); | ||||||
|  | @ -265,9 +265,11 @@ export const ReserveLimitChip = () => { | ||||||
|   const { account } = useContext(AccountContext); |   const { account } = useContext(AccountContext); | ||||||
|   if (account?.role === Role.ADMIN || account?.stats.reservations_remaining > 0) { |   if (account?.role === Role.ADMIN || account?.stats.reservations_remaining > 0) { | ||||||
|     return <></>; |     return <></>; | ||||||
|   } else if (config.enable_payments) { |   } | ||||||
|  |   if (config.enable_payments) { | ||||||
|     return account?.limits.reservations > 0 ? <LimitReachedChip /> : <ProChip />; |     return account?.limits.reservations > 0 ? <LimitReachedChip /> : <ProChip />; | ||||||
|   } else if (account) { |   } | ||||||
|  |   if (account) { | ||||||
|     return <LimitReachedChip />; |     return <LimitReachedChip />; | ||||||
|   } |   } | ||||||
|   return <></>; |   return <></>; | ||||||
|  | @ -294,7 +296,7 @@ export const ProChip = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   return ( |   return ( | ||||||
|     <Chip |     <Chip | ||||||
|       label={"ntfy Pro"} |       label="ntfy Pro" | ||||||
|       variant="outlined" |       variant="outlined" | ||||||
|       color="primary" |       color="primary" | ||||||
|       sx={{ |       sx={{ | ||||||
|  |  | ||||||
|  | @ -4,15 +4,9 @@ import Dialog from "@mui/material/Dialog"; | ||||||
| import DialogContent from "@mui/material/DialogContent"; | import DialogContent from "@mui/material/DialogContent"; | ||||||
| import DialogTitle from "@mui/material/DialogTitle"; | import DialogTitle from "@mui/material/DialogTitle"; | ||||||
| import { Alert, CardActionArea, CardContent, Chip, Link, ListItem, Switch, useMediaQuery } from "@mui/material"; | import { Alert, CardActionArea, CardContent, Chip, Link, ListItem, Switch, useMediaQuery } from "@mui/material"; | ||||||
| import theme from "./theme"; |  | ||||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||||
| import accountApi, { SubscriptionInterval } from "../app/AccountApi"; |  | ||||||
| import session from "../app/Session"; |  | ||||||
| import routes from "./routes"; |  | ||||||
| import Card from "@mui/material/Card"; | import Card from "@mui/material/Card"; | ||||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||||
| import { AccountContext } from "./App"; |  | ||||||
| import { formatBytes, formatNumber, formatPrice, formatShortDate } from "../app/utils"; |  | ||||||
| import { Trans, useTranslation } from "react-i18next"; | import { Trans, useTranslation } from "react-i18next"; | ||||||
| import List from "@mui/material/List"; | import List from "@mui/material/List"; | ||||||
| import { Check, Close } from "@mui/icons-material"; | import { Check, Close } from "@mui/icons-material"; | ||||||
|  | @ -20,9 +14,15 @@ import ListItemIcon from "@mui/material/ListItemIcon"; | ||||||
| import ListItemText from "@mui/material/ListItemText"; | import ListItemText from "@mui/material/ListItemText"; | ||||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||||
| import { NavLink } from "react-router-dom"; | import { NavLink } from "react-router-dom"; | ||||||
| import { UnauthorizedError } from "../app/errors"; |  | ||||||
| import DialogContentText from "@mui/material/DialogContentText"; | import DialogContentText from "@mui/material/DialogContentText"; | ||||||
| import DialogActions from "@mui/material/DialogActions"; | import DialogActions from "@mui/material/DialogActions"; | ||||||
|  | import { UnauthorizedError } from "../app/errors"; | ||||||
|  | import { formatBytes, formatNumber, formatPrice, formatShortDate } from "../app/utils"; | ||||||
|  | import { AccountContext } from "./App"; | ||||||
|  | import routes from "./routes"; | ||||||
|  | import session from "../app/Session"; | ||||||
|  | import accountApi, { SubscriptionInterval } from "../app/AccountApi"; | ||||||
|  | import theme from "./theme"; | ||||||
| 
 | 
 | ||||||
| const UpgradeDialog = (props) => { | const UpgradeDialog = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  | @ -52,7 +52,9 @@ const UpgradeDialog = (props) => { | ||||||
|   const currentTierCode = currentTier?.code; // May be undefined |   const currentTierCode = currentTier?.code; // May be undefined | ||||||
| 
 | 
 | ||||||
|   // Figure out buttons, labels and the submit action |   // Figure out buttons, labels and the submit action | ||||||
|   let submitAction, submitButtonLabel, banner; |   let submitAction; | ||||||
|  |   let submitButtonLabel; | ||||||
|  |   let banner; | ||||||
|   if (!account) { |   if (!account) { | ||||||
|     submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup"); |     submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup"); | ||||||
|     submitAction = Action.REDIRECT_SIGNUP; |     submitAction = Action.REDIRECT_SIGNUP; | ||||||
|  | @ -112,8 +114,8 @@ const UpgradeDialog = (props) => { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   // Figure out discount |   // Figure out discount | ||||||
|   let discount = 0, |   let discount = 0; | ||||||
|     upto = false; |   let upto = false; | ||||||
|   if (newTier?.prices) { |   if (newTier?.prices) { | ||||||
|     discount = Math.round(((newTier.prices.month * 12) / newTier.prices.year - 1) * 100); |     discount = Math.round(((newTier.prices.month * 12) / newTier.prices.year - 1) * 100); | ||||||
|   } else { |   } else { | ||||||
|  | @ -157,8 +159,8 @@ const UpgradeDialog = (props) => { | ||||||
|               <Chip |               <Chip | ||||||
|                 label={ |                 label={ | ||||||
|                   upto |                   upto | ||||||
|                     ? t("account_upgrade_dialog_interval_yearly_discount_save_up_to", { discount: discount }) |                     ? t("account_upgrade_dialog_interval_yearly_discount_save_up_to", { discount }) | ||||||
|                     : t("account_upgrade_dialog_interval_yearly_discount_save", { discount: discount }) |                     : t("account_upgrade_dialog_interval_yearly_discount_save", { discount }) | ||||||
|                 } |                 } | ||||||
|                 color="primary" |                 color="primary" | ||||||
|                 size="small" |                 size="small" | ||||||
|  | @ -269,9 +271,11 @@ const UpgradeDialog = (props) => { | ||||||
| 
 | 
 | ||||||
| const TierCard = (props) => { | const TierCard = (props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const tier = props.tier; |   const { tier } = props; | ||||||
| 
 | 
 | ||||||
|   let cardStyle, labelStyle, labelText; |   let cardStyle; | ||||||
|  |   let labelStyle; | ||||||
|  |   let labelText; | ||||||
|   if (props.selected) { |   if (props.selected) { | ||||||
|     cardStyle = { background: "#eee", border: "3px solid #338574" }; |     cardStyle = { background: "#eee", border: "3px solid #338574" }; | ||||||
|     labelStyle = { background: "#338574", color: "white" }; |     labelStyle = { background: "#338574", color: "white" }; | ||||||
|  | @ -392,16 +396,11 @@ const TierCard = (props) => { | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const Feature = (props) => { | const Feature = (props) => <FeatureItem feature>{props.children}</FeatureItem>; | ||||||
|   return <FeatureItem feature={true}>{props.children}</FeatureItem>; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const NoFeature = (props) => { | const NoFeature = (props) => <FeatureItem feature={false}>{props.children}</FeatureItem>; | ||||||
|   return <FeatureItem feature={false}>{props.children}</FeatureItem>; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const FeatureItem = (props) => { | const FeatureItem = (props) => ( | ||||||
|   return ( |  | ||||||
|   <ListItem disableGutters sx={{ m: 0, p: 0 }}> |   <ListItem disableGutters sx={{ m: 0, p: 0 }}> | ||||||
|     <ListItemIcon sx={{ minWidth: "24px" }}> |     <ListItemIcon sx={{ minWidth: "24px" }}> | ||||||
|       {props.feature && <Check fontSize="small" sx={{ color: "#338574" }} />} |       {props.feature && <Check fontSize="small" sx={{ color: "#338574" }} />} | ||||||
|  | @ -409,8 +408,7 @@ const FeatureItem = (props) => { | ||||||
|     </ListItemIcon> |     </ListItemIcon> | ||||||
|     <ListItemText sx={{ mt: "2px", mb: "2px" }} primary={<Typography variant="body1">{props.children}</Typography>} /> |     <ListItemText sx={{ mt: "2px", mb: "2px" }} primary={<Typography variant="body1">{props.children}</Typography>} /> | ||||||
|   </ListItem> |   </ListItem> | ||||||
|   ); | ); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const Action = { | const Action = { | ||||||
|   REDIRECT_SIGNUP: 1, |   REDIRECT_SIGNUP: 1, | ||||||
|  |  | ||||||
|  | @ -61,7 +61,7 @@ export const useConnectionListeners = (account, subscriptions, users) => { | ||||||
|       }; |       }; | ||||||
|     }, |     }, | ||||||
|     // We have to disable dep checking for "navigate". This is fine, it never changes.
 |     // We have to disable dep checking for "navigate". This is fine, it never changes.
 | ||||||
|     // eslint-disable-next-line
 | 
 | ||||||
|     [] |     [] | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||||
| import theme from "./theme"; |  | ||||||
| import Container from "@mui/material/Container"; | import Container from "@mui/material/Container"; | ||||||
| import { Backdrop, styled } from "@mui/material"; | import { Backdrop, styled } from "@mui/material"; | ||||||
|  | import theme from "./theme"; | ||||||
| 
 | 
 | ||||||
| export const Paragraph = styled(Typography)({ | export const Paragraph = styled(Typography)({ | ||||||
|   paddingTop: 8, |   paddingTop: 8, | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue