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