utils.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { Base64 } from "js-base64";
  2. import beep from "../sounds/beep.mp3";
  3. import juntos from "../sounds/juntos.mp3";
  4. import pristine from "../sounds/pristine.mp3";
  5. import ding from "../sounds/ding.mp3";
  6. import dadum from "../sounds/dadum.mp3";
  7. import pop from "../sounds/pop.mp3";
  8. import popSwoosh from "../sounds/pop-swoosh.mp3";
  9. import config from "./config";
  10. import emojisMapped from "./emojisMapped";
  11. export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
  12. export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
  13. export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
  14. export const expandSecureUrl = (url) => `https://${url}`;
  15. export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
  16. export const topicUrlWs = (baseUrl, topic) =>
  17. `${topicUrl(baseUrl, topic)}/ws`.replaceAll("https://", "wss://").replaceAll("http://", "ws://");
  18. export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`;
  19. export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`;
  20. export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`;
  21. export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`;
  22. export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
  23. export const webPushUrl = (baseUrl) => `${baseUrl}/v1/webpush`;
  24. export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`;
  25. export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`;
  26. export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`;
  27. export const accountSettingsUrl = (baseUrl) => `${baseUrl}/v1/account/settings`;
  28. export const accountSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/subscription`;
  29. export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reservation`;
  30. export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`;
  31. export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
  32. export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
  33. export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
  34. export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`;
  35. export const validUrl = (url) => url.match(/^https?:\/\/.+/);
  36. export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic);
  37. export const validTopic = (topic) => {
  38. if (disallowedTopic(topic)) {
  39. return false;
  40. }
  41. return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app!
  42. };
  43. export const topicDisplayName = (subscription) => {
  44. if (subscription.displayName) {
  45. return subscription.displayName;
  46. }
  47. if (subscription.baseUrl === config.base_url) {
  48. return subscription.topic;
  49. }
  50. return topicShortUrl(subscription.baseUrl, subscription.topic);
  51. };
  52. export const unmatchedTags = (tags) => {
  53. if (!tags) return [];
  54. return tags.filter((tag) => !(tag in emojisMapped));
  55. };
  56. export const encodeBase64 = (s) => Base64.encode(s);
  57. export const encodeBase64Url = (s) => Base64.encodeURI(s);
  58. export const bearerAuth = (token) => `Bearer ${token}`;
  59. export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`;
  60. export const withBearerAuth = (headers, token) => ({ ...headers, Authorization: bearerAuth(token) });
  61. export const maybeWithBearerAuth = (headers, token) => {
  62. if (token) {
  63. return withBearerAuth(headers, token);
  64. }
  65. return headers;
  66. };
  67. export const withBasicAuth = (headers, username, password) => ({ ...headers, Authorization: basicAuth(username, password) });
  68. export const maybeWithAuth = (headers, user) => {
  69. if (user?.password) {
  70. return withBasicAuth(headers, user.username, user.password);
  71. }
  72. if (user?.token) {
  73. return withBearerAuth(headers, user.token);
  74. }
  75. return headers;
  76. };
  77. export const maybeActionErrors = (notification) => {
  78. const actionErrors = (notification.actions ?? [])
  79. .map((action) => action.error)
  80. .filter((action) => !!action)
  81. .join("\n");
  82. if (actionErrors.length === 0) {
  83. return undefined;
  84. }
  85. return actionErrors;
  86. };
  87. export const shuffle = (arr) => {
  88. const returnArr = [...arr];
  89. for (let index = returnArr.length - 1; index > 0; index -= 1) {
  90. const j = Math.floor(Math.random() * (index + 1));
  91. [returnArr[index], returnArr[j]] = [returnArr[j], returnArr[index]];
  92. }
  93. return returnArr;
  94. };
  95. export const splitNoEmpty = (s, delimiter) =>
  96. s
  97. .split(delimiter)
  98. .map((x) => x.trim())
  99. .filter((x) => x !== "");
  100. /** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */
  101. export const hashCode = (s) => {
  102. let hash = 0;
  103. for (let i = 0; i < s.length; i += 1) {
  104. const char = s.charCodeAt(i);
  105. // eslint-disable-next-line no-bitwise
  106. hash = (hash << 5) - hash + char;
  107. // eslint-disable-next-line no-bitwise
  108. hash &= hash; // Convert to 32bit integer
  109. }
  110. return hash;
  111. };
  112. /**
  113. * convert `i18n.language` style str (e.g.: `en_US`) to kebab-case (e.g.: `en-US`),
  114. * which is expected by `<html lang>` and `Intl.DateTimeFormat`
  115. */
  116. export const getKebabCaseLangStr = (language) => language.replace(/_/g, "-");
  117. export const formatShortDateTime = (timestamp, language) =>
  118. new Intl.DateTimeFormat(getKebabCaseLangStr(language), {
  119. dateStyle: "short",
  120. timeStyle: "short",
  121. }).format(new Date(timestamp * 1000));
  122. export const formatShortDate = (timestamp, language) =>
  123. new Intl.DateTimeFormat(getKebabCaseLangStr(language), { dateStyle: "short" }).format(new Date(timestamp * 1000));
  124. export const formatBytes = (bytes, decimals = 2) => {
  125. if (bytes === 0) return "0 bytes";
  126. const k = 1024;
  127. const dm = decimals < 0 ? 0 : decimals;
  128. const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
  129. const i = Math.floor(Math.log(bytes) / Math.log(k));
  130. return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
  131. };
  132. export const formatNumber = (n) => {
  133. if (n === 0) {
  134. return n;
  135. }
  136. if (n % 1000 === 0) {
  137. return `${n / 1000}k`;
  138. }
  139. return n.toLocaleString();
  140. };
  141. export const formatPrice = (n) => {
  142. if (n % 100 === 0) {
  143. return `$${n / 100}`;
  144. }
  145. return `$${(n / 100).toPrecision(2)}`;
  146. };
  147. export const openUrl = (url) => {
  148. window.open(url, "_blank", "noopener,noreferrer");
  149. };
  150. export const sounds = {
  151. ding: {
  152. file: ding,
  153. label: "Ding",
  154. },
  155. juntos: {
  156. file: juntos,
  157. label: "Juntos",
  158. },
  159. pristine: {
  160. file: pristine,
  161. label: "Pristine",
  162. },
  163. dadum: {
  164. file: dadum,
  165. label: "Dadum",
  166. },
  167. pop: {
  168. file: pop,
  169. label: "Pop",
  170. },
  171. "pop-swoosh": {
  172. file: popSwoosh,
  173. label: "Pop swoosh",
  174. },
  175. beep: {
  176. file: beep,
  177. label: "Beep",
  178. },
  179. };
  180. export const playSound = async (id) => {
  181. const audio = new Audio(sounds[id].file);
  182. return audio.play();
  183. };
  184. // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
  185. // eslint-disable-next-line func-style
  186. export async function* fetchLinesIterator(fileURL, headers) {
  187. const utf8Decoder = new TextDecoder("utf-8");
  188. const response = await fetch(fileURL, {
  189. headers,
  190. });
  191. const reader = response.body.getReader();
  192. let { value: chunk, done: readerDone } = await reader.read();
  193. chunk = chunk ? utf8Decoder.decode(chunk) : "";
  194. const re = /\n|\r|\r\n/gm;
  195. let startIndex = 0;
  196. for (;;) {
  197. const result = re.exec(chunk);
  198. if (!result) {
  199. if (readerDone) {
  200. break;
  201. }
  202. const remainder = chunk.substr(startIndex);
  203. // eslint-disable-next-line no-await-in-loop
  204. ({ value: chunk, done: readerDone } = await reader.read());
  205. chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : "");
  206. startIndex = 0;
  207. re.lastIndex = 0;
  208. // eslint-disable-next-line no-continue
  209. continue;
  210. }
  211. yield chunk.substring(startIndex, result.index);
  212. startIndex = re.lastIndex;
  213. }
  214. if (startIndex < chunk.length) {
  215. yield chunk.substr(startIndex); // last line didn't end in a newline char
  216. }
  217. }
  218. export const randomAlphanumericString = (len) => {
  219. const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  220. let id = "";
  221. for (let i = 0; i < len; i += 1) {
  222. // eslint-disable-next-line no-bitwise
  223. id += alphabet[(Math.random() * alphabet.length) | 0];
  224. }
  225. return id;
  226. };
  227. export const urlB64ToUint8Array = (base64String) => {
  228. const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
  229. const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
  230. const rawData = window.atob(base64);
  231. const outputArray = new Uint8Array(rawData.length);
  232. for (let i = 0; i < rawData.length; i += 1) {
  233. outputArray[i] = rawData.charCodeAt(i);
  234. }
  235. return outputArray;
  236. };