From 049e01248e4f737894da420bce546b93370c6f14 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Sat, 16 Sep 2023 02:33:39 +0530 Subject: [PATCH 001/147] fix(mobile): user active status, network warning fix: user activity status is accurately updated Closes #463 chore: setup Vite-PWA. Closes #464 feat: added Warning banner when no network. Closes #456 --- mobile/dev-dist/registerSW.js | 1 + mobile/dev-dist/sw.js | 110 + mobile/dev-dist/workbox-b7fccfec.js | 4560 +++++++++++++++++ mobile/index.html | 15 +- mobile/package.json | 5 +- mobile/src/App.tsx | 1 - mobile/src/hooks/useIsUserActive.tsx | 7 +- mobile/src/utils/auth/Routes.tsx | 60 +- mobile/src/utils/auth/UserProvider.tsx | 9 +- .../src/utils/users/ActiveUsersProvider.tsx | 14 +- mobile/vite.config.ts | 103 +- mobile/yarn.lock | 2033 +++++++- .../markdown-viewer/MarkdownRenderer.tsx | 2 +- .../layout/AlertBanner/AlertBanner.tsx | 4 +- raven-app/src/hooks/useActiveState.tsx | 11 +- raven-app/src/hooks/useIsUserActive.ts | 6 +- .../src/utils/users/ActiveUsersProvider.tsx | 8 +- raven/api/user_availability.py | 10 +- .../doctype/raven_message/raven_message.py | 2 - 19 files changed, 6888 insertions(+), 73 deletions(-) create mode 100644 mobile/dev-dist/registerSW.js create mode 100644 mobile/dev-dist/sw.js create mode 100644 mobile/dev-dist/workbox-b7fccfec.js diff --git a/mobile/dev-dist/registerSW.js b/mobile/dev-dist/registerSW.js new file mode 100644 index 000000000..fdeaac12c --- /dev/null +++ b/mobile/dev-dist/registerSW.js @@ -0,0 +1 @@ +if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'module' }) \ No newline at end of file diff --git a/mobile/dev-dist/sw.js b/mobile/dev-dist/sw.js new file mode 100644 index 000000000..66c9955ab --- /dev/null +++ b/mobile/dev-dist/sw.js @@ -0,0 +1,110 @@ +/** + * Copyright 2018 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// If the loader is already loaded, just stop. +if (!self.define) { + let registry = {}; + + // Used for `eval` and `importScripts` where we can't get script URL by other means. + // In both cases, it's safe to use a global var because those functions are synchronous. + let nextDefineUri; + + const singleRequire = (uri, parentUri) => { + uri = new URL(uri + ".js", parentUri).href; + return registry[uri] || ( + + new Promise(resolve => { + if ("document" in self) { + const script = document.createElement("script"); + script.src = uri; + script.onload = resolve; + document.head.appendChild(script); + } else { + nextDefineUri = uri; + importScripts(uri); + resolve(); + } + }) + + .then(() => { + let promise = registry[uri]; + if (!promise) { + throw new Error(`Module ${uri} didn’t register its module`); + } + return promise; + }) + ); + }; + + self.define = (depsNames, factory) => { + const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href; + if (registry[uri]) { + // Module is already loading or loaded. + return; + } + let exports = {}; + const require = depUri => singleRequire(depUri, uri); + const specialDeps = { + module: { uri }, + exports, + require + }; + registry[uri] = Promise.all(depsNames.map( + depName => specialDeps[depName] || require(depName) + )).then(deps => { + factory(...deps); + return exports; + }); + }; +} +define(['./workbox-b7fccfec'], (function (workbox) { 'use strict'; + + self.skipWaiting(); + workbox.clientsClaim(); + + /** + * The precacheAndRoute() method efficiently caches and responds to + * requests for URLs in the manifest. + * See https://goo.gl/S9QRab + */ + workbox.precacheAndRoute([{ + "url": "registerSW.js", + "revision": "b2999fa3193e9ce6a83fa7d0681b1d1b" + }, { + "url": "index.html", + "revision": "0.e81avic025o" + }], {}); + workbox.cleanupOutdatedCaches(); + workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { + allowlist: [/^\/$/] + })); + workbox.registerRoute(/^https:\/\/fonts\.googleapis\.com\/.*/i, new workbox.CacheFirst({ + "cacheName": "google-fonts-cache", + plugins: [new workbox.ExpirationPlugin({ + maxEntries: 10, + maxAgeSeconds: 31536000 + }), new workbox.CacheableResponsePlugin({ + statuses: [0, 200] + })] + }), 'GET'); + workbox.registerRoute(/^https:\/\/fonts\.gstatic\.com\/.*/i, new workbox.CacheFirst({ + "cacheName": "gstatic-fonts-cache", + plugins: [new workbox.ExpirationPlugin({ + maxEntries: 10, + maxAgeSeconds: 31536000 + }), new workbox.CacheableResponsePlugin({ + statuses: [0, 200] + })] + }), 'GET'); + +})); diff --git a/mobile/dev-dist/workbox-b7fccfec.js b/mobile/dev-dist/workbox-b7fccfec.js new file mode 100644 index 000000000..da382ed82 --- /dev/null +++ b/mobile/dev-dist/workbox-b7fccfec.js @@ -0,0 +1,4560 @@ +define(['exports'], (function (exports) { 'use strict'; + + // @ts-ignore + try { + self['workbox:core:7.0.0'] && _(); + } catch (e) {} + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const logger = (() => { + // Don't overwrite this value if it's already set. + // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923 + if (!('__WB_DISABLE_DEV_LOGS' in globalThis)) { + self.__WB_DISABLE_DEV_LOGS = false; + } + let inGroup = false; + const methodToColorMap = { + debug: `#7f8c8d`, + log: `#2ecc71`, + warn: `#f39c12`, + error: `#c0392b`, + groupCollapsed: `#3498db`, + groupEnd: null // No colored prefix on groupEnd + }; + + const print = function (method, args) { + if (self.__WB_DISABLE_DEV_LOGS) { + return; + } + if (method === 'groupCollapsed') { + // Safari doesn't print all console.groupCollapsed() arguments: + // https://bugs.webkit.org/show_bug.cgi?id=182754 + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + console[method](...args); + return; + } + } + const styles = [`background: ${methodToColorMap[method]}`, `border-radius: 0.5em`, `color: white`, `font-weight: bold`, `padding: 2px 0.5em`]; + // When in a group, the workbox prefix is not displayed. + const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; + console[method](...logPrefix, ...args); + if (method === 'groupCollapsed') { + inGroup = true; + } + if (method === 'groupEnd') { + inGroup = false; + } + }; + // eslint-disable-next-line @typescript-eslint/ban-types + const api = {}; + const loggerMethods = Object.keys(methodToColorMap); + for (const key of loggerMethods) { + const method = key; + api[method] = (...args) => { + print(method, args); + }; + } + return api; + })(); + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const messages$1 = { + 'invalid-value': ({ + paramName, + validValueDescription, + value + }) => { + if (!paramName || !validValueDescription) { + throw new Error(`Unexpected input to 'invalid-value' error.`); + } + return `The '${paramName}' parameter was given a value with an ` + `unexpected value. ${validValueDescription} Received a value of ` + `${JSON.stringify(value)}.`; + }, + 'not-an-array': ({ + moduleName, + className, + funcName, + paramName + }) => { + if (!moduleName || !className || !funcName || !paramName) { + throw new Error(`Unexpected input to 'not-an-array' error.`); + } + return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className}.${funcName}()' must be an array.`; + }, + 'incorrect-type': ({ + expectedType, + paramName, + moduleName, + className, + funcName + }) => { + if (!expectedType || !paramName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'incorrect-type' error.`); + } + const classNameStr = className ? `${className}.` : ''; + return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}` + `${funcName}()' must be of type ${expectedType}.`; + }, + 'incorrect-class': ({ + expectedClassName, + paramName, + moduleName, + className, + funcName, + isReturnValueProblem + }) => { + if (!expectedClassName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'incorrect-class' error.`); + } + const classNameStr = className ? `${className}.` : ''; + if (isReturnValueProblem) { + return `The return value from ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`; + } + return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`; + }, + 'missing-a-method': ({ + expectedMethod, + paramName, + moduleName, + className, + funcName + }) => { + if (!expectedMethod || !paramName || !moduleName || !className || !funcName) { + throw new Error(`Unexpected input to 'missing-a-method' error.`); + } + return `${moduleName}.${className}.${funcName}() expected the ` + `'${paramName}' parameter to expose a '${expectedMethod}' method.`; + }, + 'add-to-cache-list-unexpected-type': ({ + entry + }) => { + return `An unexpected entry was passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' The entry ` + `'${JSON.stringify(entry)}' isn't supported. You must supply an array of ` + `strings with one or more characters, objects with a url property or ` + `Request objects.`; + }, + 'add-to-cache-list-conflicting-entries': ({ + firstEntry, + secondEntry + }) => { + if (!firstEntry || !secondEntry) { + throw new Error(`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`); + } + return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${firstEntry} but different revision details. Workbox is ` + `unable to cache and version the asset correctly. Please remove one ` + `of the entries.`; + }, + 'plugin-error-request-will-fetch': ({ + thrownErrorMessage + }) => { + if (!thrownErrorMessage) { + throw new Error(`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`); + } + return `An error was thrown by a plugins 'requestWillFetch()' method. ` + `The thrown error message was: '${thrownErrorMessage}'.`; + }, + 'invalid-cache-name': ({ + cacheNameId, + value + }) => { + if (!cacheNameId) { + throw new Error(`Expected a 'cacheNameId' for error 'invalid-cache-name'`); + } + return `You must provide a name containing at least one character for ` + `setCacheDetails({${cacheNameId}: '...'}). Received a value of ` + `'${JSON.stringify(value)}'`; + }, + 'unregister-route-but-not-found-with-method': ({ + method + }) => { + if (!method) { + throw new Error(`Unexpected input to ` + `'unregister-route-but-not-found-with-method' error.`); + } + return `The route you're trying to unregister was not previously ` + `registered for the method type '${method}'.`; + }, + 'unregister-route-route-not-registered': () => { + return `The route you're trying to unregister was not previously ` + `registered.`; + }, + 'queue-replay-failed': ({ + name + }) => { + return `Replaying the background sync queue '${name}' failed.`; + }, + 'duplicate-queue-name': ({ + name + }) => { + return `The Queue name '${name}' is already being used. ` + `All instances of backgroundSync.Queue must be given unique names.`; + }, + 'expired-test-without-max-age': ({ + methodName, + paramName + }) => { + return `The '${methodName}()' method can only be used when the ` + `'${paramName}' is used in the constructor.`; + }, + 'unsupported-route-type': ({ + moduleName, + className, + funcName, + paramName + }) => { + return `The supplied '${paramName}' parameter was an unsupported type. ` + `Please check the docs for ${moduleName}.${className}.${funcName} for ` + `valid input types.`; + }, + 'not-array-of-class': ({ + value, + expectedClass, + moduleName, + className, + funcName, + paramName + }) => { + return `The supplied '${paramName}' parameter must be an array of ` + `'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` + `Please check the call to ${moduleName}.${className}.${funcName}() ` + `to fix the issue.`; + }, + 'max-entries-or-age-required': ({ + moduleName, + className, + funcName + }) => { + return `You must define either config.maxEntries or config.maxAgeSeconds` + `in ${moduleName}.${className}.${funcName}`; + }, + 'statuses-or-headers-required': ({ + moduleName, + className, + funcName + }) => { + return `You must define either config.statuses or config.headers` + `in ${moduleName}.${className}.${funcName}`; + }, + 'invalid-string': ({ + moduleName, + funcName, + paramName + }) => { + if (!paramName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'invalid-string' error.`); + } + return `When using strings, the '${paramName}' parameter must start with ` + `'http' (for cross-origin matches) or '/' (for same-origin matches). ` + `Please see the docs for ${moduleName}.${funcName}() for ` + `more info.`; + }, + 'channel-name-required': () => { + return `You must provide a channelName to construct a ` + `BroadcastCacheUpdate instance.`; + }, + 'invalid-responses-are-same-args': () => { + return `The arguments passed into responsesAreSame() appear to be ` + `invalid. Please ensure valid Responses are used.`; + }, + 'expire-custom-caches-only': () => { + return `You must provide a 'cacheName' property when using the ` + `expiration plugin with a runtime caching strategy.`; + }, + 'unit-must-be-bytes': ({ + normalizedRangeHeader + }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`); + } + return `The 'unit' portion of the Range header must be set to 'bytes'. ` + `The Range header provided was "${normalizedRangeHeader}"`; + }, + 'single-range-only': ({ + normalizedRangeHeader + }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'single-range-only' error.`); + } + return `Multiple ranges are not supported. Please use a single start ` + `value, and optional end value. The Range header provided was ` + `"${normalizedRangeHeader}"`; + }, + 'invalid-range-values': ({ + normalizedRangeHeader + }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'invalid-range-values' error.`); + } + return `The Range header is missing both start and end values. At least ` + `one of those values is needed. The Range header provided was ` + `"${normalizedRangeHeader}"`; + }, + 'no-range-header': () => { + return `No Range header was found in the Request provided.`; + }, + 'range-not-satisfiable': ({ + size, + start, + end + }) => { + return `The start (${start}) and end (${end}) values in the Range are ` + `not satisfiable by the cached response, which is ${size} bytes.`; + }, + 'attempt-to-cache-non-get-request': ({ + url, + method + }) => { + return `Unable to cache '${url}' because it is a '${method}' request and ` + `only 'GET' requests can be cached.`; + }, + 'cache-put-with-no-response': ({ + url + }) => { + return `There was an attempt to cache '${url}' but the response was not ` + `defined.`; + }, + 'no-response': ({ + url, + error + }) => { + let message = `The strategy could not generate a response for '${url}'.`; + if (error) { + message += ` The underlying error is ${error}.`; + } + return message; + }, + 'bad-precaching-response': ({ + url, + status + }) => { + return `The precaching request for '${url}' failed` + (status ? ` with an HTTP status of ${status}.` : `.`); + }, + 'non-precached-url': ({ + url + }) => { + return `createHandlerBoundToURL('${url}') was called, but that URL is ` + `not precached. Please pass in a URL that is precached instead.`; + }, + 'add-to-cache-list-conflicting-integrities': ({ + url + }) => { + return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${url} with different integrity values. Please remove one of them.`; + }, + 'missing-precache-entry': ({ + cacheName, + url + }) => { + return `Unable to find a precached response in ${cacheName} for ${url}.`; + }, + 'cross-origin-copy-response': ({ + origin + }) => { + return `workbox-core.copyResponse() can only be used with same-origin ` + `responses. It was passed a response with origin ${origin}.`; + }, + 'opaque-streams-source': ({ + type + }) => { + const message = `One of the workbox-streams sources resulted in an ` + `'${type}' response.`; + if (type === 'opaqueredirect') { + return `${message} Please do not use a navigation request that results ` + `in a redirect as a source.`; + } + return `${message} Please ensure your sources are CORS-enabled.`; + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const generatorFunction = (code, details = {}) => { + const message = messages$1[code]; + if (!message) { + throw new Error(`Unable to find message for code '${code}'.`); + } + return message(details); + }; + const messageGenerator = generatorFunction; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Workbox errors should be thrown with this class. + * This allows use to ensure the type easily in tests, + * helps developers identify errors from workbox + * easily and allows use to optimise error + * messages correctly. + * + * @private + */ + class WorkboxError extends Error { + /** + * + * @param {string} errorCode The error code that + * identifies this particular error. + * @param {Object=} details Any relevant arguments + * that will help developers identify issues should + * be added as a key on the context object. + */ + constructor(errorCode, details) { + const message = messageGenerator(errorCode, details); + super(message); + this.name = errorCode; + this.details = details; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /* + * This method throws if the supplied value is not an array. + * The destructed values are required to produce a meaningful error for users. + * The destructed and restructured object is so it's clear what is + * needed. + */ + const isArray = (value, details) => { + if (!Array.isArray(value)) { + throw new WorkboxError('not-an-array', details); + } + }; + const hasMethod = (object, expectedMethod, details) => { + const type = typeof object[expectedMethod]; + if (type !== 'function') { + details['expectedMethod'] = expectedMethod; + throw new WorkboxError('missing-a-method', details); + } + }; + const isType = (object, expectedType, details) => { + if (typeof object !== expectedType) { + details['expectedType'] = expectedType; + throw new WorkboxError('incorrect-type', details); + } + }; + const isInstance = (object, + // Need the general type to do the check later. + // eslint-disable-next-line @typescript-eslint/ban-types + expectedClass, details) => { + if (!(object instanceof expectedClass)) { + details['expectedClassName'] = expectedClass.name; + throw new WorkboxError('incorrect-class', details); + } + }; + const isOneOf = (value, validValues, details) => { + if (!validValues.includes(value)) { + details['validValueDescription'] = `Valid values are ${JSON.stringify(validValues)}.`; + throw new WorkboxError('invalid-value', details); + } + }; + const isArrayOfClass = (value, + // Need general type to do check later. + expectedClass, + // eslint-disable-line + details) => { + const error = new WorkboxError('not-array-of-class', details); + if (!Array.isArray(value)) { + throw error; + } + for (const item of value) { + if (!(item instanceof expectedClass)) { + throw error; + } + } + }; + const finalAssertExports = { + hasMethod, + isArray, + isInstance, + isOneOf, + isType, + isArrayOfClass + }; + + // @ts-ignore + try { + self['workbox:routing:7.0.0'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The default HTTP method, 'GET', used when there's no specific method + * configured for a route. + * + * @type {string} + * + * @private + */ + const defaultMethod = 'GET'; + /** + * The list of valid HTTP methods associated with requests that could be routed. + * + * @type {Array} + * + * @private + */ + const validMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {function()|Object} handler Either a function, or an object with a + * 'handle' method. + * @return {Object} An object with a handle method. + * + * @private + */ + const normalizeHandler = handler => { + if (handler && typeof handler === 'object') { + { + finalAssertExports.hasMethod(handler, 'handle', { + moduleName: 'workbox-routing', + className: 'Route', + funcName: 'constructor', + paramName: 'handler' + }); + } + return handler; + } else { + { + finalAssertExports.isType(handler, 'function', { + moduleName: 'workbox-routing', + className: 'Route', + funcName: 'constructor', + paramName: 'handler' + }); + } + return { + handle: handler + }; + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A `Route` consists of a pair of callback functions, "match" and "handler". + * The "match" callback determine if a route should be used to "handle" a + * request by returning a non-falsy value if it can. The "handler" callback + * is called when there is a match and should return a Promise that resolves + * to a `Response`. + * + * @memberof workbox-routing + */ + class Route { + /** + * Constructor for Route class. + * + * @param {workbox-routing~matchCallback} match + * A callback function that determines whether the route matches a given + * `fetch` event by returning a non-falsy value. + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resolving to a Response. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + */ + constructor(match, handler, method = defaultMethod) { + { + finalAssertExports.isType(match, 'function', { + moduleName: 'workbox-routing', + className: 'Route', + funcName: 'constructor', + paramName: 'match' + }); + if (method) { + finalAssertExports.isOneOf(method, validMethods, { + paramName: 'method' + }); + } + } + // These values are referenced directly by Router so cannot be + // altered by minificaton. + this.handler = normalizeHandler(handler); + this.match = match; + this.method = method; + } + /** + * + * @param {workbox-routing-handlerCallback} handler A callback + * function that returns a Promise resolving to a Response + */ + setCatchHandler(handler) { + this.catchHandler = normalizeHandler(handler); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * RegExpRoute makes it easy to create a regular expression based + * {@link workbox-routing.Route}. + * + * For same-origin requests the RegExp only needs to match part of the URL. For + * requests against third-party servers, you must define a RegExp that matches + * the start of the URL. + * + * @memberof workbox-routing + * @extends workbox-routing.Route + */ + class RegExpRoute extends Route { + /** + * If the regular expression contains + * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references}, + * the captured values will be passed to the + * {@link workbox-routing~handlerCallback} `params` + * argument. + * + * @param {RegExp} regExp The regular expression to match against URLs. + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + */ + constructor(regExp, handler, method) { + { + finalAssertExports.isInstance(regExp, RegExp, { + moduleName: 'workbox-routing', + className: 'RegExpRoute', + funcName: 'constructor', + paramName: 'pattern' + }); + } + const match = ({ + url + }) => { + const result = regExp.exec(url.href); + // Return immediately if there's no match. + if (!result) { + return; + } + // Require that the match start at the first character in the URL string + // if it's a cross-origin request. + // See https://github.com/GoogleChrome/workbox/issues/281 for the context + // behind this behavior. + if (url.origin !== location.origin && result.index !== 0) { + { + logger.debug(`The regular expression '${regExp.toString()}' only partially matched ` + `against the cross-origin URL '${url.toString()}'. RegExpRoute's will only ` + `handle cross-origin requests if they match the entire URL.`); + } + return; + } + // If the route matches, but there aren't any capture groups defined, then + // this will return [], which is truthy and therefore sufficient to + // indicate a match. + // If there are capture groups, then it will return their values. + return result.slice(1); + }; + super(match, handler, method); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const getFriendlyURL = url => { + const urlObj = new URL(String(url), location.href); + // See https://github.com/GoogleChrome/workbox/issues/2323 + // We want to include everything, except for the origin if it's same-origin. + return urlObj.href.replace(new RegExp(`^${location.origin}`), ''); + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The Router can be used to process a `FetchEvent` using one or more + * {@link workbox-routing.Route}, responding with a `Response` if + * a matching route exists. + * + * If no route matches a given a request, the Router will use a "default" + * handler if one is defined. + * + * Should the matching Route throw an error, the Router will use a "catch" + * handler if one is defined to gracefully deal with issues and respond with a + * Request. + * + * If a request matches multiple routes, the **earliest** registered route will + * be used to respond to the request. + * + * @memberof workbox-routing + */ + class Router { + /** + * Initializes a new Router. + */ + constructor() { + this._routes = new Map(); + this._defaultHandlerMap = new Map(); + } + /** + * @return {Map>} routes A `Map` of HTTP + * method name ('GET', etc.) to an array of all the corresponding `Route` + * instances that are registered. + */ + get routes() { + return this._routes; + } + /** + * Adds a fetch event listener to respond to events when a route matches + * the event's request. + */ + addFetchListener() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener('fetch', event => { + const { + request + } = event; + const responsePromise = this.handleRequest({ + request, + event + }); + if (responsePromise) { + event.respondWith(responsePromise); + } + }); + } + /** + * Adds a message event listener for URLs to cache from the window. + * This is useful to cache resources loaded on the page prior to when the + * service worker started controlling it. + * + * The format of the message data sent from the window should be as follows. + * Where the `urlsToCache` array may consist of URL strings or an array of + * URL string + `requestInit` object (the same as you'd pass to `fetch()`). + * + * ``` + * { + * type: 'CACHE_URLS', + * payload: { + * urlsToCache: [ + * './script1.js', + * './script2.js', + * ['./script3.js', {mode: 'no-cors'}], + * ], + * }, + * } + * ``` + */ + addCacheListener() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener('message', event => { + // event.data is type 'any' + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (event.data && event.data.type === 'CACHE_URLS') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { + payload + } = event.data; + { + logger.debug(`Caching URLs from the window`, payload.urlsToCache); + } + const requestPromises = Promise.all(payload.urlsToCache.map(entry => { + if (typeof entry === 'string') { + entry = [entry]; + } + const request = new Request(...entry); + return this.handleRequest({ + request, + event + }); + // TODO(philipwalton): TypeScript errors without this typecast for + // some reason (probably a bug). The real type here should work but + // doesn't: `Array | undefined>`. + })); // TypeScript + event.waitUntil(requestPromises); + // If a MessageChannel was used, reply to the message on success. + if (event.ports && event.ports[0]) { + void requestPromises.then(() => event.ports[0].postMessage(true)); + } + } + }); + } + /** + * Apply the routing rules to a FetchEvent object to get a Response from an + * appropriate Route's handler. + * + * @param {Object} options + * @param {Request} options.request The request to handle. + * @param {ExtendableEvent} options.event The event that triggered the + * request. + * @return {Promise|undefined} A promise is returned if a + * registered route can handle the request. If there is no matching + * route and there's no `defaultHandler`, `undefined` is returned. + */ + handleRequest({ + request, + event + }) { + { + finalAssertExports.isInstance(request, Request, { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'handleRequest', + paramName: 'options.request' + }); + } + const url = new URL(request.url, location.href); + if (!url.protocol.startsWith('http')) { + { + logger.debug(`Workbox Router only supports URLs that start with 'http'.`); + } + return; + } + const sameOrigin = url.origin === location.origin; + const { + params, + route + } = this.findMatchingRoute({ + event, + request, + sameOrigin, + url + }); + let handler = route && route.handler; + const debugMessages = []; + { + if (handler) { + debugMessages.push([`Found a route to handle this request:`, route]); + if (params) { + debugMessages.push([`Passing the following params to the route's handler:`, params]); + } + } + } + // If we don't have a handler because there was no matching route, then + // fall back to defaultHandler if that's defined. + const method = request.method; + if (!handler && this._defaultHandlerMap.has(method)) { + { + debugMessages.push(`Failed to find a matching route. Falling ` + `back to the default handler for ${method}.`); + } + handler = this._defaultHandlerMap.get(method); + } + if (!handler) { + { + // No handler so Workbox will do nothing. If logs is set of debug + // i.e. verbose, we should print out this information. + logger.debug(`No route found for: ${getFriendlyURL(url)}`); + } + return; + } + { + // We have a handler, meaning Workbox is going to handle the route. + // print the routing details to the console. + logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`); + debugMessages.forEach(msg => { + if (Array.isArray(msg)) { + logger.log(...msg); + } else { + logger.log(msg); + } + }); + logger.groupEnd(); + } + // Wrap in try and catch in case the handle method throws a synchronous + // error. It should still callback to the catch handler. + let responsePromise; + try { + responsePromise = handler.handle({ + url, + request, + event, + params + }); + } catch (err) { + responsePromise = Promise.reject(err); + } + // Get route's catch handler, if it exists + const catchHandler = route && route.catchHandler; + if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) { + responsePromise = responsePromise.catch(async err => { + // If there's a route catch handler, process that first + if (catchHandler) { + { + // Still include URL here as it will be async from the console group + // and may not make sense without the URL + logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`); + logger.error(`Error thrown by:`, route); + logger.error(err); + logger.groupEnd(); + } + try { + return await catchHandler.handle({ + url, + request, + event, + params + }); + } catch (catchErr) { + if (catchErr instanceof Error) { + err = catchErr; + } + } + } + if (this._catchHandler) { + { + // Still include URL here as it will be async from the console group + // and may not make sense without the URL + logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to global Catch Handler.`); + logger.error(`Error thrown by:`, route); + logger.error(err); + logger.groupEnd(); + } + return this._catchHandler.handle({ + url, + request, + event + }); + } + throw err; + }); + } + return responsePromise; + } + /** + * Checks a request and URL (and optionally an event) against the list of + * registered routes, and if there's a match, returns the corresponding + * route along with any params generated by the match. + * + * @param {Object} options + * @param {URL} options.url + * @param {boolean} options.sameOrigin The result of comparing `url.origin` + * against the current origin. + * @param {Request} options.request The request to match. + * @param {Event} options.event The corresponding event. + * @return {Object} An object with `route` and `params` properties. + * They are populated if a matching route was found or `undefined` + * otherwise. + */ + findMatchingRoute({ + url, + sameOrigin, + request, + event + }) { + const routes = this._routes.get(request.method) || []; + for (const route of routes) { + let params; + // route.match returns type any, not possible to change right now. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const matchResult = route.match({ + url, + sameOrigin, + request, + event + }); + if (matchResult) { + { + // Warn developers that using an async matchCallback is almost always + // not the right thing to do. + if (matchResult instanceof Promise) { + logger.warn(`While routing ${getFriendlyURL(url)}, an async ` + `matchCallback function was used. Please convert the ` + `following route to use a synchronous matchCallback function:`, route); + } + } + // See https://github.com/GoogleChrome/workbox/issues/2079 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + params = matchResult; + if (Array.isArray(params) && params.length === 0) { + // Instead of passing an empty array in as params, use undefined. + params = undefined; + } else if (matchResult.constructor === Object && + // eslint-disable-line + Object.keys(matchResult).length === 0) { + // Instead of passing an empty object in as params, use undefined. + params = undefined; + } else if (typeof matchResult === 'boolean') { + // For the boolean value true (rather than just something truth-y), + // don't set params. + // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353 + params = undefined; + } + // Return early if have a match. + return { + route, + params + }; + } + } + // If no match was found above, return and empty object. + return {}; + } + /** + * Define a default `handler` that's called when no routes explicitly + * match the incoming request. + * + * Each HTTP method ('GET', 'POST', etc.) gets its own default handler. + * + * Without a default handler, unmatched requests will go against the + * network as if there were no service worker present. + * + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {string} [method='GET'] The HTTP method to associate with this + * default handler. Each method has its own default. + */ + setDefaultHandler(handler, method = defaultMethod) { + this._defaultHandlerMap.set(method, normalizeHandler(handler)); + } + /** + * If a Route throws an error while handling a request, this `handler` + * will be called and given a chance to provide a response. + * + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + */ + setCatchHandler(handler) { + this._catchHandler = normalizeHandler(handler); + } + /** + * Registers a route with the router. + * + * @param {workbox-routing.Route} route The route to register. + */ + registerRoute(route) { + { + finalAssertExports.isType(route, 'object', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route' + }); + finalAssertExports.hasMethod(route, 'match', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route' + }); + finalAssertExports.isType(route.handler, 'object', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route' + }); + finalAssertExports.hasMethod(route.handler, 'handle', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route.handler' + }); + finalAssertExports.isType(route.method, 'string', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route.method' + }); + } + if (!this._routes.has(route.method)) { + this._routes.set(route.method, []); + } + // Give precedence to all of the earlier routes by adding this additional + // route to the end of the array. + this._routes.get(route.method).push(route); + } + /** + * Unregisters a route with the router. + * + * @param {workbox-routing.Route} route The route to unregister. + */ + unregisterRoute(route) { + if (!this._routes.has(route.method)) { + throw new WorkboxError('unregister-route-but-not-found-with-method', { + method: route.method + }); + } + const routeIndex = this._routes.get(route.method).indexOf(route); + if (routeIndex > -1) { + this._routes.get(route.method).splice(routeIndex, 1); + } else { + throw new WorkboxError('unregister-route-route-not-registered'); + } + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let defaultRouter; + /** + * Creates a new, singleton Router instance if one does not exist. If one + * does already exist, that instance is returned. + * + * @private + * @return {Router} + */ + const getOrCreateDefaultRouter = () => { + if (!defaultRouter) { + defaultRouter = new Router(); + // The helpers that use the default Router assume these listeners exist. + defaultRouter.addFetchListener(); + defaultRouter.addCacheListener(); + } + return defaultRouter; + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Easily register a RegExp, string, or function with a caching + * strategy to a singleton Router instance. + * + * This method will generate a Route for you if needed and + * call {@link workbox-routing.Router#registerRoute}. + * + * @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture + * If the capture param is a `Route`, all other arguments will be ignored. + * @param {workbox-routing~handlerCallback} [handler] A callback + * function that returns a Promise resulting in a Response. This parameter + * is required if `capture` is not a `Route` object. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + * @return {workbox-routing.Route} The generated `Route`. + * + * @memberof workbox-routing + */ + function registerRoute(capture, handler, method) { + let route; + if (typeof capture === 'string') { + const captureUrl = new URL(capture, location.href); + { + if (!(capture.startsWith('/') || capture.startsWith('http'))) { + throw new WorkboxError('invalid-string', { + moduleName: 'workbox-routing', + funcName: 'registerRoute', + paramName: 'capture' + }); + } + // We want to check if Express-style wildcards are in the pathname only. + // TODO: Remove this log message in v4. + const valueToCheck = capture.startsWith('http') ? captureUrl.pathname : capture; + // See https://github.com/pillarjs/path-to-regexp#parameters + const wildcards = '[*:?+]'; + if (new RegExp(`${wildcards}`).exec(valueToCheck)) { + logger.debug(`The '$capture' parameter contains an Express-style wildcard ` + `character (${wildcards}). Strings are now always interpreted as ` + `exact matches; use a RegExp for partial or wildcard matches.`); + } + } + const matchCallback = ({ + url + }) => { + { + if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) { + logger.debug(`${capture} only partially matches the cross-origin URL ` + `${url.toString()}. This route will only handle cross-origin requests ` + `if they match the entire URL.`); + } + } + return url.href === captureUrl.href; + }; + // If `capture` is a string then `handler` and `method` must be present. + route = new Route(matchCallback, handler, method); + } else if (capture instanceof RegExp) { + // If `capture` is a `RegExp` then `handler` and `method` must be present. + route = new RegExpRoute(capture, handler, method); + } else if (typeof capture === 'function') { + // If `capture` is a function then `handler` and `method` must be present. + route = new Route(capture, handler, method); + } else if (capture instanceof Route) { + route = capture; + } else { + throw new WorkboxError('unsupported-route-type', { + moduleName: 'workbox-routing', + funcName: 'registerRoute', + paramName: 'capture' + }); + } + const defaultRouter = getOrCreateDefaultRouter(); + defaultRouter.registerRoute(route); + return route; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const _cacheNameDetails = { + googleAnalytics: 'googleAnalytics', + precache: 'precache-v2', + prefix: 'workbox', + runtime: 'runtime', + suffix: typeof registration !== 'undefined' ? registration.scope : '' + }; + const _createCacheName = cacheName => { + return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix].filter(value => value && value.length > 0).join('-'); + }; + const eachCacheNameDetail = fn => { + for (const key of Object.keys(_cacheNameDetails)) { + fn(key); + } + }; + const cacheNames = { + updateDetails: details => { + eachCacheNameDetail(key => { + if (typeof details[key] === 'string') { + _cacheNameDetails[key] = details[key]; + } + }); + }, + getGoogleAnalyticsName: userCacheName => { + return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics); + }, + getPrecacheName: userCacheName => { + return userCacheName || _createCacheName(_cacheNameDetails.precache); + }, + getPrefix: () => { + return _cacheNameDetails.prefix; + }, + getRuntimeName: userCacheName => { + return userCacheName || _createCacheName(_cacheNameDetails.runtime); + }, + getSuffix: () => { + return _cacheNameDetails.suffix; + } + }; + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A helper function that prevents a promise from being flagged as unused. + * + * @private + **/ + function dontWaitFor(promise) { + // Effective no-op. + void promise.then(() => {}); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + // Callbacks to be executed whenever there's a quota error. + // Can't change Function type right now. + // eslint-disable-next-line @typescript-eslint/ban-types + const quotaErrorCallbacks = new Set(); + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds a function to the set of quotaErrorCallbacks that will be executed if + * there's a quota error. + * + * @param {Function} callback + * @memberof workbox-core + */ + // Can't change Function type + // eslint-disable-next-line @typescript-eslint/ban-types + function registerQuotaErrorCallback(callback) { + { + finalAssertExports.isType(callback, 'function', { + moduleName: 'workbox-core', + funcName: 'register', + paramName: 'callback' + }); + } + quotaErrorCallbacks.add(callback); + { + logger.log('Registered a callback to respond to quota errors.', callback); + } + } + + function _extends() { + _extends = Object.assign ? Object.assign.bind() : function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); + } + + const instanceOfAny = (object, constructors) => constructors.some(c => object instanceof c); + let idbProxyableTypes; + let cursorAdvanceMethods; + // This is a function to prevent it throwing up in node environments. + function getIdbProxyableTypes() { + return idbProxyableTypes || (idbProxyableTypes = [IDBDatabase, IDBObjectStore, IDBIndex, IDBCursor, IDBTransaction]); + } + // This is a function to prevent it throwing up in node environments. + function getCursorAdvanceMethods() { + return cursorAdvanceMethods || (cursorAdvanceMethods = [IDBCursor.prototype.advance, IDBCursor.prototype.continue, IDBCursor.prototype.continuePrimaryKey]); + } + const cursorRequestMap = new WeakMap(); + const transactionDoneMap = new WeakMap(); + const transactionStoreNamesMap = new WeakMap(); + const transformCache = new WeakMap(); + const reverseTransformCache = new WeakMap(); + function promisifyRequest(request) { + const promise = new Promise((resolve, reject) => { + const unlisten = () => { + request.removeEventListener('success', success); + request.removeEventListener('error', error); + }; + const success = () => { + resolve(wrap(request.result)); + unlisten(); + }; + const error = () => { + reject(request.error); + unlisten(); + }; + request.addEventListener('success', success); + request.addEventListener('error', error); + }); + promise.then(value => { + // Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval + // (see wrapFunction). + if (value instanceof IDBCursor) { + cursorRequestMap.set(value, request); + } + // Catching to avoid "Uncaught Promise exceptions" + }).catch(() => {}); + // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This + // is because we create many promises from a single IDBRequest. + reverseTransformCache.set(promise, request); + return promise; + } + function cacheDonePromiseForTransaction(tx) { + // Early bail if we've already created a done promise for this transaction. + if (transactionDoneMap.has(tx)) return; + const done = new Promise((resolve, reject) => { + const unlisten = () => { + tx.removeEventListener('complete', complete); + tx.removeEventListener('error', error); + tx.removeEventListener('abort', error); + }; + const complete = () => { + resolve(); + unlisten(); + }; + const error = () => { + reject(tx.error || new DOMException('AbortError', 'AbortError')); + unlisten(); + }; + tx.addEventListener('complete', complete); + tx.addEventListener('error', error); + tx.addEventListener('abort', error); + }); + // Cache it for later retrieval. + transactionDoneMap.set(tx, done); + } + let idbProxyTraps = { + get(target, prop, receiver) { + if (target instanceof IDBTransaction) { + // Special handling for transaction.done. + if (prop === 'done') return transactionDoneMap.get(target); + // Polyfill for objectStoreNames because of Edge. + if (prop === 'objectStoreNames') { + return target.objectStoreNames || transactionStoreNamesMap.get(target); + } + // Make tx.store return the only store in the transaction, or undefined if there are many. + if (prop === 'store') { + return receiver.objectStoreNames[1] ? undefined : receiver.objectStore(receiver.objectStoreNames[0]); + } + } + // Else transform whatever we get back. + return wrap(target[prop]); + }, + set(target, prop, value) { + target[prop] = value; + return true; + }, + has(target, prop) { + if (target instanceof IDBTransaction && (prop === 'done' || prop === 'store')) { + return true; + } + return prop in target; + } + }; + function replaceTraps(callback) { + idbProxyTraps = callback(idbProxyTraps); + } + function wrapFunction(func) { + // Due to expected object equality (which is enforced by the caching in `wrap`), we + // only create one new func per func. + // Edge doesn't support objectStoreNames (booo), so we polyfill it here. + if (func === IDBDatabase.prototype.transaction && !('objectStoreNames' in IDBTransaction.prototype)) { + return function (storeNames, ...args) { + const tx = func.call(unwrap(this), storeNames, ...args); + transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]); + return wrap(tx); + }; + } + // Cursor methods are special, as the behaviour is a little more different to standard IDB. In + // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the + // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense + // with real promises, so each advance methods returns a new promise for the cursor object, or + // undefined if the end of the cursor has been reached. + if (getCursorAdvanceMethods().includes(func)) { + return function (...args) { + // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use + // the original object. + func.apply(unwrap(this), args); + return wrap(cursorRequestMap.get(this)); + }; + } + return function (...args) { + // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use + // the original object. + return wrap(func.apply(unwrap(this), args)); + }; + } + function transformCachableValue(value) { + if (typeof value === 'function') return wrapFunction(value); + // This doesn't return, it just creates a 'done' promise for the transaction, + // which is later returned for transaction.done (see idbObjectHandler). + if (value instanceof IDBTransaction) cacheDonePromiseForTransaction(value); + if (instanceOfAny(value, getIdbProxyableTypes())) return new Proxy(value, idbProxyTraps); + // Return the same value back if we're not going to transform it. + return value; + } + function wrap(value) { + // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because + // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached. + if (value instanceof IDBRequest) return promisifyRequest(value); + // If we've already transformed this value before, reuse the transformed value. + // This is faster, but it also provides object equality. + if (transformCache.has(value)) return transformCache.get(value); + const newValue = transformCachableValue(value); + // Not all types are transformed. + // These may be primitive types, so they can't be WeakMap keys. + if (newValue !== value) { + transformCache.set(value, newValue); + reverseTransformCache.set(newValue, value); + } + return newValue; + } + const unwrap = value => reverseTransformCache.get(value); + + /** + * Open a database. + * + * @param name Name of the database. + * @param version Schema version. + * @param callbacks Additional callbacks. + */ + function openDB(name, version, { + blocked, + upgrade, + blocking, + terminated + } = {}) { + const request = indexedDB.open(name, version); + const openPromise = wrap(request); + if (upgrade) { + request.addEventListener('upgradeneeded', event => { + upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event); + }); + } + if (blocked) { + request.addEventListener('blocked', event => blocked( + // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405 + event.oldVersion, event.newVersion, event)); + } + openPromise.then(db => { + if (terminated) db.addEventListener('close', () => terminated()); + if (blocking) { + db.addEventListener('versionchange', event => blocking(event.oldVersion, event.newVersion, event)); + } + }).catch(() => {}); + return openPromise; + } + /** + * Delete a database. + * + * @param name Name of the database. + */ + function deleteDB(name, { + blocked + } = {}) { + const request = indexedDB.deleteDatabase(name); + if (blocked) { + request.addEventListener('blocked', event => blocked( + // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405 + event.oldVersion, event)); + } + return wrap(request).then(() => undefined); + } + const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count']; + const writeMethods = ['put', 'add', 'delete', 'clear']; + const cachedMethods = new Map(); + function getMethod(target, prop) { + if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === 'string')) { + return; + } + if (cachedMethods.get(prop)) return cachedMethods.get(prop); + const targetFuncName = prop.replace(/FromIndex$/, ''); + const useIndex = prop !== targetFuncName; + const isWrite = writeMethods.includes(targetFuncName); + if ( + // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge. + !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))) { + return; + } + const method = async function (storeName, ...args) { + // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :( + const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly'); + let target = tx.store; + if (useIndex) target = target.index(args.shift()); + // Must reject if op rejects. + // If it's a write operation, must reject if tx.done rejects. + // Must reject with op rejection first. + // Must resolve with op value. + // Must handle both promises (no unhandled rejections) + return (await Promise.all([target[targetFuncName](...args), isWrite && tx.done]))[0]; + }; + cachedMethods.set(prop, method); + return method; + } + replaceTraps(oldTraps => _extends({}, oldTraps, { + get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver), + has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop) + })); + + // @ts-ignore + try { + self['workbox:expiration:7.0.0'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const DB_NAME = 'workbox-expiration'; + const CACHE_OBJECT_STORE = 'cache-entries'; + const normalizeURL = unNormalizedUrl => { + const url = new URL(unNormalizedUrl, location.href); + url.hash = ''; + return url.href; + }; + /** + * Returns the timestamp model. + * + * @private + */ + class CacheTimestampsModel { + /** + * + * @param {string} cacheName + * + * @private + */ + constructor(cacheName) { + this._db = null; + this._cacheName = cacheName; + } + /** + * Performs an upgrade of indexedDB. + * + * @param {IDBPDatabase} db + * + * @private + */ + _upgradeDb(db) { + // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we + // have to use the `id` keyPath here and create our own values (a + // concatenation of `url + cacheName`) instead of simply using + // `keyPath: ['url', 'cacheName']`, which is supported in other browsers. + const objStore = db.createObjectStore(CACHE_OBJECT_STORE, { + keyPath: 'id' + }); + // TODO(philipwalton): once we don't have to support EdgeHTML, we can + // create a single index with the keyPath `['cacheName', 'timestamp']` + // instead of doing both these indexes. + objStore.createIndex('cacheName', 'cacheName', { + unique: false + }); + objStore.createIndex('timestamp', 'timestamp', { + unique: false + }); + } + /** + * Performs an upgrade of indexedDB and deletes deprecated DBs. + * + * @param {IDBPDatabase} db + * + * @private + */ + _upgradeDbAndDeleteOldDbs(db) { + this._upgradeDb(db); + if (this._cacheName) { + void deleteDB(this._cacheName); + } + } + /** + * @param {string} url + * @param {number} timestamp + * + * @private + */ + async setTimestamp(url, timestamp) { + url = normalizeURL(url); + const entry = { + url, + timestamp, + cacheName: this._cacheName, + // Creating an ID from the URL and cache name won't be necessary once + // Edge switches to Chromium and all browsers we support work with + // array keyPaths. + id: this._getId(url) + }; + const db = await this.getDb(); + const tx = db.transaction(CACHE_OBJECT_STORE, 'readwrite', { + durability: 'relaxed' + }); + await tx.store.put(entry); + await tx.done; + } + /** + * Returns the timestamp stored for a given URL. + * + * @param {string} url + * @return {number | undefined} + * + * @private + */ + async getTimestamp(url) { + const db = await this.getDb(); + const entry = await db.get(CACHE_OBJECT_STORE, this._getId(url)); + return entry === null || entry === void 0 ? void 0 : entry.timestamp; + } + /** + * Iterates through all the entries in the object store (from newest to + * oldest) and removes entries once either `maxCount` is reached or the + * entry's timestamp is less than `minTimestamp`. + * + * @param {number} minTimestamp + * @param {number} maxCount + * @return {Array} + * + * @private + */ + async expireEntries(minTimestamp, maxCount) { + const db = await this.getDb(); + let cursor = await db.transaction(CACHE_OBJECT_STORE).store.index('timestamp').openCursor(null, 'prev'); + const entriesToDelete = []; + let entriesNotDeletedCount = 0; + while (cursor) { + const result = cursor.value; + // TODO(philipwalton): once we can use a multi-key index, we + // won't have to check `cacheName` here. + if (result.cacheName === this._cacheName) { + // Delete an entry if it's older than the max age or + // if we already have the max number allowed. + if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) { + // TODO(philipwalton): we should be able to delete the + // entry right here, but doing so causes an iteration + // bug in Safari stable (fixed in TP). Instead we can + // store the keys of the entries to delete, and then + // delete the separate transactions. + // https://github.com/GoogleChrome/workbox/issues/1978 + // cursor.delete(); + // We only need to return the URL, not the whole entry. + entriesToDelete.push(cursor.value); + } else { + entriesNotDeletedCount++; + } + } + cursor = await cursor.continue(); + } + // TODO(philipwalton): once the Safari bug in the following issue is fixed, + // we should be able to remove this loop and do the entry deletion in the + // cursor loop above: + // https://github.com/GoogleChrome/workbox/issues/1978 + const urlsDeleted = []; + for (const entry of entriesToDelete) { + await db.delete(CACHE_OBJECT_STORE, entry.id); + urlsDeleted.push(entry.url); + } + return urlsDeleted; + } + /** + * Takes a URL and returns an ID that will be unique in the object store. + * + * @param {string} url + * @return {string} + * + * @private + */ + _getId(url) { + // Creating an ID from the URL and cache name won't be necessary once + // Edge switches to Chromium and all browsers we support work with + // array keyPaths. + return this._cacheName + '|' + normalizeURL(url); + } + /** + * Returns an open connection to the database. + * + * @private + */ + async getDb() { + if (!this._db) { + this._db = await openDB(DB_NAME, 1, { + upgrade: this._upgradeDbAndDeleteOldDbs.bind(this) + }); + } + return this._db; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The `CacheExpiration` class allows you define an expiration and / or + * limit on the number of responses stored in a + * [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache). + * + * @memberof workbox-expiration + */ + class CacheExpiration { + /** + * To construct a new CacheExpiration instance you must provide at least + * one of the `config` properties. + * + * @param {string} cacheName Name of the cache to apply restrictions to. + * @param {Object} config + * @param {number} [config.maxEntries] The maximum number of entries to cache. + * Entries used the least will be removed as the maximum is reached. + * @param {number} [config.maxAgeSeconds] The maximum age of an entry before + * it's treated as stale and removed. + * @param {Object} [config.matchOptions] The [`CacheQueryOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete#Parameters) + * that will be used when calling `delete()` on the cache. + */ + constructor(cacheName, config = {}) { + this._isRunning = false; + this._rerunRequested = false; + { + finalAssertExports.isType(cacheName, 'string', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor', + paramName: 'cacheName' + }); + if (!(config.maxEntries || config.maxAgeSeconds)) { + throw new WorkboxError('max-entries-or-age-required', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor' + }); + } + if (config.maxEntries) { + finalAssertExports.isType(config.maxEntries, 'number', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor', + paramName: 'config.maxEntries' + }); + } + if (config.maxAgeSeconds) { + finalAssertExports.isType(config.maxAgeSeconds, 'number', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor', + paramName: 'config.maxAgeSeconds' + }); + } + } + this._maxEntries = config.maxEntries; + this._maxAgeSeconds = config.maxAgeSeconds; + this._matchOptions = config.matchOptions; + this._cacheName = cacheName; + this._timestampModel = new CacheTimestampsModel(cacheName); + } + /** + * Expires entries for the given cache and given criteria. + */ + async expireEntries() { + if (this._isRunning) { + this._rerunRequested = true; + return; + } + this._isRunning = true; + const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : 0; + const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries); + // Delete URLs from the cache + const cache = await self.caches.open(this._cacheName); + for (const url of urlsExpired) { + await cache.delete(url, this._matchOptions); + } + { + if (urlsExpired.length > 0) { + logger.groupCollapsed(`Expired ${urlsExpired.length} ` + `${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` + `${urlsExpired.length === 1 ? 'it' : 'them'} from the ` + `'${this._cacheName}' cache.`); + logger.log(`Expired the following ${urlsExpired.length === 1 ? 'URL' : 'URLs'}:`); + urlsExpired.forEach(url => logger.log(` ${url}`)); + logger.groupEnd(); + } else { + logger.debug(`Cache expiration ran and found no entries to remove.`); + } + } + this._isRunning = false; + if (this._rerunRequested) { + this._rerunRequested = false; + dontWaitFor(this.expireEntries()); + } + } + /** + * Update the timestamp for the given URL. This ensures the when + * removing entries based on maximum entries, most recently used + * is accurate or when expiring, the timestamp is up-to-date. + * + * @param {string} url + */ + async updateTimestamp(url) { + { + finalAssertExports.isType(url, 'string', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'updateTimestamp', + paramName: 'url' + }); + } + await this._timestampModel.setTimestamp(url, Date.now()); + } + /** + * Can be used to check if a URL has expired or not before it's used. + * + * This requires a look up from IndexedDB, so can be slow. + * + * Note: This method will not remove the cached entry, call + * `expireEntries()` to remove indexedDB and Cache entries. + * + * @param {string} url + * @return {boolean} + */ + async isURLExpired(url) { + if (!this._maxAgeSeconds) { + { + throw new WorkboxError(`expired-test-without-max-age`, { + methodName: 'isURLExpired', + paramName: 'maxAgeSeconds' + }); + } + } else { + const timestamp = await this._timestampModel.getTimestamp(url); + const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000; + return timestamp !== undefined ? timestamp < expireOlderThan : true; + } + } + /** + * Removes the IndexedDB object store used to keep track of cache expiration + * metadata. + */ + async delete() { + // Make sure we don't attempt another rerun if we're called in the middle of + // a cache expiration. + this._rerunRequested = false; + await this._timestampModel.expireEntries(Infinity); // Expires all. + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This plugin can be used in a `workbox-strategy` to regularly enforce a + * limit on the age and / or the number of cached requests. + * + * It can only be used with `workbox-strategy` instances that have a + * [custom `cacheName` property set](/web/tools/workbox/guides/configure-workbox#custom_cache_names_in_strategies). + * In other words, it can't be used to expire entries in strategy that uses the + * default runtime cache name. + * + * Whenever a cached response is used or updated, this plugin will look + * at the associated cache and remove any old or extra responses. + * + * When using `maxAgeSeconds`, responses may be used *once* after expiring + * because the expiration clean up will not have occurred until *after* the + * cached response has been used. If the response has a "Date" header, then + * a light weight expiration check is performed and the response will not be + * used immediately. + * + * When using `maxEntries`, the entry least-recently requested will be removed + * from the cache first. + * + * @memberof workbox-expiration + */ + class ExpirationPlugin { + /** + * @param {ExpirationPluginOptions} config + * @param {number} [config.maxEntries] The maximum number of entries to cache. + * Entries used the least will be removed as the maximum is reached. + * @param {number} [config.maxAgeSeconds] The maximum age of an entry before + * it's treated as stale and removed. + * @param {Object} [config.matchOptions] The [`CacheQueryOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete#Parameters) + * that will be used when calling `delete()` on the cache. + * @param {boolean} [config.purgeOnQuotaError] Whether to opt this cache in to + * automatic deletion if the available storage quota has been exceeded. + */ + constructor(config = {}) { + /** + * A "lifecycle" callback that will be triggered automatically by the + * `workbox-strategies` handlers when a `Response` is about to be returned + * from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to + * the handler. It allows the `Response` to be inspected for freshness and + * prevents it from being used if the `Response`'s `Date` header value is + * older than the configured `maxAgeSeconds`. + * + * @param {Object} options + * @param {string} options.cacheName Name of the cache the response is in. + * @param {Response} options.cachedResponse The `Response` object that's been + * read from a cache and whose freshness should be checked. + * @return {Response} Either the `cachedResponse`, if it's + * fresh, or `null` if the `Response` is older than `maxAgeSeconds`. + * + * @private + */ + this.cachedResponseWillBeUsed = async ({ + event, + request, + cacheName, + cachedResponse + }) => { + if (!cachedResponse) { + return null; + } + const isFresh = this._isResponseDateFresh(cachedResponse); + // Expire entries to ensure that even if the expiration date has + // expired, it'll only be used once. + const cacheExpiration = this._getCacheExpiration(cacheName); + dontWaitFor(cacheExpiration.expireEntries()); + // Update the metadata for the request URL to the current timestamp, + // but don't `await` it as we don't want to block the response. + const updateTimestampDone = cacheExpiration.updateTimestamp(request.url); + if (event) { + try { + event.waitUntil(updateTimestampDone); + } catch (error) { + { + // The event may not be a fetch event; only log the URL if it is. + if ('request' in event) { + logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache entry for ` + `'${getFriendlyURL(event.request.url)}'.`); + } + } + } + } + return isFresh ? cachedResponse : null; + }; + /** + * A "lifecycle" callback that will be triggered automatically by the + * `workbox-strategies` handlers when an entry is added to a cache. + * + * @param {Object} options + * @param {string} options.cacheName Name of the cache that was updated. + * @param {string} options.request The Request for the cached entry. + * + * @private + */ + this.cacheDidUpdate = async ({ + cacheName, + request + }) => { + { + finalAssertExports.isType(cacheName, 'string', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'cacheDidUpdate', + paramName: 'cacheName' + }); + finalAssertExports.isInstance(request, Request, { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'cacheDidUpdate', + paramName: 'request' + }); + } + const cacheExpiration = this._getCacheExpiration(cacheName); + await cacheExpiration.updateTimestamp(request.url); + await cacheExpiration.expireEntries(); + }; + { + if (!(config.maxEntries || config.maxAgeSeconds)) { + throw new WorkboxError('max-entries-or-age-required', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'constructor' + }); + } + if (config.maxEntries) { + finalAssertExports.isType(config.maxEntries, 'number', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'constructor', + paramName: 'config.maxEntries' + }); + } + if (config.maxAgeSeconds) { + finalAssertExports.isType(config.maxAgeSeconds, 'number', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'constructor', + paramName: 'config.maxAgeSeconds' + }); + } + } + this._config = config; + this._maxAgeSeconds = config.maxAgeSeconds; + this._cacheExpirations = new Map(); + if (config.purgeOnQuotaError) { + registerQuotaErrorCallback(() => this.deleteCacheAndMetadata()); + } + } + /** + * A simple helper method to return a CacheExpiration instance for a given + * cache name. + * + * @param {string} cacheName + * @return {CacheExpiration} + * + * @private + */ + _getCacheExpiration(cacheName) { + if (cacheName === cacheNames.getRuntimeName()) { + throw new WorkboxError('expire-custom-caches-only'); + } + let cacheExpiration = this._cacheExpirations.get(cacheName); + if (!cacheExpiration) { + cacheExpiration = new CacheExpiration(cacheName, this._config); + this._cacheExpirations.set(cacheName, cacheExpiration); + } + return cacheExpiration; + } + /** + * @param {Response} cachedResponse + * @return {boolean} + * + * @private + */ + _isResponseDateFresh(cachedResponse) { + if (!this._maxAgeSeconds) { + // We aren't expiring by age, so return true, it's fresh + return true; + } + // Check if the 'date' header will suffice a quick expiration check. + // See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for + // discussion. + const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse); + if (dateHeaderTimestamp === null) { + // Unable to parse date, so assume it's fresh. + return true; + } + // If we have a valid headerTime, then our response is fresh iff the + // headerTime plus maxAgeSeconds is greater than the current time. + const now = Date.now(); + return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000; + } + /** + * This method will extract the data header and parse it into a useful + * value. + * + * @param {Response} cachedResponse + * @return {number|null} + * + * @private + */ + _getDateHeaderTimestamp(cachedResponse) { + if (!cachedResponse.headers.has('date')) { + return null; + } + const dateHeader = cachedResponse.headers.get('date'); + const parsedDate = new Date(dateHeader); + const headerTime = parsedDate.getTime(); + // If the Date header was invalid for some reason, parsedDate.getTime() + // will return NaN. + if (isNaN(headerTime)) { + return null; + } + return headerTime; + } + /** + * This is a helper method that performs two operations: + * + * - Deletes *all* the underlying Cache instances associated with this plugin + * instance, by calling caches.delete() on your behalf. + * - Deletes the metadata from IndexedDB used to keep track of expiration + * details for each Cache instance. + * + * When using cache expiration, calling this method is preferable to calling + * `caches.delete()` directly, since this will ensure that the IndexedDB + * metadata is also cleanly removed and open IndexedDB instances are deleted. + * + * Note that if you're *not* using cache expiration for a given cache, calling + * `caches.delete()` and passing in the cache's name should be sufficient. + * There is no Workbox-specific method needed for cleanup in that case. + */ + async deleteCacheAndMetadata() { + // Do this one at a time instead of all at once via `Promise.all()` to + // reduce the chance of inconsistency if a promise rejects. + for (const [cacheName, cacheExpiration] of this._cacheExpirations) { + await self.caches.delete(cacheName); + await cacheExpiration.delete(); + } + // Reset this._cacheExpirations to its initial state. + this._cacheExpirations = new Map(); + } + } + + // @ts-ignore + try { + self['workbox:cacheable-response:7.0.0'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This class allows you to set up rules determining what + * status codes and/or headers need to be present in order for a + * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) + * to be considered cacheable. + * + * @memberof workbox-cacheable-response + */ + class CacheableResponse { + /** + * To construct a new CacheableResponse instance you must provide at least + * one of the `config` properties. + * + * If both `statuses` and `headers` are specified, then both conditions must + * be met for the `Response` to be considered cacheable. + * + * @param {Object} config + * @param {Array} [config.statuses] One or more status codes that a + * `Response` can have and be considered cacheable. + * @param {Object} [config.headers] A mapping of header names + * and expected values that a `Response` can have and be considered cacheable. + * If multiple headers are provided, only one needs to be present. + */ + constructor(config = {}) { + { + if (!(config.statuses || config.headers)) { + throw new WorkboxError('statuses-or-headers-required', { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor' + }); + } + if (config.statuses) { + finalAssertExports.isArray(config.statuses, { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor', + paramName: 'config.statuses' + }); + } + if (config.headers) { + finalAssertExports.isType(config.headers, 'object', { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor', + paramName: 'config.headers' + }); + } + } + this._statuses = config.statuses; + this._headers = config.headers; + } + /** + * Checks a response to see whether it's cacheable or not, based on this + * object's configuration. + * + * @param {Response} response The response whose cacheability is being + * checked. + * @return {boolean} `true` if the `Response` is cacheable, and `false` + * otherwise. + */ + isResponseCacheable(response) { + { + finalAssertExports.isInstance(response, Response, { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'isResponseCacheable', + paramName: 'response' + }); + } + let cacheable = true; + if (this._statuses) { + cacheable = this._statuses.includes(response.status); + } + if (this._headers && cacheable) { + cacheable = Object.keys(this._headers).some(headerName => { + return response.headers.get(headerName) === this._headers[headerName]; + }); + } + { + if (!cacheable) { + logger.groupCollapsed(`The request for ` + `'${getFriendlyURL(response.url)}' returned a response that does ` + `not meet the criteria for being cached.`); + logger.groupCollapsed(`View cacheability criteria here.`); + logger.log(`Cacheable statuses: ` + JSON.stringify(this._statuses)); + logger.log(`Cacheable headers: ` + JSON.stringify(this._headers, null, 2)); + logger.groupEnd(); + const logFriendlyHeaders = {}; + response.headers.forEach((value, key) => { + logFriendlyHeaders[key] = value; + }); + logger.groupCollapsed(`View response status and headers here.`); + logger.log(`Response status: ${response.status}`); + logger.log(`Response headers: ` + JSON.stringify(logFriendlyHeaders, null, 2)); + logger.groupEnd(); + logger.groupCollapsed(`View full response details here.`); + logger.log(response.headers); + logger.log(response); + logger.groupEnd(); + logger.groupEnd(); + } + } + return cacheable; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A class implementing the `cacheWillUpdate` lifecycle callback. This makes it + * easier to add in cacheability checks to requests made via Workbox's built-in + * strategies. + * + * @memberof workbox-cacheable-response + */ + class CacheableResponsePlugin { + /** + * To construct a new CacheableResponsePlugin instance you must provide at + * least one of the `config` properties. + * + * If both `statuses` and `headers` are specified, then both conditions must + * be met for the `Response` to be considered cacheable. + * + * @param {Object} config + * @param {Array} [config.statuses] One or more status codes that a + * `Response` can have and be considered cacheable. + * @param {Object} [config.headers] A mapping of header names + * and expected values that a `Response` can have and be considered cacheable. + * If multiple headers are provided, only one needs to be present. + */ + constructor(config) { + /** + * @param {Object} options + * @param {Response} options.response + * @return {Response|null} + * @private + */ + this.cacheWillUpdate = async ({ + response + }) => { + if (this._cacheableResponse.isResponseCacheable(response)) { + return response; + } + return null; + }; + this._cacheableResponse = new CacheableResponse(config); + } + } + + /* + Copyright 2020 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + function stripParams(fullURL, ignoreParams) { + const strippedURL = new URL(fullURL); + for (const param of ignoreParams) { + strippedURL.searchParams.delete(param); + } + return strippedURL.href; + } + /** + * Matches an item in the cache, ignoring specific URL params. This is similar + * to the `ignoreSearch` option, but it allows you to ignore just specific + * params (while continuing to match on the others). + * + * @private + * @param {Cache} cache + * @param {Request} request + * @param {Object} matchOptions + * @param {Array} ignoreParams + * @return {Promise} + */ + async function cacheMatchIgnoreParams(cache, request, ignoreParams, matchOptions) { + const strippedRequestURL = stripParams(request.url, ignoreParams); + // If the request doesn't include any ignored params, match as normal. + if (request.url === strippedRequestURL) { + return cache.match(request, matchOptions); + } + // Otherwise, match by comparing keys + const keysOptions = Object.assign(Object.assign({}, matchOptions), { + ignoreSearch: true + }); + const cacheKeys = await cache.keys(request, keysOptions); + for (const cacheKey of cacheKeys) { + const strippedCacheKeyURL = stripParams(cacheKey.url, ignoreParams); + if (strippedRequestURL === strippedCacheKeyURL) { + return cache.match(cacheKey, matchOptions); + } + } + return; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The Deferred class composes Promises in a way that allows for them to be + * resolved or rejected from outside the constructor. In most cases promises + * should be used directly, but Deferreds can be necessary when the logic to + * resolve a promise must be separate. + * + * @private + */ + class Deferred { + /** + * Creates a promise and exposes its resolve and reject functions as methods. + */ + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Runs all of the callback functions, one at a time sequentially, in the order + * in which they were registered. + * + * @memberof workbox-core + * @private + */ + async function executeQuotaErrorCallbacks() { + { + logger.log(`About to run ${quotaErrorCallbacks.size} ` + `callbacks to clean up caches.`); + } + for (const callback of quotaErrorCallbacks) { + await callback(); + { + logger.log(callback, 'is complete.'); + } + } + { + logger.log('Finished running callbacks.'); + } + } + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Returns a promise that resolves and the passed number of milliseconds. + * This utility is an async/await-friendly version of `setTimeout`. + * + * @param {number} ms + * @return {Promise} + * @private + */ + function timeout(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // @ts-ignore + try { + self['workbox:strategies:7.0.0'] && _(); + } catch (e) {} + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + function toRequest(input) { + return typeof input === 'string' ? new Request(input) : input; + } + /** + * A class created every time a Strategy instance instance calls + * {@link workbox-strategies.Strategy~handle} or + * {@link workbox-strategies.Strategy~handleAll} that wraps all fetch and + * cache actions around plugin callbacks and keeps track of when the strategy + * is "done" (i.e. all added `event.waitUntil()` promises have resolved). + * + * @memberof workbox-strategies + */ + class StrategyHandler { + /** + * Creates a new instance associated with the passed strategy and event + * that's handling the request. + * + * The constructor also initializes the state that will be passed to each of + * the plugins handling this request. + * + * @param {workbox-strategies.Strategy} strategy + * @param {Object} options + * @param {Request|string} options.request A request to run this strategy for. + * @param {ExtendableEvent} options.event The event associated with the + * request. + * @param {URL} [options.url] + * @param {*} [options.params] The return value from the + * {@link workbox-routing~matchCallback} (if applicable). + */ + constructor(strategy, options) { + this._cacheKeys = {}; + /** + * The request the strategy is performing (passed to the strategy's + * `handle()` or `handleAll()` method). + * @name request + * @instance + * @type {Request} + * @memberof workbox-strategies.StrategyHandler + */ + /** + * The event associated with this request. + * @name event + * @instance + * @type {ExtendableEvent} + * @memberof workbox-strategies.StrategyHandler + */ + /** + * A `URL` instance of `request.url` (if passed to the strategy's + * `handle()` or `handleAll()` method). + * Note: the `url` param will be present if the strategy was invoked + * from a workbox `Route` object. + * @name url + * @instance + * @type {URL|undefined} + * @memberof workbox-strategies.StrategyHandler + */ + /** + * A `param` value (if passed to the strategy's + * `handle()` or `handleAll()` method). + * Note: the `param` param will be present if the strategy was invoked + * from a workbox `Route` object and the + * {@link workbox-routing~matchCallback} returned + * a truthy value (it will be that value). + * @name params + * @instance + * @type {*|undefined} + * @memberof workbox-strategies.StrategyHandler + */ + { + finalAssertExports.isInstance(options.event, ExtendableEvent, { + moduleName: 'workbox-strategies', + className: 'StrategyHandler', + funcName: 'constructor', + paramName: 'options.event' + }); + } + Object.assign(this, options); + this.event = options.event; + this._strategy = strategy; + this._handlerDeferred = new Deferred(); + this._extendLifetimePromises = []; + // Copy the plugins list (since it's mutable on the strategy), + // so any mutations don't affect this handler instance. + this._plugins = [...strategy.plugins]; + this._pluginStateMap = new Map(); + for (const plugin of this._plugins) { + this._pluginStateMap.set(plugin, {}); + } + this.event.waitUntil(this._handlerDeferred.promise); + } + /** + * Fetches a given request (and invokes any applicable plugin callback + * methods) using the `fetchOptions` (for non-navigation requests) and + * `plugins` defined on the `Strategy` object. + * + * The following plugin lifecycle methods are invoked when using this method: + * - `requestWillFetch()` + * - `fetchDidSucceed()` + * - `fetchDidFail()` + * + * @param {Request|string} input The URL or request to fetch. + * @return {Promise} + */ + async fetch(input) { + const { + event + } = this; + let request = toRequest(input); + if (request.mode === 'navigate' && event instanceof FetchEvent && event.preloadResponse) { + const possiblePreloadResponse = await event.preloadResponse; + if (possiblePreloadResponse) { + { + logger.log(`Using a preloaded navigation response for ` + `'${getFriendlyURL(request.url)}'`); + } + return possiblePreloadResponse; + } + } + // If there is a fetchDidFail plugin, we need to save a clone of the + // original request before it's either modified by a requestWillFetch + // plugin or before the original request's body is consumed via fetch(). + const originalRequest = this.hasCallback('fetchDidFail') ? request.clone() : null; + try { + for (const cb of this.iterateCallbacks('requestWillFetch')) { + request = await cb({ + request: request.clone(), + event + }); + } + } catch (err) { + if (err instanceof Error) { + throw new WorkboxError('plugin-error-request-will-fetch', { + thrownErrorMessage: err.message + }); + } + } + // The request can be altered by plugins with `requestWillFetch` making + // the original request (most likely from a `fetch` event) different + // from the Request we make. Pass both to `fetchDidFail` to aid debugging. + const pluginFilteredRequest = request.clone(); + try { + let fetchResponse; + // See https://github.com/GoogleChrome/workbox/issues/1796 + fetchResponse = await fetch(request, request.mode === 'navigate' ? undefined : this._strategy.fetchOptions); + if ("development" !== 'production') { + logger.debug(`Network request for ` + `'${getFriendlyURL(request.url)}' returned a response with ` + `status '${fetchResponse.status}'.`); + } + for (const callback of this.iterateCallbacks('fetchDidSucceed')) { + fetchResponse = await callback({ + event, + request: pluginFilteredRequest, + response: fetchResponse + }); + } + return fetchResponse; + } catch (error) { + { + logger.log(`Network request for ` + `'${getFriendlyURL(request.url)}' threw an error.`, error); + } + // `originalRequest` will only exist if a `fetchDidFail` callback + // is being used (see above). + if (originalRequest) { + await this.runCallbacks('fetchDidFail', { + error: error, + event, + originalRequest: originalRequest.clone(), + request: pluginFilteredRequest.clone() + }); + } + throw error; + } + } + /** + * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on + * the response generated by `this.fetch()`. + * + * The call to `this.cachePut()` automatically invokes `this.waitUntil()`, + * so you do not have to manually call `waitUntil()` on the event. + * + * @param {Request|string} input The request or URL to fetch and cache. + * @return {Promise} + */ + async fetchAndCachePut(input) { + const response = await this.fetch(input); + const responseClone = response.clone(); + void this.waitUntil(this.cachePut(input, responseClone)); + return response; + } + /** + * Matches a request from the cache (and invokes any applicable plugin + * callback methods) using the `cacheName`, `matchOptions`, and `plugins` + * defined on the strategy object. + * + * The following plugin lifecycle methods are invoked when using this method: + * - cacheKeyWillByUsed() + * - cachedResponseWillByUsed() + * + * @param {Request|string} key The Request or URL to use as the cache key. + * @return {Promise} A matching response, if found. + */ + async cacheMatch(key) { + const request = toRequest(key); + let cachedResponse; + const { + cacheName, + matchOptions + } = this._strategy; + const effectiveRequest = await this.getCacheKey(request, 'read'); + const multiMatchOptions = Object.assign(Object.assign({}, matchOptions), { + cacheName + }); + cachedResponse = await caches.match(effectiveRequest, multiMatchOptions); + { + if (cachedResponse) { + logger.debug(`Found a cached response in '${cacheName}'.`); + } else { + logger.debug(`No cached response found in '${cacheName}'.`); + } + } + for (const callback of this.iterateCallbacks('cachedResponseWillBeUsed')) { + cachedResponse = (await callback({ + cacheName, + matchOptions, + cachedResponse, + request: effectiveRequest, + event: this.event + })) || undefined; + } + return cachedResponse; + } + /** + * Puts a request/response pair in the cache (and invokes any applicable + * plugin callback methods) using the `cacheName` and `plugins` defined on + * the strategy object. + * + * The following plugin lifecycle methods are invoked when using this method: + * - cacheKeyWillByUsed() + * - cacheWillUpdate() + * - cacheDidUpdate() + * + * @param {Request|string} key The request or URL to use as the cache key. + * @param {Response} response The response to cache. + * @return {Promise} `false` if a cacheWillUpdate caused the response + * not be cached, and `true` otherwise. + */ + async cachePut(key, response) { + const request = toRequest(key); + // Run in the next task to avoid blocking other cache reads. + // https://github.com/w3c/ServiceWorker/issues/1397 + await timeout(0); + const effectiveRequest = await this.getCacheKey(request, 'write'); + { + if (effectiveRequest.method && effectiveRequest.method !== 'GET') { + throw new WorkboxError('attempt-to-cache-non-get-request', { + url: getFriendlyURL(effectiveRequest.url), + method: effectiveRequest.method + }); + } + // See https://github.com/GoogleChrome/workbox/issues/2818 + const vary = response.headers.get('Vary'); + if (vary) { + logger.debug(`The response for ${getFriendlyURL(effectiveRequest.url)} ` + `has a 'Vary: ${vary}' header. ` + `Consider setting the {ignoreVary: true} option on your strategy ` + `to ensure cache matching and deletion works as expected.`); + } + } + if (!response) { + { + logger.error(`Cannot cache non-existent response for ` + `'${getFriendlyURL(effectiveRequest.url)}'.`); + } + throw new WorkboxError('cache-put-with-no-response', { + url: getFriendlyURL(effectiveRequest.url) + }); + } + const responseToCache = await this._ensureResponseSafeToCache(response); + if (!responseToCache) { + { + logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` + `will not be cached.`, responseToCache); + } + return false; + } + const { + cacheName, + matchOptions + } = this._strategy; + const cache = await self.caches.open(cacheName); + const hasCacheUpdateCallback = this.hasCallback('cacheDidUpdate'); + const oldResponse = hasCacheUpdateCallback ? await cacheMatchIgnoreParams( + // TODO(philipwalton): the `__WB_REVISION__` param is a precaching + // feature. Consider into ways to only add this behavior if using + // precaching. + cache, effectiveRequest.clone(), ['__WB_REVISION__'], matchOptions) : null; + { + logger.debug(`Updating the '${cacheName}' cache with a new Response ` + `for ${getFriendlyURL(effectiveRequest.url)}.`); + } + try { + await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache); + } catch (error) { + if (error instanceof Error) { + // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError + if (error.name === 'QuotaExceededError') { + await executeQuotaErrorCallbacks(); + } + throw error; + } + } + for (const callback of this.iterateCallbacks('cacheDidUpdate')) { + await callback({ + cacheName, + oldResponse, + newResponse: responseToCache.clone(), + request: effectiveRequest, + event: this.event + }); + } + return true; + } + /** + * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and + * executes any of those callbacks found in sequence. The final `Request` + * object returned by the last plugin is treated as the cache key for cache + * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have + * been registered, the passed request is returned unmodified + * + * @param {Request} request + * @param {string} mode + * @return {Promise} + */ + async getCacheKey(request, mode) { + const key = `${request.url} | ${mode}`; + if (!this._cacheKeys[key]) { + let effectiveRequest = request; + for (const callback of this.iterateCallbacks('cacheKeyWillBeUsed')) { + effectiveRequest = toRequest(await callback({ + mode, + request: effectiveRequest, + event: this.event, + // params has a type any can't change right now. + params: this.params // eslint-disable-line + })); + } + + this._cacheKeys[key] = effectiveRequest; + } + return this._cacheKeys[key]; + } + /** + * Returns true if the strategy has at least one plugin with the given + * callback. + * + * @param {string} name The name of the callback to check for. + * @return {boolean} + */ + hasCallback(name) { + for (const plugin of this._strategy.plugins) { + if (name in plugin) { + return true; + } + } + return false; + } + /** + * Runs all plugin callbacks matching the given name, in order, passing the + * given param object (merged ith the current plugin state) as the only + * argument. + * + * Note: since this method runs all plugins, it's not suitable for cases + * where the return value of a callback needs to be applied prior to calling + * the next callback. See + * {@link workbox-strategies.StrategyHandler#iterateCallbacks} + * below for how to handle that case. + * + * @param {string} name The name of the callback to run within each plugin. + * @param {Object} param The object to pass as the first (and only) param + * when executing each callback. This object will be merged with the + * current plugin state prior to callback execution. + */ + async runCallbacks(name, param) { + for (const callback of this.iterateCallbacks(name)) { + // TODO(philipwalton): not sure why `any` is needed. It seems like + // this should work with `as WorkboxPluginCallbackParam[C]`. + await callback(param); + } + } + /** + * Accepts a callback and returns an iterable of matching plugin callbacks, + * where each callback is wrapped with the current handler state (i.e. when + * you call each callback, whatever object parameter you pass it will + * be merged with the plugin's current state). + * + * @param {string} name The name fo the callback to run + * @return {Array} + */ + *iterateCallbacks(name) { + for (const plugin of this._strategy.plugins) { + if (typeof plugin[name] === 'function') { + const state = this._pluginStateMap.get(plugin); + const statefulCallback = param => { + const statefulParam = Object.assign(Object.assign({}, param), { + state + }); + // TODO(philipwalton): not sure why `any` is needed. It seems like + // this should work with `as WorkboxPluginCallbackParam[C]`. + return plugin[name](statefulParam); + }; + yield statefulCallback; + } + } + } + /** + * Adds a promise to the + * [extend lifetime promises]{@link https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises} + * of the event event associated with the request being handled (usually a + * `FetchEvent`). + * + * Note: you can await + * {@link workbox-strategies.StrategyHandler~doneWaiting} + * to know when all added promises have settled. + * + * @param {Promise} promise A promise to add to the extend lifetime promises + * of the event that triggered the request. + */ + waitUntil(promise) { + this._extendLifetimePromises.push(promise); + return promise; + } + /** + * Returns a promise that resolves once all promises passed to + * {@link workbox-strategies.StrategyHandler~waitUntil} + * have settled. + * + * Note: any work done after `doneWaiting()` settles should be manually + * passed to an event's `waitUntil()` method (not this handler's + * `waitUntil()` method), otherwise the service worker thread my be killed + * prior to your work completing. + */ + async doneWaiting() { + let promise; + while (promise = this._extendLifetimePromises.shift()) { + await promise; + } + } + /** + * Stops running the strategy and immediately resolves any pending + * `waitUntil()` promises. + */ + destroy() { + this._handlerDeferred.resolve(null); + } + /** + * This method will call cacheWillUpdate on the available plugins (or use + * status === 200) to determine if the Response is safe and valid to cache. + * + * @param {Request} options.request + * @param {Response} options.response + * @return {Promise} + * + * @private + */ + async _ensureResponseSafeToCache(response) { + let responseToCache = response; + let pluginsUsed = false; + for (const callback of this.iterateCallbacks('cacheWillUpdate')) { + responseToCache = (await callback({ + request: this.request, + response: responseToCache, + event: this.event + })) || undefined; + pluginsUsed = true; + if (!responseToCache) { + break; + } + } + if (!pluginsUsed) { + if (responseToCache && responseToCache.status !== 200) { + responseToCache = undefined; + } + { + if (responseToCache) { + if (responseToCache.status !== 200) { + if (responseToCache.status === 0) { + logger.warn(`The response for '${this.request.url}' ` + `is an opaque response. The caching strategy that you're ` + `using will not cache opaque responses by default.`); + } else { + logger.debug(`The response for '${this.request.url}' ` + `returned a status code of '${response.status}' and won't ` + `be cached as a result.`); + } + } + } + } + } + return responseToCache; + } + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * An abstract base class that all other strategy classes must extend from: + * + * @memberof workbox-strategies + */ + class Strategy { + /** + * Creates a new instance of the strategy and sets all documented option + * properties as public instance properties. + * + * Note: if a custom strategy class extends the base Strategy class and does + * not need more than these properties, it does not need to define its own + * constructor. + * + * @param {Object} [options] + * @param {string} [options.cacheName] Cache name to store and retrieve + * requests. Defaults to the cache names provided by + * {@link workbox-core.cacheNames}. + * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * to use in conjunction with this caching strategy. + * @param {Object} [options.fetchOptions] Values passed along to the + * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) + * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796) + * `fetch()` requests made by this strategy. + * @param {Object} [options.matchOptions] The + * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions} + * for any `cache.match()` or `cache.put()` calls made by this strategy. + */ + constructor(options = {}) { + /** + * Cache name to store and retrieve + * requests. Defaults to the cache names provided by + * {@link workbox-core.cacheNames}. + * + * @type {string} + */ + this.cacheName = cacheNames.getRuntimeName(options.cacheName); + /** + * The list + * [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * used by this strategy. + * + * @type {Array} + */ + this.plugins = options.plugins || []; + /** + * Values passed along to the + * [`init`]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters} + * of all fetch() requests made by this strategy. + * + * @type {Object} + */ + this.fetchOptions = options.fetchOptions; + /** + * The + * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions} + * for any `cache.match()` or `cache.put()` calls made by this strategy. + * + * @type {Object} + */ + this.matchOptions = options.matchOptions; + } + /** + * Perform a request strategy and returns a `Promise` that will resolve with + * a `Response`, invoking all relevant plugin callbacks. + * + * When a strategy instance is registered with a Workbox + * {@link workbox-routing.Route}, this method is automatically + * called when the route matches. + * + * Alternatively, this method can be used in a standalone `FetchEvent` + * listener by passing it to `event.respondWith()`. + * + * @param {FetchEvent|Object} options A `FetchEvent` or an object with the + * properties listed below. + * @param {Request|string} options.request A request to run this strategy for. + * @param {ExtendableEvent} options.event The event associated with the + * request. + * @param {URL} [options.url] + * @param {*} [options.params] + */ + handle(options) { + const [responseDone] = this.handleAll(options); + return responseDone; + } + /** + * Similar to {@link workbox-strategies.Strategy~handle}, but + * instead of just returning a `Promise` that resolves to a `Response` it + * it will return an tuple of `[response, done]` promises, where the former + * (`response`) is equivalent to what `handle()` returns, and the latter is a + * Promise that will resolve once any promises that were added to + * `event.waitUntil()` as part of performing the strategy have completed. + * + * You can await the `done` promise to ensure any extra work performed by + * the strategy (usually caching responses) completes successfully. + * + * @param {FetchEvent|Object} options A `FetchEvent` or an object with the + * properties listed below. + * @param {Request|string} options.request A request to run this strategy for. + * @param {ExtendableEvent} options.event The event associated with the + * request. + * @param {URL} [options.url] + * @param {*} [options.params] + * @return {Array} A tuple of [response, done] + * promises that can be used to determine when the response resolves as + * well as when the handler has completed all its work. + */ + handleAll(options) { + // Allow for flexible options to be passed. + if (options instanceof FetchEvent) { + options = { + event: options, + request: options.request + }; + } + const event = options.event; + const request = typeof options.request === 'string' ? new Request(options.request) : options.request; + const params = 'params' in options ? options.params : undefined; + const handler = new StrategyHandler(this, { + event, + request, + params + }); + const responseDone = this._getResponse(handler, request, event); + const handlerDone = this._awaitComplete(responseDone, handler, request, event); + // Return an array of promises, suitable for use with Promise.all(). + return [responseDone, handlerDone]; + } + async _getResponse(handler, request, event) { + await handler.runCallbacks('handlerWillStart', { + event, + request + }); + let response = undefined; + try { + response = await this._handle(request, handler); + // The "official" Strategy subclasses all throw this error automatically, + // but in case a third-party Strategy doesn't, ensure that we have a + // consistent failure when there's no response or an error response. + if (!response || response.type === 'error') { + throw new WorkboxError('no-response', { + url: request.url + }); + } + } catch (error) { + if (error instanceof Error) { + for (const callback of handler.iterateCallbacks('handlerDidError')) { + response = await callback({ + error, + event, + request + }); + if (response) { + break; + } + } + } + if (!response) { + throw error; + } else { + logger.log(`While responding to '${getFriendlyURL(request.url)}', ` + `an ${error instanceof Error ? error.toString() : ''} error occurred. Using a fallback response provided by ` + `a handlerDidError plugin.`); + } + } + for (const callback of handler.iterateCallbacks('handlerWillRespond')) { + response = await callback({ + event, + request, + response + }); + } + return response; + } + async _awaitComplete(responseDone, handler, request, event) { + let response; + let error; + try { + response = await responseDone; + } catch (error) { + // Ignore errors, as response errors should be caught via the `response` + // promise above. The `done` promise will only throw for errors in + // promises passed to `handler.waitUntil()`. + } + try { + await handler.runCallbacks('handlerDidRespond', { + event, + request, + response + }); + await handler.doneWaiting(); + } catch (waitUntilError) { + if (waitUntilError instanceof Error) { + error = waitUntilError; + } + } + await handler.runCallbacks('handlerDidComplete', { + event, + request, + response, + error: error + }); + handler.destroy(); + if (error) { + throw error; + } + } + } + /** + * Classes extending the `Strategy` based class should implement this method, + * and leverage the {@link workbox-strategies.StrategyHandler} + * arg to perform all fetching and cache logic, which will ensure all relevant + * cache, cache options, fetch options and plugins are used (per the current + * strategy instance). + * + * @name _handle + * @instance + * @abstract + * @function + * @param {Request} request + * @param {workbox-strategies.StrategyHandler} handler + * @return {Promise} + * + * @memberof workbox-strategies.Strategy + */ + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const messages = { + strategyStart: (strategyName, request) => `Using ${strategyName} to respond to '${getFriendlyURL(request.url)}'`, + printFinalResponse: response => { + if (response) { + logger.groupCollapsed(`View the final response here.`); + logger.log(response || '[No response returned]'); + logger.groupEnd(); + } + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * An implementation of a [cache-first](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#cache-first-falling-back-to-network) + * request strategy. + * + * A cache first strategy is useful for assets that have been revisioned, + * such as URLs like `/styles/example.a8f5f1.css`, since they + * can be cached for long periods of time. + * + * If the network request fails, and there is no cache match, this will throw + * a `WorkboxError` exception. + * + * @extends workbox-strategies.Strategy + * @memberof workbox-strategies + */ + class CacheFirst extends Strategy { + /** + * @private + * @param {Request|string} request A request to run this strategy for. + * @param {workbox-strategies.StrategyHandler} handler The event that + * triggered the request. + * @return {Promise} + */ + async _handle(request, handler) { + const logs = []; + { + finalAssertExports.isInstance(request, Request, { + moduleName: 'workbox-strategies', + className: this.constructor.name, + funcName: 'makeRequest', + paramName: 'request' + }); + } + let response = await handler.cacheMatch(request); + let error = undefined; + if (!response) { + { + logs.push(`No response found in the '${this.cacheName}' cache. ` + `Will respond with a network request.`); + } + try { + response = await handler.fetchAndCachePut(request); + } catch (err) { + if (err instanceof Error) { + error = err; + } + } + { + if (response) { + logs.push(`Got response from network.`); + } else { + logs.push(`Unable to get a response from the network.`); + } + } + } else { + { + logs.push(`Found a cached response in the '${this.cacheName}' cache.`); + } + } + { + logger.groupCollapsed(messages.strategyStart(this.constructor.name, request)); + for (const log of logs) { + logger.log(log); + } + messages.printFinalResponse(response); + logger.groupEnd(); + } + if (!response) { + throw new WorkboxError('no-response', { + url: request.url, + error + }); + } + return response; + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Claim any currently available clients once the service worker + * becomes active. This is normally used in conjunction with `skipWaiting()`. + * + * @memberof workbox-core + */ + function clientsClaim() { + self.addEventListener('activate', () => self.clients.claim()); + } + + /* + Copyright 2020 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A utility method that makes it easier to use `event.waitUntil` with + * async functions and return the result. + * + * @param {ExtendableEvent} event + * @param {Function} asyncFn + * @return {Function} + * @private + */ + function waitUntil(event, asyncFn) { + const returnPromise = asyncFn(); + event.waitUntil(returnPromise); + return returnPromise; + } + + // @ts-ignore + try { + self['workbox:precaching:7.0.0'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + // Name of the search parameter used to store revision info. + const REVISION_SEARCH_PARAM = '__WB_REVISION__'; + /** + * Converts a manifest entry into a versioned URL suitable for precaching. + * + * @param {Object|string} entry + * @return {string} A URL with versioning info. + * + * @private + * @memberof workbox-precaching + */ + function createCacheKey(entry) { + if (!entry) { + throw new WorkboxError('add-to-cache-list-unexpected-type', { + entry + }); + } + // If a precache manifest entry is a string, it's assumed to be a versioned + // URL, like '/app.abcd1234.js'. Return as-is. + if (typeof entry === 'string') { + const urlObject = new URL(entry, location.href); + return { + cacheKey: urlObject.href, + url: urlObject.href + }; + } + const { + revision, + url + } = entry; + if (!url) { + throw new WorkboxError('add-to-cache-list-unexpected-type', { + entry + }); + } + // If there's just a URL and no revision, then it's also assumed to be a + // versioned URL. + if (!revision) { + const urlObject = new URL(url, location.href); + return { + cacheKey: urlObject.href, + url: urlObject.href + }; + } + // Otherwise, construct a properly versioned URL using the custom Workbox + // search parameter along with the revision info. + const cacheKeyURL = new URL(url, location.href); + const originalURL = new URL(url, location.href); + cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision); + return { + cacheKey: cacheKeyURL.href, + url: originalURL.href + }; + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A plugin, designed to be used with PrecacheController, to determine the + * of assets that were updated (or not updated) during the install event. + * + * @private + */ + class PrecacheInstallReportPlugin { + constructor() { + this.updatedURLs = []; + this.notUpdatedURLs = []; + this.handlerWillStart = async ({ + request, + state + }) => { + // TODO: `state` should never be undefined... + if (state) { + state.originalRequest = request; + } + }; + this.cachedResponseWillBeUsed = async ({ + event, + state, + cachedResponse + }) => { + if (event.type === 'install') { + if (state && state.originalRequest && state.originalRequest instanceof Request) { + // TODO: `state` should never be undefined... + const url = state.originalRequest.url; + if (cachedResponse) { + this.notUpdatedURLs.push(url); + } else { + this.updatedURLs.push(url); + } + } + } + return cachedResponse; + }; + } + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A plugin, designed to be used with PrecacheController, to translate URLs into + * the corresponding cache key, based on the current revision info. + * + * @private + */ + class PrecacheCacheKeyPlugin { + constructor({ + precacheController + }) { + this.cacheKeyWillBeUsed = async ({ + request, + params + }) => { + // Params is type any, can't change right now. + /* eslint-disable */ + const cacheKey = (params === null || params === void 0 ? void 0 : params.cacheKey) || this._precacheController.getCacheKeyForURL(request.url); + /* eslint-enable */ + return cacheKey ? new Request(cacheKey, { + headers: request.headers + }) : request; + }; + this._precacheController = precacheController; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {string} groupTitle + * @param {Array} deletedURLs + * + * @private + */ + const logGroup = (groupTitle, deletedURLs) => { + logger.groupCollapsed(groupTitle); + for (const url of deletedURLs) { + logger.log(url); + } + logger.groupEnd(); + }; + /** + * @param {Array} deletedURLs + * + * @private + * @memberof workbox-precaching + */ + function printCleanupDetails(deletedURLs) { + const deletionCount = deletedURLs.length; + if (deletionCount > 0) { + logger.groupCollapsed(`During precaching cleanup, ` + `${deletionCount} cached ` + `request${deletionCount === 1 ? ' was' : 's were'} deleted.`); + logGroup('Deleted Cache Requests', deletedURLs); + logger.groupEnd(); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {string} groupTitle + * @param {Array} urls + * + * @private + */ + function _nestedGroup(groupTitle, urls) { + if (urls.length === 0) { + return; + } + logger.groupCollapsed(groupTitle); + for (const url of urls) { + logger.log(url); + } + logger.groupEnd(); + } + /** + * @param {Array} urlsToPrecache + * @param {Array} urlsAlreadyPrecached + * + * @private + * @memberof workbox-precaching + */ + function printInstallDetails(urlsToPrecache, urlsAlreadyPrecached) { + const precachedCount = urlsToPrecache.length; + const alreadyPrecachedCount = urlsAlreadyPrecached.length; + if (precachedCount || alreadyPrecachedCount) { + let message = `Precaching ${precachedCount} file${precachedCount === 1 ? '' : 's'}.`; + if (alreadyPrecachedCount > 0) { + message += ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? ' is' : 's are'} already cached.`; + } + logger.groupCollapsed(message); + _nestedGroup(`View newly precached URLs.`, urlsToPrecache); + _nestedGroup(`View previously precached URLs.`, urlsAlreadyPrecached); + logger.groupEnd(); + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let supportStatus; + /** + * A utility function that determines whether the current browser supports + * constructing a new `Response` from a `response.body` stream. + * + * @return {boolean} `true`, if the current browser can successfully + * construct a `Response` from a `response.body` stream, `false` otherwise. + * + * @private + */ + function canConstructResponseFromBodyStream() { + if (supportStatus === undefined) { + const testResponse = new Response(''); + if ('body' in testResponse) { + try { + new Response(testResponse.body); + supportStatus = true; + } catch (error) { + supportStatus = false; + } + } + supportStatus = false; + } + return supportStatus; + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Allows developers to copy a response and modify its `headers`, `status`, + * or `statusText` values (the values settable via a + * [`ResponseInit`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#Syntax} + * object in the constructor). + * To modify these values, pass a function as the second argument. That + * function will be invoked with a single object with the response properties + * `{headers, status, statusText}`. The return value of this function will + * be used as the `ResponseInit` for the new `Response`. To change the values + * either modify the passed parameter(s) and return it, or return a totally + * new object. + * + * This method is intentionally limited to same-origin responses, regardless of + * whether CORS was used or not. + * + * @param {Response} response + * @param {Function} modifier + * @memberof workbox-core + */ + async function copyResponse(response, modifier) { + let origin = null; + // If response.url isn't set, assume it's cross-origin and keep origin null. + if (response.url) { + const responseURL = new URL(response.url); + origin = responseURL.origin; + } + if (origin !== self.location.origin) { + throw new WorkboxError('cross-origin-copy-response', { + origin + }); + } + const clonedResponse = response.clone(); + // Create a fresh `ResponseInit` object by cloning the headers. + const responseInit = { + headers: new Headers(clonedResponse.headers), + status: clonedResponse.status, + statusText: clonedResponse.statusText + }; + // Apply any user modifications. + const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit; + // Create the new response from the body stream and `ResponseInit` + // modifications. Note: not all browsers support the Response.body stream, + // so fall back to reading the entire body into memory as a blob. + const body = canConstructResponseFromBodyStream() ? clonedResponse.body : await clonedResponse.blob(); + return new Response(body, modifiedResponseInit); + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A {@link workbox-strategies.Strategy} implementation + * specifically designed to work with + * {@link workbox-precaching.PrecacheController} + * to both cache and fetch precached assets. + * + * Note: an instance of this class is created automatically when creating a + * `PrecacheController`; it's generally not necessary to create this yourself. + * + * @extends workbox-strategies.Strategy + * @memberof workbox-precaching + */ + class PrecacheStrategy extends Strategy { + /** + * + * @param {Object} [options] + * @param {string} [options.cacheName] Cache name to store and retrieve + * requests. Defaults to the cache names provided by + * {@link workbox-core.cacheNames}. + * @param {Array} [options.plugins] {@link https://developers.google.com/web/tools/workbox/guides/using-plugins|Plugins} + * to use in conjunction with this caching strategy. + * @param {Object} [options.fetchOptions] Values passed along to the + * {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init} + * of all fetch() requests made by this strategy. + * @param {Object} [options.matchOptions] The + * {@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions|CacheQueryOptions} + * for any `cache.match()` or `cache.put()` calls made by this strategy. + * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to + * get the response from the network if there's a precache miss. + */ + constructor(options = {}) { + options.cacheName = cacheNames.getPrecacheName(options.cacheName); + super(options); + this._fallbackToNetwork = options.fallbackToNetwork === false ? false : true; + // Redirected responses cannot be used to satisfy a navigation request, so + // any redirected response must be "copied" rather than cloned, so the new + // response doesn't contain the `redirected` flag. See: + // https://bugs.chromium.org/p/chromium/issues/detail?id=669363&desc=2#c1 + this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin); + } + /** + * @private + * @param {Request|string} request A request to run this strategy for. + * @param {workbox-strategies.StrategyHandler} handler The event that + * triggered the request. + * @return {Promise} + */ + async _handle(request, handler) { + const response = await handler.cacheMatch(request); + if (response) { + return response; + } + // If this is an `install` event for an entry that isn't already cached, + // then populate the cache. + if (handler.event && handler.event.type === 'install') { + return await this._handleInstall(request, handler); + } + // Getting here means something went wrong. An entry that should have been + // precached wasn't found in the cache. + return await this._handleFetch(request, handler); + } + async _handleFetch(request, handler) { + let response; + const params = handler.params || {}; + // Fall back to the network if we're configured to do so. + if (this._fallbackToNetwork) { + { + logger.warn(`The precached response for ` + `${getFriendlyURL(request.url)} in ${this.cacheName} was not ` + `found. Falling back to the network.`); + } + const integrityInManifest = params.integrity; + const integrityInRequest = request.integrity; + const noIntegrityConflict = !integrityInRequest || integrityInRequest === integrityInManifest; + // Do not add integrity if the original request is no-cors + // See https://github.com/GoogleChrome/workbox/issues/3096 + response = await handler.fetch(new Request(request, { + integrity: request.mode !== 'no-cors' ? integrityInRequest || integrityInManifest : undefined + })); + // It's only "safe" to repair the cache if we're using SRI to guarantee + // that the response matches the precache manifest's expectations, + // and there's either a) no integrity property in the incoming request + // or b) there is an integrity, and it matches the precache manifest. + // See https://github.com/GoogleChrome/workbox/issues/2858 + // Also if the original request users no-cors we don't use integrity. + // See https://github.com/GoogleChrome/workbox/issues/3096 + if (integrityInManifest && noIntegrityConflict && request.mode !== 'no-cors') { + this._useDefaultCacheabilityPluginIfNeeded(); + const wasCached = await handler.cachePut(request, response.clone()); + { + if (wasCached) { + logger.log(`A response for ${getFriendlyURL(request.url)} ` + `was used to "repair" the precache.`); + } + } + } + } else { + // This shouldn't normally happen, but there are edge cases: + // https://github.com/GoogleChrome/workbox/issues/1441 + throw new WorkboxError('missing-precache-entry', { + cacheName: this.cacheName, + url: request.url + }); + } + { + const cacheKey = params.cacheKey || (await handler.getCacheKey(request, 'read')); + // Workbox is going to handle the route. + // print the routing details to the console. + logger.groupCollapsed(`Precaching is responding to: ` + getFriendlyURL(request.url)); + logger.log(`Serving the precached url: ${getFriendlyURL(cacheKey instanceof Request ? cacheKey.url : cacheKey)}`); + logger.groupCollapsed(`View request details here.`); + logger.log(request); + logger.groupEnd(); + logger.groupCollapsed(`View response details here.`); + logger.log(response); + logger.groupEnd(); + logger.groupEnd(); + } + return response; + } + async _handleInstall(request, handler) { + this._useDefaultCacheabilityPluginIfNeeded(); + const response = await handler.fetch(request); + // Make sure we defer cachePut() until after we know the response + // should be cached; see https://github.com/GoogleChrome/workbox/issues/2737 + const wasCached = await handler.cachePut(request, response.clone()); + if (!wasCached) { + // Throwing here will lead to the `install` handler failing, which + // we want to do if *any* of the responses aren't safe to cache. + throw new WorkboxError('bad-precaching-response', { + url: request.url, + status: response.status + }); + } + return response; + } + /** + * This method is complex, as there a number of things to account for: + * + * The `plugins` array can be set at construction, and/or it might be added to + * to at any time before the strategy is used. + * + * At the time the strategy is used (i.e. during an `install` event), there + * needs to be at least one plugin that implements `cacheWillUpdate` in the + * array, other than `copyRedirectedCacheableResponsesPlugin`. + * + * - If this method is called and there are no suitable `cacheWillUpdate` + * plugins, we need to add `defaultPrecacheCacheabilityPlugin`. + * + * - If this method is called and there is exactly one `cacheWillUpdate`, then + * we don't have to do anything (this might be a previously added + * `defaultPrecacheCacheabilityPlugin`, or it might be a custom plugin). + * + * - If this method is called and there is more than one `cacheWillUpdate`, + * then we need to check if one is `defaultPrecacheCacheabilityPlugin`. If so, + * we need to remove it. (This situation is unlikely, but it could happen if + * the strategy is used multiple times, the first without a `cacheWillUpdate`, + * and then later on after manually adding a custom `cacheWillUpdate`.) + * + * See https://github.com/GoogleChrome/workbox/issues/2737 for more context. + * + * @private + */ + _useDefaultCacheabilityPluginIfNeeded() { + let defaultPluginIndex = null; + let cacheWillUpdatePluginCount = 0; + for (const [index, plugin] of this.plugins.entries()) { + // Ignore the copy redirected plugin when determining what to do. + if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) { + continue; + } + // Save the default plugin's index, in case it needs to be removed. + if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) { + defaultPluginIndex = index; + } + if (plugin.cacheWillUpdate) { + cacheWillUpdatePluginCount++; + } + } + if (cacheWillUpdatePluginCount === 0) { + this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin); + } else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) { + // Only remove the default plugin; multiple custom plugins are allowed. + this.plugins.splice(defaultPluginIndex, 1); + } + // Nothing needs to be done if cacheWillUpdatePluginCount is 1 + } + } + + PrecacheStrategy.defaultPrecacheCacheabilityPlugin = { + async cacheWillUpdate({ + response + }) { + if (!response || response.status >= 400) { + return null; + } + return response; + } + }; + PrecacheStrategy.copyRedirectedCacheableResponsesPlugin = { + async cacheWillUpdate({ + response + }) { + return response.redirected ? await copyResponse(response) : response; + } + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Performs efficient precaching of assets. + * + * @memberof workbox-precaching + */ + class PrecacheController { + /** + * Create a new PrecacheController. + * + * @param {Object} [options] + * @param {string} [options.cacheName] The cache to use for precaching. + * @param {string} [options.plugins] Plugins to use when precaching as well + * as responding to fetch events for precached assets. + * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to + * get the response from the network if there's a precache miss. + */ + constructor({ + cacheName, + plugins = [], + fallbackToNetwork = true + } = {}) { + this._urlsToCacheKeys = new Map(); + this._urlsToCacheModes = new Map(); + this._cacheKeysToIntegrities = new Map(); + this._strategy = new PrecacheStrategy({ + cacheName: cacheNames.getPrecacheName(cacheName), + plugins: [...plugins, new PrecacheCacheKeyPlugin({ + precacheController: this + })], + fallbackToNetwork + }); + // Bind the install and activate methods to the instance. + this.install = this.install.bind(this); + this.activate = this.activate.bind(this); + } + /** + * @type {workbox-precaching.PrecacheStrategy} The strategy created by this controller and + * used to cache assets and respond to fetch events. + */ + get strategy() { + return this._strategy; + } + /** + * Adds items to the precache list, removing any duplicates and + * stores the files in the + * {@link workbox-core.cacheNames|"precache cache"} when the service + * worker installs. + * + * This method can be called multiple times. + * + * @param {Array} [entries=[]] Array of entries to precache. + */ + precache(entries) { + this.addToCacheList(entries); + if (!this._installAndActiveListenersAdded) { + self.addEventListener('install', this.install); + self.addEventListener('activate', this.activate); + this._installAndActiveListenersAdded = true; + } + } + /** + * This method will add items to the precache list, removing duplicates + * and ensuring the information is valid. + * + * @param {Array} entries + * Array of entries to precache. + */ + addToCacheList(entries) { + { + finalAssertExports.isArray(entries, { + moduleName: 'workbox-precaching', + className: 'PrecacheController', + funcName: 'addToCacheList', + paramName: 'entries' + }); + } + const urlsToWarnAbout = []; + for (const entry of entries) { + // See https://github.com/GoogleChrome/workbox/issues/2259 + if (typeof entry === 'string') { + urlsToWarnAbout.push(entry); + } else if (entry && entry.revision === undefined) { + urlsToWarnAbout.push(entry.url); + } + const { + cacheKey, + url + } = createCacheKey(entry); + const cacheMode = typeof entry !== 'string' && entry.revision ? 'reload' : 'default'; + if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) { + throw new WorkboxError('add-to-cache-list-conflicting-entries', { + firstEntry: this._urlsToCacheKeys.get(url), + secondEntry: cacheKey + }); + } + if (typeof entry !== 'string' && entry.integrity) { + if (this._cacheKeysToIntegrities.has(cacheKey) && this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) { + throw new WorkboxError('add-to-cache-list-conflicting-integrities', { + url + }); + } + this._cacheKeysToIntegrities.set(cacheKey, entry.integrity); + } + this._urlsToCacheKeys.set(url, cacheKey); + this._urlsToCacheModes.set(url, cacheMode); + if (urlsToWarnAbout.length > 0) { + const warningMessage = `Workbox is precaching URLs without revision ` + `info: ${urlsToWarnAbout.join(', ')}\nThis is generally NOT safe. ` + `Learn more at https://bit.ly/wb-precache`; + { + logger.warn(warningMessage); + } + } + } + } + /** + * Precaches new and updated assets. Call this method from the service worker + * install event. + * + * Note: this method calls `event.waitUntil()` for you, so you do not need + * to call it yourself in your event handlers. + * + * @param {ExtendableEvent} event + * @return {Promise} + */ + install(event) { + // waitUntil returns Promise + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return waitUntil(event, async () => { + const installReportPlugin = new PrecacheInstallReportPlugin(); + this.strategy.plugins.push(installReportPlugin); + // Cache entries one at a time. + // See https://github.com/GoogleChrome/workbox/issues/2528 + for (const [url, cacheKey] of this._urlsToCacheKeys) { + const integrity = this._cacheKeysToIntegrities.get(cacheKey); + const cacheMode = this._urlsToCacheModes.get(url); + const request = new Request(url, { + integrity, + cache: cacheMode, + credentials: 'same-origin' + }); + await Promise.all(this.strategy.handleAll({ + params: { + cacheKey + }, + request, + event + })); + } + const { + updatedURLs, + notUpdatedURLs + } = installReportPlugin; + { + printInstallDetails(updatedURLs, notUpdatedURLs); + } + return { + updatedURLs, + notUpdatedURLs + }; + }); + } + /** + * Deletes assets that are no longer present in the current precache manifest. + * Call this method from the service worker activate event. + * + * Note: this method calls `event.waitUntil()` for you, so you do not need + * to call it yourself in your event handlers. + * + * @param {ExtendableEvent} event + * @return {Promise} + */ + activate(event) { + // waitUntil returns Promise + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return waitUntil(event, async () => { + const cache = await self.caches.open(this.strategy.cacheName); + const currentlyCachedRequests = await cache.keys(); + const expectedCacheKeys = new Set(this._urlsToCacheKeys.values()); + const deletedURLs = []; + for (const request of currentlyCachedRequests) { + if (!expectedCacheKeys.has(request.url)) { + await cache.delete(request); + deletedURLs.push(request.url); + } + } + { + printCleanupDetails(deletedURLs); + } + return { + deletedURLs + }; + }); + } + /** + * Returns a mapping of a precached URL to the corresponding cache key, taking + * into account the revision information for the URL. + * + * @return {Map} A URL to cache key mapping. + */ + getURLsToCacheKeys() { + return this._urlsToCacheKeys; + } + /** + * Returns a list of all the URLs that have been precached by the current + * service worker. + * + * @return {Array} The precached URLs. + */ + getCachedURLs() { + return [...this._urlsToCacheKeys.keys()]; + } + /** + * Returns the cache key used for storing a given URL. If that URL is + * unversioned, like `/index.html', then the cache key will be the original + * URL with a search parameter appended to it. + * + * @param {string} url A URL whose cache key you want to look up. + * @return {string} The versioned URL that corresponds to a cache key + * for the original URL, or undefined if that URL isn't precached. + */ + getCacheKeyForURL(url) { + const urlObject = new URL(url, location.href); + return this._urlsToCacheKeys.get(urlObject.href); + } + /** + * @param {string} url A cache key whose SRI you want to look up. + * @return {string} The subresource integrity associated with the cache key, + * or undefined if it's not set. + */ + getIntegrityForCacheKey(cacheKey) { + return this._cacheKeysToIntegrities.get(cacheKey); + } + /** + * This acts as a drop-in replacement for + * [`cache.match()`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match) + * with the following differences: + * + * - It knows what the name of the precache is, and only checks in that cache. + * - It allows you to pass in an "original" URL without versioning parameters, + * and it will automatically look up the correct cache key for the currently + * active revision of that URL. + * + * E.g., `matchPrecache('index.html')` will find the correct precached + * response for the currently active service worker, even if the actual cache + * key is `'/index.html?__WB_REVISION__=1234abcd'`. + * + * @param {string|Request} request The key (without revisioning parameters) + * to look up in the precache. + * @return {Promise} + */ + async matchPrecache(request) { + const url = request instanceof Request ? request.url : request; + const cacheKey = this.getCacheKeyForURL(url); + if (cacheKey) { + const cache = await self.caches.open(this.strategy.cacheName); + return cache.match(cacheKey); + } + return undefined; + } + /** + * Returns a function that looks up `url` in the precache (taking into + * account revision information), and returns the corresponding `Response`. + * + * @param {string} url The precached URL which will be used to lookup the + * `Response`. + * @return {workbox-routing~handlerCallback} + */ + createHandlerBoundToURL(url) { + const cacheKey = this.getCacheKeyForURL(url); + if (!cacheKey) { + throw new WorkboxError('non-precached-url', { + url + }); + } + return options => { + options.request = new Request(url); + options.params = Object.assign({ + cacheKey + }, options.params); + return this.strategy.handle(options); + }; + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let precacheController; + /** + * @return {PrecacheController} + * @private + */ + const getOrCreatePrecacheController = () => { + if (!precacheController) { + precacheController = new PrecacheController(); + } + return precacheController; + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Removes any URL search parameters that should be ignored. + * + * @param {URL} urlObject The original URL. + * @param {Array} ignoreURLParametersMatching RegExps to test against + * each search parameter name. Matches mean that the search parameter should be + * ignored. + * @return {URL} The URL with any ignored search parameters removed. + * + * @private + * @memberof workbox-precaching + */ + function removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching = []) { + // Convert the iterable into an array at the start of the loop to make sure + // deletion doesn't mess up iteration. + for (const paramName of [...urlObject.searchParams.keys()]) { + if (ignoreURLParametersMatching.some(regExp => regExp.test(paramName))) { + urlObject.searchParams.delete(paramName); + } + } + return urlObject; + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Generator function that yields possible variations on the original URL to + * check, one at a time. + * + * @param {string} url + * @param {Object} options + * + * @private + * @memberof workbox-precaching + */ + function* generateURLVariations(url, { + ignoreURLParametersMatching = [/^utm_/, /^fbclid$/], + directoryIndex = 'index.html', + cleanURLs = true, + urlManipulation + } = {}) { + const urlObject = new URL(url, location.href); + urlObject.hash = ''; + yield urlObject.href; + const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching); + yield urlWithoutIgnoredParams.href; + if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith('/')) { + const directoryURL = new URL(urlWithoutIgnoredParams.href); + directoryURL.pathname += directoryIndex; + yield directoryURL.href; + } + if (cleanURLs) { + const cleanURL = new URL(urlWithoutIgnoredParams.href); + cleanURL.pathname += '.html'; + yield cleanURL.href; + } + if (urlManipulation) { + const additionalURLs = urlManipulation({ + url: urlObject + }); + for (const urlToAttempt of additionalURLs) { + yield urlToAttempt.href; + } + } + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A subclass of {@link workbox-routing.Route} that takes a + * {@link workbox-precaching.PrecacheController} + * instance and uses it to match incoming requests and handle fetching + * responses from the precache. + * + * @memberof workbox-precaching + * @extends workbox-routing.Route + */ + class PrecacheRoute extends Route { + /** + * @param {PrecacheController} precacheController A `PrecacheController` + * instance used to both match requests and respond to fetch events. + * @param {Object} [options] Options to control how requests are matched + * against the list of precached URLs. + * @param {string} [options.directoryIndex=index.html] The `directoryIndex` will + * check cache entries for a URLs ending with '/' to see if there is a hit when + * appending the `directoryIndex` value. + * @param {Array} [options.ignoreURLParametersMatching=[/^utm_/, /^fbclid$/]] An + * array of regex's to remove search params when looking for a cache match. + * @param {boolean} [options.cleanURLs=true] The `cleanURLs` option will + * check the cache for the URL with a `.html` added to the end of the end. + * @param {workbox-precaching~urlManipulation} [options.urlManipulation] + * This is a function that should take a URL and return an array of + * alternative URLs that should be checked for precache matches. + */ + constructor(precacheController, options) { + const match = ({ + request + }) => { + const urlsToCacheKeys = precacheController.getURLsToCacheKeys(); + for (const possibleURL of generateURLVariations(request.url, options)) { + const cacheKey = urlsToCacheKeys.get(possibleURL); + if (cacheKey) { + const integrity = precacheController.getIntegrityForCacheKey(cacheKey); + return { + cacheKey, + integrity + }; + } + } + { + logger.debug(`Precaching did not find a match for ` + getFriendlyURL(request.url)); + } + return; + }; + super(match, precacheController.strategy); + } + } + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Add a `fetch` listener to the service worker that will + * respond to + * [network requests]{@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Custom_responses_to_requests} + * with precached assets. + * + * Requests for assets that aren't precached, the `FetchEvent` will not be + * responded to, allowing the event to fall through to other `fetch` event + * listeners. + * + * @param {Object} [options] See the {@link workbox-precaching.PrecacheRoute} + * options. + * + * @memberof workbox-precaching + */ + function addRoute(options) { + const precacheController = getOrCreatePrecacheController(); + const precacheRoute = new PrecacheRoute(precacheController, options); + registerRoute(precacheRoute); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds items to the precache list, removing any duplicates and + * stores the files in the + * {@link workbox-core.cacheNames|"precache cache"} when the service + * worker installs. + * + * This method can be called multiple times. + * + * Please note: This method **will not** serve any of the cached files for you. + * It only precaches files. To respond to a network request you call + * {@link workbox-precaching.addRoute}. + * + * If you have a single array of files to precache, you can just call + * {@link workbox-precaching.precacheAndRoute}. + * + * @param {Array} [entries=[]] Array of entries to precache. + * + * @memberof workbox-precaching + */ + function precache(entries) { + const precacheController = getOrCreatePrecacheController(); + precacheController.precache(entries); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This method will add entries to the precache list and add a route to + * respond to fetch events. + * + * This is a convenience method that will call + * {@link workbox-precaching.precache} and + * {@link workbox-precaching.addRoute} in a single call. + * + * @param {Array} entries Array of entries to precache. + * @param {Object} [options] See the + * {@link workbox-precaching.PrecacheRoute} options. + * + * @memberof workbox-precaching + */ + function precacheAndRoute(entries, options) { + precache(entries); + addRoute(options); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const SUBSTRING_TO_FIND = '-precache-'; + /** + * Cleans up incompatible precaches that were created by older versions of + * Workbox, by a service worker registered under the current scope. + * + * This is meant to be called as part of the `activate` event. + * + * This should be safe to use as long as you don't include `substringToFind` + * (defaulting to `-precache-`) in your non-precache cache names. + * + * @param {string} currentPrecacheName The cache name currently in use for + * precaching. This cache won't be deleted. + * @param {string} [substringToFind='-precache-'] Cache names which include this + * substring will be deleted (excluding `currentPrecacheName`). + * @return {Array} A list of all the cache names that were deleted. + * + * @private + * @memberof workbox-precaching + */ + const deleteOutdatedCaches = async (currentPrecacheName, substringToFind = SUBSTRING_TO_FIND) => { + const cacheNames = await self.caches.keys(); + const cacheNamesToDelete = cacheNames.filter(cacheName => { + return cacheName.includes(substringToFind) && cacheName.includes(self.registration.scope) && cacheName !== currentPrecacheName; + }); + await Promise.all(cacheNamesToDelete.map(cacheName => self.caches.delete(cacheName))); + return cacheNamesToDelete; + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds an `activate` event listener which will clean up incompatible + * precaches that were created by older versions of Workbox. + * + * @memberof workbox-precaching + */ + function cleanupOutdatedCaches() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener('activate', event => { + const cacheName = cacheNames.getPrecacheName(); + event.waitUntil(deleteOutdatedCaches(cacheName).then(cachesDeleted => { + { + if (cachesDeleted.length > 0) { + logger.log(`The following out-of-date precaches were cleaned up ` + `automatically:`, cachesDeleted); + } + } + })); + }); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * NavigationRoute makes it easy to create a + * {@link workbox-routing.Route} that matches for browser + * [navigation requests]{@link https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests}. + * + * It will only match incoming Requests whose + * {@link https://fetch.spec.whatwg.org/#concept-request-mode|mode} + * is set to `navigate`. + * + * You can optionally only apply this route to a subset of navigation requests + * by using one or both of the `denylist` and `allowlist` parameters. + * + * @memberof workbox-routing + * @extends workbox-routing.Route + */ + class NavigationRoute extends Route { + /** + * If both `denylist` and `allowlist` are provided, the `denylist` will + * take precedence and the request will not match this route. + * + * The regular expressions in `allowlist` and `denylist` + * are matched against the concatenated + * [`pathname`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/pathname} + * and [`search`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/search} + * portions of the requested URL. + * + * *Note*: These RegExps may be evaluated against every destination URL during + * a navigation. Avoid using + * [complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077), + * or else your users may see delays when navigating your site. + * + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {Object} options + * @param {Array} [options.denylist] If any of these patterns match, + * the route will not handle the request (even if a allowlist RegExp matches). + * @param {Array} [options.allowlist=[/./]] If any of these patterns + * match the URL's pathname and search parameter, the route will handle the + * request (assuming the denylist doesn't match). + */ + constructor(handler, { + allowlist = [/./], + denylist = [] + } = {}) { + { + finalAssertExports.isArrayOfClass(allowlist, RegExp, { + moduleName: 'workbox-routing', + className: 'NavigationRoute', + funcName: 'constructor', + paramName: 'options.allowlist' + }); + finalAssertExports.isArrayOfClass(denylist, RegExp, { + moduleName: 'workbox-routing', + className: 'NavigationRoute', + funcName: 'constructor', + paramName: 'options.denylist' + }); + } + super(options => this._match(options), handler); + this._allowlist = allowlist; + this._denylist = denylist; + } + /** + * Routes match handler. + * + * @param {Object} options + * @param {URL} options.url + * @param {Request} options.request + * @return {boolean} + * + * @private + */ + _match({ + url, + request + }) { + if (request && request.mode !== 'navigate') { + return false; + } + const pathnameAndSearch = url.pathname + url.search; + for (const regExp of this._denylist) { + if (regExp.test(pathnameAndSearch)) { + { + logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL matches this denylist pattern: ` + `${regExp.toString()}`); + } + return false; + } + } + if (this._allowlist.some(regExp => regExp.test(pathnameAndSearch))) { + { + logger.debug(`The navigation route ${pathnameAndSearch} ` + `is being used.`); + } + return true; + } + { + logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL being navigated to doesn't ` + `match the allowlist.`); + } + return false; + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Helper function that calls + * {@link PrecacheController#createHandlerBoundToURL} on the default + * {@link PrecacheController} instance. + * + * If you are creating your own {@link PrecacheController}, then call the + * {@link PrecacheController#createHandlerBoundToURL} on that instance, + * instead of using this function. + * + * @param {string} url The precached URL which will be used to lookup the + * `Response`. + * @param {boolean} [fallbackToNetwork=true] Whether to attempt to get the + * response from the network if there's a precache miss. + * @return {workbox-routing~handlerCallback} + * + * @memberof workbox-precaching + */ + function createHandlerBoundToURL(url) { + const precacheController = getOrCreatePrecacheController(); + return precacheController.createHandlerBoundToURL(url); + } + + exports.CacheFirst = CacheFirst; + exports.CacheableResponsePlugin = CacheableResponsePlugin; + exports.ExpirationPlugin = ExpirationPlugin; + exports.NavigationRoute = NavigationRoute; + exports.cleanupOutdatedCaches = cleanupOutdatedCaches; + exports.clientsClaim = clientsClaim; + exports.createHandlerBoundToURL = createHandlerBoundToURL; + exports.precacheAndRoute = precacheAndRoute; + exports.registerRoute = registerRoute; + +})); diff --git a/mobile/index.html b/mobile/index.html index 16dab3ea6..01210deab 100644 --- a/mobile/index.html +++ b/mobile/index.html @@ -6,10 +6,11 @@ Raven + - + @@ -122,12 +123,12 @@ - - - - - - + + + + \ No newline at end of file diff --git a/raven-app/src/assets/icons/file.svg b/raven-app/src/assets/icons/file.svg new file mode 100644 index 000000000..688cdc092 --- /dev/null +++ b/raven-app/src/assets/icons/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/raven-app/src/assets/icons/image.svg b/raven-app/src/assets/icons/image.svg new file mode 100644 index 000000000..111815ee4 --- /dev/null +++ b/raven-app/src/assets/icons/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/raven-app/src/assets/icons/pdf.svg b/raven-app/src/assets/icons/pdf.svg new file mode 100644 index 000000000..31556e151 --- /dev/null +++ b/raven-app/src/assets/icons/pdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/raven-app/src/assets/icons/word.svg b/raven-app/src/assets/icons/word.svg new file mode 100644 index 000000000..1d095463f --- /dev/null +++ b/raven-app/src/assets/icons/word.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/raven-app/src/components/feature/channel-details/rename-channel/EditChannelNameButton.tsx b/raven-app/src/components/feature/channel-details/rename-channel/EditChannelNameButton.tsx index 8d5042018..23710ca30 100644 --- a/raven-app/src/components/feature/channel-details/rename-channel/EditChannelNameButton.tsx +++ b/raven-app/src/components/feature/channel-details/rename-channel/EditChannelNameButton.tsx @@ -1,7 +1,7 @@ import { RenameChannelModalContent } from '@/components/feature/channel-details/rename-channel/ChannelRenameModal' import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { Dialog, IconButton } from '@radix-ui/themes' -import { Pencil2Icon } from '@radix-ui/react-icons' +import { PenSquare } from 'lucide-react' import { IconButtonProps } from '@radix-ui/themes/dist/cjs/components/icon-button' import { useState } from "react" import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' @@ -29,7 +29,7 @@ export const EditChannelNameButton = ({ channelID, channel_name, channelType, .. aria-label="Click to edit channel name" title='Edit channel name' {...props}> - + diff --git a/raven-app/src/components/feature/channel-member-details/ChannelMemberDetails.tsx b/raven-app/src/components/feature/channel-member-details/ChannelMemberDetails.tsx index ee9b2e89a..2d2d8c14b 100644 --- a/raven-app/src/components/feature/channel-member-details/ChannelMemberDetails.tsx +++ b/raven-app/src/components/feature/channel-member-details/ChannelMemberDetails.tsx @@ -1,14 +1,12 @@ import { useContext, useState } from "react" -import { BsFillCircleFill, BsCircle } from "react-icons/bs" import { useDebounce } from "../../../hooks/useDebounce" import { UserContext } from "../../../utils/auth/UserProvider" -import { RiVipCrownFill } from "react-icons/ri" import { ChannelListItem } from "@/utils/channel/ChannelListProvider" import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider" import { AddMembersButton } from "./add-members/AddMembersButton" import { RemoveMemberButton } from "./remove-members/RemoveMemberButton" import { Box, Flex, TextField, Text } from "@radix-ui/themes" -import { MagnifyingGlassIcon } from "@radix-ui/react-icons" +import { Search, Circle, Crown } from "lucide-react" import { UserAvatar } from "@/components/common/UserAvatar" interface MemberDetailsProps { channelData: ChannelListItem, @@ -41,7 +39,7 @@ export const ChannelMemberDetails = ({ channelData, channelMembers, activeUsers,
- + @@ -75,14 +73,14 @@ export const ChannelMemberDetails = ({ channelData, channelMembers, activeUsers, {member.first_name} {activeUsers.includes(member.name) ? ( - + ) : ( - + )} {member.full_name} {member.name === currentUser && (You)} - {channelMembers[member.name].is_admin == 1 && } + {channelMembers[member.name].is_admin == 1 && } diff --git a/raven-app/src/components/feature/channel-member-details/add-members/AddMembersButton.tsx b/raven-app/src/components/feature/channel-member-details/add-members/AddMembersButton.tsx index 8ca328837..5dda3966a 100644 --- a/raven-app/src/components/feature/channel-member-details/add-members/AddMembersButton.tsx +++ b/raven-app/src/components/feature/channel-member-details/add-members/AddMembersButton.tsx @@ -1,12 +1,11 @@ import { AddChannelMembersModalContent } from "./AddChannelMemberModal" -import { RiUserAddLine } from "react-icons/ri" import { ChannelListItem } from "@/utils/channel/ChannelListProvider" -import { BiUserPlus } from "react-icons/bi" import { useState } from "react" import { Button, Dialog, IconButton } from "@radix-ui/themes" import { ButtonProps } from "@radix-ui/themes/dist/cjs/components/button" import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider" import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog" +import { UserPlus } from 'lucide-react' interface AddMembersButtonProps extends ButtonProps { channelData: ChannelListItem, @@ -27,11 +26,11 @@ export const AddMembersButton = ({ channelData, updateMembers, isIconButton = fa {isIconButton ? - + : + Add members } diff --git a/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelButton.tsx b/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelButton.tsx index 109bd1e8b..9b3116ed9 100644 --- a/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelButton.tsx +++ b/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelButton.tsx @@ -1,4 +1,4 @@ -import { BsArchive } from "react-icons/bs"; +import { Archive } from "lucide-react"; import { ArchiveChannelModal } from "./ArchiveChannelModal"; import { ChannelListItem } from "@/utils/channel/ChannelListProvider"; import { useState } from "react"; @@ -21,7 +21,7 @@ export const ArchiveChannelButton = ({ onClose: onCloseParent, channelData }: Ar diff --git a/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx b/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx index 1dde9a65e..1c2e0cc51 100644 --- a/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx +++ b/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx @@ -1,5 +1,5 @@ import { ChannelListItem } from '@/utils/channel/ChannelListProvider' -import { BiHash, BiLockAlt } from 'react-icons/bi' +import { Hash, Lock } from 'lucide-react' import { ChangeChannelTypeModal } from './ChangeChannelTypeModal' import { useState } from 'react' import { Button, Dialog } from '@radix-ui/themes' @@ -19,7 +19,7 @@ export const ChangeChannelTypeButton = ({ channelData }: ChangeChannelTypeButton diff --git a/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelButton.tsx b/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelButton.tsx index 87e907c89..f1542bc43 100644 --- a/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelButton.tsx +++ b/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelButton.tsx @@ -1,6 +1,6 @@ import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { DeleteChannelModal } from './DeleteChannelModal' -import { BsTrash } from 'react-icons/bs' +import { Trash2 } from 'lucide-react' import { useState } from 'react' import { AlertDialog, Button } from '@radix-ui/themes' import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' @@ -21,7 +21,7 @@ export const DeleteChannelButton = ({ onClose: onCloseParent, channelData }: Del diff --git a/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx b/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx index b64beff12..2dda20bca 100644 --- a/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx +++ b/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx @@ -6,7 +6,7 @@ import { useFrappeDeleteDoc } from 'frappe-react-sdk' import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { AlertDialog, Button, Callout, Checkbox, Flex, Text } from '@radix-ui/themes' import { Loader } from '@/components/common/Loader' -import { ExclamationTriangleIcon } from '@radix-ui/react-icons' +import { AlertTriangle } from 'lucide-react' type DeleteChannelModalProps = { onClose: () => void, @@ -65,23 +65,25 @@ export const DeleteChannelModal = ({ onClose, onCloseParent, channelData }: Dele - + - + This action is permanent and cannot be undone. When you delete a channel, all messages from this channel will be removed immediately.
    -
  • All messages, including files and images will be removed
  • -
  • You can archive this channel instead to preserve your messages
  • +
  • All messages, including files and images will be removed
  • +
  • You can archive this channel instead to preserve your messages
- - setAllowDelete(!allowDelete)} color='red' /> - Yes, I understand, permanently delete this channel - + + + setAllowDelete(!allowDelete)} color='red' /> + Yes, I understand, permanently delete this channel + + diff --git a/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx b/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx index 66c744a8b..f94e11ca9 100644 --- a/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx +++ b/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx @@ -1,9 +1,9 @@ -import { getFileExtensionIcon } from "../../../utils/layout/fileExtensionIcon"; +import { FileExtensionIcon } from "../../../utils/layout/FileExtensionIcon"; import { useFrappeGetCall } from "frappe-react-sdk"; import { useParams } from "react-router-dom"; import { ErrorBanner } from "../../layout/AlertBanner"; import { DateObjectToFormattedDateString, getFileExtension, getFileName } from "../../../utils/operations"; -import { BsDownload } from "react-icons/bs"; +import { Download } from "lucide-react"; import { FileMessage } from "../../../../../types/Messaging/Message"; import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider"; import { Box, Flex, IconButton, Link, Text } from "@radix-ui/themes"; @@ -39,10 +39,10 @@ export const FilesSharedInChannel = ({ channelMembers }: FilesSharedInChannelPro - - {f.message_type === 'File' &&
{getFileExtensionIcon(getFileExtension(f.file))}
} + + {f.message_type === 'File' && } {f.message_type === 'Image' && File preview} -
+
{getFileName(f.file)} Shared by {channelMembers[f.owner]?.full_name} on {DateObjectToFormattedDateString(new Date(f.creation ?? ''))} @@ -55,7 +55,7 @@ export const FilesSharedInChannel = ({ channelMembers }: FilesSharedInChannelPro size='1' color='gray' variant='ghost'> - + diff --git a/raven-app/src/components/feature/channels/ChannelList.tsx b/raven-app/src/components/feature/channels/ChannelList.tsx index 8f5dd2737..f490b6417 100644 --- a/raven-app/src/components/feature/channels/ChannelList.tsx +++ b/raven-app/src/components/feature/channels/ChannelList.tsx @@ -46,7 +46,7 @@ const ChannelItem = ({ channel, unreadCount }: { channel: ChannelListItem, unrea return ( - + {channel.channel_name} diff --git a/raven-app/src/components/feature/channels/CreateChannelModal.tsx b/raven-app/src/components/feature/channels/CreateChannelModal.tsx index ebb261955..4e7ba74a3 100644 --- a/raven-app/src/components/feature/channels/CreateChannelModal.tsx +++ b/raven-app/src/components/feature/channels/CreateChannelModal.tsx @@ -2,11 +2,11 @@ import { useToast } from '@chakra-ui/react' import { useFrappeCreateDoc } from 'frappe-react-sdk' import { ChangeEvent, useCallback, useMemo, useState } from 'react' import { Controller, FormProvider, useForm } from 'react-hook-form' -import { BiGlobe, BiHash, BiLockAlt } from 'react-icons/bi' +import { Globe, Hash, Lock } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { ErrorBanner } from '../../layout/AlertBanner' import { Box, Button, Dialog, Flex, IconButton, RadioGroup, Text, TextArea, TextField } from '@radix-ui/themes' -import { FiPlus } from 'react-icons/fi' +import { Plus } from 'lucide-react' import { ErrorText, HelperText, Label } from '@/components/common/Form' import { Loader } from '@/components/common/Loader' import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' @@ -79,19 +79,19 @@ export const CreateChannelButton = ({ updateChannelList }: { updateChannelList: switch (channelType) { case 'Private': return { - channelIcon: , + channelIcon: , header: 'Create a private channel', helperText: 'When a channel is set to private, it can only be viewed or joined by invitation.' } case 'Open': return { - channelIcon: , + channelIcon: , header: 'Create an open channel', helperText: 'When a channel is set to open, everyone is a member.' } default: return { - channelIcon: , + channelIcon: , header: 'Create a public channel', helperText: 'When a channel is set to public, anyone can join the channel and read messages, but only members can post messages.' } @@ -101,7 +101,7 @@ export const CreateChannelButton = ({ updateChannelList }: { updateChannelList: return - + @@ -210,7 +210,8 @@ export const CreateChannelButton = ({ updateChannelList }: { updateChannelList: )} /> - + {/* Added min height to avoid layout shift when two lines of text are shown */} + {helperText} diff --git a/raven-app/src/components/feature/chat-header/SearchButton.tsx b/raven-app/src/components/feature/chat-header/SearchButton.tsx index 895f94c05..12c930e03 100644 --- a/raven-app/src/components/feature/chat-header/SearchButton.tsx +++ b/raven-app/src/components/feature/chat-header/SearchButton.tsx @@ -1,7 +1,7 @@ -import { HiOutlineSearch } from 'react-icons/hi' +import { Search } from 'lucide-react' import { CommandPalette } from '../command-palette' import { useState } from 'react' -import { Button, Dialog, Tooltip } from '@radix-ui/themes' +import { Button, Dialog, Kbd, Tooltip } from '@radix-ui/themes' import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' export const SearchButton = () => { @@ -11,6 +11,10 @@ export const SearchButton = () => { setOpen(false) } + const onCommandPaletteToggle = () => { + setOpen(!open) + } + return ( @@ -20,16 +24,18 @@ export const SearchButton = () => { size='2' variant='soft' aria-label="search"> - + Search + {/*  K */} - {/* */} + /> ) diff --git a/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx b/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx index 9fb0fcc77..4a17040a8 100644 --- a/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx +++ b/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx @@ -1,4 +1,3 @@ -import { RiUserLine } from "react-icons/ri" import { ViewChannelDetailsModalContent } from "../channels/ViewChannelDetailsModal" import { useContext, useState } from "react" import { ActiveUsersContext } from "@/utils/users/ActiveUsersProvider" @@ -6,6 +5,7 @@ import { ChannelListItem } from "@/utils/channel/ChannelListProvider" import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider" import { Button, Dialog, Tooltip } from "@radix-ui/themes" import { UserAvatar } from "@/components/common/UserAvatar" +import { User } from "lucide-react" interface ViewChannelDetailsButtonProps { channelData: ChannelListItem, @@ -36,7 +36,7 @@ export const ViewChannelDetailsButton = ({ channelData, channelMembers, updateMe return })} {totalMembers > 3 &&
+ {totalMembers - 3}
} -
: } + : } diff --git a/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx b/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx index cfb1105a7..105e51a86 100644 --- a/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx +++ b/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx @@ -1,12 +1,10 @@ import { useCurrentEditor } from '@tiptap/react' -import { BiAt, BiHash, BiSmile } from 'react-icons/bi' +import { AtSign, Hash, Smile, SendHorizontal, Paperclip } from 'lucide-react' import { DEFAULT_BUTTON_STYLE, ICON_PROPS } from './ToolPanel' import { EmojiPicker } from '../../../common/EmojiPicker/EmojiPicker' import { ToolbarFileProps } from './Tiptap' -import { AiOutlinePaperClip } from 'react-icons/ai' import { Flex, IconButton, Inset, Popover, Separator } from '@radix-ui/themes' import { Loader } from '@/components/common/Loader' -import { IoMdSend } from 'react-icons/io' type RightToolbarButtonsProps = { fileProps?: ToolbarFileProps, @@ -59,7 +57,7 @@ const MentionButtons = () => { .insertContent('#') .run() || !editor.isEditable }> - + editor.chain().focus().insertContent('@').run()} @@ -75,7 +73,7 @@ const MentionButtons = () => { .insertContent('@') .run() || !editor.isEditable }> - + } @@ -97,7 +95,7 @@ const EmojiPickerButton = () => { title='Add emoji' disabled={!editor.can().chain().focus().insertContent('😅').run() || !editor.isEditable} aria-label={"add emoji"}> - + @@ -124,7 +122,7 @@ const FilePickerButton = ({ fileProps }: { fileProps: ToolbarFileProps }) => { disabled={editor?.isEditable === false} title='Attach file' aria-label={"attach file"}> - + } @@ -166,7 +164,7 @@ const SendButton = ({ sendMessage, messageSending }: { onClick={onClick} > {messageSending ? : - + } } \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx b/raven-app/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx index be407884a..6cf0c5ae3 100644 --- a/raven-app/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx +++ b/raven-app/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx @@ -1,6 +1,5 @@ import { useCurrentEditor } from '@tiptap/react' -import { BiBold, BiCodeAlt, BiHighlight, BiItalic, BiLink, BiListOl, BiListUl, BiStrikethrough, BiUnderline } from 'react-icons/bi' -import { BsBlockquoteLeft } from 'react-icons/bs' +import { Bold, Code2, Highlighter, Italic, Link, ListOrdered, List, Strikethrough, Underline, TextQuote } from 'lucide-react' import { DEFAULT_BUTTON_STYLE, ICON_PROPS } from './ToolPanel' import { Box, Flex, IconButton, Separator } from '@radix-ui/themes' @@ -30,7 +29,7 @@ export const TextFormattingMenu = () => { .toggleBold() .run() }> - + { .toggleItalic() .run() }> - + editor.chain().focus().toggleUnderline().run()} @@ -64,7 +63,7 @@ export const TextFormattingMenu = () => { .run() } > - + @@ -87,7 +86,7 @@ export const TextFormattingMenu = () => { .run() } > - + editor.chain().focus().toggleStrike().run()} @@ -104,7 +103,7 @@ export const TextFormattingMenu = () => { .run() } > - + @@ -124,7 +123,7 @@ export const TextFormattingMenu = () => { .run() } > - + @@ -144,7 +143,7 @@ export const TextFormattingMenu = () => { .run() } > - + editor.chain().focus().liftEmptyBlock().toggleBulletList().run()} @@ -161,7 +160,7 @@ export const TextFormattingMenu = () => { .run() } > - + @@ -182,7 +181,7 @@ export const TextFormattingMenu = () => { .run() } > - + diff --git a/raven-app/src/components/feature/chat/ChatInput/ToolPanel.tsx b/raven-app/src/components/feature/chat/ChatInput/ToolPanel.tsx index e0cc9ddc3..4cd9b1951 100644 --- a/raven-app/src/components/feature/chat/ChatInput/ToolPanel.tsx +++ b/raven-app/src/components/feature/chat/ChatInput/ToolPanel.tsx @@ -2,7 +2,7 @@ import { Flex } from '@radix-ui/themes' import { FlexProps } from '@radix-ui/themes/dist/cjs/components/flex' export const ICON_PROPS = { - size: '18px' + size: '18' } export const DEFAULT_BUTTON_STYLE = 'bg-transparent text-[var(--gray-11)] hover:bg-[var(--accent-a3)] hover:text-[var(--accent-a11)]' diff --git a/raven-app/src/components/feature/chat/chat-footer/ArchivedChannelBox.tsx b/raven-app/src/components/feature/chat/chat-footer/ArchivedChannelBox.tsx index 8c6bb798b..d30706cae 100644 --- a/raven-app/src/components/feature/chat/chat-footer/ArchivedChannelBox.tsx +++ b/raven-app/src/components/feature/chat/chat-footer/ArchivedChannelBox.tsx @@ -6,7 +6,7 @@ import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider" import { Text, Box, HStack, Stack, useToast, Button } from "@chakra-ui/react" import { useFrappeUpdateDoc } from "frappe-react-sdk" import { useContext } from "react" -import { BiHash } from "react-icons/bi" +import { Hash } from "lucide-react" interface ArchivedChannelBoxProps { channelData: ChannelListItem, @@ -53,7 +53,7 @@ export const ArchivedChannelBox = ({ channelData, channelMembers }: ArchivedChan - + {channelData.channel_name} diff --git a/raven-app/src/components/feature/chat/chat-footer/JoinChannelBox.tsx b/raven-app/src/components/feature/chat/chat-footer/JoinChannelBox.tsx index 6d0530f61..dc91d3e5b 100644 --- a/raven-app/src/components/feature/chat/chat-footer/JoinChannelBox.tsx +++ b/raven-app/src/components/feature/chat/chat-footer/JoinChannelBox.tsx @@ -2,7 +2,7 @@ import { ErrorBanner } from "@/components/layout/AlertBanner" import { ChannelListItem } from "@/utils/channel/ChannelListProvider" import { Text, Box, HStack, Stack, Center, ButtonGroup, Button, useToast, useDisclosure } from "@chakra-ui/react" import { useFrappeCreateDoc } from "frappe-react-sdk" -import { BiHash } from "react-icons/bi" +import { Hash } from "lucide-react" import { ViewChannelDetailsModalContent } from "../../channels/ViewChannelDetailsModal" import { useContext } from "react" import { ActiveUsersContext } from "@/utils/users/ActiveUsersProvider" @@ -50,7 +50,7 @@ export const JoinChannelBox = ({ channelData, channelMembers, user }: JoinChanne - + {channelData?.channel_name}
diff --git a/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx b/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx index b6a2ba940..44ccc0939 100644 --- a/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx +++ b/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx @@ -1,9 +1,9 @@ import { Collapse, HStack, Icon, IconButton, Link, Stack, Text, useBoolean, Image } from "@chakra-ui/react" -import { getFileExtensionIcon } from "../../../../utils/layout/fileExtensionIcon" +import { FileExtensionIcon } from "../../../../utils/layout/FileExtensionIcon" import { FileMessage } from "../../../../../../types/Messaging/Message" import { useCallback } from "react" -import { BsFillCaretDownFill, BsFillCaretRightFill } from "react-icons/bs" import { getFileExtension, getFileName } from "../../../../utils/operations" +import { ChevronDown, ChevronRight } from "lucide-react" interface FileMessageProps extends Partial { onFilePreviewModalOpen: ({ file, owner, creation, message_type }: Partial) => void @@ -26,7 +26,7 @@ export const FileMessageBlock = ({ file, owner, creation, message_type, onFilePr return (
- {getFileExtensionIcon(getFileExtension(file))} + {}
{getFileExtension(file).toLowerCase() === 'pdf' ? @@ -48,7 +48,7 @@ export const FileMessageBlock = ({ file, owner, creation, message_type, onFilePr {{getFileName(file)}} : } /> + icon={showImage ? : } /> File preview :
- {getFileExtensionIcon(getFileExtension(previous_message_content.file) ?? '')} + {FileExtensionIcon(getFileExtension(previous_message_content.file) ?? '')}
}
{getFileName(previous_message_content.file)} @@ -70,7 +70,7 @@ export const PreviousMessageBox = ({ previous_message_id, previous_message_conte size="xs" title='Remove message' variant="ghost" - icon={} + icon={} aria-label="Remove message" />
@@ -152,7 +152,7 @@ const PreviousMessageBoxInChat = ({ previous_message_id, channelData, users }: P {data.message_type === 'Image' ? File preview :
- {getFileExtensionIcon(getFileExtension(data.file) ?? '')} + {FileExtensionIcon(getFileExtension(data.file) ?? '')}
} {getFileName(data.file)} diff --git a/raven-app/src/components/feature/command-palette/CommandPalette.tsx b/raven-app/src/components/feature/command-palette/CommandPalette.tsx index 20aa1be3e..740f89e73 100644 --- a/raven-app/src/components/feature/command-palette/CommandPalette.tsx +++ b/raven-app/src/components/feature/command-palette/CommandPalette.tsx @@ -8,7 +8,7 @@ import { useDebounce } from '../../../hooks/useDebounce' import { useFrappePostCall } from 'frappe-react-sdk' import { useNavigate } from 'react-router-dom' import { UserContext } from '../../../utils/auth/UserProvider' -import { TbSearch } from 'react-icons/tb' +import { Search } from 'lucide-react' import { UserListContext } from '@/utils/users/UserListProvider' import { ActiveUsersContext } from '@/utils/users/ActiveUsersProvider' import { ModalTypes, useModalManager } from '@/hooks/useModalManager' @@ -103,7 +103,7 @@ export const CommandPalette = ({ isOpen, onClose, onToggle }: CommandPaletteProp })} />
))} - {!activePage && } + {!activePage && } - + {channelData.is_direct_message ? (channelData.is_self_message ? `Find in direct messages with ${users[currentUser].first_name}` : @@ -102,7 +101,7 @@ export const Home = ({ searchChange, input, isGlobalSearchModalOpen, children, i +
diff --git a/raven-app/tsconfig.json b/raven-app/tsconfig.json index bec634033..dcfdac4ed 100644 --- a/raven-app/tsconfig.json +++ b/raven-app/tsconfig.json @@ -7,6 +7,9 @@ "DOM.Iterable", "ESNext" ], + "types": [ + "vite-plugin-svgr/client" + ], "allowJs": false, "skipLibCheck": true, "esModuleInterop": false, diff --git a/raven-app/vite.config.ts b/raven-app/vite.config.ts index ccb91e8ef..8d37c548b 100644 --- a/raven-app/vite.config.ts +++ b/raven-app/vite.config.ts @@ -2,10 +2,12 @@ import path from 'path'; import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react' import proxyOptions from './proxyOptions'; +import svgr from "vite-plugin-svgr"; +/// // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), svgr()], server: { port: 8080, proxy: proxyOptions diff --git a/raven-app/yarn.lock b/raven-app/yarn.lock index b34ab2f05..850b3ceb2 100644 --- a/raven-app/yarn.lock +++ b/raven-app/yarn.lock @@ -28,7 +28,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.3.tgz#3febd552541e62b5e883a25eb3effd7c7379db11" integrity sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ== -"@babel/core@^7.20.12": +"@babel/core@^7.20.12", "@babel/core@^7.21.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.3.tgz#5ec09c8803b91f51cc887dedc2654a35852849c9" integrity sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew== @@ -211,7 +211,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.3", "@babel/types@^7.23.4": +"@babel/types@^7.21.3", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.3", "@babel/types@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.4.tgz#7206a1810fc512a7f7f7d4dace4cb4c1c9dbfb8e" integrity sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ== @@ -418,13 +418,6 @@ dependencies: "@chakra-ui/shared-utils" "2.0.5" -"@chakra-ui/icons@^2.0.17": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@chakra-ui/icons/-/icons-2.1.1.tgz#58ff0f9e703f2f4f89debd600ce4e438f43f9c9a" - integrity sha512-3p30hdo4LlRZTT5CwoAJq3G9fHI0wDc0pBaMHj4SUn0yomO+RcDRlzhdXqdr5cVnzax44sqXJVnf3oQG0eI+4g== - dependencies: - "@chakra-ui/icon" "3.2.0" - "@chakra-ui/image@2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@chakra-ui/image/-/image-2.1.0.tgz#6c205f1ca148e3bf58345b0b5d4eb3d959eb9f87" @@ -1638,11 +1631,6 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-controllable-state" "1.0.1" -"@radix-ui/react-icons@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69" - integrity sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw== - "@radix-ui/react-id@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e" @@ -2134,11 +2122,103 @@ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.0.tgz#7e29c4ee85176d9c08cb0f4456bff74d092c5065" integrity sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA== +"@rollup/pluginutils@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz#bbb4c175e19ebfeeb8c132c2eea0ecb89941a66c" + integrity sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + "@socket.io/component-emitter@~3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== +"@svgr/babel-plugin-add-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" + integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== + +"@svgr/babel-plugin-remove-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" + integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== + +"@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" + integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== + +"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27" + integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== + +"@svgr/babel-plugin-svg-dynamic-title@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0" + integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== + +"@svgr/babel-plugin-svg-em-dimensions@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501" + integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== + +"@svgr/babel-plugin-transform-react-native-svg@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754" + integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== + +"@svgr/babel-plugin-transform-svg-component@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e" + integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== + +"@svgr/babel-preset@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece" + integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0" + "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0" + "@svgr/babel-plugin-svg-dynamic-title" "8.0.0" + "@svgr/babel-plugin-svg-em-dimensions" "8.0.0" + "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" + "@svgr/babel-plugin-transform-svg-component" "8.0.0" + +"@svgr/core@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88" + integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + camelcase "^6.2.0" + cosmiconfig "^8.1.3" + snake-case "^3.0.4" + +"@svgr/hast-util-to-babel-ast@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4" + integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== + dependencies: + "@babel/types" "^7.21.3" + entities "^4.4.0" + +"@svgr/plugin-jsx@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928" + integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + "@svgr/hast-util-to-babel-ast" "8.0.0" + svg-parser "^2.0.4" + "@tiptap/core@^2.1.12": version "2.1.12" resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.1.12.tgz#904fdf147e91b5e60561c76e7563c1b5a32f54ab" @@ -2354,6 +2434,11 @@ dependencies: "@types/ms" "*" +"@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/hast@^2.0.0": version "2.3.8" resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.8.tgz#4ac5caf38b262b7bd5ca3202dda71f0271635660" @@ -2618,6 +2703,11 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: version "1.0.30001564" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz#eaa8bbc58c0cbccdcb7b41186df39dd2ba591889" @@ -2767,6 +2857,16 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +cosmiconfig@^8.1.3: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + crelt@^1.0.0: version "1.0.6" resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" @@ -2853,6 +2953,14 @@ dom-helpers@^5.0.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + downshift@^8.2.3: version "8.2.3" resolved "https://registry.yarnpkg.com/downshift/-/downshift-8.2.3.tgz#27106a5d9f408a6f6f9350ca465801d07e52db87" @@ -2890,6 +2998,11 @@ engine.io-parser@~5.2.1: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + entities@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" @@ -2950,6 +3063,11 @@ escape-string-regexp@^5.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + extend@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -3196,7 +3314,7 @@ html-void-elements@^2.0.0: resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f" integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A== -import-fresh@^3.2.1: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -3309,6 +3427,13 @@ js-cookie@^3.0.5: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -3373,6 +3498,13 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + lowlight@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-3.1.0.tgz#aa394c5f3a7689fce35fa49a7c850ba3ead4f590" @@ -3389,6 +3521,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lucide-react@^0.293.0: + version "0.293.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.293.0.tgz#02703dbcc56bb38779f4e576cc03be8cc0046fcc" + integrity sha512-g3AN0EYITCpAjNgLHrKrFWvIJzZy0Y9OPBaonyKw1cM+nZE6piOM+TiuQdYfha7oa76TMiDaWXQHE44CEqsrzw== + magic-string@^0.27.0: version "0.27.0" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" @@ -3894,6 +4031,14 @@ nanoid@^3.3.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + node-releases@^2.0.13: version "2.0.13" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" @@ -3952,7 +4097,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -4279,11 +4424,6 @@ react-hotkeys-hook@^4.3.5: resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz#1f7a7a1c9c21d4fa3280bf340fcca8fd77d81994" integrity sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw== -react-icons@^4.7.1: - version "4.12.0" - resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.12.0.tgz#54806159a966961bfd5cdb26e492f4dafd6a8d78" - integrity sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw== - react-idle-timer@^5.5.3: version "5.7.2" resolved "https://registry.yarnpkg.com/react-idle-timer/-/react-idle-timer-5.7.2.tgz#f506db28a86645dd1b87987116501703e512142b" @@ -4546,6 +4686,14 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + socket.io-client@^4.7.1: version "4.7.2" resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08" @@ -4616,6 +4764,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svg-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + swr@^2.2.2: version "2.2.4" resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07" @@ -4882,6 +5035,15 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" +vite-plugin-svgr@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz#9f3bf5206b0ec510287e56d16f1915e729bb4e6b" + integrity sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA== + dependencies: + "@rollup/pluginutils" "^5.0.5" + "@svgr/core" "^8.1.0" + "@svgr/plugin-jsx" "^8.1.0" + vite@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26" From 2260e2f2b06f7a0e7b004700911e8dc8622d2932 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Sun, 26 Nov 2023 05:15:52 +0530 Subject: [PATCH 082/147] feat: added radix toast --- raven-app/package.json | 2 + raven-app/src/App.tsx | 2 + .../src/components/common/Toast/Toast.tsx | 128 ++++++++++++ .../src/components/common/Toast/Toaster.tsx | 33 +++ .../src/components/common/TooltipButton.tsx | 14 -- .../src/components/common/UserAvatar.tsx | 3 +- .../EditChannelDescriptionModal.tsx | 13 +- .../leave-channel/LeaveChannelModal.tsx | 21 +- .../rename-channel/ChannelRenameModal.tsx | 32 +-- .../add-members/AddChannelMemberModal.tsx | 41 +--- .../RemoveChannelMemberModal.tsx | 23 +-- .../archive-channel/ArchiveChannelModal.tsx | 17 +- .../ChangeChannelTypeModal.tsx | 17 +- .../delete-channel/DeleteChannelModal.tsx | 19 +- .../feature/channels/CreateChannelModal.tsx | 9 +- .../chat-header/ViewChannelDetailsButton.tsx | 5 +- .../chat-header/ViewOrAddMembersButton.tsx | 7 +- .../chat/ChatInput/ChannelMentionList.tsx | 83 ++++---- .../feature/chat/ChatInput/MentionList.tsx | 92 +++++---- .../chat/chat-footer/ArchivedChannelBox.tsx | 56 +++-- .../chat/chat-footer/JoinChannelBox.tsx | 60 ++---- .../feature/chat/chat-history/ChatHistory.tsx | 6 +- .../feature/chat/chat-message/DateTooltip.tsx | 26 ++- .../feature/chat/chat-message/FileMessage.tsx | 3 +- .../chat/chat-message/UserNameInMessage.tsx | 38 ++-- .../chat/message-reply/PreviousMessageBox.tsx | 12 +- .../direct-messages/DirectMessageList.tsx | 8 +- .../feature/global-search/MessageSearch.tsx | 26 +-- .../DeleteMessageModal.tsx | 25 +-- .../EditMessageModal.tsx | 19 +- .../feature/raven-users/AddRavenUsers.tsx | 21 +- .../feature/saved-messages/SavedMessages.tsx | 23 +-- .../components/feature/sorting/SortMenu.tsx | 21 -- .../src/components/feature/sorting/index.ts | 3 +- .../user-details/UserProfileDrawer.tsx | 94 --------- .../layout/AlertBanner/AlertBanner.tsx | 30 --- .../layout/AlertBanner/ErrorBanner.tsx | 5 +- .../components/layout/AlertBanner/index.ts | 1 - .../layout/Divider/DividerWithText.tsx | 36 ++-- .../layout/EmptyState/EmptyState.tsx | 1 - .../layout/Loaders/ChatMessageLoader.tsx | 15 -- .../layout/Loaders/FullPageLoader.tsx | 7 +- .../components/layout/Loaders/TableLoader.tsx | 2 - .../src/components/layout/Loaders/index.ts | 3 +- raven-app/src/hooks/useActiveState.tsx | 27 +-- raven-app/src/hooks/useBoolean.ts | 19 ++ raven-app/src/hooks/useToast.tsx | 191 ++++++++++++++++++ raven-app/src/pages/ChatSpace.tsx | 44 ++-- raven-app/src/pages/auth/Login.tsx | 10 +- .../src/utils/channel/ChannelListProvider.tsx | 6 +- .../src/utils/users/UserListProvider.tsx | 11 +- raven-app/tailwind.config.js | 4 +- raven-app/yarn.lock | 14 +- 53 files changed, 717 insertions(+), 711 deletions(-) create mode 100644 raven-app/src/components/common/Toast/Toast.tsx create mode 100644 raven-app/src/components/common/Toast/Toaster.tsx delete mode 100644 raven-app/src/components/common/TooltipButton.tsx delete mode 100644 raven-app/src/components/feature/sorting/SortMenu.tsx delete mode 100644 raven-app/src/components/feature/user-details/UserProfileDrawer.tsx delete mode 100644 raven-app/src/components/layout/AlertBanner/AlertBanner.tsx delete mode 100644 raven-app/src/components/layout/Loaders/ChatMessageLoader.tsx create mode 100644 raven-app/src/hooks/useBoolean.ts create mode 100644 raven-app/src/hooks/useToast.tsx diff --git a/raven-app/package.json b/raven-app/package.json index ffba293fe..5ed8382ba 100644 --- a/raven-app/package.json +++ b/raven-app/package.json @@ -30,6 +30,7 @@ "chakra-react-select": "^4.6.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", + "cva": "npm:class-variance-authority", "downshift": "^8.2.3", "emoji-picker-element": "^1.18.4", "framer-motion": "^9.0.2", @@ -61,6 +62,7 @@ "@vitejs/plugin-react": "^3.1.0", "autoprefixer": "^10.4.16", "tailwindcss": "^3.3.5", + "tailwindcss-animate": "^1.0.7", "typescript": "^4.9.3", "vite": "^4.5.0", "vite-plugin-svgr": "^4.2.0" diff --git a/raven-app/src/App.tsx b/raven-app/src/App.tsx index 15ab57a57..653b94a77 100644 --- a/raven-app/src/App.tsx +++ b/raven-app/src/App.tsx @@ -10,6 +10,7 @@ import { ChannelRedirect } from './utils/channel/ChannelRedirect' import "cal-sans"; import { useState } from 'react' import { ThemeProvider } from './ThemeProvider' +import { Toaster } from './components/common/Toast/Toaster' function App() { @@ -56,6 +57,7 @@ function App() { + diff --git a/raven-app/src/components/common/Toast/Toast.tsx b/raven-app/src/components/common/Toast/Toast.tsx new file mode 100644 index 000000000..2da85ac27 --- /dev/null +++ b/raven-app/src/components/common/Toast/Toast.tsx @@ -0,0 +1,128 @@ +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "cva" +import { X } from "lucide-react" +import { clsx } from "clsx" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-[var(--color-background)] text-[var(--gray-12)]", + destructive: + "group destructive border-[var(--red-9)] bg-[var(--red-9)] text-[var(--red-9-contrast)]", + success: "success group border-[var(--green-9)] bg-[var(--green-9)] text-[var(--green-9-contrast)]", + accent: "accent group border-[var(--accent-9)] bg-[var(--accent-9)] text-[var(--accent-9-contrast)]", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName +//TODO: Improve design of toast action button +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/raven-app/src/components/common/Toast/Toaster.tsx b/raven-app/src/components/common/Toast/Toaster.tsx new file mode 100644 index 000000000..a8a605fb9 --- /dev/null +++ b/raven-app/src/components/common/Toast/Toaster.tsx @@ -0,0 +1,33 @@ +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "./Toast" +import { useToast } from "@/hooks/useToast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} \ No newline at end of file diff --git a/raven-app/src/components/common/TooltipButton.tsx b/raven-app/src/components/common/TooltipButton.tsx deleted file mode 100644 index 10c2cb34e..000000000 --- a/raven-app/src/components/common/TooltipButton.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Button, ButtonProps, IconButton, IconButtonProps } from "@chakra-ui/react"; -import { forwardRef } from "react"; - -export const TooltipButton = forwardRef(({ children, ...rest }: ButtonProps, ref) => ( - -)) - -export const TooltipIconButton = forwardRef(({ children, ...rest }: IconButtonProps, ref) => ( - - {children} - -)) \ No newline at end of file diff --git a/raven-app/src/components/common/UserAvatar.tsx b/raven-app/src/components/common/UserAvatar.tsx index 4adf3d23f..847ac8674 100644 --- a/raven-app/src/components/common/UserAvatar.tsx +++ b/raven-app/src/components/common/UserAvatar.tsx @@ -3,6 +3,7 @@ import { Skeleton } from './Skeleton' import { Avatar } from '@radix-ui/themes' import { AvatarProps } from '@radix-ui/themes/dist/cjs/components/avatar' import { BoxProps } from '@radix-ui/themes/dist/cjs/components/box' +import { clsx } from 'clsx' interface UserAvatarProps extends Partial { alt?: string, @@ -47,7 +48,7 @@ export const UserAvatar = ({ src, alt, size = '1', radius = 'medium', isActive, } {isActive && - + } diff --git a/raven-app/src/components/feature/channel-details/edit-channel-description/EditChannelDescriptionModal.tsx b/raven-app/src/components/feature/channel-details/edit-channel-description/EditChannelDescriptionModal.tsx index 52da8c329..81c100b17 100644 --- a/raven-app/src/components/feature/channel-details/edit-channel-description/EditChannelDescriptionModal.tsx +++ b/raven-app/src/components/feature/channel-details/edit-channel-description/EditChannelDescriptionModal.tsx @@ -1,4 +1,3 @@ -import { useToast } from "@chakra-ui/react" import { useFrappeUpdateDoc } from "frappe-react-sdk" import { FormProvider, useForm } from "react-hook-form" import { ErrorBanner } from "../../../layout/AlertBanner" @@ -6,6 +5,8 @@ import { ChannelListItem } from "@/utils/channel/ChannelListProvider" import { Box, Dialog, Flex, Button, TextArea, Text } from "@radix-ui/themes" import { Loader } from "@/components/common/Loader" import { ErrorText, Label } from "@/components/common/Form" +import { useToast } from "@/hooks/useToast" +import { ToastAction } from "@/components/common/Toast/Toast" interface RenameChannelForm { channel_description: string @@ -25,7 +26,7 @@ export const EditChannelDescriptionModalContent = ({ channelData, onClose }: Ren }) const { register, handleSubmit, formState: { errors } } = methods const { updateDoc, loading: updatingDoc, error } = useFrappeUpdateDoc() - const toast = useToast() + const { toast } = useToast() const onSubmit = (data: RenameChannelForm) => { updateDoc("Raven Channel", channelData?.name ?? null, { @@ -33,18 +34,14 @@ export const EditChannelDescriptionModalContent = ({ channelData, onClose }: Ren }).then(() => { toast({ title: "Channel description updated", - status: "success", - duration: 2000, - isClosable: true + variant: 'success', }) onClose() }).catch((err) => { toast({ title: "Error updating channel description", description: err.message, - status: "error", - duration: 2000, - isClosable: true + variant: "destructive", }) }) } diff --git a/raven-app/src/components/feature/channel-details/leave-channel/LeaveChannelModal.tsx b/raven-app/src/components/feature/channel-details/leave-channel/LeaveChannelModal.tsx index 2bdd28307..86f1c3bf6 100644 --- a/raven-app/src/components/feature/channel-details/leave-channel/LeaveChannelModal.tsx +++ b/raven-app/src/components/feature/channel-details/leave-channel/LeaveChannelModal.tsx @@ -1,4 +1,3 @@ -import { useToast } from '@chakra-ui/react' import { useFrappeDeleteDoc, useFrappeGetCall } from 'frappe-react-sdk' import { useContext, useRef } from 'react' import { useNavigate } from 'react-router-dom' @@ -8,6 +7,7 @@ import { ChannelListContext, ChannelListContextType, ChannelListItem } from '@/u import { ChannelIcon } from '@/utils/layout/channelIcon' import { AlertDialog, Button, Flex, Text } from '@radix-ui/themes' import { Loader } from '@/components/common/Loader' +import { useToast } from '@/hooks/useToast' interface LeaveChannelModalProps { onClose: () => void, @@ -19,7 +19,7 @@ export const LeaveChannelModal = ({ onClose, channelData, closeDetailsModal }: L const { currentUser } = useContext(UserContext) const { deleteDoc, loading: deletingDoc, error } = useFrappeDeleteDoc() - const toast = useToast() + const { toast } = useToast() const navigate = useNavigate() const { data: channelMember } = useFrappeGetCall<{ message: { name: string } }>('frappe.client.get_value', { @@ -32,15 +32,10 @@ export const LeaveChannelModal = ({ onClose, channelData, closeDetailsModal }: L const { mutate } = useContext(ChannelListContext) as ChannelListContextType - const onSubmit = () => { + const onSubmit = async () => { return deleteDoc('Raven Channel Member', channelMember?.message.name).then(() => { toast({ - title: 'Channel left successfully', - status: 'success', - duration: 1500, - position: 'bottom', - variant: 'solid', - isClosable: true + title: 'You have left the channel', }) onClose() mutate() @@ -48,12 +43,8 @@ export const LeaveChannelModal = ({ onClose, channelData, closeDetailsModal }: L closeDetailsModal() }).catch((e) => { toast({ - title: 'Error: could leave channel.', - status: 'error', - duration: 3000, - position: 'bottom', - variant: 'solid', - isClosable: true, + title: 'Error: Could leave channel.', + variant: 'destructive', description: `${e.message}` }) }) diff --git a/raven-app/src/components/feature/channel-details/rename-channel/ChannelRenameModal.tsx b/raven-app/src/components/feature/channel-details/rename-channel/ChannelRenameModal.tsx index 430b574f1..616b34fca 100644 --- a/raven-app/src/components/feature/channel-details/rename-channel/ChannelRenameModal.tsx +++ b/raven-app/src/components/feature/channel-details/rename-channel/ChannelRenameModal.tsx @@ -1,4 +1,3 @@ -import { useToast } from "@chakra-ui/react" import { useFrappeUpdateDoc } from "frappe-react-sdk" import { ChangeEvent, useCallback } from "react" import { Controller, FormProvider, useForm } from "react-hook-form" @@ -8,6 +7,7 @@ import { ChannelListItem } from "@/utils/channel/ChannelListProvider" import { Box, Dialog, Flex, Text, TextField, Button } from "@radix-ui/themes" import { ErrorText, Label } from "@/components/common/Form" import { Loader } from "@/components/common/Loader" +import { useToast } from "@/hooks/useToast" interface RenameChannelForm { channel_name: string @@ -30,38 +30,18 @@ export const RenameChannelModalContent = ({ channelID, channelName, type, onClos }) const { control, handleSubmit, setValue, formState: { errors } } = methods const { updateDoc, loading: updatingDoc, error } = useFrappeUpdateDoc() - const toast = useToast() + const { toast } = useToast() - const onSubmit = (data: RenameChannelForm) => { - updateDoc("Raven Channel", channelID ?? null, { + const onSubmit = async (data: RenameChannelForm) => { + return updateDoc("Raven Channel", channelID ?? null, { channel_name: data.channel_name }).then(() => { toast({ title: "Channel name updated", - status: "success", - duration: 2000, - isClosable: true + variant: 'success', + duration: 800 }) onClose() - }).catch((err) => { - if (err.httpStatus === 409) { - toast({ - title: "Error renaming channel", - description: "Channel name already exists", - status: "error", - duration: 2000, - isClosable: true - }) - } - else { - toast({ - title: "Error renaming channel", - description: err.httpStatusText, - status: "error", - duration: 2000, - isClosable: true - }) - } }) } diff --git a/raven-app/src/components/feature/channel-member-details/add-members/AddChannelMemberModal.tsx b/raven-app/src/components/feature/channel-member-details/add-members/AddChannelMemberModal.tsx index f78a6811d..ea0ac95d6 100644 --- a/raven-app/src/components/feature/channel-member-details/add-members/AddChannelMemberModal.tsx +++ b/raven-app/src/components/feature/channel-member-details/add-members/AddChannelMemberModal.tsx @@ -1,4 +1,3 @@ -import { useToast } from '@chakra-ui/react' import { Controller, FormProvider, useForm } from 'react-hook-form' import { useFrappeCreateDoc } from 'frappe-react-sdk' import { ErrorBanner } from '../../../layout/AlertBanner' @@ -9,7 +8,8 @@ import { RavenChannel } from '../../../../../../types/RavenChannelManagement/Rav import { ChannelMembers } from '@/utils/channel/ChannelMembersProvider' import { Suspense, lazy } from 'react' import { UserFields } from '@/utils/users/UserListProvider' -import { ErrorText, Label } from '@/components/common/Form' +import { ErrorText } from '@/components/common/Form' +import { useToast } from '@/hooks/useToast' const AddMembersDropdown = lazy(() => import('../../select-member/AddMembersDropdown')) interface AddChannelMemberForm { add_members: UserFields[] | null @@ -33,12 +33,8 @@ export const AddChannelMembersModalContent = ({ channelID, channel_name, onClose } }) - const { setValue, watch } = methods - - const members = watch('add_members') ?? [] - const { handleSubmit, control } = methods - const toast = useToast() + const { toast } = useToast() const onSubmit = (data: AddChannelMemberForm) => { if (data.add_members && data.add_members.length > 0) { @@ -53,38 +49,11 @@ export const AddChannelMembersModalContent = ({ channelID, channel_name, onClose .then(() => { toast({ title: 'Members added successfully', - status: 'success', - duration: 2000, - position: 'bottom', - variant: 'solid', - isClosable: true, + variant: 'success', + duration: 1000 }) updateMembers() onClose() - }).catch((e) => { - if (e.httpStatus === 409) { - toast({ - duration: 4000, - position: 'bottom', - variant: 'solid', - isClosable: true, - status: 'warning', - title: `${e.httpStatus} - skipped pre-existing members`, - description: 'One or more members already exist in this channel' - }) - onClose() - } - else { - toast({ - duration: 3000, - position: 'bottom', - variant: 'solid', - isClosable: true, - status: 'error', - title: 'An error occurred', - description: `${e.httpStatus} - ${e.httpStatusText}` - }) - } }) } } diff --git a/raven-app/src/components/feature/channel-member-details/remove-members/RemoveChannelMemberModal.tsx b/raven-app/src/components/feature/channel-member-details/remove-members/RemoveChannelMemberModal.tsx index 1366edd97..0e4b0c809 100644 --- a/raven-app/src/components/feature/channel-member-details/remove-members/RemoveChannelMemberModal.tsx +++ b/raven-app/src/components/feature/channel-member-details/remove-members/RemoveChannelMemberModal.tsx @@ -1,4 +1,3 @@ -import { useToast } from '@chakra-ui/react' import { useFrappeDeleteDoc, useFrappeGetCall } from 'frappe-react-sdk' import { ErrorBanner } from '../../../layout/AlertBanner' import { ChannelListItem } from '@/utils/channel/ChannelListProvider' @@ -6,6 +5,7 @@ import { ChannelMembers } from '@/utils/channel/ChannelMembersProvider' import { ChannelIcon } from '@/utils/layout/channelIcon' import { AlertDialog, Button, Flex, Text } from '@radix-ui/themes' import { Loader } from '@/components/common/Loader' +import { useToast } from '@/hooks/useToast' interface RemoveChannelMemberModalProps { onClose: (refresh?: boolean) => void, @@ -18,7 +18,7 @@ interface RemoveChannelMemberModalProps { export const RemoveChannelMemberModal = ({ onClose, user_id, channelData, channelMembers, updateMembers }: RemoveChannelMemberModalProps) => { const { deleteDoc, error, loading: deletingDoc } = useFrappeDeleteDoc() - const toast = useToast() + const { toast } = useToast() const { data: member, error: errorFetchingChannelMember } = useFrappeGetCall<{ message: { name: string } }>('frappe.client.get_value', { doctype: "Raven Channel Member", @@ -28,28 +28,15 @@ export const RemoveChannelMemberModal = ({ onClose, user_id, channelData, channe revalidateOnFocus: false }) - const onSubmit = () => { + const onSubmit = async () => { return deleteDoc('Raven Channel Member', member?.message.name).then(() => { toast({ title: 'Member removed successfully', - status: 'success', - duration: 1500, - position: 'bottom', - variant: 'solid', - isClosable: true + variant: 'success', + duration: 1000 }) updateMembers() onClose() - }).catch((e) => { - toast({ - title: 'Error: could not remove member.', - status: 'error', - duration: 3000, - position: 'bottom', - variant: 'solid', - isClosable: true, - description: `${e.message}` - }) }) } diff --git a/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelModal.tsx b/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelModal.tsx index 398366d00..aa5b1baf5 100644 --- a/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelModal.tsx +++ b/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelModal.tsx @@ -1,10 +1,10 @@ -import { useToast } from '@chakra-ui/react' import { useFrappeUpdateDoc } from 'frappe-react-sdk' import { ErrorBanner } from '../../../layout/AlertBanner' import { useNavigate } from 'react-router-dom' import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { AlertDialog, Flex, Text, Button } from '@radix-ui/themes' import { Loader } from '@/components/common/Loader' +import { useToast } from '@/hooks/useToast' interface ArchiveChannelModalProps { onClose: () => void, @@ -14,7 +14,7 @@ interface ArchiveChannelModalProps { export const ArchiveChannelModal = ({ onClose, onCloseViewDetails, channelData }: ArchiveChannelModalProps) => { - const toast = useToast() + const { toast } = useToast() const { updateDoc, loading: archivingDoc, error } = useFrappeUpdateDoc() const navigate = useNavigate() @@ -27,17 +27,8 @@ export const ArchiveChannelModal = ({ onClose, onCloseViewDetails, channelData } navigate('/channel/general') toast({ title: "Channel archived", - status: "success", - duration: 3000, - isClosable: true - }) - }).catch((e) => { - toast({ - title: "Error archiving channel", - description: e.message, - status: "error", - duration: 3000, - isClosable: true + variant: "success", + duration: 1000, }) }) } diff --git a/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx b/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx index b6b2035a4..5c749e1a9 100644 --- a/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx +++ b/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx @@ -1,9 +1,9 @@ -import { useToast } from '@chakra-ui/react' import { useFrappeUpdateDoc } from 'frappe-react-sdk' import { ErrorBanner } from '../../../layout/AlertBanner' import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { Button, Dialog, Flex, Text } from '@radix-ui/themes' import { Loader } from '@/components/common/Loader' +import { useToast } from '@/hooks/useToast' interface ChangeChannelTypeModalProps { onClose: () => void @@ -12,7 +12,7 @@ interface ChangeChannelTypeModalProps { export const ChangeChannelTypeModal = ({ onClose, channelData }: ChangeChannelTypeModalProps) => { - const toast = useToast() + const { toast } = useToast() const { updateDoc, loading: updatingDoc, error } = useFrappeUpdateDoc() const new_channel_type = channelData?.type === 'Public' ? 'Private' : 'Public' @@ -22,19 +22,10 @@ export const ChangeChannelTypeModal = ({ onClose, channelData }: ChangeChannelTy }).then(() => { toast({ title: "Channel type updated", - status: "success", - duration: 2000, - isClosable: true + variant: "success", + duration: 1000, }) onClose() - }).catch((err) => { - toast({ - title: "Error updating channel type", - description: err.message, - status: "error", - duration: 2000, - isClosable: true - }) }) } diff --git a/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx b/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx index 2dda20bca..fde7b6d2c 100644 --- a/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx +++ b/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx @@ -1,4 +1,3 @@ -import { useToast } from '@chakra-ui/react' import { ErrorBanner } from '../../../layout/AlertBanner' import { useState } from 'react' import { useNavigate } from 'react-router-dom' @@ -7,6 +6,7 @@ import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { AlertDialog, Button, Callout, Checkbox, Flex, Text } from '@radix-ui/themes' import { Loader } from '@/components/common/Loader' import { AlertTriangle } from 'lucide-react' +import { useToast } from '@/hooks/useToast' type DeleteChannelModalProps = { onClose: () => void, @@ -23,7 +23,7 @@ export const DeleteChannelModal = ({ onClose, onCloseParent, channelData }: Dele reset() } - const toast = useToast() + const { toast } = useToast() const navigate = useNavigate() const onSubmit = () => { @@ -35,19 +35,8 @@ export const DeleteChannelModal = ({ onClose, onCloseParent, channelData }: Dele localStorage.removeItem('ravenLastChannel') navigate('/channel') toast({ - title: 'Success', - description: 'Channel deleted successfully', - status: 'success', - duration: 2000, - isClosable: true, - }) - }).catch(() => { - toast({ - title: 'Error', - description: 'Error deleting channel', - status: 'error', - duration: 3000, - isClosable: true, + title: `Channel ${channelData.name} deleted`, + variant: 'success', }) }) } diff --git a/raven-app/src/components/feature/channels/CreateChannelModal.tsx b/raven-app/src/components/feature/channels/CreateChannelModal.tsx index 4e7ba74a3..c80d149bd 100644 --- a/raven-app/src/components/feature/channels/CreateChannelModal.tsx +++ b/raven-app/src/components/feature/channels/CreateChannelModal.tsx @@ -1,4 +1,3 @@ -import { useToast } from '@chakra-ui/react' import { useFrappeCreateDoc } from 'frappe-react-sdk' import { ChangeEvent, useCallback, useMemo, useState } from 'react' import { Controller, FormProvider, useForm } from 'react-hook-form' @@ -10,6 +9,7 @@ import { Plus } from 'lucide-react' import { ErrorText, HelperText, Label } from '@/components/common/Form' import { Loader } from '@/components/common/Loader' import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' +import { useToast } from '@/hooks/useToast' interface ChannelCreationForm { channel_name: string, channel_description: string, @@ -53,7 +53,7 @@ export const CreateChannelButton = ({ updateChannelList }: { updateChannelList: } - const toast = useToast() + const { toast } = useToast() const channelType = watch('type') @@ -62,9 +62,8 @@ export const CreateChannelButton = ({ updateChannelList }: { updateChannelList: if (result) { toast({ title: "Channel Created", - status: "success", - duration: 2000, - isClosable: true + variant: "success", + duration: 1000, }) onClose(result.name) } diff --git a/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx b/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx index 4a17040a8..d5e3673d4 100644 --- a/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx +++ b/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx @@ -28,12 +28,11 @@ export const ViewChannelDetailsButton = ({ channelData, channelMembers, updateMe - } - -
+ + This channel has been archived. + {channelMembers[currentUser].is_admin === 1 && } + + ) } \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-footer/JoinChannelBox.tsx b/raven-app/src/components/feature/chat/chat-footer/JoinChannelBox.tsx index dc91d3e5b..27833e3df 100644 --- a/raven-app/src/components/feature/chat/chat-footer/JoinChannelBox.tsx +++ b/raven-app/src/components/feature/chat/chat-footer/JoinChannelBox.tsx @@ -1,14 +1,10 @@ import { ErrorBanner } from "@/components/layout/AlertBanner" import { ChannelListItem } from "@/utils/channel/ChannelListProvider" -import { Text, Box, HStack, Stack, Center, ButtonGroup, Button, useToast, useDisclosure } from "@chakra-ui/react" import { useFrappeCreateDoc } from "frappe-react-sdk" -import { Hash } from "lucide-react" -import { ViewChannelDetailsModalContent } from "../../channels/ViewChannelDetailsModal" import { useContext } from "react" -import { ActiveUsersContext } from "@/utils/users/ActiveUsersProvider" import { ChannelMembers, ChannelMembersContext, ChannelMembersContextType } from "@/utils/channel/ChannelMembersProvider" -import { useTheme } from "@/ThemeProvider" - +import { Box, Flex, Text, Button } from "@radix-ui/themes" +import { Loader } from "@/components/common/Loader" interface JoinChannelBoxProps { channelData: ChannelListItem, channelMembers: ChannelMembers, @@ -17,12 +13,9 @@ interface JoinChannelBoxProps { export const JoinChannelBox = ({ channelData, channelMembers, user }: JoinChannelBoxProps) => { - const { appearance } = useTheme() - const { mutate: updateMembers } = useContext(ChannelMembersContext) as ChannelMembersContextType const { createDoc, error, loading } = useFrappeCreateDoc() - const toast = useToast() const joinChannel = async () => { return createDoc('Raven Channel Member', { @@ -30,42 +23,31 @@ export const JoinChannelBox = ({ channelData, channelMembers, user }: JoinChanne user_id: user }).then(() => { updateMembers() - }).catch((e) => { - toast({ - title: 'Error: could not join channel.', - status: 'error', - duration: 3000, - position: 'bottom', - variant: 'solid', - isClosable: true, - description: `${e.message}` - }) }) } - const { isOpen, onOpen, onClose } = useDisclosure() - const activeUsers = useContext(ActiveUsersContext) - return ( - + - - {channelData?.channel_name} - -
- - {/* */} - - -
-
- {/* */} + You are not a member of this channel. + +
) } \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx b/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx index ea42c55f0..bb62ff291 100644 --- a/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx +++ b/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx @@ -1,4 +1,3 @@ -import { Box } from "@chakra-ui/react"; import { DividerWithText } from "../../../layout/Divider/DividerWithText"; import { DateObjectToFormattedDateString } from "../../../../utils/operations"; import { DateBlock, FileMessage, Message, MessageBlock, MessagesWithDate } from "../../../../../../types/Messaging/Message"; @@ -12,6 +11,7 @@ import { FilePreviewModal } from "../../file-preview/FilePreviewModal"; import { Virtuoso } from 'react-virtuoso'; import { VirtuosoRefContext } from "../../../../utils/message/VirtuosoRefProvider"; import { ChannelListItem, DMChannelListItem } from "@/utils/channel/ChannelListProvider"; +import { Box } from "@radix-ui/themes"; interface ChatHistoryProps { parsedMessages: MessagesWithDate, @@ -42,7 +42,7 @@ export const ChatHistory = ({ parsedMessages, replyToMessage, channelData }: Cha const renderItem = (block: DateBlock | MessageBlock) => { if (block.block_type === 'date') { return ( - + {DateObjectToFormattedDateString(new Date(block.data))} ) @@ -63,7 +63,7 @@ export const ChatHistory = ({ parsedMessages, replyToMessage, channelData }: Cha } return ( - + { return ( - - {DateObjectToTimeString(new Date(timestamp))} + + + {DateObjectToTimeString(new Date(timestamp))} + ) } export const DateTooltipShort = ({ timestamp, showButtons }: { timestamp: string, showButtons: {} }) => { return ( - - {DateObjectToTimeString(new Date(timestamp)).split(' ')[0]} + + + {DateObjectToTimeString(new Date(timestamp)).split(' ')[0]} + ) } \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx b/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx index 44ccc0939..457e0e514 100644 --- a/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx +++ b/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx @@ -1,9 +1,10 @@ -import { Collapse, HStack, Icon, IconButton, Link, Stack, Text, useBoolean, Image } from "@chakra-ui/react" +import { Collapse, HStack, IconButton, Link, Stack, Text, Image } from "@chakra-ui/react" import { FileExtensionIcon } from "../../../../utils/layout/FileExtensionIcon" import { FileMessage } from "../../../../../../types/Messaging/Message" import { useCallback } from "react" import { getFileExtension, getFileName } from "../../../../utils/operations" import { ChevronDown, ChevronRight } from "lucide-react" +import { useBoolean } from "@/hooks/useBoolean" interface FileMessageProps extends Partial { onFilePreviewModalOpen: ({ file, owner, creation, message_type }: Partial) => void diff --git a/raven-app/src/components/feature/chat/chat-message/UserNameInMessage.tsx b/raven-app/src/components/feature/chat/chat-message/UserNameInMessage.tsx index f06835f17..97708d25e 100644 --- a/raven-app/src/components/feature/chat/chat-message/UserNameInMessage.tsx +++ b/raven-app/src/components/feature/chat/chat-message/UserNameInMessage.tsx @@ -1,10 +1,7 @@ -import { Text, Button, HStack, StackDivider } from "@chakra-ui/react" import { DateTooltip } from "./DateTooltip" -import { ModalTypes, useModalManager } from "@/hooks/useModalManager" -import { UserProfileDrawer } from "../../user-details/UserProfileDrawer" import { useGetUserRecords } from "@/hooks/useGetUserRecords" -import { UserFields } from "@/utils/users/UserListProvider" import { useTheme } from "@/ThemeProvider" +import { Flex, Separator, Text } from "@radix-ui/themes" interface UserNameInMessageProps { timestamp: string, @@ -16,29 +13,20 @@ export const UserNameInMessage = ({ timestamp, user }: UserNameInMessageProps) = const { appearance } = useTheme() const textColor = appearance === 'light' ? 'gray.800' : 'gray.50' - const modalManager = useModalManager() - - const onOpenUserDetailsDrawer = (selectedUser: UserFields) => { - if (selectedUser) { - modalManager.openModal(ModalTypes.UserDetails, selectedUser) - } - } - const users = useGetUserRecords() return ( - <> - } align='flex-start'> - - - - - + + + {users?.[user]?.full_name ?? user} + + + + ) } \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx b/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx index 12962b971..2ec34d3e6 100644 --- a/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx +++ b/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx @@ -5,7 +5,6 @@ import { DateObjectToFormattedDateStringWithoutYear, DateObjectToTimeString, get import { useContext } from 'react' import { FileExtensionIcon } from '../../../../utils/layout/FileExtensionIcon' import { useFrappeGetDoc, useFrappePostCall } from 'frappe-react-sdk' -import { AlertBanner } from '../../../layout/AlertBanner' import { VirtuosoRefContext } from '../../../../utils/message/VirtuosoRefProvider' import { useNavigate } from "react-router-dom" import { ChannelListItem, DMChannelListItem } from '@/utils/channel/ChannelListProvider' @@ -13,6 +12,7 @@ import { UserFields } from '@/utils/users/UserListProvider' import { useGetUserRecords } from '@/hooks/useGetUserRecords' import { X } from 'lucide-react' import { useTheme } from '@/ThemeProvider' +import { ErrorCallout } from '@/components/layout/AlertBanner/ErrorBanner' interface PreviousMessageBoxProps { previous_message_id?: string, @@ -57,7 +57,7 @@ export const PreviousMessageBox = ({ previous_message_id, previous_message_conte {previous_message_content.message_type === 'Image' ? File preview :
- {FileExtensionIcon(getFileExtension(previous_message_content.file) ?? '')} +
} {getFileName(previous_message_content.file)} @@ -120,10 +120,12 @@ const PreviousMessageBoxInChat = ({ previous_message_id, channelData, users }: P } if (indexingError) { - return + return + There was an error while searching for the previous message. + } if (error) { - return + return Previous message not found, this message may have been deleted. } if (data) { return handleScrollToMessage(previous_message_id)} p='2' border={'1px'} borderColor={appearance === 'light' ? 'gray.400' : 'gray.600'} rounded={'md'} _hover={{ cursor: 'pointer', boxShadow: 'sm', bgColor: appearance === 'light' ? 'white' : 'black' }}> @@ -152,7 +154,7 @@ const PreviousMessageBoxInChat = ({ previous_message_id, channelData, users }: P {data.message_type === 'Image' ? File preview :
- {FileExtensionIcon(getFileExtension(data.file) ?? '')} +
} {getFileName(data.file)} diff --git a/raven-app/src/components/feature/direct-messages/DirectMessageList.tsx b/raven-app/src/components/feature/direct-messages/DirectMessageList.tsx index 70a86e133..e3aff7919 100644 --- a/raven-app/src/components/feature/direct-messages/DirectMessageList.tsx +++ b/raven-app/src/components/feature/direct-messages/DirectMessageList.tsx @@ -1,4 +1,3 @@ -import { useToast } from "@chakra-ui/react" import { useFrappePostCall } from "frappe-react-sdk" import { useContext, useMemo, useState } from "react" import { useNavigate } from "react-router-dom" @@ -10,6 +9,7 @@ import { useIsUserActive } from "@/hooks/useIsUserActive" import { ChannelListContext, ChannelListContextType, DMChannelListItem, ExtraUsersData, UnreadCountData } from "../../../utils/channel/ChannelListProvider" import { Flex, Text } from "@radix-ui/themes" import { UserAvatar } from "@/components/common/UserAvatar" +import { useToast } from "@/hooks/useToast" export const DirectMessageList = ({ unread_count }: { unread_count?: UnreadCountData }) => { @@ -81,7 +81,7 @@ const ExtraUsersItemList = () => { const { extra_users, mutate } = useContext(ChannelListContext) as ChannelListContextType const { call } = useFrappePostCall<{ message: string }>("raven.raven_channel_management.doctype.raven_channel.raven_channel.create_direct_message_channel") - const toast = useToast() + const { toast } = useToast() const navigate = useNavigate() const createDMChannel = async (user_id: string) => { @@ -94,10 +94,8 @@ const ExtraUsersItemList = () => { toast({ title: "Error", description: "Could not create channel.", - status: "error", + variant: 'destructive', duration: 2000, - isClosable: true, - position: 'top-right' }) }) } diff --git a/raven-app/src/components/feature/global-search/MessageSearch.tsx b/raven-app/src/components/feature/global-search/MessageSearch.tsx index 7da56885d..fab995559 100644 --- a/raven-app/src/components/feature/global-search/MessageSearch.tsx +++ b/raven-app/src/components/feature/global-search/MessageSearch.tsx @@ -1,11 +1,11 @@ import { Bookmark, BookmarkCheck, Search } from 'lucide-react' -import { Avatar, Button, Center, chakra, FormControl, HStack, IconButton, Input, InputGroup, InputLeftElement, Spinner, Stack, TabPanel, Text, useToast } from '@chakra-ui/react' +import { Avatar, Button, Center, chakra, FormControl, HStack, IconButton, Input, InputGroup, InputLeftElement, Spinner, Stack, TabPanel, Text } from '@chakra-ui/react' import { useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk' import { useContext, useState, useMemo, useEffect } from 'react' import { FormProvider, Controller, useForm } from 'react-hook-form' import { useDebounce } from '../../../hooks/useDebounce' import { GetMessageSearchResult } from '../../../../../types/Search/Search' -import { AlertBanner } from '../../layout/AlertBanner' +import { ErrorBanner } from '../../layout/AlertBanner' import { EmptyStateForSearch } from '../../layout/EmptyState/EmptyState' import { SelectInput, SelectOption } from '../search-filters/SelectInput' import { Sort } from '../sorting' @@ -18,6 +18,7 @@ import { UserFields } from '@/utils/users/UserListProvider' import { ChannelListContext, ChannelListContextType, ChannelListItem } from '@/utils/channel/ChannelListProvider' import { useTheme } from '@/ThemeProvider' import { ChannelIcon } from '@/utils/layout/channelIcon' +import { useToast } from '@/hooks/useToast' interface FilterInput { 'from-user-filter': SelectOption[], @@ -78,21 +79,13 @@ export const MessageSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSa }) } - const toast = useToast() + const { toast } = useToast() - if (indexingError && !toast.isActive('message-indexing-error')) { + if (indexingError) { toast({ description: "There was an error while indexing the message.", - status: "error", - duration: 4000, - size: 'sm', - render: ({ onClose }) => - There was an error while indexing the message.
You have been redirected to the channel. -
, - id: 'message-indexing-error', - variant: 'left-accent', - isClosable: true, - position: 'bottom-right' + duration: 1000, + variant: "destructive" }) } @@ -259,9 +252,10 @@ export const MessageSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSa - {error ? {error.httpStatus} - {error.httpStatusText} : + + { ((isLoading && isValidating) || loading ?
: - (!!!error && data?.message && data.message.length > 0 && showResults ? + (data?.message && data.message.length > 0 && showResults ? <> setSortByField(selField)} diff --git a/raven-app/src/components/feature/message-action-palette/DeleteMessageModal.tsx b/raven-app/src/components/feature/message-action-palette/DeleteMessageModal.tsx index a96d91cb3..a216785f8 100644 --- a/raven-app/src/components/feature/message-action-palette/DeleteMessageModal.tsx +++ b/raven-app/src/components/feature/message-action-palette/DeleteMessageModal.tsx @@ -1,10 +1,10 @@ -import { useToast } from "@chakra-ui/react" import { useFrappeDeleteDoc, useSWRConfig } from "frappe-react-sdk" import { ErrorBanner } from "../../layout/AlertBanner" import { useParams } from "react-router-dom" import { AlertDialog, Button, Callout, Flex, Text } from "@radix-ui/themes" import { Loader } from "@/components/common/Loader" import { AlertTriangle } from "lucide-react" +import { useToast } from "@/hooks/useToast" interface DeleteMessageModalProps { onClose: (refresh?: boolean) => void, @@ -14,7 +14,7 @@ interface DeleteMessageModalProps { export const DeleteMessageModal = ({ onClose, channelMessageID }: DeleteMessageModalProps) => { const { deleteDoc, error, loading: deletingDoc } = useFrappeDeleteDoc() - const toast = useToast() + const { toast } = useToast() const { mutate } = useSWRConfig() @@ -24,29 +24,16 @@ export const DeleteMessageModal = ({ onClose, channelMessageID }: DeleteMessageM const { channelID } = useParams() - const onSubmit = () => { + const onSubmit = async () => { return deleteDoc('Raven Message', channelMessageID ).then(() => { toast({ - title: 'Message deleted successfully', - status: 'success', - duration: 1500, - position: 'bottom', - variant: 'solid', - isClosable: true + title: 'Message deleted', + duration: 1000, + variant: 'destructive' }) mutate(`get_messages_for_channel_${channelID}`) onClose() - }).catch((e) => { - toast({ - title: 'Error: could not delete message.', - status: 'error', - duration: 3000, - position: 'bottom', - variant: 'solid', - isClosable: true, - description: `${e.message}` - }) }) } diff --git a/raven-app/src/components/feature/message-action-palette/EditMessageModal.tsx b/raven-app/src/components/feature/message-action-palette/EditMessageModal.tsx index 6aee61072..e1abdbc96 100644 --- a/raven-app/src/components/feature/message-action-palette/EditMessageModal.tsx +++ b/raven-app/src/components/feature/message-action-palette/EditMessageModal.tsx @@ -1,10 +1,10 @@ -import { useToast } from "@chakra-ui/react" import { useFrappeUpdateDoc, useSWRConfig } from "frappe-react-sdk" import { useEffect } from "react" import { ErrorBanner } from "../../layout/AlertBanner" import { Tiptap } from "../chat/ChatInput/Tiptap" import { IconButton, Dialog, Flex, Text } from "@radix-ui/themes" import { X } from "lucide-react" +import { useToast } from "@/hooks/useToast" interface EditMessageModalProps { onClose: (refresh?: boolean) => void, @@ -15,7 +15,7 @@ interface EditMessageModalProps { export const EditMessageModal = ({ onClose, channelMessageID, originalText }: EditMessageModalProps) => { const { mutate } = useSWRConfig() - const toast = useToast() + const { toast } = useToast() const { updateDoc, error, loading: updatingDoc, reset } = useFrappeUpdateDoc() useEffect(() => { @@ -29,20 +29,11 @@ export const EditMessageModal = ({ onClose, channelMessageID, originalText }: Ed toast({ title: "Message updated", description: "Your message has been updated", - status: "success", - duration: 3000, - isClosable: true + variant: "success", + duration: 1000, }) - mutate(`get_messages_for_channel_${d.channel_id}`) + return mutate(`get_messages_for_channel_${d.channel_id}`) - }).catch((e) => { - toast({ - title: "Error", - description: e.message, - status: "error", - duration: 3000, - isClosable: true - }) }) } diff --git a/raven-app/src/components/feature/raven-users/AddRavenUsers.tsx b/raven-app/src/components/feature/raven-users/AddRavenUsers.tsx index bd6852ba3..d28d3d060 100644 --- a/raven-app/src/components/feature/raven-users/AddRavenUsers.tsx +++ b/raven-app/src/components/feature/raven-users/AddRavenUsers.tsx @@ -1,7 +1,6 @@ import { useDebounce } from "@/hooks/useDebounce" import { usePaginationWithDoctype } from "@/hooks/usePagination" import { User } from "@/types/Core/User" -import { useToast } from "@chakra-ui/react" import { Filter, useFrappeCreateDoc, useFrappeGetDocList, useSWRConfig } from "frappe-react-sdk" import { ChangeEvent, useContext, useState } from "react" import { Sort } from "../sorting" @@ -15,6 +14,7 @@ import { Button, Dialog, Flex, Text, TextField } from "@radix-ui/themes" import { Loader } from "@/components/common/Loader" import { Search } from "lucide-react" import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog" +import { useToast } from "@/hooks/useToast" export const AddRavenUsers = ({ isOpen, onOpenChange }: any) => { @@ -58,7 +58,7 @@ export const AddRavenUsersModal = ({ onClose }: { onClose: VoidFunction }) => { const [selected, setSelected] = useState([]) const { createDoc, loading } = useFrappeCreateDoc() - const toast = useToast() + const { toast } = useToast() const handleAddUsers = async () => { if (selected.length > 0) { @@ -68,24 +68,13 @@ export const AddRavenUsersModal = ({ onClose }: { onClose: VoidFunction }) => { Promise.all(createPromises) .then(() => { toast({ - title: 'Users added', - description: 'Users have been added to Raven', - status: 'success', - duration: 3000, - isClosable: true + title: `You have added ${selected.length} users to Raven`, + variant: 'success', + duration: 1000 }) onClose() mutate('raven.api.raven_users.get_list') }) - .catch(err => { - toast({ - title: 'Error', - description: err.message, - status: 'error', - duration: 3000, - isClosable: true - }) - }) } } diff --git a/raven-app/src/components/feature/saved-messages/SavedMessages.tsx b/raven-app/src/components/feature/saved-messages/SavedMessages.tsx index 0d5d38725..5202cc671 100644 --- a/raven-app/src/components/feature/saved-messages/SavedMessages.tsx +++ b/raven-app/src/components/feature/saved-messages/SavedMessages.tsx @@ -1,16 +1,17 @@ -import { Stack, useToast, Tooltip, Button, useDisclosure, Box } from "@chakra-ui/react" +import { Stack, Tooltip, Button, useDisclosure, Box } from "@chakra-ui/react" import { useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk" import { useContext } from "react" import { Search } from "lucide-react" import { useNavigate } from "react-router-dom" import { TextMessage } from "../../../../../types/Messaging/Message" import { VirtuosoRefContext } from "../../../utils/message/VirtuosoRefProvider" -import { AlertBanner, ErrorBanner } from "../../layout/AlertBanner" +import { ErrorBanner } from "../../layout/AlertBanner" import { EmptyStateForSavedMessages } from "../../layout/EmptyState/EmptyState" import { PageHeader } from "../../layout/Heading/PageHeader" import { CommandPalette } from "../command-palette" import { MessageBox } from "../global-search/MessageBox" import { Heading } from "@radix-ui/themes" +import { useToast } from "@/hooks/useToast" interface SavedMessage extends TextMessage { channel_id: string, @@ -21,6 +22,8 @@ export const SavedMessages = () => { const navigate = useNavigate() + const { toast } = useToast() + const { virtuosoRef } = useContext(VirtuosoRefContext) const { isOpen: isCommandPaletteOpen, onClose: onCommandPaletteClose, onToggle: onCommandPaletteToggle } = useDisclosure() @@ -51,21 +54,13 @@ export const SavedMessages = () => { }) } - const toast = useToast() - if (indexingError && !toast.isActive('message-indexing-error')) { + + if (indexingError) { toast({ description: "There was an error while indexing the message.", - status: "error", - duration: 4000, - size: 'sm', - render: ({ onClose }) => - There was an error while indexing the message.
You have been redirected to the channel. -
, - id: 'message-indexing-error', - variant: 'left-accent', - isClosable: true, - position: 'bottom-right' + variant: "destructive", + duration: 1000, }) } if (error) { diff --git a/raven-app/src/components/feature/sorting/SortMenu.tsx b/raven-app/src/components/feature/sorting/SortMenu.tsx deleted file mode 100644 index 96db3a21d..000000000 --- a/raven-app/src/components/feature/sorting/SortMenu.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { MenuList, MenuItem } from "@chakra-ui/react" -import { SortFields } from "../../../../../types/Sort" - -export interface Props { - sortingFields: SortFields[], - selectedField: (field: string) => void -} - -/** - * Renders 'Sort by' fields menu list. - * @param sortingFields array of custom 'Sort by' fields & respective labels. Type - SortFields[] - * @param selectedField callback for returning selected field to parent i.e., 'Sort' component. - * @returns fields & callback to render fields & select fields respectively. - */ -export const SortMenu = ({ sortingFields, selectedField }: Props) => { - return ( - selectedField(e.target.value)}> - {sortingFields.map(field => {field.label})} - - ) -} \ No newline at end of file diff --git a/raven-app/src/components/feature/sorting/index.ts b/raven-app/src/components/feature/sorting/index.ts index b83cdef6e..0e27fe589 100644 --- a/raven-app/src/components/feature/sorting/index.ts +++ b/raven-app/src/components/feature/sorting/index.ts @@ -1,2 +1 @@ -export { Sort } from './Sort' -export { SortMenu } from './SortMenu' \ No newline at end of file +export { Sort } from './Sort' \ No newline at end of file diff --git a/raven-app/src/components/feature/user-details/UserProfileDrawer.tsx b/raven-app/src/components/feature/user-details/UserProfileDrawer.tsx deleted file mode 100644 index 7a4ad490f..000000000 --- a/raven-app/src/components/feature/user-details/UserProfileDrawer.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Circle, Clock, Mail, MessageSquare } from "lucide-react" -import { Text, Avatar, Divider, Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerHeader, DrawerOverlay, Stack, HStack, IconButton, Button, Icon } from "@chakra-ui/react" -import { useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk" -import { useNavigate } from "react-router-dom" -import { DateObjectToTimeString } from "../../../utils/operations" -import { useUserData } from "@/hooks/useUserData" -import { ErrorBanner } from "@/components/layout/AlertBanner" -import { UserFields } from "@/utils/users/UserListProvider" -import { useTheme } from "@/ThemeProvider" - -interface UserProfileDrawerProps { - isOpen: boolean - onClose: () => void, - user: UserFields -} - -export const UserProfileDrawer = ({ isOpen, onClose, user }: UserProfileDrawerProps) => { - - const { appearance } = useTheme() - const textColor = appearance === 'light' ? 'blue.500' : 'blue.300' - - const navigate = useNavigate() - const { call, error: channelError, loading, reset } = useFrappePostCall<{ message: string }>("raven.raven_channel_management.doctype.raven_channel.raven_channel.create_direct_message_channel") - const { data: activeUsers, error: activeUsersError } = useFrappeGetCall<{ message: string[] }>('raven.api.user_availability.get_active_users', undefined, undefined, { - revalidateOnFocus: false - }) - - const gotoDMChannel = async (user: string) => { - reset() - const result = await call({ user_id: user }) - navigate(`/channel/${result?.message}`) - } - - const { name: currentUserName } = useUserData() - - return ( - - - - - - Profile - - - - {user && } - - - - {user && {user.full_name}} - {user && (activeUsers?.message.includes(user.name) && !!!activeUsersError) ? - - Active - : - - - Away - } - - - - {DateObjectToTimeString(new Date())} local time - - - - - - Contact Information - - } cursor='initial' /> - - Email Address - {user && {user.name}} - - - - - - - - ) -} \ No newline at end of file diff --git a/raven-app/src/components/layout/AlertBanner/AlertBanner.tsx b/raven-app/src/components/layout/AlertBanner/AlertBanner.tsx deleted file mode 100644 index f96011358..000000000 --- a/raven-app/src/components/layout/AlertBanner/AlertBanner.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Alert, AlertIcon, AlertProps, Box, CloseButton, Text } from '@chakra-ui/react' -import { OPACITY_ON_LOAD } from '../../../utils/layout/animations' - -interface Props extends AlertProps { - heading?: string, - children?: React.ReactNode, - onClose?: () => void -} - -export const AlertBanner = ({ variant = "left-accent", heading, onClose, children, ...props }: Props) => { - - - return ( - - - - {heading && {heading}} - {children && {children}} - - - ) -} diff --git a/raven-app/src/components/layout/AlertBanner/ErrorBanner.tsx b/raven-app/src/components/layout/AlertBanner/ErrorBanner.tsx index 0f2753f70..f246e208b 100644 --- a/raven-app/src/components/layout/AlertBanner/ErrorBanner.tsx +++ b/raven-app/src/components/layout/AlertBanner/ErrorBanner.tsx @@ -1,12 +1,11 @@ import { FrappeError } from 'frappe-react-sdk' -import { AlertProps } from '@chakra-ui/react' import { PropsWithChildren, useMemo } from 'react' import React from 'react' import { MarkdownRenderer } from '@/components/feature/markdown-viewer/MarkdownRenderer' import { Callout } from '@radix-ui/themes' import { AlertTriangle } from 'lucide-react' -interface ErrorBannerProps extends AlertProps { +interface ErrorBannerProps { error?: FrappeError | null, overrideHeading?: string, children?: React.ReactNode @@ -17,7 +16,7 @@ interface ParsedErrorMessage { title?: string, indicator?: string, } -export const ErrorBanner = ({ error, overrideHeading, children, ...props }: ErrorBannerProps) => { +export const ErrorBanner = ({ error, overrideHeading, children }: ErrorBannerProps) => { //exc_type: "ValidationError" or "PermissionError" etc diff --git a/raven-app/src/components/layout/AlertBanner/index.ts b/raven-app/src/components/layout/AlertBanner/index.ts index 97d2f486f..74714f064 100644 --- a/raven-app/src/components/layout/AlertBanner/index.ts +++ b/raven-app/src/components/layout/AlertBanner/index.ts @@ -1,2 +1 @@ -export { AlertBanner } from './AlertBanner' export { ErrorBanner } from './ErrorBanner' \ No newline at end of file diff --git a/raven-app/src/components/layout/Divider/DividerWithText.tsx b/raven-app/src/components/layout/Divider/DividerWithText.tsx index 7bca7e368..9bb2cdde4 100644 --- a/raven-app/src/components/layout/Divider/DividerWithText.tsx +++ b/raven-app/src/components/layout/Divider/DividerWithText.tsx @@ -1,22 +1,32 @@ -import { useColorModeValue } from '@/ThemeProvider' -import { Box, Divider, Flex, FlexProps, Text } from '@chakra-ui/react' +import { Separator, Flex, Text } from '@radix-ui/themes' +import { FlexProps } from '@radix-ui/themes/dist/cjs/components/flex' export const DividerWithText = (props: FlexProps) => { const { children, ...flexProps } = props - const BORDERCOLOR = useColorModeValue('gray.300', 'gray.600') return ( - - - - - - {children} - - - - + + + + + + + {children} + + + + + + ) } \ No newline at end of file diff --git a/raven-app/src/components/layout/EmptyState/EmptyState.tsx b/raven-app/src/components/layout/EmptyState/EmptyState.tsx index 90236d15e..de58caff3 100644 --- a/raven-app/src/components/layout/EmptyState/EmptyState.tsx +++ b/raven-app/src/components/layout/EmptyState/EmptyState.tsx @@ -1,5 +1,4 @@ import { DateObjectToFormattedDateString } from "../../../utils/operations" -import { UserProfileDrawer } from "../../feature/user-details/UserProfileDrawer" import { ChannelListItem, DMChannelListItem } from "@/utils/channel/ChannelListProvider" import { useCurrentChannelData } from "@/hooks/useCurrentChannelData" import { useContext, useState } from "react" diff --git a/raven-app/src/components/layout/Loaders/ChatMessageLoader.tsx b/raven-app/src/components/layout/Loaders/ChatMessageLoader.tsx deleted file mode 100644 index b075903bf..000000000 --- a/raven-app/src/components/layout/Loaders/ChatMessageLoader.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Avatar, Box, HStack, Skeleton, Stack } from "@chakra-ui/react" - -export const ChatMessageLoader = () => { - return ( - - - - - - - - - - ) -} \ No newline at end of file diff --git a/raven-app/src/components/layout/Loaders/FullPageLoader.tsx b/raven-app/src/components/layout/Loaders/FullPageLoader.tsx index 57f2d36f1..a59f3c8f3 100644 --- a/raven-app/src/components/layout/Loaders/FullPageLoader.tsx +++ b/raven-app/src/components/layout/Loaders/FullPageLoader.tsx @@ -1,5 +1,7 @@ +import { Loader } from '@/components/common/Loader' import { Flex, Text } from '@radix-ui/themes' import { FlexProps } from '@radix-ui/themes/dist/cjs/components/flex' +import { clsx } from 'clsx' interface Props extends FlexProps { text?: string @@ -7,8 +9,9 @@ interface Props extends FlexProps { export const FullPageLoader = ({ text = "Ravens are finding their way to you...", ...props }: Props) => { return ( - - + + + {text} diff --git a/raven-app/src/components/layout/Loaders/TableLoader.tsx b/raven-app/src/components/layout/Loaders/TableLoader.tsx index e9711bd0f..582d7adb6 100644 --- a/raven-app/src/components/layout/Loaders/TableLoader.tsx +++ b/raven-app/src/components/layout/Loaders/TableLoader.tsx @@ -1,5 +1,3 @@ -// import { Table, Tr, Td, Skeleton, TableProps, Tbody } from '@chakra-ui/react' - import { Skeleton } from "@/components/common/Skeleton" import { Table } from "@radix-ui/themes" import { TableRootProps } from "@radix-ui/themes/dist/cjs/components/table" diff --git a/raven-app/src/components/layout/Loaders/index.ts b/raven-app/src/components/layout/Loaders/index.ts index d27235ae7..a74f1fdb5 100644 --- a/raven-app/src/components/layout/Loaders/index.ts +++ b/raven-app/src/components/layout/Loaders/index.ts @@ -1,2 +1 @@ -export { FullPageLoader } from './FullPageLoader' -export { ChatMessageLoader } from './ChatMessageLoader' \ No newline at end of file +export { FullPageLoader } from './FullPageLoader' \ No newline at end of file diff --git a/raven-app/src/hooks/useActiveState.tsx b/raven-app/src/hooks/useActiveState.tsx index 1a3e6178b..2b55dabd2 100644 --- a/raven-app/src/hooks/useActiveState.tsx +++ b/raven-app/src/hooks/useActiveState.tsx @@ -1,8 +1,8 @@ import { useContext, useEffect } from 'react' -import { useBoolean, useToast } from '@chakra-ui/react' import { useIdleTimer, PresenceType } from 'react-idle-timer' import { FrappeContext, FrappeConfig } from 'frappe-react-sdk' -import { AlertBanner } from '../components/layout/AlertBanner' +import { useBoolean } from './useBoolean' +import { useToast } from './useToast' /** * We need to track and sync user's active state with the server @@ -16,10 +16,9 @@ export const useActiveState = () => { const [isActive, { on: activate, off: deactivate, - toggle: toggleActive }] = useBoolean(true) - const toast = useToast() + const { toast } = useToast() /** * Make an API call to the server to refresh the user's active state @@ -44,21 +43,11 @@ export const useActiveState = () => { const showToast = () => { // Check if the toast is already active // If it is, don't show it again - if (!toast.isActive('refresh-active-state-error')) { - toast({ - description: "There was an error while refreshing your active state. You may appear offline to other users.", - status: "error", - duration: 4000, - size: 'sm', - render: ({ onClose }) => - There was an error while refreshing your login state.
You may appear offline to other users. -
, - id: 'refresh-active-state-error', - variant: 'left-accent', - isClosable: true, - position: 'bottom-right' - }) - } + toast({ + description: "There was an error while refreshing your active state. You may appear offline to other users.", + duration: 4000, + variant: 'destructive', + }) } /** diff --git a/raven-app/src/hooks/useBoolean.ts b/raven-app/src/hooks/useBoolean.ts new file mode 100644 index 000000000..e25119d3f --- /dev/null +++ b/raven-app/src/hooks/useBoolean.ts @@ -0,0 +1,19 @@ +import { useCallback, useState } from "react"; + +/** + * Simple hook to manage boolean (on - off) states + * @param initialState + * @returns + */ +export function useBoolean(initialState: boolean) { + + const [value, setValue] = useState(initialState); + + const on = useCallback(() => setValue(true), []); + + const off = useCallback(() => setValue(false), []); + + const toggle = useCallback(() => setValue(value => !value), []); + + return [value, { on, off, toggle }] as const; +} \ No newline at end of file diff --git a/raven-app/src/hooks/useToast.tsx b/raven-app/src/hooks/useToast.tsx new file mode 100644 index 000000000..800a515f6 --- /dev/null +++ b/raven-app/src/hooks/useToast.tsx @@ -0,0 +1,191 @@ +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/common/Toast/Toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1500 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } \ No newline at end of file diff --git a/raven-app/src/pages/ChatSpace.tsx b/raven-app/src/pages/ChatSpace.tsx index 0a323a78a..ef76dc3a2 100644 --- a/raven-app/src/pages/ChatSpace.tsx +++ b/raven-app/src/pages/ChatSpace.tsx @@ -17,10 +17,7 @@ export const ChatSpace = () => { const className = 'bg-white dark:from-[var(--accent-1)] dark:to-95% dark:to-[var(--accent-2)] dark:bg-gradient-to-b' return - {channelID ? : - No channel found - - } + {channelID && } } @@ -29,7 +26,7 @@ const ChatSpaceArea = ({ channelID }: { channelID: string }) => { const { channel, error, isLoading } = useCurrentChannelData(channelID) const { mutate, cache } = useSWRConfig() - + console.log("Mounted") useEffect(() => { //If the cached value of unread message count is 0, then no need to update it const channels = cache.get('unread_channel_count')?.data?.message?.channels @@ -44,31 +41,14 @@ const ChatSpaceArea = ({ channelID }: { channelID: string }) => { } }, [channelID]) - if (isLoading) { - - } - - if (error) { - return - } - - if (channel) { - // depending on channel type render ChannelSpace or DirectMessageSpace - return ( - - {channel.type === "dm" ? - - : - } - - ) - } - - return + return + {isLoading && } + + {channel && + {channel.type === "dm" ? + + : + } + } + } \ No newline at end of file diff --git a/raven-app/src/pages/auth/Login.tsx b/raven-app/src/pages/auth/Login.tsx index d6fff7568..8683f0d7d 100644 --- a/raven-app/src/pages/auth/Login.tsx +++ b/raven-app/src/pages/auth/Login.tsx @@ -3,18 +3,19 @@ import { Box, Button, Flex, FormControl, FormLabel, IconButton, Input, InputGrou import { useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; import { Link } from "react-router-dom"; -import { AlertBanner } from "../../components/layout/AlertBanner"; +import { ErrorBanner } from "../../components/layout/AlertBanner"; import { UserContext } from "../../utils/auth/UserProvider"; import { isEmailValid } from "../../utils/validations"; import { FullPageLoader } from "../../components/layout/Loaders"; import { Text } from "@radix-ui/themes"; +import { FrappeError } from "frappe-react-sdk"; type Inputs = { email: string; password: string; }; export const Login = () => { - const [error, setError] = useState(null) + const [error, setError] = useState(null) const { login, isLoading } = useContext(UserContext) const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm(); const { isOpen, onToggle } = useDisclosure(); @@ -42,10 +43,7 @@ export const Login = () => { - - {error != null && - {error.message} - } + { */ export const useFetchChannelList = (): ChannelListContextType => { - const toast = useToast() + const { toast } = useToast() const { data, mutate, ...rest } = useFrappeGetCall<{ message: ChannelList }>("raven.raven_channel_management.doctype.raven_channel.raven_channel.get_all_channels", undefined, undefined, { revalidateOnFocus: false, revalidateIfStale: false, onError: (error) => { toast({ title: error.message, - status: 'error' + variant: 'destructive' }) } }) diff --git a/raven-app/src/utils/users/UserListProvider.tsx b/raven-app/src/utils/users/UserListProvider.tsx index aab94e69e..adccd632e 100644 --- a/raven-app/src/utils/users/UserListProvider.tsx +++ b/raven-app/src/utils/users/UserListProvider.tsx @@ -1,10 +1,9 @@ import { useFrappeDocTypeEventListener, useFrappeGetCall } from "frappe-react-sdk"; import { PropsWithChildren, createContext } from "react"; import { User } from "../../../../types/Core/User"; -import { Box, Button, Center } from "@chakra-ui/react"; import { ErrorBanner } from "@/components/layout/AlertBanner"; import { FullPageLoader } from "@/components/layout/Loaders"; -import { ChevronRight } from "lucide-react"; +import { Box, Flex, Link } from "@radix-ui/themes"; export const UserListContext = createContext<{ users: UserFields[] }>({ @@ -27,13 +26,13 @@ export const UserListProvider = ({ children }: PropsWithChildren) => { return } if (usersError) { - return
+ return - - + + View Raven Users -
+
} return diff --git a/raven-app/tailwind.config.js b/raven-app/tailwind.config.js index 0547d95d3..b06ec1620 100644 --- a/raven-app/tailwind.config.js +++ b/raven-app/tailwind.config.js @@ -9,7 +9,9 @@ export default { }, }, darkMode: 'class', - plugins: [], + plugins: [ + require("tailwindcss-animate"), + ], corePlugins: { preflight: false, } diff --git a/raven-app/yarn.lock b/raven-app/yarn.lock index 850b3ceb2..d93904eac 100644 --- a/raven-app/yarn.lock +++ b/raven-app/yarn.lock @@ -2769,7 +2769,7 @@ client-only@^0.0.1: resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== -clsx@^2.0.0: +clsx@2.0.0, clsx@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== @@ -2889,6 +2889,13 @@ csstype@^3.0.2, csstype@^3.1.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +"cva@npm:class-variance-authority": + version "0.7.0" + resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.0.tgz#1c3134d634d80271b1837452b06d821915954522" + integrity sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A== + dependencies: + clsx "2.0.0" + dash-get@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/dash-get/-/dash-get-1.0.2.tgz#4c9e9ad5ef04c4bf9d3c9a451f6f7997298dcc7c" @@ -4777,6 +4784,11 @@ swr@^2.2.2: client-only "^0.0.1" use-sync-external-store "^1.2.0" +tailwindcss-animate@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4" + integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA== + tailwindcss@^3.3.5: version "3.3.5" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.5.tgz#22a59e2fbe0ecb6660809d9cc5f3976b077be3b8" From ceb075c79146c982057f56676b3863afd52e8744 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Sun, 26 Nov 2023 06:14:28 +0530 Subject: [PATCH 083/147] perf: code split modules --- raven-app/package.json | 3 +- .../common/EmojiPicker/EmojiPicker.tsx | 6 ++-- .../feature/chat/ChatInput/NoNewLine.tsx | 23 ------------ .../chat/ChatInput/RightToolbarButtons.tsx | 8 +++-- .../feature/chat/ChatInput/Tiptap.tsx | 6 ++-- .../feature/chat/chat-history/ChatBoxBody.tsx | 36 ++++++++++--------- .../EditMessageModal.tsx | 10 ++++-- .../EmojiPickerButton.tsx | 10 ++++-- raven-app/yarn.lock | 20 +++-------- 9 files changed, 53 insertions(+), 69 deletions(-) delete mode 100644 raven-app/src/components/feature/chat/ChatInput/NoNewLine.tsx diff --git a/raven-app/package.json b/raven-app/package.json index 5ed8382ba..9b9102c57 100644 --- a/raven-app/package.json +++ b/raven-app/package.json @@ -32,7 +32,7 @@ "cmdk": "^0.2.0", "cva": "npm:class-variance-authority", "downshift": "^8.2.3", - "emoji-picker-element": "^1.18.4", + "emoji-picker-element": "^1.19.2", "framer-motion": "^9.0.2", "frappe-react-sdk": "^1.3.8", "highlight.js": "^11.9.0", @@ -52,7 +52,6 @@ "react-virtuoso": "^4.3.8", "rehype-raw": "^6.1.1", "remark-gfm": "^3.0.1", - "timeago-react": "^3.0.5", "tippy.js": "^6.3.7" }, "devDependencies": { diff --git a/raven-app/src/components/common/EmojiPicker/EmojiPicker.tsx b/raven-app/src/components/common/EmojiPicker/EmojiPicker.tsx index 0c1613363..35b0bef77 100644 --- a/raven-app/src/components/common/EmojiPicker/EmojiPicker.tsx +++ b/raven-app/src/components/common/EmojiPicker/EmojiPicker.tsx @@ -2,7 +2,7 @@ import { createElement, useEffect, useRef } from "react" import 'emoji-picker-element' import './emojiPicker.styles.css' -export const EmojiPicker = ({ onSelect }: { onSelect: (emoji: string) => void }) => { +const EmojiPicker = ({ onSelect }: { onSelect: (emoji: string) => void }) => { const ref = useRef(null) @@ -24,4 +24,6 @@ export const EmojiPicker = ({ onSelect }: { onSelect: (emoji: string) => void }) }, []) return createElement('emoji-picker', { ref }) -} \ No newline at end of file +} + +export default EmojiPicker \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatInput/NoNewLine.tsx b/raven-app/src/components/feature/chat/ChatInput/NoNewLine.tsx deleted file mode 100644 index e769bed26..000000000 --- a/raven-app/src/components/feature/chat/ChatInput/NoNewLine.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Extension } from '@tiptap/core' -import { Plugin, PluginKey } from 'prosemirror-state' - -export const NoNewLine = Extension.create({ - name: 'no_new_line', - - addProseMirrorPlugins() { - return [ - new Plugin({ - key: new PluginKey('eventHandler'), - props: { - handleKeyDown: (view, event) => { - if (event.key === 'Enter' && !event.shiftKey) { - return true - } - } - // … and many, many more. - // Here is the full list: https://prosemirror.net/docs/ref/#view.EditorProps - }, - }), - ] - }, -}) \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx b/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx index 105e51a86..eea739fc7 100644 --- a/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx +++ b/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx @@ -1,10 +1,12 @@ import { useCurrentEditor } from '@tiptap/react' import { AtSign, Hash, Smile, SendHorizontal, Paperclip } from 'lucide-react' import { DEFAULT_BUTTON_STYLE, ICON_PROPS } from './ToolPanel' -import { EmojiPicker } from '../../../common/EmojiPicker/EmojiPicker' import { ToolbarFileProps } from './Tiptap' import { Flex, IconButton, Inset, Popover, Separator } from '@radix-ui/themes' import { Loader } from '@/components/common/Loader' +import { Suspense, lazy } from 'react' + +const EmojiPicker = lazy(() => import('@/components/common/EmojiPicker/EmojiPicker')) type RightToolbarButtonsProps = { fileProps?: ToolbarFileProps, @@ -100,7 +102,9 @@ const EmojiPickerButton = () => { - editor.chain().focus().insertContent(e).run()} /> + }> + editor.chain().focus().insertContent(e).run()} /> + diff --git a/raven-app/src/components/feature/chat/ChatInput/Tiptap.tsx b/raven-app/src/components/feature/chat/ChatInput/Tiptap.tsx index b14f0094d..17dfb683d 100644 --- a/raven-app/src/components/feature/chat/ChatInput/Tiptap.tsx +++ b/raven-app/src/components/feature/chat/ChatInput/Tiptap.tsx @@ -78,7 +78,7 @@ const ChannelMention = Mention.extend({ pluginKey: new PluginKey('channelMention'), } }) -export const Tiptap = ({ slotAfter, slotBefore, fileProps, onMessageSend, messageSending, defaultText = '' }: TiptapEditorProps) => { +const Tiptap = ({ slotAfter, slotBefore, fileProps, onMessageSend, messageSending, defaultText = '' }: TiptapEditorProps) => { const { users } = useContext(UserListContext) @@ -416,4 +416,6 @@ export const Tiptap = ({ slotAfter, slotBefore, fileProps, onMessageSend, messag ) -} \ No newline at end of file +} + +export default Tiptap \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx b/raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx index 58a2b5857..bff797f46 100644 --- a/raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx +++ b/raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx @@ -4,20 +4,22 @@ import { useFrappeDocumentEventListener, useFrappeEventListener, useFrappeGetCal import { Message, MessagesWithDate } from "../../../../../../types/Messaging/Message" import { FullPageLoader } from "@/components/layout/Loaders" import { ErrorBanner } from "@/components/layout/AlertBanner" -import { useContext, useMemo, useState } from "react" +import { Suspense, lazy, useContext, useMemo, useState } from "react" import { ArchivedChannelBox } from "../chat-footer/ArchivedChannelBox" import { ChannelListItem, DMChannelListItem } from "@/utils/channel/ChannelListProvider" import { JoinChannelBox } from "../chat-footer/JoinChannelBox" import { useUserData } from "@/hooks/useUserData" import { ChannelMembersContext, ChannelMembersContextType } from "@/utils/channel/ChannelMembersProvider" import { UserContext } from "@/utils/auth/UserProvider" -import { Tiptap } from "../ChatInput/Tiptap" import useFileUpload from "../ChatInput/FileInput/useFileUpload" import { CustomFile, FileDrop } from "../../file-upload/FileDrop" import { FileListItem } from "../../file-upload/FileListItem" import { PreviousMessageBox } from "../message-reply/PreviousMessageBox" import { useSendMessage } from "../ChatInput/useSendMessage" +import { Loader } from "@/components/common/Loader" + +const Tiptap = lazy(() => import("../ChatInput/Tiptap")) interface ChatBoxBodyProps { channelData: ChannelListItem | DMChannelListItem } @@ -102,20 +104,22 @@ export const ChatBoxBody = ({ channelData }: ChatBoxBodyProps) => { {channelData?.is_archived == 0 && (isUserInChannel || channelData?.type === 'Open') && -
} - /> + }> + } {channelData?.is_archived == 0 && (!isUserInChannel && channelData?.type !== 'Open' && import("../chat/ChatInput/Tiptap")) interface EditMessageModalProps { onClose: (refresh?: boolean) => void, @@ -50,7 +52,9 @@ export const EditMessageModal = ({ onClose, channelMessageID, originalText }: Ed - + }> + + Press Enter to save diff --git a/raven-app/src/components/feature/message-action-palette/EmojiPickerButton.tsx b/raven-app/src/components/feature/message-action-palette/EmojiPickerButton.tsx index 99bbbfdfd..baaa7b6ce 100644 --- a/raven-app/src/components/feature/message-action-palette/EmojiPickerButton.tsx +++ b/raven-app/src/components/feature/message-action-palette/EmojiPickerButton.tsx @@ -1,8 +1,10 @@ -import { useState } from 'react' -import { EmojiPicker } from '../../common/EmojiPicker/EmojiPicker' +import { lazy, useState, Suspense } from 'react' import { Box, Flex, IconButton, Popover, Portal, Tooltip } from '@radix-ui/themes' import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' import { Smile } from 'lucide-react' +import { Loader } from '@/components/common/Loader' + +const EmojiPicker = lazy(() => import('@/components/common/EmojiPicker/EmojiPicker')) interface EmojiPickerButtonProps { saveReaction: (emoji: string) => void @@ -38,7 +40,9 @@ export const EmojiPickerButton = ({ saveReaction }: EmojiPickerButtonProps) => { - + }> + + diff --git a/raven-app/yarn.lock b/raven-app/yarn.lock index d93904eac..f780de3d8 100644 --- a/raven-app/yarn.lock +++ b/raven-app/yarn.lock @@ -2984,10 +2984,10 @@ electron-to-chromium@^1.4.535: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.593.tgz#f71b157f7382f3d3a164f73ff2da772d4b3fd984" integrity sha512-c7+Hhj87zWmdpmjDONbvNKNo24tvmD4mjal1+qqTYTrlF0/sNpAcDlU0Ki84ftA/5yj3BF2QhSGEC0Rky6larg== -emoji-picker-element@^1.18.4: - version "1.19.1" - resolved "https://registry.yarnpkg.com/emoji-picker-element/-/emoji-picker-element-1.19.1.tgz#8c3ba086406884c0582349e770383b42bcb802ab" - integrity sha512-EPkKg3IuxL1oyK2hmTCIzabErhvhHUUeFsUqu0e32dPxQZCeP+BkbgKZ8t4pJuvHjpdvLuB+L4vuO4ko1la7lA== +emoji-picker-element@^1.19.2: + version "1.19.2" + resolved "https://registry.yarnpkg.com/emoji-picker-element/-/emoji-picker-element-1.19.2.tgz#2e6f7a001c5b66e5114ef25f5130298d1e6c26a6" + integrity sha512-oBwJnF1DjUiY/eT7XVUbMV6iXXRkvdFYnpjDyKhn0zfkmkNNigo0UTGxHDiPUNpy4q0qsfKnwNdf79ozTxinnA== engine.io-client@~6.5.2: version "6.5.3" @@ -4836,18 +4836,6 @@ throttle-debounce@^3.0.1: resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb" integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg== -timeago-react@^3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/timeago-react/-/timeago-react-3.0.6.tgz#d1b55af67ed21b1c5429b9ba8043d0d0f7dab295" - integrity sha512-4ywnCX3iFjdp84WPK7gt8s4n0FxXbYM+xv8hYL73p83dpcMxzmO+0W4xJuxflnkWNvum5aEaqTe6LZ3lUIudjQ== - dependencies: - timeago.js "^4.0.0" - -timeago.js@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-4.0.2.tgz#724e8c8833e3490676c7bb0a75f5daf20e558028" - integrity sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w== - tiny-invariant@^1.0.6: version "1.3.1" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" From a68a07ce4bc3a4f1141e8fd8d1cfc7a60bb7c6f2 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Sun, 26 Nov 2023 06:33:23 +0530 Subject: [PATCH 084/147] perf: add raven users modal code split --- .../feature/raven-users/AddRavenUsers.tsx | 136 ++---------------- .../raven-users/AddRavenUsersContent.tsx | 131 +++++++++++++++++ .../layout/Sidebar/SidebarFooter.tsx | 2 +- 3 files changed, 141 insertions(+), 128 deletions(-) create mode 100644 raven-app/src/components/feature/raven-users/AddRavenUsersContent.tsx diff --git a/raven-app/src/components/feature/raven-users/AddRavenUsers.tsx b/raven-app/src/components/feature/raven-users/AddRavenUsers.tsx index d28d3d060..5010384d8 100644 --- a/raven-app/src/components/feature/raven-users/AddRavenUsers.tsx +++ b/raven-app/src/components/feature/raven-users/AddRavenUsers.tsx @@ -1,20 +1,9 @@ -import { useDebounce } from "@/hooks/useDebounce" -import { usePaginationWithDoctype } from "@/hooks/usePagination" -import { User } from "@/types/Core/User" -import { Filter, useFrappeCreateDoc, useFrappeGetDocList, useSWRConfig } from "frappe-react-sdk" -import { ChangeEvent, useContext, useState } from "react" -import { Sort } from "../sorting" -import { PageLengthSelector } from "../pagination/PageLengthSelector" -import { PageSelector } from "../pagination/PageSelector" -import { ErrorBanner } from "@/components/layout/AlertBanner" -import { TableLoader } from "@/components/layout/Loaders/TableLoader" -import { UsersTable } from "./UsersTable" -import { UserListContext } from "@/utils/users/UserListProvider" -import { Button, Dialog, Flex, Text, TextField } from "@radix-ui/themes" import { Loader } from "@/components/common/Loader" -import { Search } from "lucide-react" import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog" -import { useToast } from "@/hooks/useToast" +import { Dialog, Flex } from "@radix-ui/themes" +import { lazy, Suspense } from "react" + +const AddRavenUsersContent = lazy(() => import("./AddRavenUsersContent")) export const AddRavenUsers = ({ isOpen, onOpenChange }: any) => { @@ -22,119 +11,12 @@ export const AddRavenUsers = ({ isOpen, onOpenChange }: any) => { onOpenChange(false) } return - - - -} - -export const AddRavenUsersModal = ({ onClose }: { onClose: VoidFunction }) => { - - const { mutate } = useSWRConfig() - const [searchText, setSearchText] = useState("") - const debouncedText = useDebounce(searchText, 200) - - const handleChange = (event: ChangeEvent) => { - setSearchText(event.target.value) - } - - const filters: Filter[] = [['enabled', '=', 1], ['name', 'not in', ['Guest', 'Administrator']], ['user_type', '!=', 'Website User'], ['full_name', 'like', `%${debouncedText}%`]] - - const { start, count, selectedPageLength, setPageLength, nextPage, previousPage } = usePaginationWithDoctype("User", 10, filters) - const [sortOrder, setSortOder] = useState<"asc" | "desc">("desc") - - const { data, error } = useFrappeGetDocList("User", { - fields: ["name", "full_name", "user_image", "creation", "enabled", "user_type"], - filters, - orderBy: { - field: 'creation', - order: sortOrder - }, - limit_start: start > 0 ? (start - 1) : 0, - limit: selectedPageLength - }) - - const users = useContext(UserListContext) - const ravenUsersArray = users.users.map(user => user.name) - - const [selected, setSelected] = useState([]) - const { createDoc, loading } = useFrappeCreateDoc() - const { toast } = useToast() - - const handleAddUsers = async () => { - if (selected.length > 0) { - - const createPromises = selected.map(user => createDoc('Raven User', { user: user })) - - Promise.all(createPromises) - .then(() => { - toast({ - title: `You have added ${selected.length} users to Raven`, - variant: 'success', - duration: 1000 - }) - onClose() - mutate('raven.api.raven_users.get_list') - }) - } - } - - return ( - Add users to Raven - - - - - - - - - - - {debouncedText.length > 0 && debouncedText.length < 2 && Continue typing...} - - - setSortOder(order)} /> - setPageLength(value)} /> - nextPage()} - gotoPreviousPage={() => previousPage()} /> - - - - - {!data && !error && } - - {data && data.length === 0 && debouncedText.length >= 2 && - - No results found - } + }> + + + - {data && data.length !== 0 && } + - - - - - - - -
- ) } \ No newline at end of file diff --git a/raven-app/src/components/feature/raven-users/AddRavenUsersContent.tsx b/raven-app/src/components/feature/raven-users/AddRavenUsersContent.tsx new file mode 100644 index 000000000..f71a85564 --- /dev/null +++ b/raven-app/src/components/feature/raven-users/AddRavenUsersContent.tsx @@ -0,0 +1,131 @@ +import { useDebounce } from "@/hooks/useDebounce" +import { usePaginationWithDoctype } from "@/hooks/usePagination" +import { User } from "@/types/Core/User" +import { Filter, useFrappeCreateDoc, useFrappeGetDocList, useSWRConfig } from "frappe-react-sdk" +import { ChangeEvent, useContext, useState } from "react" +import { Sort } from "../sorting" +import { PageLengthSelector } from "../pagination/PageLengthSelector" +import { PageSelector } from "../pagination/PageSelector" +import { ErrorBanner } from "@/components/layout/AlertBanner" +import { TableLoader } from "@/components/layout/Loaders/TableLoader" +import { UsersTable } from "./UsersTable" +import { UserListContext } from "@/utils/users/UserListProvider" +import { Button, Dialog, Flex, Text, TextField } from "@radix-ui/themes" +import { Loader } from "@/components/common/Loader" +import { Search } from "lucide-react" +import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog" +import { useToast } from "@/hooks/useToast" + +const AddRavenUsersContent = ({ onClose }: { onClose: VoidFunction }) => { + + const { mutate } = useSWRConfig() + const [searchText, setSearchText] = useState("") + const debouncedText = useDebounce(searchText, 200) + + const handleChange = (event: ChangeEvent) => { + setSearchText(event.target.value) + } + + const filters: Filter[] = [['enabled', '=', 1], ['name', 'not in', ['Guest', 'Administrator']], ['user_type', '!=', 'Website User'], ['full_name', 'like', `%${debouncedText}%`]] + + const { start, count, selectedPageLength, setPageLength, nextPage, previousPage } = usePaginationWithDoctype("User", 10, filters) + const [sortOrder, setSortOder] = useState<"asc" | "desc">("desc") + + const { data, error } = useFrappeGetDocList("User", { + fields: ["name", "full_name", "user_image", "creation", "enabled", "user_type"], + filters, + orderBy: { + field: 'creation', + order: sortOrder + }, + limit_start: start > 0 ? (start - 1) : 0, + limit: selectedPageLength + }) + + const users = useContext(UserListContext) + const ravenUsersArray = users.users.map(user => user.name) + + const [selected, setSelected] = useState([]) + const { createDoc, loading } = useFrappeCreateDoc() + const { toast } = useToast() + + const handleAddUsers = async () => { + if (selected.length > 0) { + + const createPromises = selected.map(user => createDoc('Raven User', { user: user })) + + Promise.all(createPromises) + .then(() => { + toast({ + title: `You have added ${selected.length} users to Raven`, + variant: 'success', + duration: 1000 + }) + onClose() + mutate('raven.api.raven_users.get_list') + }) + } + } + + return ( +
+ Add users to Raven + + + + + + + + + + + {debouncedText.length > 0 && debouncedText.length < 2 && Continue typing...} + + + setSortOder(order)} /> + setPageLength(value)} /> + nextPage()} + gotoPreviousPage={() => previousPage()} /> + + + + + {!data && !error && } + + {data && data.length === 0 && debouncedText.length >= 2 && + + No results found + } + + {data && data.length !== 0 && } + + + + + + + + +
+ ) +} + +export default AddRavenUsersContent \ No newline at end of file diff --git a/raven-app/src/components/layout/Sidebar/SidebarFooter.tsx b/raven-app/src/components/layout/Sidebar/SidebarFooter.tsx index 306f6528e..700086e80 100644 --- a/raven-app/src/components/layout/Sidebar/SidebarFooter.tsx +++ b/raven-app/src/components/layout/Sidebar/SidebarFooter.tsx @@ -32,7 +32,7 @@ export const SidebarFooter = () => { setIsAddUserModalOpen(true)} className="cursor-pointer"> - Add Raven users + Add users to Raven From e339b2de53c3de03bc7a99c6fc7b7ce587f66d04 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Sun, 26 Nov 2023 07:28:37 +0530 Subject: [PATCH 085/147] perf: code split routes --- raven-app/package.json | 2 +- raven-app/src/App.tsx | 36 +++++++++++-------- .../feature/saved-messages/SavedMessages.tsx | 7 ++-- raven-app/src/main.tsx | 14 ++++---- raven-app/src/pages/ChatSpace.tsx | 7 ++-- raven-app/src/pages/auth/Login.tsx | 7 ++-- raven-app/src/pages/auth/index.ts | 1 - raven-app/yarn.lock | 2 +- 8 files changed, 44 insertions(+), 32 deletions(-) delete mode 100644 raven-app/src/pages/auth/index.ts diff --git a/raven-app/package.json b/raven-app/package.json index 9b9102c57..236fd3f89 100644 --- a/raven-app/package.json +++ b/raven-app/package.json @@ -48,7 +48,7 @@ "react-idle-timer": "^5.5.3", "react-intersection-observer": "^9.5.2", "react-markdown": "^8.0.5", - "react-router-dom": "^6.8.1", + "react-router-dom": "^6.20.0", "react-virtuoso": "^4.3.8", "rehype-raw": "^6.1.1", "remark-gfm": "^3.0.1", diff --git a/raven-app/src/App.tsx b/raven-app/src/App.tsx index 653b94a77..7cb107f28 100644 --- a/raven-app/src/App.tsx +++ b/raven-app/src/App.tsx @@ -1,8 +1,5 @@ import { FrappeProvider } from 'frappe-react-sdk' -import { Route, Routes } from 'react-router-dom' -import { SavedMessages } from './components/feature/saved-messages/SavedMessages' -import { Login } from './pages/auth' -import { ChatSpace } from './pages/ChatSpace' +import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom' import { MainPage } from './pages/MainPage' import { ProtectedRoute } from './utils/auth/ProtectedRoute' import { UserProvider } from './utils/auth/UserProvider' @@ -11,7 +8,26 @@ import "cal-sans"; import { useState } from 'react' import { ThemeProvider } from './ThemeProvider' import { Toaster } from './components/common/Toast/Toaster' +import { FullPageLoader } from './components/layout/Loaders' + +const router = createBrowserRouter( + createRoutesFromElements( + <> + import('@/pages/auth/Login')} /> + }> + } /> + } > + } /> + import('./components/feature/saved-messages/SavedMessages')} /> + import('@/pages/ChatSpace')} /> + + + + ), { + basename: `/${import.meta.env.VITE_BASE_NAME}` ?? '', +} +) function App() { const [appearance, setAppearance] = useState<'dark' | 'light'>('dark'); @@ -46,17 +62,7 @@ function App() { accentColor='iris' panelBackground='translucent' toggleTheme={toggleTheme}> - - } /> - }> - } /> - } > - } /> - } /> - } /> - - - + } /> diff --git a/raven-app/src/components/feature/saved-messages/SavedMessages.tsx b/raven-app/src/components/feature/saved-messages/SavedMessages.tsx index 5202cc671..e2246d373 100644 --- a/raven-app/src/components/feature/saved-messages/SavedMessages.tsx +++ b/raven-app/src/components/feature/saved-messages/SavedMessages.tsx @@ -18,7 +18,7 @@ interface SavedMessage extends TextMessage { file: string } -export const SavedMessages = () => { +const SavedMessages = () => { const navigate = useNavigate() @@ -95,4 +95,7 @@ export const SavedMessages = () => { onToggle={onCommandPaletteToggle} /> ) -} \ No newline at end of file +} + +export const Component = SavedMessages +// Component.displayName = "SavedMessages" \ No newline at end of file diff --git a/raven-app/src/main.tsx b/raven-app/src/main.tsx index 5480dc79b..fda2f6498 100644 --- a/raven-app/src/main.tsx +++ b/raven-app/src/main.tsx @@ -8,15 +8,15 @@ import './index.css' ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - {/* */} + + {/* */} - - {/* */} - - + + {/* */} + + {/* */} ) diff --git a/raven-app/src/pages/ChatSpace.tsx b/raven-app/src/pages/ChatSpace.tsx index ef76dc3a2..446e4b866 100644 --- a/raven-app/src/pages/ChatSpace.tsx +++ b/raven-app/src/pages/ChatSpace.tsx @@ -7,10 +7,9 @@ import { ChannelMembersProvider } from "@/utils/channel/ChannelMembersProvider" import { useEffect } from "react" import { Box } from '@radix-ui/themes' import { useParams } from "react-router-dom" -import { FrappeError, useSWRConfig } from "frappe-react-sdk" -import { ErrorCallout } from "@/components/layout/AlertBanner/ErrorBanner" +import { useSWRConfig } from "frappe-react-sdk" -export const ChatSpace = () => { +const ChatSpace = () => { // only if channelID is present render ChatSpaceArea component' const { channelID } = useParams<{ channelID: string }>() @@ -22,6 +21,8 @@ export const ChatSpace = () => { } +export const Component = ChatSpace + const ChatSpaceArea = ({ channelID }: { channelID: string }) => { const { channel, error, isLoading } = useCurrentChannelData(channelID) diff --git a/raven-app/src/pages/auth/Login.tsx b/raven-app/src/pages/auth/Login.tsx index 8683f0d7d..95df5241e 100644 --- a/raven-app/src/pages/auth/Login.tsx +++ b/raven-app/src/pages/auth/Login.tsx @@ -14,7 +14,8 @@ type Inputs = { email: string; password: string; }; -export const Login = () => { + +export const Component = () => { const [error, setError] = useState(null) const { login, isLoading } = useContext(UserContext) const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm(); @@ -108,4 +109,6 @@ export const Login = () => { ) -} \ No newline at end of file +} + +Component.displayName = "LoginPage"; \ No newline at end of file diff --git a/raven-app/src/pages/auth/index.ts b/raven-app/src/pages/auth/index.ts deleted file mode 100644 index 705d0118d..000000000 --- a/raven-app/src/pages/auth/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Login } from './Login' \ No newline at end of file diff --git a/raven-app/yarn.lock b/raven-app/yarn.lock index f780de3d8..a76d3ed80 100644 --- a/raven-app/yarn.lock +++ b/raven-app/yarn.lock @@ -4518,7 +4518,7 @@ react-remove-scroll@^2.5.6: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-router-dom@^6.8.1: +react-router-dom@^6.20.0: version "6.20.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.20.0.tgz#7b9527a1e29c7fb90736a5f89d54ca01f40e264b" integrity sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ== From 80ebd8eacd2a70e96ea60ac11aa4393dc3605fc0 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Sun, 26 Nov 2023 08:51:25 +0530 Subject: [PATCH 086/147] perf: split moment into separate file with fallbacks --- .../channel-details/ChannelDetails.tsx | 9 +-- .../FilesSharedInChannel.tsx | 5 +- .../feature/chat/chat-history/ChatHistory.tsx | 4 +- .../feature/chat/chat-message/DateTooltip.tsx | 11 ++- .../chat/message-reply/PreviousMessageBox.tsx | 15 +--- .../feature/file-preview/FilePreviewModal.tsx | 5 +- .../feature/global-search/FileSearch.tsx | 5 +- .../feature/raven-users/UsersTable.tsx | 4 +- .../layout/EmptyState/EmptyState.tsx | 4 +- .../FrappeTimestampToReadableDate.tsx | 10 +++ raven-app/src/utils/dateConversions/index.tsx | 74 +++++++++++++++++++ raven-app/src/utils/dateConversions/utils.ts | 20 +++++ raven-app/src/utils/operations.ts | 58 --------------- 13 files changed, 132 insertions(+), 92 deletions(-) create mode 100644 raven-app/src/utils/dateConversions/FrappeTimestampToReadableDate.tsx create mode 100644 raven-app/src/utils/dateConversions/index.tsx create mode 100644 raven-app/src/utils/dateConversions/utils.ts diff --git a/raven-app/src/components/feature/channel-details/ChannelDetails.tsx b/raven-app/src/components/feature/channel-details/ChannelDetails.tsx index 2981a3115..26c2c1c0a 100644 --- a/raven-app/src/components/feature/channel-details/ChannelDetails.tsx +++ b/raven-app/src/components/feature/channel-details/ChannelDetails.tsx @@ -1,6 +1,5 @@ import { useContext } from "react" import { UserContext } from "../../../utils/auth/UserProvider" -import { DateObjectToFormattedDateString } from "../../../utils/operations" import { ChannelListItem } from "@/utils/channel/ChannelListProvider" import { ChannelIcon } from "@/utils/layout/channelIcon" import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider" @@ -9,6 +8,7 @@ import { EditDescriptionButton } from "./edit-channel-description/EditDescriptio import { useGetUserRecords } from "@/hooks/useGetUserRecords" import { LeaveChannelButton } from "./leave-channel/LeaveChannelButton" import { Box, Flex, Separator, Text } from "@radix-ui/themes" +import { DateMonthYear } from "@/utils/dateConversions" interface ChannelDetailsProps { channelData: ChannelListItem, @@ -21,7 +21,6 @@ export const ChannelDetails = ({ channelData, channelMembers, onClose }: Channel const { currentUser } = useContext(UserContext) const admin = Object.values(channelMembers).find(user => user.is_admin === 1) const users = useGetUserRecords() - const type = channelData.type return ( @@ -30,8 +29,8 @@ export const ChannelDetails = ({ channelData, channelMembers, onClose }: Channel Channel name - - + + {channelData?.channel_name} @@ -57,7 +56,7 @@ export const ChannelDetails = ({ channelData, channelMembers, onClose }: Channel Created by {channelData?.owner && {users[channelData.owner]?.full_name}} - on {DateObjectToFormattedDateString(new Date(channelData?.creation ?? ''))} + {channelData.creation && on } diff --git a/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx b/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx index f94e11ca9..e05668c5d 100644 --- a/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx +++ b/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx @@ -2,11 +2,12 @@ import { FileExtensionIcon } from "../../../utils/layout/FileExtensionIcon"; import { useFrappeGetCall } from "frappe-react-sdk"; import { useParams } from "react-router-dom"; import { ErrorBanner } from "../../layout/AlertBanner"; -import { DateObjectToFormattedDateString, getFileExtension, getFileName } from "../../../utils/operations"; +import { getFileExtension, getFileName } from "../../../utils/operations"; import { Download } from "lucide-react"; import { FileMessage } from "../../../../../types/Messaging/Message"; import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider"; import { Box, Flex, IconButton, Link, Text } from "@radix-ui/themes"; +import { DateMonthYear } from "@/utils/dateConversions"; interface ChannelFile extends FileMessage { name: string, @@ -45,7 +46,7 @@ export const FilesSharedInChannel = ({ channelMembers }: FilesSharedInChannelPro {getFileName(f.file)} - Shared by {channelMembers[f.owner]?.full_name} on {DateObjectToFormattedDateString(new Date(f.creation ?? ''))} + {f.creation && Shared by {channelMembers[f.owner]?.full_name} on } diff --git a/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx b/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx index bb62ff291..276507671 100644 --- a/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx +++ b/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx @@ -1,5 +1,4 @@ import { DividerWithText } from "../../../layout/Divider/DividerWithText"; -import { DateObjectToFormattedDateString } from "../../../../utils/operations"; import { DateBlock, FileMessage, Message, MessageBlock, MessagesWithDate } from "../../../../../../types/Messaging/Message"; import { ChannelHistoryFirstMessage } from "../../../layout/EmptyState/EmptyState"; import { useContext, useRef } from "react"; @@ -12,6 +11,7 @@ import { Virtuoso } from 'react-virtuoso'; import { VirtuosoRefContext } from "../../../../utils/message/VirtuosoRefProvider"; import { ChannelListItem, DMChannelListItem } from "@/utils/channel/ChannelListProvider"; import { Box } from "@radix-ui/themes"; +import { DateMonthYear } from "@/utils/dateConversions"; interface ChatHistoryProps { parsedMessages: MessagesWithDate, @@ -43,7 +43,7 @@ export const ChatHistory = ({ parsedMessages, replyToMessage, channelData }: Cha if (block.block_type === 'date') { return ( - {DateObjectToFormattedDateString(new Date(block.data))} + ) } diff --git a/raven-app/src/components/feature/chat/chat-message/DateTooltip.tsx b/raven-app/src/components/feature/chat/chat-message/DateTooltip.tsx index db069644c..0844af318 100644 --- a/raven-app/src/components/feature/chat/chat-message/DateTooltip.tsx +++ b/raven-app/src/components/feature/chat/chat-message/DateTooltip.tsx @@ -1,16 +1,15 @@ -import { DateObjectToFormattedDateStringWithoutYear, DateObjectToTimeString } from '@/utils/operations' +import { DateMonthAtHourMinuteAmPm, HourMinuteAmPm } from '@/utils/dateConversions' import { Tooltip, Text } from '@radix-ui/themes' - export const DateTooltip = ({ timestamp }: { timestamp: string }) => { return ( - + }> - {DateObjectToTimeString(new Date(timestamp))} + ) @@ -18,7 +17,7 @@ export const DateTooltip = ({ timestamp }: { timestamp: string }) => { export const DateTooltipShort = ({ timestamp, showButtons }: { timestamp: string, showButtons: {} }) => { return ( - + }> - {DateObjectToTimeString(new Date(timestamp)).split(' ')[0]} + ) diff --git a/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx b/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx index 2ec34d3e6..b197b8750 100644 --- a/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx +++ b/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx @@ -1,7 +1,7 @@ import { Text, Box, HStack, Stack, Center, Image, IconButton, StackDivider, LinkBox } from '@chakra-ui/react' import { Message } from '../../../../../../types/Messaging/Message' import { MarkdownRenderer } from '../../markdown-viewer/MarkdownRenderer' -import { DateObjectToFormattedDateStringWithoutYear, DateObjectToTimeString, getFileExtension, getFileName } from '../../../../utils/operations' +import { getFileExtension, getFileName } from '../../../../utils/operations' import { useContext } from 'react' import { FileExtensionIcon } from '../../../../utils/layout/FileExtensionIcon' import { useFrappeGetDoc, useFrappePostCall } from 'frappe-react-sdk' @@ -13,6 +13,7 @@ import { useGetUserRecords } from '@/hooks/useGetUserRecords' import { X } from 'lucide-react' import { useTheme } from '@/ThemeProvider' import { ErrorCallout } from '@/components/layout/AlertBanner/ErrorBanner' +import { DateMonthAtHourMinuteAmPm } from '@/utils/dateConversions' interface PreviousMessageBoxProps { previous_message_id?: string, @@ -36,11 +37,7 @@ export const PreviousMessageBox = ({ previous_message_id, previous_message_conte }> {users?.[previous_message_content.owner]?.full_name ?? previous_message_content.owner} - - {DateObjectToFormattedDateStringWithoutYear(new Date(previous_message_content.creation))} - at - {DateObjectToTimeString(new Date(previous_message_content.creation))} - + {/* message content */} {previous_message_content.message_type === 'Text' && @@ -133,11 +130,7 @@ const PreviousMessageBoxInChat = ({ previous_message_id, channelData, users }: P {users?.[data.owner]?.full_name ?? data.owner} - - {DateObjectToFormattedDateStringWithoutYear(new Date(data.creation))} - at - {DateObjectToTimeString(new Date(data.creation))} - + {/* message content */} {data.message_type === 'Text' && diff --git a/raven-app/src/components/feature/file-preview/FilePreviewModal.tsx b/raven-app/src/components/feature/file-preview/FilePreviewModal.tsx index 326c5c2ec..2ef35e531 100644 --- a/raven-app/src/components/feature/file-preview/FilePreviewModal.tsx +++ b/raven-app/src/components/feature/file-preview/FilePreviewModal.tsx @@ -1,9 +1,10 @@ import { Avatar, Center, HStack, Link, Modal, ModalBody, Image, ModalFooter, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Stack, StackDivider, Text } from "@chakra-ui/react" -import { DateObjectToTimeString, getFileName } from "../../../utils/operations" +import { getFileName } from "../../../utils/operations" import { FileMessage } from "../../../../../types/Messaging/Message" import { useGetUserRecords } from "@/hooks/useGetUserRecords" import { useTheme } from "@/ThemeProvider" import { Download } from "lucide-react" +import { HourMinuteAmPm } from "@/utils/dateConversions" interface FilePreviewModalProps extends FileMessage { isOpen: boolean, @@ -28,7 +29,7 @@ export const FilePreviewModal = ({ isOpen, onClose, owner, file, creation, messa } spacing={2} alignItems='center'> {users?.[owner]?.full_name ?? owner} - {DateObjectToTimeString(creation)} + {file && diff --git a/raven-app/src/components/feature/global-search/FileSearch.tsx b/raven-app/src/components/feature/global-search/FileSearch.tsx index 43512cff3..99145a2cf 100644 --- a/raven-app/src/components/feature/global-search/FileSearch.tsx +++ b/raven-app/src/components/feature/global-search/FileSearch.tsx @@ -6,7 +6,7 @@ import { Controller, FormProvider, useForm } from 'react-hook-form' import { useDebounce } from '../../../hooks/useDebounce' import { GetFileSearchResult } from '../../../../../types/Search/Search' import { FileExtensionIcon } from '../../../utils/layout/FileExtensionIcon' -import { DateObjectToFormattedDateString, getFileExtension, getFileName } from '../../../utils/operations' +import { getFileExtension, getFileName } from '../../../utils/operations' import { ErrorBanner } from '../../layout/AlertBanner' import { EmptyStateForSearch } from '../../layout/EmptyState/EmptyState' import { SelectInput, SelectOption } from '../search-filters/SelectInput' @@ -20,6 +20,7 @@ import { ChannelListContext, ChannelListContextType, ChannelListItem } from '@/u import { useGetUserRecords } from '@/hooks/useGetUserRecords' import { useTheme } from '@/ThemeProvider' import { ChannelIcon } from '@/utils/layout/channelIcon' +import { DateMonthYear } from '@/utils/dateConversions' interface FilterInput { 'from-user-filter': SelectOption[], @@ -256,7 +257,7 @@ export const FileSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSaved {f.file && {getFileName(f.file)}} - {users && Shared by {Object.values(users).find((user: UserFields) => user.name === f.owner)?.full_name} on {DateObjectToFormattedDateString(new Date(f.creation ?? ''))}} + {users && Shared by {Object.values(users).find((user: UserFields) => user.name === f.owner)?.full_name} on } ) diff --git a/raven-app/src/components/feature/raven-users/UsersTable.tsx b/raven-app/src/components/feature/raven-users/UsersTable.tsx index b27fda4f8..d3f8de751 100644 --- a/raven-app/src/components/feature/raven-users/UsersTable.tsx +++ b/raven-app/src/components/feature/raven-users/UsersTable.tsx @@ -1,8 +1,8 @@ import { User } from "@/types/Core/User" import { useMemo } from "react" -import { convertFrappeTimestampToReadableDate } from "@/utils/operations" import { Checkbox, Flex, Table } from "@radix-ui/themes" import { UserAvatar } from "@/components/common/UserAvatar" +import { StandardDate } from "@/utils/dateConversions" interface UsersTableProps { data: User[], @@ -78,7 +78,7 @@ export const UsersTable = ({ data, selected, setSelected, defaultSelected }: Use {user.name} - {convertFrappeTimestampToReadableDate(user.creation)} + ) })} diff --git a/raven-app/src/components/layout/EmptyState/EmptyState.tsx b/raven-app/src/components/layout/EmptyState/EmptyState.tsx index de58caff3..b5d34dd06 100644 --- a/raven-app/src/components/layout/EmptyState/EmptyState.tsx +++ b/raven-app/src/components/layout/EmptyState/EmptyState.tsx @@ -1,4 +1,3 @@ -import { DateObjectToFormattedDateString } from "../../../utils/operations" import { ChannelListItem, DMChannelListItem } from "@/utils/channel/ChannelListProvider" import { useCurrentChannelData } from "@/hooks/useCurrentChannelData" import { useContext, useState } from "react" @@ -11,6 +10,7 @@ import { Box, Button, Flex, Heading, Link, Text } from "@radix-ui/themes" import { UserAvatar } from "@/components/common/UserAvatar" import { ChannelIcon } from "@/utils/layout/channelIcon" import { Bookmark } from "lucide-react" +import { DateMonthYear } from "@/utils/dateConversions" export const EmptyStateForSearch = () => { return ( @@ -47,7 +47,7 @@ const EmptyStateForChannel = ({ channelData, channelMembers, updateMembers }: Em {channelData?.channel_name} - {users[channelData.owner]?.full_name} created this channel on {DateObjectToFormattedDateString(new Date(channelData?.creation ?? ''))}. This is the very beginning of the {channelData?.channel_name} channel. + {users[channelData.owner]?.full_name} created this channel on . This is the very beginning of the {channelData?.channel_name} channel. {channelData?.channel_description && {channelData?.channel_description}} {channelData?.is_archived == 0 && channelMembers[currentUser] && diff --git a/raven-app/src/utils/dateConversions/FrappeTimestampToReadableDate.tsx b/raven-app/src/utils/dateConversions/FrappeTimestampToReadableDate.tsx new file mode 100644 index 000000000..320d9163c --- /dev/null +++ b/raven-app/src/utils/dateConversions/FrappeTimestampToReadableDate.tsx @@ -0,0 +1,10 @@ +import React from 'react' + +import { USER_DATE_FORMAT, convertFrappeTimestampToUserTimezone } from './utils' + +const FrappeTimestampToReadableDate = ({ date, format = USER_DATE_FORMAT }: { format?: string, date: string }) => { + + return convertFrappeTimestampToUserTimezone(date).format(format) +} + +export default FrappeTimestampToReadableDate \ No newline at end of file diff --git a/raven-app/src/utils/dateConversions/index.tsx b/raven-app/src/utils/dateConversions/index.tsx new file mode 100644 index 000000000..999f84050 --- /dev/null +++ b/raven-app/src/utils/dateConversions/index.tsx @@ -0,0 +1,74 @@ +import { lazy, Suspense } from "react"; + +const FrappeTimestampToReadableDate = lazy(() => import("./FrappeTimestampToReadableDate")); + +interface Props { + date: string; +} +/** + * Returns a date in the standard format that the user has set in their preferences + * @param props + * @returns + */ +export const StandardDate = (props: Props) => { + + const parseDateString = (date: string) => { + const dateObj = new Date(date) + return dateObj.toLocaleDateString('en-GB', { day: 'numeric', month: 'numeric', year: 'numeric' }) + } + return ( + + + + ) +} + +/** + * Returns a date in DD MMM YYYY format + */ +export const DateMonthYear = (props: Props) => { + + const parseDateString = (date: string) => { + const dateObj = new Date(date) + return dateObj.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }) + } + return ( + + + + ) +} + +interface HourMinuteAmPmProps extends Props { + amPm?: boolean +} +/** + * + * @returns JSX Element of the format hh:mm AM/PM + * @example 08:15 PM or 12:00 AM + */ +export const HourMinuteAmPm = ({ amPm = true, date }: HourMinuteAmPmProps) => { + + const parseDateString = (d: string) => { + const dateObj = new Date(d) + return dateObj.toLocaleTimeString('en-GB', { hour: 'numeric', minute: 'numeric', hour12: amPm }) + } + return ( + + + + ) +} + +export const DateMonthAtHourMinuteAmPm = (props: Props) => { + + const parseDateString = (date: string) => { + const dateObj = new Date(date) + return dateObj.toLocaleDateString('en-GB', { month: 'long', day: 'numeric' }) + " at " + dateObj.toLocaleTimeString('en-GB', { hour: 'numeric', minute: 'numeric', hour12: true }) + } + return ( + + + + ) +} \ No newline at end of file diff --git a/raven-app/src/utils/dateConversions/utils.ts b/raven-app/src/utils/dateConversions/utils.ts new file mode 100644 index 000000000..08c1fba4e --- /dev/null +++ b/raven-app/src/utils/dateConversions/utils.ts @@ -0,0 +1,20 @@ +import moment, { Moment } from "moment-timezone" + +const DEFAULT_TIME_ZONE = 'Asia/Kolkata' +//@ts-expect-error +export const SYSTEM_TIMEZONE = window.frappe?.boot?.time_zone?.system || DEFAULT_TIME_ZONE +//@ts-expect-error +export const USER_TIMEZONE = window.frappe?.boot?.time_zone?.user || DEFAULT_TIME_ZONE +//@ts-expect-error +export const USER_DATE_FORMAT = (window.frappe?.boot?.user?.defaults?.date_format?.toUpperCase() || window.frappe?.boot?.sysdefaults?.date_format?.toUpperCase() + || 'DD/MM/YYYY') + +export const FRAPPE_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' +export const FRAPPE_DATE_FORMAT = 'YYYY-MM-DD' +export const FRAPPE_TIME_FORMAT = 'HH:mm:ss' + + +export const convertFrappeTimestampToUserTimezone = (timestamp: string): Moment => { + + return moment.tz(timestamp, SYSTEM_TIMEZONE).clone().tz(USER_TIMEZONE) +} \ No newline at end of file diff --git a/raven-app/src/utils/operations.ts b/raven-app/src/utils/operations.ts index b9d2f0013..9d92158e1 100644 --- a/raven-app/src/utils/operations.ts +++ b/raven-app/src/utils/operations.ts @@ -1,62 +1,4 @@ import { UserFields } from "./users/UserListProvider" -import moment from "moment-timezone" - -/** - * Utility to convert Date object to DD-MM-YYYY format - * @param date takes Javascript Date object - * @returns Date string in DD-MM-YYYY format - */ -export const DateObjectToDateString = (date: Date): string => { - return (date.getDate() < 10 ? date.getDate().toString().padStart(2, "0") : date.getDate()) + "-" + (date.getMonth() < 9 ? (date.getMonth() + 1).toString().padStart(2, "0") : date.getMonth() + 1) + "-" + date.getFullYear() -} - -const MonthNames = ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" -] - -/** - * Utility to convert Date object to DD MonthName YYYY format - * @param date takes Javascript Date object - * @returns Date string in DD MonthName YYYY format - * @example 1 January 2021 - */ -export const DateObjectToFormattedDateString = (date: Date): string => { - return date.getDate() + " " + MonthNames[date.getMonth()] + " " + date.getFullYear() -} - -/** - * Utility to convert Date object to DD MonthName YYYY format - * @param date takes Javascript Date object - * @returns Date string in DD MonthName YYYY format - * @example 1 January 2021 - */ -export const DateObjectToFormattedDateStringWithoutYear = (date: Date): string => { - return date.getDate() + " " + MonthNames[date.getMonth()] -} - -/** - * Utility to convert Date-Time object to hour:minute format - * @param date takes Javascript Date object - * @returns Time string in hour:minute format - * @example 08:15 PM or 12:00 AM - */ -export const DateObjectToTimeString = (date: Date): string => { - var date = new Date(date) - return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) -} - -/** - * Converts Frappe datetime timestamp to readable string - * @param timestamp A frappe timestamp string in the format YYYY-MM-DD HH:mm:ss - * @param format Format can include both date and time formats - * @returns - */ -export const convertFrappeTimestampToReadableDate = (timestamp?: string, format: string = 'DD-MM-YYYY') => { - if (timestamp) { - return moment(timestamp, 'YYYY-MM-DD HH:mm:ss').format(format) - } - return '' -} /** * Function to return extension of a file From ad7f58dd4aac8fb5544d55cf0421986bbfd4cd72 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Sun, 26 Nov 2023 08:52:48 +0530 Subject: [PATCH 087/147] fix: changed name of bundle --- .../dateConversions/FrappeTimestampToReadableDate.tsx | 10 ---------- .../src/utils/dateConversions/MomentConverter.tsx | 8 ++++++++ raven-app/src/utils/dateConversions/index.tsx | 10 +++++----- 3 files changed, 13 insertions(+), 15 deletions(-) delete mode 100644 raven-app/src/utils/dateConversions/FrappeTimestampToReadableDate.tsx create mode 100644 raven-app/src/utils/dateConversions/MomentConverter.tsx diff --git a/raven-app/src/utils/dateConversions/FrappeTimestampToReadableDate.tsx b/raven-app/src/utils/dateConversions/FrappeTimestampToReadableDate.tsx deleted file mode 100644 index 320d9163c..000000000 --- a/raven-app/src/utils/dateConversions/FrappeTimestampToReadableDate.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' - -import { USER_DATE_FORMAT, convertFrappeTimestampToUserTimezone } from './utils' - -const FrappeTimestampToReadableDate = ({ date, format = USER_DATE_FORMAT }: { format?: string, date: string }) => { - - return convertFrappeTimestampToUserTimezone(date).format(format) -} - -export default FrappeTimestampToReadableDate \ No newline at end of file diff --git a/raven-app/src/utils/dateConversions/MomentConverter.tsx b/raven-app/src/utils/dateConversions/MomentConverter.tsx new file mode 100644 index 000000000..2210992ee --- /dev/null +++ b/raven-app/src/utils/dateConversions/MomentConverter.tsx @@ -0,0 +1,8 @@ +import { USER_DATE_FORMAT, convertFrappeTimestampToUserTimezone } from './utils' + +const MomentConverter = ({ date, format = USER_DATE_FORMAT }: { format?: string, date: string }) => { + + return convertFrappeTimestampToUserTimezone(date).format(format) +} + +export default MomentConverter \ No newline at end of file diff --git a/raven-app/src/utils/dateConversions/index.tsx b/raven-app/src/utils/dateConversions/index.tsx index 999f84050..ef8396927 100644 --- a/raven-app/src/utils/dateConversions/index.tsx +++ b/raven-app/src/utils/dateConversions/index.tsx @@ -1,6 +1,6 @@ import { lazy, Suspense } from "react"; -const FrappeTimestampToReadableDate = lazy(() => import("./FrappeTimestampToReadableDate")); +const MomentConverter = lazy(() => import("./MomentConverter")); interface Props { date: string; @@ -18,7 +18,7 @@ export const StandardDate = (props: Props) => { } return ( - + ) } @@ -34,7 +34,7 @@ export const DateMonthYear = (props: Props) => { } return ( - + ) } @@ -55,7 +55,7 @@ export const HourMinuteAmPm = ({ amPm = true, date }: HourMinuteAmPmProps) => { } return ( - + ) } @@ -68,7 +68,7 @@ export const DateMonthAtHourMinuteAmPm = (props: Props) => { } return ( - + ) } \ No newline at end of file From 7720d692c18034a773d891c69a37c6d725e01e05 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Sun, 26 Nov 2023 17:57:23 +0530 Subject: [PATCH 088/147] fix: removed chakra styles from Tiptap --- .../src/components/feature/chat/ChatInput/tiptap.styles.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/raven-app/src/components/feature/chat/ChatInput/tiptap.styles.css b/raven-app/src/components/feature/chat/ChatInput/tiptap.styles.css index a65e7ea72..0fa8c342e 100644 --- a/raven-app/src/components/feature/chat/ChatInput/tiptap.styles.css +++ b/raven-app/src/components/feature/chat/ChatInput/tiptap.styles.css @@ -22,19 +22,19 @@ } .tiptap blockquote { - border-left: 3px solid var(--chakra-colors-gray-300); + border-left: 3px solid var(--gray-11); padding-left: 0.8rem; margin: 1rem; } .tiptap ul, .tiptap ol { - padding-left: var(--chakra-space-8); + padding-left: var(--space-5); } .tiptap a, .tiptap .mention { - color: var(--chakra-colors-blue-500); + color: var(--accent-a10); } .tiptap a { From cbbc83e26be51ba237eca0d7512704a306da3b80 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Mon, 27 Nov 2023 13:48:34 +0530 Subject: [PATCH 089/147] wip: remove chakra --- raven-app/src/components/common/Loader.tsx | 2 +- .../feature/chat/chat-history/ChatBoxBody.tsx | 31 ++++---- .../feature/file-upload/FileListItem.tsx | 71 +++++++++---------- .../layout/Loaders/FullPageLoader.tsx | 2 +- .../src/utils/layout/fileExtensionIcon.tsx | 2 +- 5 files changed, 50 insertions(+), 58 deletions(-) diff --git a/raven-app/src/components/common/Loader.tsx b/raven-app/src/components/common/Loader.tsx index 5606fb2b0..e5179e9e6 100644 --- a/raven-app/src/components/common/Loader.tsx +++ b/raven-app/src/components/common/Loader.tsx @@ -1,6 +1,6 @@ export const Loader = () => { return ( - + diff --git a/raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx b/raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx index bff797f46..9de2282c1 100644 --- a/raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx +++ b/raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx @@ -1,4 +1,3 @@ -import { Box, Stack, Wrap, WrapItem } from "@chakra-ui/react" import { ChatHistory } from "./ChatHistory" import { useFrappeDocumentEventListener, useFrappeEventListener, useFrappeGetCall } from "frappe-react-sdk" import { Message, MessagesWithDate } from "../../../../../../types/Messaging/Message" @@ -17,6 +16,7 @@ import { FileListItem } from "../../file-upload/FileListItem" import { PreviousMessageBox } from "../message-reply/PreviousMessageBox" import { useSendMessage } from "../ChatInput/useSendMessage" import { Loader } from "@/components/common/Loader" +import { Flex, Box } from "@radix-ui/themes" const Tiptap = lazy(() => import("../ChatInput/Tiptap")) @@ -81,16 +81,17 @@ export const ChatBoxBody = ({ channelData }: ChatBoxBodyProps) => { } if (isLoading) { + //TODO: Replace with skeleton loader return } if (error) { - return + return } if (data) { return ( - + { {channelData?.is_archived == 0 && (isUserInChannel || channelData?.type === 'Open') && - }> + }> { }} onMessageSend={sendMessage} messageSending={loading} - slotBefore={} + {files && files.length > 0 && + {files.map((f: CustomFile) => removeFile(f.fileID)} />)} + } + } /> } @@ -126,17 +127,9 @@ export const ChatBoxBody = ({ channelData }: ChatBoxBodyProps) => { channelData={channelData} channelMembers={channelMembers} user={user} />)} + {channelData && channelData.is_archived == 1 && } - {/* {channelData?.is_archived == 0 && ((isUserInChannel || channelData?.type === 'Open') && - )} */} - - {channelData && channelData.is_archived == 1 && } - + ) } diff --git a/raven-app/src/components/feature/file-upload/FileListItem.tsx b/raven-app/src/components/feature/file-upload/FileListItem.tsx index e69d10035..e511a3d0a 100644 --- a/raven-app/src/components/feature/file-upload/FileListItem.tsx +++ b/raven-app/src/components/feature/file-upload/FileListItem.tsx @@ -1,11 +1,11 @@ -import { Text, Stack, IconButton, HStack, Image, Center, CircularProgress, CircularProgressLabel, } from '@chakra-ui/react' import { Trash2 } from 'lucide-react' import { useGetFilePreviewUrl } from '../../../hooks/useGetFilePreviewUrl' import { FileExtensionIcon } from '../../../utils/layout/FileExtensionIcon' import { getFileExtension } from '../../../utils/operations' import { CustomFile } from './FileDrop' import { FileUploadProgress } from '../chat/ChatInput/FileInput/useFileUpload' -import { useColorModeValue } from '@/ThemeProvider' +import { IconButton, Flex, Text } from '@radix-ui/themes' +import { Loader } from '@/components/common/Loader' interface FileListItemProps { file: CustomFile, @@ -15,13 +15,6 @@ interface FileListItemProps { export const FileListItem = ({ file, removeFile, uploadProgress }: FileListItemProps) => { - const { borderColor, bgColor } = useColorModeValue({ - borderColor: 'gray.200', - bgColor: 'white' - }, { - borderColor: 'gray.800', - bgColor: 'gray.900' - }) const previewURL = useGetFilePreviewUrl(file) const fileSizeString = getFileSize(file) @@ -29,35 +22,41 @@ export const FileListItem = ({ file, removeFile, uploadProgress }: FileListItemP const progress = uploadProgress?.[file.fileID]?.progress ?? 0 return ( - -
- {previewURL ? File preview :
{FileExtensionIcon(getFileExtension(file.name) ?? '')}
} -
- - - {file.name} - + + + {previewURL ? File preview : } + + + + {file.name} + {fileSizeString} - - {!isUploadComplete && progress > 0 - && - - {progress}% - - } - { - uploadProgress?.[file.fileID] === undefined && - } - aria-label="Remove File" /> - } - -
+ + + {!isUploadComplete && progress > 0 + && + + + {progress}% + + } + { + uploadProgress?.[file.fileID] === undefined && + + + + } + + + ) } diff --git a/raven-app/src/components/layout/Loaders/FullPageLoader.tsx b/raven-app/src/components/layout/Loaders/FullPageLoader.tsx index a59f3c8f3..290205202 100644 --- a/raven-app/src/components/layout/Loaders/FullPageLoader.tsx +++ b/raven-app/src/components/layout/Loaders/FullPageLoader.tsx @@ -12,7 +12,7 @@ export const FullPageLoader = ({ text = "Ravens are finding their way to you..." - {text} + {text} ) diff --git a/raven-app/src/utils/layout/fileExtensionIcon.tsx b/raven-app/src/utils/layout/fileExtensionIcon.tsx index afeca0d25..a0c5a6250 100644 --- a/raven-app/src/utils/layout/fileExtensionIcon.tsx +++ b/raven-app/src/utils/layout/fileExtensionIcon.tsx @@ -29,7 +29,7 @@ export const FileExtensionIcon = ({ ext, ...props }: FileExtensionIconProps) => {isImage && } {isWord && } {isPdf && } - {!isExcel && !isImage && !isWord && !isPdf && } + {!isExcel && !isImage && !isWord && !isPdf && } } \ No newline at end of file From 576196b2c5982c2bd33b647a91cd9538271339bc Mon Sep 17 00:00:00 2001 From: Yash Jane Date: Mon, 27 Nov 2023 18:59:01 +0530 Subject: [PATCH 090/147] fix: upload JPEG images w/ EXIF transpose --- raven/api/upload_file.py | 87 +++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/raven/api/upload_file.py b/raven/api/upload_file.py index 360edf1d6..cb71adc77 100644 --- a/raven/api/upload_file.py +++ b/raven/api/upload_file.py @@ -1,13 +1,43 @@ import frappe from frappe.handler import upload_file -from PIL import Image +import base64 +import io +from PIL import Image, ImageOps +from mimetypes import guess_type from frappe.core.doctype.file.utils import get_local_image -import os from frappe import _ + +def upload_JPEG_wrt_EXIF(content, filename): + ''' + When a user uploads a JPEG file, we need to transpose the image based on the EXIF data. + This is because the image is rotated when it is uploaded to the server. + ''' + content_type = guess_type(filename)[0] + + # if file format is JPEG, we need to transpose the image + if content_type.startswith("image/jpeg"): + with Image.open(io.BytesIO(content)) as image: + # transpose the image + transposed_image = ImageOps.exif_transpose(image) + # convert the image to bytes + buffer = io.BytesIO() + # save the image to the buffer + transposed_image.save(buffer, format="JPEG") + # get the value of the buffer + buffer = buffer.getvalue() + else: + buffer = decoded_content + + return frappe.get_doc({ + "doctype": "File", + "file_name": filename, + "content": buffer, + }).insert() + + @frappe.whitelist() def upload_file_with_message(): - ''' When the user uploads a file on Raven, this API is called. Along with the file, the user also send additional information: the channel ID @@ -17,8 +47,9 @@ def upload_file_with_message(): 2. Upload the file 3. If the file is an image, we need to measure it's dimensions 4. Store the file URL and the dimensions in the Raven Message Doc - ''' + fileExt = ['jpg', 'JPG', 'jpeg', 'JPEG', 'png', 'PNG', 'gif', 'GIF'] + thumbnailExt = ['jpg', 'JPG', 'jpeg', 'JPEG', 'png', 'PNG'] frappe.form_dict.doctype = "Raven Message" @@ -29,16 +60,26 @@ def upload_file_with_message(): frappe.form_dict.docname = message_doc.name - file_doc = upload_file() + # Get the files + files = frappe.request.files + # Get the file & content + if 'file' in files: + file = files['file'] + filename = file.filename + ''' + If the file is a JPEG, we need to transpose the image + Else, we need to upload the file as is + ''' + if filename.endswith('.jpeg'): + content = file.stream.read() + file_doc = upload_JPEG_wrt_EXIF(content, filename) + else: + file_doc = upload_file() message_doc.reload() message_doc.file = file_doc.file_url - fileExt = ['jpg', 'JPG', 'jpeg', 'JPEG', 'png', 'PNG', 'gif', 'GIF'] - - thumbnailExt = ['jpg', 'JPG', 'jpeg', 'JPEG', 'png', 'PNG'] - if file_doc.file_type in fileExt: message_doc.message_type = "Image" @@ -55,27 +96,27 @@ def upload_file_with_message(): # if extn in thumbnailExt: - # TODO: Generate thumbnail of the image + # TODO: Generate thumbnail of the image + + # Need to add a provision in Frappe to generate thumbnails for all images - not just public files + # Generated thumbnail here throws a permissions error when trying to access. + # thumbnail_url = f"{filename}_small.{extn}" - # Need to add a provision in Frappe to generate thumbnails for all images - not just public files - # Generated thumbnail here throws a permissions error when trying to access. - # thumbnail_url = f"{filename}_small.{extn}" + # path = os.path.abspath(frappe.get_site_path(thumbnail_url.lstrip("/"))) + # image.thumbnail(thumbnail_size, Image.Resampling.LANCZOS) - # path = os.path.abspath(frappe.get_site_path(thumbnail_url.lstrip("/"))) - # image.thumbnail(thumbnail_size, Image.Resampling.LANCZOS) - - # try: - # image.save(path) - # except OSError: - # frappe.msgprint(_("Unable to write file format for {0}").format(path)) - # thumbnail_url = file_doc.file_url + # try: + # image.save(path) + # except OSError: + # frappe.msgprint(_("Unable to write file format for {0}").format(path)) + # thumbnail_url = file_doc.file_url message_doc.image_width = width message_doc.image_height = height # message_doc.file_thumbnail = thumbnail_url message_doc.thumbnail_width = thumbnail_width message_doc.thumbnail_height = thumbnail_height - + message_doc.save() - return message_doc.name \ No newline at end of file + return message_doc.name From 4499517a68b202272b4111d4678d53d3d32013b8 Mon Sep 17 00:00:00 2001 From: Janhvi Patil Date: Mon, 27 Nov 2023 19:14:44 +0530 Subject: [PATCH 091/147] login page updated --- raven-app/src/pages/auth/Login.tsx | 132 ++++++++++++++--------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/raven-app/src/pages/auth/Login.tsx b/raven-app/src/pages/auth/Login.tsx index 95df5241e..0223088c6 100644 --- a/raven-app/src/pages/auth/Login.tsx +++ b/raven-app/src/pages/auth/Login.tsx @@ -1,5 +1,4 @@ import { useState, useContext } from "react"; -import { Box, Button, Flex, FormControl, FormLabel, IconButton, Input, InputGroup, InputRightElement, Stack, useDisclosure, chakra, FormErrorMessage } from "@chakra-ui/react"; import { useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; import { Link } from "react-router-dom"; @@ -7,8 +6,10 @@ import { ErrorBanner } from "../../components/layout/AlertBanner"; import { UserContext } from "../../utils/auth/UserProvider"; import { isEmailValid } from "../../utils/validations"; import { FullPageLoader } from "../../components/layout/Loaders"; -import { Text } from "@radix-ui/themes"; +import { Box, Button, Flex, IconButton, Text, TextField } from "@radix-ui/themes"; import { FrappeError } from "frappe-react-sdk"; +import { Loader } from "@/components/common/Loader"; +import { ErrorText, Label } from "@/components/common/Form"; type Inputs = { email: string; @@ -18,10 +19,11 @@ type Inputs = { export const Component = () => { const [error, setError] = useState(null) const { login, isLoading } = useContext(UserContext) - const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm(); - const { isOpen, onToggle } = useDisclosure(); + const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm() + const [isOpen, setIsOpen] = useState(false) + const onClickReveal = () => { - onToggle() + setIsOpen(!isOpen) } async function onSubmit(values: Inputs) { @@ -31,59 +33,46 @@ export const Component = () => { } return ( - - + + {isLoading ? : - - - - - - raven - - + + - - - - - Email address - isEmailValid(email) || "Please enter a valid email address.", - required: "Email is required." - })} - name="email" - type="email" - autoComplete="email" - required - placeholder="e.g. example@gmail.com" - tabIndex={0} /> - {errors?.email?.message} - - - Password - - - : } - onClick={onClickReveal} - tabIndex={-1} /> - - + + raven + + + + + +
+ + + + + + + isEmailValid(email) || "Please enter a valid email address.", + required: "Email is required." + })} + name="email" + type="email" + autoComplete="email" + required + placeholder="e.g. example@gmail.com" + tabIndex={0} /> + + {errors?.email && {errors?.email?.message}} + + + + + + { autoComplete="current-password" required placeholder="***********" /> - - {errors?.password?.message} - - - - - - + + + {isOpen ? : } + + + + {errors?.password && {errors.password?.message}} + + + + + +
+
}
From 8b7f3a701c839a1c5d20b7007ff3d5944c022fe1 Mon Sep 17 00:00:00 2001 From: Janhvi Patil Date: Mon, 27 Nov 2023 19:16:31 +0530 Subject: [PATCH 092/147] fix: password reveal button type --- raven-app/src/pages/auth/Login.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/raven-app/src/pages/auth/Login.tsx b/raven-app/src/pages/auth/Login.tsx index 0223088c6..4e91db153 100644 --- a/raven-app/src/pages/auth/Login.tsx +++ b/raven-app/src/pages/auth/Login.tsx @@ -85,6 +85,7 @@ export const Component = () => { placeholder="***********" /> Date: Mon, 27 Nov 2023 19:29:30 +0530 Subject: [PATCH 093/147] wip: icons --- raven-app/package.json | 1 + raven-app/src/components/common/Toast/Toast.tsx | 6 +++--- .../rename-channel/EditChannelNameButton.tsx | 4 ++-- raven-app/src/hooks/useToast.tsx | 3 ++- raven-app/yarn.lock | 5 +++++ 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/raven-app/package.json b/raven-app/package.json index 236fd3f89..947f03217 100644 --- a/raven-app/package.json +++ b/raven-app/package.json @@ -45,6 +45,7 @@ "react-dropzone": "^14.2.3", "react-hook-form": "^7.43.1", "react-hotkeys-hook": "^4.3.5", + "react-icons": "^4.12.0", "react-idle-timer": "^5.5.3", "react-intersection-observer": "^9.5.2", "react-markdown": "^8.0.5", diff --git a/raven-app/src/components/common/Toast/Toast.tsx b/raven-app/src/components/common/Toast/Toast.tsx index 2da85ac27..d583837b6 100644 --- a/raven-app/src/components/common/Toast/Toast.tsx +++ b/raven-app/src/components/common/Toast/Toast.tsx @@ -1,7 +1,7 @@ import * as React from "react" import * as ToastPrimitives from "@radix-ui/react-toast" import { cva, type VariantProps } from "cva" -import { X } from "lucide-react" +import { BiX } from "react-icons/bi" import { clsx } from "clsx" const ToastProvider = ToastPrimitives.Provider @@ -76,13 +76,13 @@ const ToastClose = React.forwardRef< - + )) ToastClose.displayName = ToastPrimitives.Close.displayName diff --git a/raven-app/src/components/feature/channel-details/rename-channel/EditChannelNameButton.tsx b/raven-app/src/components/feature/channel-details/rename-channel/EditChannelNameButton.tsx index 23710ca30..59c3663f0 100644 --- a/raven-app/src/components/feature/channel-details/rename-channel/EditChannelNameButton.tsx +++ b/raven-app/src/components/feature/channel-details/rename-channel/EditChannelNameButton.tsx @@ -1,7 +1,7 @@ import { RenameChannelModalContent } from '@/components/feature/channel-details/rename-channel/ChannelRenameModal' import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { Dialog, IconButton } from '@radix-ui/themes' -import { PenSquare } from 'lucide-react' +import { BiEdit } from 'react-icons/bi' import { IconButtonProps } from '@radix-ui/themes/dist/cjs/components/icon-button' import { useState } from "react" import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' @@ -29,7 +29,7 @@ export const EditChannelNameButton = ({ channelID, channel_name, channelType, .. aria-label="Click to edit channel name" title='Edit channel name' {...props}> - + diff --git a/raven-app/src/hooks/useToast.tsx b/raven-app/src/hooks/useToast.tsx index 800a515f6..7e8a65905 100644 --- a/raven-app/src/hooks/useToast.tsx +++ b/raven-app/src/hooks/useToast.tsx @@ -6,7 +6,7 @@ import type { } from "@/components/common/Toast/Toast" const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1500 +const TOAST_REMOVE_DELAY = 1000 type ToasterToast = ToastProps & { id: string @@ -152,6 +152,7 @@ function toast({ ...props }: Toast) { dispatch({ type: "ADD_TOAST", toast: { + duration: TOAST_REMOVE_DELAY, ...props, id, open: true, diff --git a/raven-app/yarn.lock b/raven-app/yarn.lock index a76d3ed80..a16fa5a2f 100644 --- a/raven-app/yarn.lock +++ b/raven-app/yarn.lock @@ -4431,6 +4431,11 @@ react-hotkeys-hook@^4.3.5: resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz#1f7a7a1c9c21d4fa3280bf340fcca8fd77d81994" integrity sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw== +react-icons@^4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.12.0.tgz#54806159a966961bfd5cdb26e492f4dafd6a8d78" + integrity sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw== + react-idle-timer@^5.5.3: version "5.7.2" resolved "https://registry.yarnpkg.com/react-idle-timer/-/react-idle-timer-5.7.2.tgz#f506db28a86645dd1b87987116501703e512142b" From 45303e3e111fc3dc16e1b3f5a6a1ad1ddf15b547 Mon Sep 17 00:00:00 2001 From: Janhvi Patil Date: Tue, 28 Nov 2023 13:13:52 +0530 Subject: [PATCH 094/147] WIP - change icons to BoxIcons --- .../ChannelMemberDetails.tsx | 10 ++++----- .../add-members/AddMembersButton.tsx | 6 ++--- .../archive-channel/ArchiveChannelButton.tsx | 4 ++-- .../ChangeChannelTypeButton.tsx | 4 ++-- .../delete-channel/DeleteChannelButton.tsx | 4 ++-- .../FilesSharedInChannel.tsx | 4 ++-- .../feature/channels/CreateChannelModal.tsx | 12 +++++----- .../feature/chat-header/SearchButton.tsx | 4 ++-- .../chat-header/ViewChannelDetailsButton.tsx | 4 ++-- .../chat/ChatInput/RightToolbarButtons.tsx | 12 +++++----- .../chat/ChatInput/TextFormattingMenu.tsx | 22 +++++++++---------- .../feature/chat/chat-message/FileMessage.tsx | 4 ++-- .../command-palette/CommandPalette.tsx | 4 ++-- .../command-palette/CommandPaletteActions.tsx | 19 ++++++++-------- .../feature/file-preview/FilePreviewModal.tsx | 4 ++-- .../feature/file-upload/FileListItem.tsx | 4 ++-- .../feature/global-search/ChannelSearch.tsx | 10 ++++----- .../feature/global-search/FileSearch.tsx | 6 ++--- .../feature/global-search/MessageSearch.tsx | 6 ++--- .../markdown-viewer/MarkdownRenderer.tsx | 4 ++-- .../message-action-palette/BookmarkButton.tsx | 4 ++-- .../DeleteMessageButton.tsx | 4 ++-- .../message-action-palette/DownloadButton.tsx | 5 ++--- .../EditMessageButton.tsx | 4 ++-- .../EmojiPickerButton.tsx | 4 ++-- .../message-action-palette/ReplyButton.tsx | 4 ++-- .../feature/pagination/PageSelector.tsx | 6 ++--- .../raven-users/AddRavenUsersContent.tsx | 5 ++--- .../feature/saved-messages/SavedMessages.tsx | 15 +++++++------ .../src/components/feature/sorting/Sort.tsx | 4 ++-- .../layout/EmptyState/EmptyState.tsx | 6 ++--- .../components/layout/Sidebar/SidebarBody.tsx | 4 ++-- .../components/layout/Sidebar/SidebarComp.tsx | 4 ++-- .../layout/Sidebar/SidebarFooter.tsx | 4 ++-- .../layout/Sidebar/SidebarHeader.tsx | 4 ++-- raven-app/src/pages/auth/Login.tsx | 4 ++-- raven-app/src/utils/layout/channelIcon.tsx | 16 +++++++------- .../src/utils/layout/fileExtensionIcon.tsx | 4 ++-- raven-app/src/utils/layout/keyboardKey.tsx | 3 ++- types/Search/Search.ts | 2 +- 40 files changed, 127 insertions(+), 126 deletions(-) diff --git a/raven-app/src/components/feature/channel-member-details/ChannelMemberDetails.tsx b/raven-app/src/components/feature/channel-member-details/ChannelMemberDetails.tsx index 2d2d8c14b..3eb852ade 100644 --- a/raven-app/src/components/feature/channel-member-details/ChannelMemberDetails.tsx +++ b/raven-app/src/components/feature/channel-member-details/ChannelMemberDetails.tsx @@ -6,7 +6,7 @@ import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider" import { AddMembersButton } from "./add-members/AddMembersButton" import { RemoveMemberButton } from "./remove-members/RemoveMemberButton" import { Box, Flex, TextField, Text } from "@radix-ui/themes" -import { Search, Circle, Crown } from "lucide-react" +import { BiSearch, BiCircle, BiSolidCrown } from "react-icons/bi" import { UserAvatar } from "@/components/common/UserAvatar" interface MemberDetailsProps { channelData: ChannelListItem, @@ -39,7 +39,7 @@ export const ChannelMemberDetails = ({ channelData, channelMembers, activeUsers,
- + @@ -73,14 +73,14 @@ export const ChannelMemberDetails = ({ channelData, channelMembers, activeUsers, {member.first_name} {activeUsers.includes(member.name) ? ( - + ) : ( - + )} {member.full_name} {member.name === currentUser && (You)} - {channelMembers[member.name].is_admin == 1 && } + {channelMembers[member.name].is_admin == 1 && } diff --git a/raven-app/src/components/feature/channel-member-details/add-members/AddMembersButton.tsx b/raven-app/src/components/feature/channel-member-details/add-members/AddMembersButton.tsx index 5dda3966a..342bd2465 100644 --- a/raven-app/src/components/feature/channel-member-details/add-members/AddMembersButton.tsx +++ b/raven-app/src/components/feature/channel-member-details/add-members/AddMembersButton.tsx @@ -5,7 +5,7 @@ import { Button, Dialog, IconButton } from "@radix-ui/themes" import { ButtonProps } from "@radix-ui/themes/dist/cjs/components/button" import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider" import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog" -import { UserPlus } from 'lucide-react' +import { BiUserPlus } from "react-icons/bi" interface AddMembersButtonProps extends ButtonProps { channelData: ChannelListItem, @@ -26,11 +26,11 @@ export const AddMembersButton = ({ channelData, updateMembers, isIconButton = fa {isIconButton ? - + : + Add members } diff --git a/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelButton.tsx b/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelButton.tsx index 9b3116ed9..ccd2d5a6d 100644 --- a/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelButton.tsx +++ b/raven-app/src/components/feature/channel-settings/archive-channel/ArchiveChannelButton.tsx @@ -1,4 +1,4 @@ -import { Archive } from "lucide-react"; +import { BiBox } from "react-icons/bi"; import { ArchiveChannelModal } from "./ArchiveChannelModal"; import { ChannelListItem } from "@/utils/channel/ChannelListProvider"; import { useState } from "react"; @@ -21,7 +21,7 @@ export const ArchiveChannelButton = ({ onClose: onCloseParent, channelData }: Ar diff --git a/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx b/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx index 1c2e0cc51..1dde9a65e 100644 --- a/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx +++ b/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx @@ -1,5 +1,5 @@ import { ChannelListItem } from '@/utils/channel/ChannelListProvider' -import { Hash, Lock } from 'lucide-react' +import { BiHash, BiLockAlt } from 'react-icons/bi' import { ChangeChannelTypeModal } from './ChangeChannelTypeModal' import { useState } from 'react' import { Button, Dialog } from '@radix-ui/themes' @@ -19,7 +19,7 @@ export const ChangeChannelTypeButton = ({ channelData }: ChangeChannelTypeButton diff --git a/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelButton.tsx b/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelButton.tsx index f1542bc43..ca211bfcb 100644 --- a/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelButton.tsx +++ b/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelButton.tsx @@ -1,6 +1,6 @@ import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { DeleteChannelModal } from './DeleteChannelModal' -import { Trash2 } from 'lucide-react' +import { BiTrash } from 'react-icons/bi' import { useState } from 'react' import { AlertDialog, Button } from '@radix-ui/themes' import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' @@ -21,7 +21,7 @@ export const DeleteChannelButton = ({ onClose: onCloseParent, channelData }: Del diff --git a/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx b/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx index e05668c5d..4afc9198f 100644 --- a/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx +++ b/raven-app/src/components/feature/channel-shared-files/FilesSharedInChannel.tsx @@ -3,7 +3,7 @@ import { useFrappeGetCall } from "frappe-react-sdk"; import { useParams } from "react-router-dom"; import { ErrorBanner } from "../../layout/AlertBanner"; import { getFileExtension, getFileName } from "../../../utils/operations"; -import { Download } from "lucide-react"; +import { BiDownload } from "react-icons/bi"; import { FileMessage } from "../../../../../types/Messaging/Message"; import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider"; import { Box, Flex, IconButton, Link, Text } from "@radix-ui/themes"; @@ -56,7 +56,7 @@ export const FilesSharedInChannel = ({ channelMembers }: FilesSharedInChannelPro size='1' color='gray' variant='ghost'> - + diff --git a/raven-app/src/components/feature/channels/CreateChannelModal.tsx b/raven-app/src/components/feature/channels/CreateChannelModal.tsx index c80d149bd..2a579bcf5 100644 --- a/raven-app/src/components/feature/channels/CreateChannelModal.tsx +++ b/raven-app/src/components/feature/channels/CreateChannelModal.tsx @@ -1,11 +1,11 @@ import { useFrappeCreateDoc } from 'frappe-react-sdk' import { ChangeEvent, useCallback, useMemo, useState } from 'react' import { Controller, FormProvider, useForm } from 'react-hook-form' -import { Globe, Hash, Lock } from 'lucide-react' +import { BiGlobe, BiHash, BiLockAlt } from 'react-icons/bi' import { useNavigate } from 'react-router-dom' import { ErrorBanner } from '../../layout/AlertBanner' import { Box, Button, Dialog, Flex, IconButton, RadioGroup, Text, TextArea, TextField } from '@radix-ui/themes' -import { Plus } from 'lucide-react' +import { BiPlus } from 'react-icons/bi' import { ErrorText, HelperText, Label } from '@/components/common/Form' import { Loader } from '@/components/common/Loader' import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' @@ -78,19 +78,19 @@ export const CreateChannelButton = ({ updateChannelList }: { updateChannelList: switch (channelType) { case 'Private': return { - channelIcon: , + channelIcon: , header: 'Create a private channel', helperText: 'When a channel is set to private, it can only be viewed or joined by invitation.' } case 'Open': return { - channelIcon: , + channelIcon: , header: 'Create an open channel', helperText: 'When a channel is set to open, everyone is a member.' } default: return { - channelIcon: , + channelIcon: , header: 'Create a public channel', helperText: 'When a channel is set to public, anyone can join the channel and read messages, but only members can post messages.' } @@ -100,7 +100,7 @@ export const CreateChannelButton = ({ updateChannelList }: { updateChannelList: return - + diff --git a/raven-app/src/components/feature/chat-header/SearchButton.tsx b/raven-app/src/components/feature/chat-header/SearchButton.tsx index 12c930e03..54db68f84 100644 --- a/raven-app/src/components/feature/chat-header/SearchButton.tsx +++ b/raven-app/src/components/feature/chat-header/SearchButton.tsx @@ -1,4 +1,4 @@ -import { Search } from 'lucide-react' +import { BiSearch } from 'react-icons/bi' import { CommandPalette } from '../command-palette' import { useState } from 'react' import { Button, Dialog, Kbd, Tooltip } from '@radix-ui/themes' @@ -24,7 +24,7 @@ export const SearchButton = () => { size='2' variant='soft' aria-label="search"> - + Search {/*  K */} diff --git a/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx b/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx index d5e3673d4..53149662e 100644 --- a/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx +++ b/raven-app/src/components/feature/chat-header/ViewChannelDetailsButton.tsx @@ -5,7 +5,7 @@ import { ChannelListItem } from "@/utils/channel/ChannelListProvider" import { ChannelMembers } from "@/utils/channel/ChannelMembersProvider" import { Button, Dialog, Tooltip } from "@radix-ui/themes" import { UserAvatar } from "@/components/common/UserAvatar" -import { User } from "lucide-react" +import { BiSolidUser } from "react-icons/bi" interface ViewChannelDetailsButtonProps { channelData: ChannelListItem, @@ -35,7 +35,7 @@ export const ViewChannelDetailsButton = ({ channelData, channelMembers, updateMe return })} {totalMembers > 3 &&
+ {totalMembers - 3}
} -
: } + : }
diff --git a/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx b/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx index eea739fc7..06c143806 100644 --- a/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx +++ b/raven-app/src/components/feature/chat/ChatInput/RightToolbarButtons.tsx @@ -1,5 +1,5 @@ import { useCurrentEditor } from '@tiptap/react' -import { AtSign, Hash, Smile, SendHorizontal, Paperclip } from 'lucide-react' +import { BiAt, BiHash, BiSmile, BiPaperclip, BiSolidSend } from 'react-icons/bi' import { DEFAULT_BUTTON_STYLE, ICON_PROPS } from './ToolPanel' import { ToolbarFileProps } from './Tiptap' import { Flex, IconButton, Inset, Popover, Separator } from '@radix-ui/themes' @@ -59,7 +59,7 @@ const MentionButtons = () => { .insertContent('#') .run() || !editor.isEditable }> - + editor.chain().focus().insertContent('@').run()} @@ -75,7 +75,7 @@ const MentionButtons = () => { .insertContent('@') .run() || !editor.isEditable }> - + } @@ -97,7 +97,7 @@ const EmojiPickerButton = () => { title='Add emoji' disabled={!editor.can().chain().focus().insertContent('😅').run() || !editor.isEditable} aria-label={"add emoji"}> - + @@ -126,7 +126,7 @@ const FilePickerButton = ({ fileProps }: { fileProps: ToolbarFileProps }) => { disabled={editor?.isEditable === false} title='Attach file' aria-label={"attach file"}> - + } @@ -168,7 +168,7 @@ const SendButton = ({ sendMessage, messageSending }: { onClick={onClick} > {messageSending ? : - + } } \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx b/raven-app/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx index 6cf0c5ae3..9db1873f6 100644 --- a/raven-app/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx +++ b/raven-app/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx @@ -1,5 +1,5 @@ import { useCurrentEditor } from '@tiptap/react' -import { Bold, Code2, Highlighter, Italic, Link, ListOrdered, List, Strikethrough, Underline, TextQuote } from 'lucide-react' +import { BiBold, BiCodeAlt, BiHighlight, BiItalic, BiLink, BiListOl, BiListUl, BiStrikethrough, BiUnderline, BiSolidQuoteAltRight } from 'react-icons/bi' import { DEFAULT_BUTTON_STYLE, ICON_PROPS } from './ToolPanel' import { Box, Flex, IconButton, Separator } from '@radix-ui/themes' @@ -29,7 +29,7 @@ export const TextFormattingMenu = () => { .toggleBold() .run() }> - + { .toggleItalic() .run() }> - + editor.chain().focus().toggleUnderline().run()} @@ -63,7 +63,7 @@ export const TextFormattingMenu = () => { .run() } > - + @@ -86,7 +86,7 @@ export const TextFormattingMenu = () => { .run() } > - + editor.chain().focus().toggleStrike().run()} @@ -103,7 +103,7 @@ export const TextFormattingMenu = () => { .run() } > - + @@ -123,7 +123,7 @@ export const TextFormattingMenu = () => { .run() } > - + @@ -143,7 +143,7 @@ export const TextFormattingMenu = () => { .run() } > - + editor.chain().focus().liftEmptyBlock().toggleBulletList().run()} @@ -160,7 +160,7 @@ export const TextFormattingMenu = () => { .run() } > - + @@ -181,9 +181,9 @@ export const TextFormattingMenu = () => { .run() } > - + - + ) } \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx b/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx index 457e0e514..2e6660b56 100644 --- a/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx +++ b/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx @@ -3,7 +3,7 @@ import { FileExtensionIcon } from "../../../../utils/layout/FileExtensionIcon" import { FileMessage } from "../../../../../../types/Messaging/Message" import { useCallback } from "react" import { getFileExtension, getFileName } from "../../../../utils/operations" -import { ChevronDown, ChevronRight } from "lucide-react" +import { BiChevronDown, BiChevronRight } from "react-icons/bi" import { useBoolean } from "@/hooks/useBoolean" interface FileMessageProps extends Partial { @@ -49,7 +49,7 @@ export const FileMessageBlock = ({ file, owner, creation, message_type, onFilePr {{getFileName(file)}} : } /> + icon={showImage ? : } /> ))} - {!activePage && } + {!activePage && }
diff --git a/raven-app/src/components/feature/sorting/Sort.tsx b/raven-app/src/components/feature/sorting/Sort.tsx index f73fbe7dc..dafd9262f 100644 --- a/raven-app/src/components/feature/sorting/Sort.tsx +++ b/raven-app/src/components/feature/sorting/Sort.tsx @@ -1,5 +1,5 @@ import { IconButton } from "@radix-ui/themes" -import { ArrowDownWideNarrow, ArrowUpNarrowWide } from "lucide-react" +import { BiDownArrowAlt, BiUpArrowAlt } from "react-icons/bi" export interface SortProps { sortOrder: string, @@ -28,7 +28,7 @@ export const Sort = ({ sortOrder, onSortOrderChange }: SortProps) => { title={sortOrder === "asc" ? "newest first" : "oldest first"} onClick={handleSortOrder} aria-label={sortOrder === "asc" ? "click to sort by newest first" : "click to sort by oldest first"}> - {sortOrder === "asc" ? : } + {sortOrder === "asc" ? : } ) } \ No newline at end of file diff --git a/raven-app/src/components/layout/EmptyState/EmptyState.tsx b/raven-app/src/components/layout/EmptyState/EmptyState.tsx index b5d34dd06..b295700d6 100644 --- a/raven-app/src/components/layout/EmptyState/EmptyState.tsx +++ b/raven-app/src/components/layout/EmptyState/EmptyState.tsx @@ -9,7 +9,7 @@ import { useGetUserRecords } from "@/hooks/useGetUserRecords" import { Box, Button, Flex, Heading, Link, Text } from "@radix-ui/themes" import { UserAvatar } from "@/components/common/UserAvatar" import { ChannelIcon } from "@/utils/layout/channelIcon" -import { Bookmark } from "lucide-react" +import { BiBookmark } from "react-icons/bi" import { DateMonthYear } from "@/utils/dateConversions" export const EmptyStateForSearch = () => { @@ -105,14 +105,14 @@ export const EmptyStateForSavedMessages = () => { return ( - + Your saved messages will appear here Saved messages are a convenient way to keep track of important information or messages you want to refer back to later. You can save messages by simply clicking on the bookmark icon - + in message actions. diff --git a/raven-app/src/components/layout/Sidebar/SidebarBody.tsx b/raven-app/src/components/layout/Sidebar/SidebarBody.tsx index 29af6d66e..174b08100 100644 --- a/raven-app/src/components/layout/Sidebar/SidebarBody.tsx +++ b/raven-app/src/components/layout/Sidebar/SidebarBody.tsx @@ -1,4 +1,4 @@ -import { Bookmark } from 'lucide-react' +import { BiBookmark } from 'react-icons/bi' import { ChannelList } from '../../feature/channels/ChannelList' import { DirectMessageList } from '../../feature/direct-messages/DirectMessageList' import { SidebarItem, SidebarGroupLabel } from './SidebarComp' @@ -22,7 +22,7 @@ export const SidebarBody = () => { - + Saved Messages diff --git a/raven-app/src/components/layout/Sidebar/SidebarComp.tsx b/raven-app/src/components/layout/Sidebar/SidebarComp.tsx index c0075448b..736c448bf 100644 --- a/raven-app/src/components/layout/Sidebar/SidebarComp.tsx +++ b/raven-app/src/components/layout/Sidebar/SidebarComp.tsx @@ -6,7 +6,7 @@ import { TextProps } from '@radix-ui/themes/dist/cjs/components/text'; import { BoxProps } from '@radix-ui/themes/dist/cjs/components/box'; import { IconButtonProps } from '@radix-ui/themes/dist/cjs/components/icon-button'; import { BadgeProps } from '@radix-ui/themes/dist/cjs/components/badge'; -import { ChevronDown, ChevronRight } from 'lucide-react'; +import { BiCaretDown, BiCaretRight } from 'react-icons/bi'; interface SidebarGroupProps extends FlexProps { children: ReactNode; @@ -158,7 +158,7 @@ export const SidebarViewMoreButton = ({ onClick, ...props }: SidebarViewMoreButt }} {...props} > - {isViewMore ? : } + {isViewMore ? : } ) } diff --git a/raven-app/src/components/layout/Sidebar/SidebarFooter.tsx b/raven-app/src/components/layout/Sidebar/SidebarFooter.tsx index 700086e80..042cd58d9 100644 --- a/raven-app/src/components/layout/Sidebar/SidebarFooter.tsx +++ b/raven-app/src/components/layout/Sidebar/SidebarFooter.tsx @@ -3,7 +3,7 @@ import { UserContext } from '../../../utils/auth/UserProvider' import { useUserData } from '@/hooks/useUserData' import { AddRavenUsers } from '@/components/feature/raven-users/AddRavenUsers' import { DropdownMenu, Flex, IconButton, Link, Separator, Text } from '@radix-ui/themes' -import { MoreHorizontal } from 'lucide-react' +import { BiDotsHorizontalRounded } from 'react-icons/bi' import { UserAvatar } from '@/components/common/UserAvatar' export const SidebarFooter = () => { @@ -27,7 +27,7 @@ export const SidebarFooter = () => { - + diff --git a/raven-app/src/components/layout/Sidebar/SidebarHeader.tsx b/raven-app/src/components/layout/Sidebar/SidebarHeader.tsx index e0e9cc262..3e58de0b8 100644 --- a/raven-app/src/components/layout/Sidebar/SidebarHeader.tsx +++ b/raven-app/src/components/layout/Sidebar/SidebarHeader.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@/ThemeProvider' import { Flex, IconButton, Text } from '@radix-ui/themes' -import { Moon, Sun } from 'lucide-react' +import { BiMoon, BiSun } from 'react-icons/bi' export const SidebarHeader = () => { return ( @@ -23,7 +23,7 @@ const ColorModeToggleButton = () => { color='gray' variant='ghost' onClick={toggleTheme}> - {appearance === 'light' ? : } + {appearance === 'light' ? : } } \ No newline at end of file diff --git a/raven-app/src/pages/auth/Login.tsx b/raven-app/src/pages/auth/Login.tsx index 4e91db153..dd4c50cc4 100644 --- a/raven-app/src/pages/auth/Login.tsx +++ b/raven-app/src/pages/auth/Login.tsx @@ -1,6 +1,6 @@ import { useState, useContext } from "react"; import { useForm } from "react-hook-form"; -import { Eye, EyeOff } from "lucide-react"; +import { BiShow, BiHide } from "react-icons/bi"; import { Link } from "react-router-dom"; import { ErrorBanner } from "../../components/layout/AlertBanner"; import { UserContext } from "../../utils/auth/UserProvider"; @@ -91,7 +91,7 @@ export const Component = () => { aria-label={isOpen ? "Mask password" : "Reveal password"} onClick={onClickReveal} tabIndex={-1}> - {isOpen ? : } + {isOpen ? : } diff --git a/raven-app/src/utils/layout/channelIcon.tsx b/raven-app/src/utils/layout/channelIcon.tsx index e36385b80..c9f8cb361 100644 --- a/raven-app/src/utils/layout/channelIcon.tsx +++ b/raven-app/src/utils/layout/channelIcon.tsx @@ -1,16 +1,16 @@ -import { Globe, Hash, Lock, LucideProps } from "lucide-react"; +import { BiGlobe, BiHash, BiLockAlt } from 'react-icons/bi'; import { RavenChannel } from "../../../../types/RavenChannelManagement/RavenChannel"; export const getChannelIcon = (type: RavenChannel['type']) => { switch (type) { - case 'Private': return Lock - case 'Open': return Globe - default: return Hash + case 'Private': return BiLockAlt + case 'Open': return BiGlobe + default: return BiHash } } -interface ChannelIconProps extends LucideProps { +interface ChannelIconProps { type: RavenChannel['type'] } @@ -18,8 +18,8 @@ export const ChannelIcon = ({ type, ...props }: ChannelIconProps) => { if (!type) return null - if (type === 'Private') return - if (type === 'Open') return - return + if (type === 'Private') return + if (type === 'Open') return + return } diff --git a/raven-app/src/utils/layout/fileExtensionIcon.tsx b/raven-app/src/utils/layout/fileExtensionIcon.tsx index a0c5a6250..4bd87319d 100644 --- a/raven-app/src/utils/layout/fileExtensionIcon.tsx +++ b/raven-app/src/utils/layout/fileExtensionIcon.tsx @@ -1,6 +1,6 @@ import { SVGAttributes, Suspense, lazy } from 'react' -import { File } from 'lucide-react' +import { BiSolidFile } from 'react-icons/bi' const Excel = lazy(() => import('@/assets/icons/excel.svg?react')) const Word = lazy(() => import('@/assets/icons/word.svg?react')) @@ -24,7 +24,7 @@ export const FileExtensionIcon = ({ ext, ...props }: FileExtensionIconProps) => const isImage = imageExt.includes(ext) const isWord = wordExt.includes(ext) const isPdf = ext === "pdf" - return }> + return }> {isExcel && } {isImage && } {isWord && } diff --git a/raven-app/src/utils/layout/keyboardKey.tsx b/raven-app/src/utils/layout/keyboardKey.tsx index d9836b8fe..0badc9cde 100644 --- a/raven-app/src/utils/layout/keyboardKey.tsx +++ b/raven-app/src/utils/layout/keyboardKey.tsx @@ -1,4 +1,5 @@ import { Command, ChevronUp, ArrowBigUpDash, LucideProps, Option, Delete, CornerDownLeft, Space, ArrowRightToLine, ArrowBigUp, ArrowBigDown, ArrowBigLeft, ArrowBigRight } from 'lucide-react' +import { BiSpaceBar } from 'react-icons/bi' export const getKeyboardMetaKeyString = () => { if (navigator.platform.toUpperCase().indexOf('MAC') >= 0) { @@ -49,7 +50,7 @@ export const KeyboardKeyIcon = ({ key, ...props }: KeyboardKeyIconProps) => { case 'delete': return case 'return': return case 'enter': return - case 'space': return + case 'space': return case 'tab': return case 'up': return case 'down': return diff --git a/types/Search/Search.ts b/types/Search/Search.ts index d1a90ffa8..c2e74654b 100644 --- a/types/Search/Search.ts +++ b/types/Search/Search.ts @@ -16,7 +16,7 @@ export interface GetMessageSearchResult { } export interface GetChannelSearchResult { - type: string + type: "Private" | "Public" | "Open" name: string channel_name: string is_archived: 1 | 0 From ed0a3ca0ca2d58541d556027e07c1236d6580e0f Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Sat, 2 Dec 2023 20:44:15 +0530 Subject: [PATCH 095/147] feat: new message renderer using Tiptap (#559) * fix: colors * fix: removed lucide * fix: styles in header * wip: message block * feat: message reactions panel created * fix: moved rendering to separate component with context * fix: delete reactions permanently * feat: image file preview * fix: adjust image thumbnail width * wip: file message * feat: file message block * feat: text formatting rendering * feat: link previews * wip: message actions * feat: message context menu * feat: quick actions menu * fix: alignment of messages * feat: reply message block * fix: replied message fetch * feat: scroll to message on reply click --- .../chat-space/chat-view/MessageBlock.tsx | 4 +- raven-app/package.json | 5 +- raven-app/src/App.tsx | 2 +- raven-app/src/assets/icons/excel.svg | 1 - raven-app/src/assets/icons/file.svg | 1 - raven-app/src/assets/icons/image.svg | 1 - raven-app/src/assets/icons/pdf.svg | 1 - raven-app/src/assets/icons/word.svg | 1 - .../src/components/common/UserAvatar.tsx | 4 +- .../delete-channel/DeleteChannelModal.tsx | 4 +- .../feature/channels/ChannelList.tsx | 12 +- .../feature/chat-header/ChannelHeader.tsx | 6 +- .../feature/chat/ChatInput/Tiptap.tsx | 55 +++-- .../feature/chat/ChatInput/ToolPanel.tsx | 4 +- .../feature/chat/ChatInput/tiptap.styles.css | 18 +- .../ActionModals}/DeleteMessageModal.tsx | 26 +-- .../ActionModals}/EditMessageModal.tsx | 18 +- .../MessageActions/DeleteMessage.tsx | 43 ++++ .../MessageActions/EditMessage.tsx | 42 ++++ .../MessageActions/MessageActions.tsx | 153 ++++++++++++++ .../QuickActions}/EmojiPickerButton.tsx | 4 +- .../QuickActions/QuickActionButton.tsx | 20 ++ .../QuickActions/QuickActions.tsx | 104 ++++++++++ .../MessageActions/useMessageCopy.ts | 59 ++++++ .../feature/chat/ChatMessage/MessageItem.tsx | 196 ++++++++++++++++++ .../MessageReactions.tsx | 40 ++-- .../ChatMessage/Renderers/DateTooltip.tsx | 31 +++ .../ChatMessage/Renderers/FileMessage.tsx | 145 +++++++++++++ .../ChatMessage/Renderers/ImageMessage.tsx | 90 ++++++++ .../Renderers/TiptapRenderer/Blockquote.tsx | 19 ++ .../Renderers/TiptapRenderer/Bold.tsx | 14 ++ .../Renderers/TiptapRenderer/Italic.tsx | 13 ++ .../Renderers/TiptapRenderer/Link.tsx | 78 +++++++ .../Renderers/TiptapRenderer/List.tsx | 0 .../Renderers/TiptapRenderer/Mention.tsx | 75 +++++++ .../TiptapRenderer/TiptapRenderer.tsx | 97 +++++++++ .../Renderers/TiptapRenderer/Underline.tsx | 15 ++ .../ReplyMessageBox/ReplyMessageBox.tsx | 77 +++++++ .../ChatBoxBody.tsx | 21 +- .../feature/chat/ChatStream/ChatHistory.tsx | 150 ++++++++++++++ .../feature/chat/chat-history/ChatHistory.tsx | 87 -------- .../chat/chat-message/ChatMessageBox.tsx | 81 -------- .../feature/chat/chat-message/DateTooltip.tsx | 32 --- .../feature/chat/chat-message/FileMessage.tsx | 64 ------ .../chat/chat-message/UserNameInMessage.tsx | 32 --- .../feature/chat/chat-space/ChannelSpace.tsx | 2 +- .../chat/chat-space/DirectMessageSpace.tsx | 2 +- .../src/components/feature/chat/index.ts | 4 +- .../chat/message-reply/PreviousMessageBox.tsx | 162 --------------- .../command-palette/CommandPaletteActions.tsx | 11 +- .../direct-messages/DirectMessageList.tsx | 4 +- .../feature/file-preview/FilePreviewModal.tsx | 55 ----- .../feature/global-search/FileSearch.tsx | 6 - .../feature/global-search/MessageBox.tsx | 7 +- .../message-action-palette/ActionsPalette.tsx | 66 ------ .../message-action-palette/BookmarkButton.tsx | 40 ---- .../DeleteMessageButton.tsx | 31 --- .../message-action-palette/DownloadButton.tsx | 18 -- .../EditMessageButton.tsx | 37 ---- .../message-action-palette/EmojiButton.tsx | 16 -- .../message-action-palette/ReplyButton.tsx | 28 --- .../layout/AlertBanner/ErrorBanner.tsx | 12 +- .../layout/EmptyState/EmptyState.tsx | 25 +-- .../components/layout/Heading/PageHeader.tsx | 3 +- .../components/layout/Sidebar/SidebarBody.tsx | 22 +- .../components/layout/Sidebar/SidebarComp.tsx | 21 +- .../layout/Sidebar/SidebarFooter.tsx | 11 +- .../layout/Sidebar/SidebarHeader.tsx | 16 +- raven-app/src/index.css | 105 +++++++++- raven-app/src/pages/ChatSpace.tsx | 6 +- raven-app/src/pages/MainPage.tsx | 6 +- raven-app/src/utils/layout/animations.ts | 7 - raven-app/src/utils/layout/channelIcon.tsx | 3 +- .../src/utils/layout/fileExtensionIcon.tsx | 40 ++-- raven-app/src/utils/layout/keyboardKey.tsx | 41 ++-- raven-app/src/utils/operations.ts | 15 +- raven-app/yarn.lock | 22 +- raven/api/reactions.py | 2 +- raven/api/upload_file.py | 2 +- types/Messaging/Message.ts | 15 +- 80 files changed, 1809 insertions(+), 999 deletions(-) delete mode 100644 raven-app/src/assets/icons/excel.svg delete mode 100644 raven-app/src/assets/icons/file.svg delete mode 100644 raven-app/src/assets/icons/image.svg delete mode 100644 raven-app/src/assets/icons/pdf.svg delete mode 100644 raven-app/src/assets/icons/word.svg rename raven-app/src/components/feature/{message-action-palette => chat/ChatMessage/ActionModals}/DeleteMessageModal.tsx (71%) rename raven-app/src/components/feature/{message-action-palette => chat/ChatMessage/ActionModals}/EditMessageModal.tsx (78%) create mode 100644 raven-app/src/components/feature/chat/ChatMessage/MessageActions/DeleteMessage.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/MessageActions/EditMessage.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx rename raven-app/src/components/feature/{message-action-palette => chat/ChatMessage/MessageActions/QuickActions}/EmojiPickerButton.tsx (91%) create mode 100644 raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActionButton.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActions.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/MessageActions/useMessageCopy.ts create mode 100644 raven-app/src/components/feature/chat/ChatMessage/MessageItem.tsx rename raven-app/src/components/feature/chat/{chat-message => ChatMessage}/MessageReactions.tsx (68%) create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/DateTooltip.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/FileMessage.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/ImageMessage.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Blockquote.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Bold.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Italic.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Link.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/List.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Mention.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Underline.tsx create mode 100644 raven-app/src/components/feature/chat/ChatMessage/ReplyMessageBox/ReplyMessageBox.tsx rename raven-app/src/components/feature/chat/{chat-history => ChatStream}/ChatBoxBody.tsx (90%) create mode 100644 raven-app/src/components/feature/chat/ChatStream/ChatHistory.tsx delete mode 100644 raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx delete mode 100644 raven-app/src/components/feature/chat/chat-message/ChatMessageBox.tsx delete mode 100644 raven-app/src/components/feature/chat/chat-message/DateTooltip.tsx delete mode 100644 raven-app/src/components/feature/chat/chat-message/FileMessage.tsx delete mode 100644 raven-app/src/components/feature/chat/chat-message/UserNameInMessage.tsx delete mode 100644 raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx delete mode 100644 raven-app/src/components/feature/file-preview/FilePreviewModal.tsx delete mode 100644 raven-app/src/components/feature/message-action-palette/ActionsPalette.tsx delete mode 100644 raven-app/src/components/feature/message-action-palette/BookmarkButton.tsx delete mode 100644 raven-app/src/components/feature/message-action-palette/DeleteMessageButton.tsx delete mode 100644 raven-app/src/components/feature/message-action-palette/DownloadButton.tsx delete mode 100644 raven-app/src/components/feature/message-action-palette/EditMessageButton.tsx delete mode 100644 raven-app/src/components/feature/message-action-palette/EmojiButton.tsx delete mode 100644 raven-app/src/components/feature/message-action-palette/ReplyButton.tsx delete mode 100644 raven-app/src/utils/layout/animations.ts diff --git a/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx b/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx index a99488eb5..b4a57d9f5 100644 --- a/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx +++ b/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx @@ -1,5 +1,5 @@ import React, { memo, useContext, useMemo } from 'react' -import { FileMessage, Message, MessageBlock, TextMessage } from '../../../../../../types/Messaging/Message' +import { FileMessage, ImageMessage, Message, MessageBlock, TextMessage } from '../../../../../../types/Messaging/Message' import { ChannelMembersMap } from '../ChatInterface' import { IonIcon, IonItem, IonSkeletonText, IonText } from '@ionic/react' import { SquareAvatar, UserAvatar } from '@/components/common/UserAvatar' @@ -101,7 +101,7 @@ const options = { triggerOnce: true }; -const ImageMessageBlock = ({ message }: { message: FileMessage }) => { +const ImageMessageBlock = ({ message }: { message: ImageMessage }) => { const { ref, inView } = useInView(options); const height = `${message.thumbnail_height}px` diff --git a/raven-app/package.json b/raven-app/package.json index 947f03217..7184da5d8 100644 --- a/raven-app/package.json +++ b/raven-app/package.json @@ -38,7 +38,6 @@ "highlight.js": "^11.9.0", "js-cookie": "^3.0.5", "lowlight": "^3.1.0", - "lucide-react": "^0.293.0", "moment-timezone": "^0.5.43", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -53,12 +52,14 @@ "react-virtuoso": "^4.3.8", "rehype-raw": "^6.1.1", "remark-gfm": "^3.0.1", - "tippy.js": "^6.3.7" + "tippy.js": "^6.3.7", + "turndown": "^7.1.2" }, "devDependencies": { "@types/js-cookie": "^3.0.6", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", + "@types/turndown": "^5.0.4", "@vitejs/plugin-react": "^3.1.0", "autoprefixer": "^10.4.16", "tailwindcss": "^3.3.5", diff --git a/raven-app/src/App.tsx b/raven-app/src/App.tsx index 7cb107f28..dd20f3559 100644 --- a/raven-app/src/App.tsx +++ b/raven-app/src/App.tsx @@ -58,7 +58,7 @@ function App() { diff --git a/raven-app/src/assets/icons/excel.svg b/raven-app/src/assets/icons/excel.svg deleted file mode 100644 index d8aca46f1..000000000 --- a/raven-app/src/assets/icons/excel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/raven-app/src/assets/icons/file.svg b/raven-app/src/assets/icons/file.svg deleted file mode 100644 index 688cdc092..000000000 --- a/raven-app/src/assets/icons/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/raven-app/src/assets/icons/image.svg b/raven-app/src/assets/icons/image.svg deleted file mode 100644 index 111815ee4..000000000 --- a/raven-app/src/assets/icons/image.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/raven-app/src/assets/icons/pdf.svg b/raven-app/src/assets/icons/pdf.svg deleted file mode 100644 index 31556e151..000000000 --- a/raven-app/src/assets/icons/pdf.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/raven-app/src/assets/icons/word.svg b/raven-app/src/assets/icons/word.svg deleted file mode 100644 index 1d095463f..000000000 --- a/raven-app/src/assets/icons/word.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/raven-app/src/components/common/UserAvatar.tsx b/raven-app/src/components/common/UserAvatar.tsx index 847ac8674..5197565f5 100644 --- a/raven-app/src/components/common/UserAvatar.tsx +++ b/raven-app/src/components/common/UserAvatar.tsx @@ -48,8 +48,8 @@ export const UserAvatar = ({ src, alt, size = '1', radius = 'medium', isActive, } {isActive && - - + + } diff --git a/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx b/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx index fde7b6d2c..7f57c7d2b 100644 --- a/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx +++ b/raven-app/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx @@ -5,8 +5,8 @@ import { useFrappeDeleteDoc } from 'frappe-react-sdk' import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { AlertDialog, Button, Callout, Checkbox, Flex, Text } from '@radix-ui/themes' import { Loader } from '@/components/common/Loader' -import { AlertTriangle } from 'lucide-react' import { useToast } from '@/hooks/useToast' +import { FiAlertTriangle } from 'react-icons/fi' type DeleteChannelModalProps = { onClose: () => void, @@ -54,7 +54,7 @@ export const DeleteChannelModal = ({ onClose, onCloseParent, channelData }: Dele - + This action is permanent and cannot be undone. diff --git a/raven-app/src/components/feature/channels/ChannelList.tsx b/raven-app/src/components/feature/channels/ChannelList.tsx index f490b6417..ac1afded5 100644 --- a/raven-app/src/components/feature/channels/ChannelList.tsx +++ b/raven-app/src/components/feature/channels/ChannelList.tsx @@ -4,7 +4,7 @@ import { CreateChannelButton } from "./CreateChannelModal" import { useContext, useMemo, useState } from "react" import { ChannelListContext, ChannelListContextType, ChannelListItem, UnreadCountData } from "../../../utils/channel/ChannelListProvider" import { ChannelIcon } from "@/utils/layout/channelIcon" -import { Box, Flex, Text } from "@radix-ui/themes" +import { Flex, Text } from "@radix-ui/themes" export const ChannelList = ({ unread_count }: { unread_count?: UnreadCountData }) => { @@ -18,10 +18,10 @@ export const ChannelList = ({ unread_count }: { unread_count?: UnreadCountData } return ( - + - + Channels @@ -29,7 +29,7 @@ export const ChannelList = ({ unread_count }: { unread_count?: UnreadCountData } - + {showData && filteredChannels.map((channel) => - + - {channel.channel_name} + {channel.channel_name} diff --git a/raven-app/src/components/feature/chat-header/ChannelHeader.tsx b/raven-app/src/components/feature/chat-header/ChannelHeader.tsx index ec3e3f9e8..62a9b63d9 100644 --- a/raven-app/src/components/feature/chat-header/ChannelHeader.tsx +++ b/raven-app/src/components/feature/chat-header/ChannelHeader.tsx @@ -17,9 +17,9 @@ export const ChannelHeader = ({ channelData }: ChannelHeaderProps) => { return ( - - - {channelData.channel_name} + + + {channelData.channel_name} diff --git a/raven-app/src/components/feature/chat/ChatInput/Tiptap.tsx b/raven-app/src/components/feature/chat/ChatInput/Tiptap.tsx index 17dfb683d..b28697e60 100644 --- a/raven-app/src/components/feature/chat/ChatInput/Tiptap.tsx +++ b/raven-app/src/components/feature/chat/ChatInput/Tiptap.tsx @@ -25,7 +25,7 @@ import html from 'highlight.js/lib/languages/xml' import json from 'highlight.js/lib/languages/json' import python from 'highlight.js/lib/languages/python' import { Plugin } from 'prosemirror-state' -import { Card, Inset } from '@radix-ui/themes' +import { Box } from '@radix-ui/themes' const lowlight = createLowlight(common) lowlight.register('html', html) @@ -59,7 +59,7 @@ const COOL_PLACEHOLDERS = [ "Type a message..." ] -const UserMention = Mention.extend({ +export const UserMention = Mention.extend({ name: 'userMention', }) .configure({ @@ -69,7 +69,7 @@ const UserMention = Mention.extend({ } }) -const ChannelMention = Mention.extend({ +export const ChannelMention = Mention.extend({ name: 'channelMention', }) .configure({ @@ -84,7 +84,6 @@ const Tiptap = ({ slotAfter, slotBefore, fileProps, onMessageSend, messageSendin const { channels } = useContext(ChannelListContext) as ChannelListContextType - // this is a dummy extension only to create custom keydown behavior const KeyboardHandler = Extension.create({ name: 'keyboardHandler', @@ -239,6 +238,16 @@ const Tiptap = ({ slotAfter, slotBefore, fileProps, onMessageSend, messageSendin StarterKit.configure({ heading: false, codeBlock: false, + listItem: { + HTMLAttributes: { + class: 'rt-Text rt-r-size-2' + } + }, + paragraph: { + HTMLAttributes: { + class: 'rt-Text rt-r-size-2' + } + } }), UserMention.configure({ HTMLAttributes: { @@ -383,6 +392,9 @@ const Tiptap = ({ slotAfter, slotBefore, fileProps, onMessageSend, messageSendin Underline, Highlight.configure({ multicolor: true, + HTMLAttributes: { + class: 'bg-[var(--yellow-6)] dark:bg-[var(--yellow-11)] px-2 py-1' + } }), Link.configure({ protocols: ['mailto', 'https', 'http'] @@ -398,22 +410,25 @@ const Tiptap = ({ slotAfter, slotBefore, fileProps, onMessageSend, messageSendin ] return ( - - - - - - - - - - + + + + + + + + ) } diff --git a/raven-app/src/components/feature/chat/ChatInput/ToolPanel.tsx b/raven-app/src/components/feature/chat/ChatInput/ToolPanel.tsx index 4cd9b1951..52a0fa340 100644 --- a/raven-app/src/components/feature/chat/ChatInput/ToolPanel.tsx +++ b/raven-app/src/components/feature/chat/ChatInput/ToolPanel.tsx @@ -5,14 +5,14 @@ export const ICON_PROPS = { size: '18' } -export const DEFAULT_BUTTON_STYLE = 'bg-transparent text-[var(--gray-11)] hover:bg-[var(--accent-a3)] hover:text-[var(--accent-a11)]' +export const DEFAULT_BUTTON_STYLE = 'bg-transparent dark:text-[var(--gray-10)] text-[var(--gray-11)] hover:bg-[var(--accent-a3)] hover:text-[var(--accent-a11)]' export const ToolPanel = (props: FlexProps) => { return ( diff --git a/raven-app/src/components/feature/chat/ChatInput/tiptap.styles.css b/raven-app/src/components/feature/chat/ChatInput/tiptap.styles.css index 0fa8c342e..a91fab10c 100644 --- a/raven-app/src/components/feature/chat/ChatInput/tiptap.styles.css +++ b/raven-app/src/components/feature/chat/ChatInput/tiptap.styles.css @@ -1,4 +1,4 @@ -.tiptap p.is-editor-empty:first-child::before { +.tiptap-editor p.is-editor-empty:first-child::before { color: var(--gray-9); content: attr(data-placeholder); float: left; @@ -7,7 +7,7 @@ pointer-events: none; } -.tiptap.ProseMirror { +.tiptap-editor.ProseMirror { min-height: 60px; max-height: 150px; overflow: auto; @@ -16,28 +16,28 @@ border-top-right-radius: var(--radius-4); } -.tiptap.ProseMirror:focus { +.tiptap-editor.ProseMirror:focus { outline: 1.5px solid var(--color-focus-root); outline-offset: -1.5px; } -.tiptap blockquote { +.tiptap-editor blockquote { border-left: 3px solid var(--gray-11); padding-left: 0.8rem; margin: 1rem; } -.tiptap ul, -.tiptap ol { +.tiptap-editor ul, +.tiptap-editor ol { padding-left: var(--space-5); } -.tiptap a, -.tiptap .mention { +.tiptap-editor a, +.tiptap-editor .mention { color: var(--accent-a10); } -.tiptap a { +.tiptap-editor a { text-decoration: underline; cursor: pointer; } diff --git a/raven-app/src/components/feature/message-action-palette/DeleteMessageModal.tsx b/raven-app/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx similarity index 71% rename from raven-app/src/components/feature/message-action-palette/DeleteMessageModal.tsx rename to raven-app/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx index a216785f8..7b3d9f362 100644 --- a/raven-app/src/components/feature/message-action-palette/DeleteMessageModal.tsx +++ b/raven-app/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx @@ -1,38 +1,32 @@ import { useFrappeDeleteDoc, useSWRConfig } from "frappe-react-sdk" -import { ErrorBanner } from "../../layout/AlertBanner" -import { useParams } from "react-router-dom" +import { ErrorBanner } from "../../../../layout/AlertBanner" import { AlertDialog, Button, Callout, Flex, Text } from "@radix-ui/themes" import { Loader } from "@/components/common/Loader" -import { AlertTriangle } from "lucide-react" import { useToast } from "@/hooks/useToast" +import { FiAlertTriangle } from "react-icons/fi" +import { Message } from "../../../../../../../types/Messaging/Message" interface DeleteMessageModalProps { onClose: (refresh?: boolean) => void, - channelMessageID: string + message: Message } -export const DeleteMessageModal = ({ onClose, channelMessageID }: DeleteMessageModalProps) => { +export const DeleteMessageModal = ({ onClose, message }: DeleteMessageModalProps) => { const { deleteDoc, error, loading: deletingDoc } = useFrappeDeleteDoc() const { toast } = useToast() const { mutate } = useSWRConfig() - // const updateMessages = useCallback(() => { - // mutate(`get_messages_for_channel_${channelMessageID}`) - // }, [mutate, channelMessageID]) - - const { channelID } = useParams() - const onSubmit = async () => { - return deleteDoc('Raven Message', channelMessageID + return deleteDoc('Raven Message', message.name ).then(() => { toast({ title: 'Message deleted', duration: 1000, variant: 'destructive' }) - mutate(`get_messages_for_channel_${channelID}`) + mutate(`get_messages_for_channel_${message.channel_id}`) onClose() }) } @@ -46,15 +40,15 @@ export const DeleteMessageModal = ({ onClose, channelMessageID }: DeleteMessageM - + - + This action is permanent and cannot be undone. - Are you sure you want to delete this message? It will be deleted for all users. + Are you sure you want to delete this message? It will be deleted for all users. diff --git a/raven-app/src/components/feature/message-action-palette/EditMessageModal.tsx b/raven-app/src/components/feature/chat/ChatMessage/ActionModals/EditMessageModal.tsx similarity index 78% rename from raven-app/src/components/feature/message-action-palette/EditMessageModal.tsx rename to raven-app/src/components/feature/chat/ChatMessage/ActionModals/EditMessageModal.tsx index a7e6b30f8..1a2022d79 100644 --- a/raven-app/src/components/feature/message-action-palette/EditMessageModal.tsx +++ b/raven-app/src/components/feature/chat/ChatMessage/ActionModals/EditMessageModal.tsx @@ -1,20 +1,20 @@ import { useFrappeUpdateDoc, useSWRConfig } from "frappe-react-sdk" import { Suspense, lazy, useEffect } from "react" -import { ErrorBanner } from "../../layout/AlertBanner" +import { ErrorBanner } from "../../../../layout/AlertBanner" import { IconButton, Dialog, Flex, Text } from "@radix-ui/themes" -import { X } from "lucide-react" +import { BiX } from "react-icons/bi" import { useToast } from "@/hooks/useToast" import { Loader } from "@/components/common/Loader" +import { Message, TextMessage } from "../../../../../../../types/Messaging/Message" -const Tiptap = lazy(() => import("../chat/ChatInput/Tiptap")) +const Tiptap = lazy(() => import("../../ChatInput/Tiptap")) interface EditMessageModalProps { onClose: (refresh?: boolean) => void, - channelMessageID: string, - originalText: string + message: TextMessage, } -export const EditMessageModal = ({ onClose, channelMessageID, originalText }: EditMessageModalProps) => { +export const EditMessageModal = ({ onClose, message }: EditMessageModalProps) => { const { mutate } = useSWRConfig() const { toast } = useToast() @@ -25,7 +25,7 @@ export const EditMessageModal = ({ onClose, channelMessageID, originalText }: Ed }, [reset]) const onSubmit = async (html: string, json: any) => { - return updateDoc('Raven Message', channelMessageID, + return updateDoc('Raven Message', message.name, { text: html, json }).then((d) => { onClose(true) toast({ @@ -45,7 +45,7 @@ export const EditMessageModal = ({ onClose, channelMessageID, originalText }: Ed Edit Message - + @@ -53,7 +53,7 @@ export const EditMessageModal = ({ onClose, channelMessageID, originalText }: Ed }> - + Press Enter to save diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/DeleteMessage.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/DeleteMessage.tsx new file mode 100644 index 000000000..5bb75cc2e --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/DeleteMessage.tsx @@ -0,0 +1,43 @@ +import { useCallback, useState } from "react" +import { Message } from "../../../../../../../types/Messaging/Message" +import { AlertDialog } from "@radix-ui/themes" +import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog" +import { DeleteMessageModal } from "@/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal" + +export const useDeleteMessage = () => { + const [message, setMessage] = useState(null) + + const onClose = useCallback(() => { + setMessage(null) + }, []) + + + + return { + message, + setDeleteMessage: setMessage, + isOpen: message !== null, + onClose + } + +} +interface DeleteMessageDialogProps { + message: Message | null, + isOpen: boolean, + onClose: () => void +} +export const DeleteMessageDialog = ({ message, isOpen, onClose }: DeleteMessageDialogProps) => { + + + return + + {message && + + } + + + +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/EditMessage.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/EditMessage.tsx new file mode 100644 index 000000000..a90ea1338 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/EditMessage.tsx @@ -0,0 +1,42 @@ +import { useCallback, useState } from "react" +import { Message, TextMessage } from "../../../../../../../types/Messaging/Message" +import { Dialog } from "@radix-ui/themes" +import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog" +import { EditMessageModal } from "@/components/feature/chat/ChatMessage/ActionModals/EditMessageModal" + +export const useEditMessage = () => { + const [message, setMessage] = useState(null) + + const onClose = useCallback(() => { + setMessage(null) + }, []) + + + + return { + message, + setEditMessage: setMessage, + isOpen: message !== null, + onClose + } + +} +interface EditMessageDialogProps { + message: Message | null, + isOpen: boolean, + onClose: () => void +} +export const EditMessageDialog = ({ message, isOpen, onClose }: EditMessageDialogProps) => { + + + return + + {message && + + } + + + +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx new file mode 100644 index 000000000..abb93f1de --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx @@ -0,0 +1,153 @@ +import { ContextMenu, Flex } from '@radix-ui/themes' +import { FileMessage, Message } from '../../../../../../../types/Messaging/Message' +import { useContext } from 'react' +import { UserContext } from '@/utils/auth/UserProvider' +import { BiBookmarkMinus, BiBookmarkPlus, BiCopy, BiDownload, BiEditAlt, BiLink, BiTrash } from 'react-icons/bi' +import { HiReply } from 'react-icons/hi' +import { FrappeConfig, FrappeContext } from 'frappe-react-sdk' +import { useMessageCopy } from './useMessageCopy' +import { useToast } from '@/hooks/useToast' + +export interface MessageContextMenuProps { + message: Message, + onDelete: VoidFunction + onEdit: VoidFunction, + onReply: VoidFunction, + updateMessages: VoidFunction, + isOwner: boolean +} + +export const MessageContextMenu = ({ message, onDelete, onEdit, onReply, updateMessages, isOwner }: MessageContextMenuProps) => { + + const copy = useMessageCopy(message) + + return ( + + + + + Reply + + + + + {message.message_type === 'Text' && + + + + Copy + + + } + + {['File', 'Image'].includes(message.message_type) && + + + + + Copy link + + + + + + + + Download + + + + + } + + + + + + + + + {/* + + + Link with document + + + + + + + Send in an email + + */} + + + + + + + {isOwner && + + {message.message_type === 'Text' && + + + + Edit + + + } + + + + Delete + + + } + + + ) +} + + +const SaveMessageAction = ({ message, updateMessages }: { message: Message, updateMessages: VoidFunction }) => { + + const { currentUser } = useContext(UserContext) + const isSaved = JSON.parse(message._liked_by ?? '[]').includes(currentUser) + + const { call } = useContext(FrappeContext) as FrappeConfig + + const { toast } = useToast() + + const handleLike = () => { + call.post('frappe.desk.like.toggle_like', { + doctype: 'Raven Message', + name: message.name, + add: isSaved ? 'No' : 'Yes' + }).then(() => { + toast({ + title: isSaved ? 'Message unsaved' : 'Message saved', + variant: isSaved ? 'default' : 'accent', + duration: 800, + }) + updateMessages() + }) + .catch(() => { + toast({ + title: 'Could not perform the action', + variant: 'destructive', + duration: 800, + }) + }) + } + + return + + {!isSaved && } + {isSaved && } + {!isSaved ? "Save" : "Unsave"} message + + + + + +} \ No newline at end of file diff --git a/raven-app/src/components/feature/message-action-palette/EmojiPickerButton.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/EmojiPickerButton.tsx similarity index 91% rename from raven-app/src/components/feature/message-action-palette/EmojiPickerButton.tsx rename to raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/EmojiPickerButton.tsx index 7d7bdd06a..08289bd51 100644 --- a/raven-app/src/components/feature/message-action-palette/EmojiPickerButton.tsx +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/EmojiPickerButton.tsx @@ -3,6 +3,7 @@ import { Box, Flex, IconButton, Popover, Portal, Tooltip } from '@radix-ui/theme import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' import { BiSmile } from 'react-icons/bi' import { Loader } from '@/components/common/Loader' +import { QUICK_ACTION_BUTTON_CLASS } from './QuickActionButton' const EmojiPicker = lazy(() => import('@/components/common/EmojiPicker/EmojiPicker')) @@ -30,7 +31,8 @@ export const EmojiPickerButton = ({ saveReaction }: EmojiPickerButtonProps) => { diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActionButton.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActionButton.tsx new file mode 100644 index 000000000..c2b164c89 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActionButton.tsx @@ -0,0 +1,20 @@ +import { IconButton, Tooltip } from '@radix-ui/themes' +import { IconButtonProps } from '@radix-ui/themes/dist/cjs/components/icon-button' +import { clsx } from 'clsx' + +interface QuickActionButtonProps extends IconButtonProps { + tooltip: string +} + +export const QUICK_ACTION_BUTTON_CLASS = 'bg-[var(--gray-4)] dark:bg-[var(--gray-6)] hover:bg-[var(--gray-5)] dark:hover:bg-[var(--gray-8)] text-[var(--gray-12)] hover:text-[var(--gray-12)]' +export const QuickActionButton = ({ className, tooltip, ...props }: QuickActionButtonProps) => { + return ( + + + + ) +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActions.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActions.tsx new file mode 100644 index 000000000..1d6b3308c --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActions.tsx @@ -0,0 +1,104 @@ +import { Box, Flex, Tooltip } from '@radix-ui/themes' +import { MessageContextMenuProps } from '../MessageActions' +import { QUICK_ACTION_BUTTON_CLASS, QuickActionButton } from './QuickActionButton' +import { BiDotsHorizontal, BiEditAlt } from 'react-icons/bi' +import { HiReply } from 'react-icons/hi' +import { MouseEventHandler, useContext, useRef } from 'react' +import { FrappeConfig, FrappeContext } from 'frappe-react-sdk' +import { EmojiPickerButton } from './EmojiPickerButton' + + +const QUICK_EMOJIS = ['👍', '✅', '👀', '🎉'] +export const QuickActions = ({ message, onReply, onEdit, updateMessages, isOwner }: MessageContextMenuProps) => { + + const toolbarRef = useRef(null) + + const { call } = useContext(FrappeContext) as FrappeConfig + + /** + * When the user clicks on the more button, we want to trigger a right click event + * so that we open the context menu instead of duplicating the actions in a dropdown menu + * @param e - MouseEvent + */ + const onMoreClick: MouseEventHandler = (e) => { + e.preventDefault() + + var evt = new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + clientX: e.clientX, + clientY: e.clientY, + screenX: e.screenX, + screenY: e.screenY, + buttons: 2 + }); + e.target.dispatchEvent(evt); + } + + const onEmojiReact = (emoji: string) => { + call.post('raven.api.reactions.react', { + message_id: message.name, + reaction: emoji + }).then(() => updateMessages()) + } + + + return ( + + + + {QUICK_EMOJIS.map((emoji) => { + return { + onEmojiReact(emoji) + }}> + {emoji} + + })} + + + + {isOwner ? + + + + + : + + + + } + + + + + + + + ) +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/useMessageCopy.ts b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/useMessageCopy.ts new file mode 100644 index 000000000..3a811b1b6 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/useMessageCopy.ts @@ -0,0 +1,59 @@ +import { Message } from '../../../../../../../types/Messaging/Message' +import { useToast } from '@/hooks/useToast' +import turndown from 'turndown' +type Props = {} + +export const useMessageCopy = (message: Message) => { + const { toast } = useToast() + + const copy = () => { + if (message.message_type === 'Text') { + + // Remove all empty lines + let text = message.text.replace(/^\s*[\r\n]/gm, "") + + var turndownService = new turndown({ + codeBlockStyle: 'fenced', + }) + + // We want the links to not be converted to markdown links + + turndownService.addRule('links', { + filter: 'a', + replacement: function (content, node, options) { + return content + } + }) + var markdown = turndownService.turndown(text) + if (markdown) { + navigator.clipboard.writeText(markdown) + toast({ + title: 'Text copied', + duration: 800, + variant: 'accent' + }) + } else { + toast({ + title: 'Could not copy text', + duration: 800, + variant: 'destructive' + }) + } + + } else { + if (message.file.startsWith('http') || message.file.startsWith('https')) { + navigator.clipboard.writeText(message.file) + } + else { + navigator.clipboard.writeText(window.location.origin + message.file) + } + toast({ + title: 'Link copied', + duration: 800, + variant: 'accent' + }) + } + } + + return copy +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageItem.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageItem.tsx new file mode 100644 index 000000000..ee6494534 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageItem.tsx @@ -0,0 +1,196 @@ +import { Box, ContextMenu, Flex, HoverCard, Link, Separator, Text } from '@radix-ui/themes' +import { Message, MessageBlock } from '../../../../../../types/Messaging/Message' +import { MessageContextMenu } from './MessageActions/MessageActions' +import { DateTooltip, DateTooltipShort } from './Renderers/DateTooltip' +import { BoxProps } from '@radix-ui/themes/dist/cjs/components/box' +import { clsx } from 'clsx' +import { UserAvatar } from '@/components/common/UserAvatar' +import { useGetUser } from '@/hooks/useGetUser' +import { useIsUserActive } from '@/hooks/useIsUserActive' +import { UserFields } from '@/utils/users/UserListProvider' +import { BsFillCircleFill } from 'react-icons/bs' +import { MessageReactions } from './MessageReactions' +import { ImageMessageBlock } from './Renderers/ImageMessage' +import { FileMessageBlock } from './Renderers/FileMessage' +import { TiptapRenderer } from './Renderers/TiptapRenderer/TiptapRenderer' +import { QuickActions } from './MessageActions/QuickActions/QuickActions' +import { useContext } from 'react' +import { UserContext } from '@/utils/auth/UserProvider' +import { ReplyMessage } from './ReplyMessageBox/ReplyMessageBox' + +interface MessageBlockProps { + message: MessageBlock['data'], + setDeleteMessage: (message: Message) => void, + setEditMessage: (message: Message) => void, + replyToMessage: (message: Message) => void, + updateMessages: () => void, + onReplyMessageClick: (messageID: string) => void, +} + +export const MessageItem = ({ message, setDeleteMessage, onReplyMessageClick, setEditMessage, replyToMessage, updateMessages }: MessageBlockProps) => { + + const { name, owner: userID, creation: timestamp, message_reactions, is_continuation, is_reply, linked_message } = message + + const { user, isActive } = useGetUserDetails(userID) + + const onDelete = () => { + setDeleteMessage(message) + } + + const onEdit = () => { + setEditMessage(message) + } + + const onReply = () => { + replyToMessage(message) + } + + const { currentUser } = useContext(UserContext) + + const isOwner = currentUser === message.owner + + return ( + + + + + + + {!is_continuation ? + + + + + : null} + {/* Message content goes here */} + + {/* If it's a reply, then show the linked message */} + {linked_message && onReplyMessageClick(linked_message)} + messageID={linked_message} />} + {/* Show message according to type */} + + {message_reactions?.length && + + } + + + + + + + + + + ) +} + +interface MessageLeftElementProps extends BoxProps { + message: MessageBlock['data'], + user?: UserFields, + isActive?: boolean +} +const MessageLeftElement = ({ message, className, user, isActive, ...props }: MessageLeftElementProps) => { + + // If it's a continuation, then show the timestamp + + // Else, show the avatar + return + {message.is_continuation ? + + : + } + + +} + +const useGetUserDetails = (userID: string) => { + + const user = useGetUser(userID) + + const isActive = useIsUserActive(userID) + + return { user, isActive } +} + +interface UserProps { + user?: UserFields + userID: string + isActive?: boolean +} +const Avatar = ({ user, userID, isActive = false }: UserProps) => { + + return +} + +const UserHoverCard = ({ user, userID, isActive }: UserProps) => { + + return + + + {user?.full_name ?? userID} + + + + + + + + {user?.full_name ?? userID} + {isActive && + + Online + } + + {user && {user?.name}} + + + + + +} +interface MessageContentProps extends BoxProps { + user?: UserFields + message: Message +} +const MessageContent = ({ message, user, ...props }: MessageContentProps) => { + + return + {message.message_type === 'Image' && } + {message.message_type === 'File' && } + {message.message_type === 'Text' && } + +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-message/MessageReactions.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageReactions.tsx similarity index 68% rename from raven-app/src/components/feature/chat/chat-message/MessageReactions.tsx rename to raven-app/src/components/feature/chat/ChatMessage/MessageReactions.tsx index 5836257f5..71e291812 100644 --- a/raven-app/src/components/feature/chat/chat-message/MessageReactions.tsx +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageReactions.tsx @@ -1,10 +1,10 @@ -import { HStack, Tag, Tooltip } from "@chakra-ui/react" import { useFrappePostCall } from "frappe-react-sdk" import { useCallback, useContext, useMemo } from "react" import { UserContext } from "../../../../utils/auth/UserProvider" import { getUsers } from "../../../../utils/operations" import { useGetUserRecords } from "@/hooks/useGetUserRecords" -import { useColorModeValue } from "@/ThemeProvider" +import { Flex, IconButton, Text, Tooltip } from "@radix-ui/themes" +import { clsx } from "clsx" interface ReactionObject { // The emoji @@ -14,20 +14,20 @@ interface ReactionObject { // The number of users who reacted with this emoji count: number } -export const MessageReactions = ({ name, message_reactions, updateMessages }: { name: string, message_reactions?: string | null, updateMessages: VoidFunction }) => { +export const MessageReactions = ({ messageID, message_reactions, updateMessages }: { messageID: string, message_reactions?: string | null, updateMessages: VoidFunction }) => { const { currentUser } = useContext(UserContext) const { call: reactToMessage } = useFrappePostCall('raven.api.reactions.react') const saveReaction = useCallback((emoji: string) => { - if (name) { + if (messageID) { return reactToMessage({ - message_id: name, + message_id: messageID, reaction: emoji }).then(() => updateMessages()) } - }, [name, updateMessages, reactToMessage]) + }, [messageID, updateMessages, reactToMessage]) const allUsers = useGetUserRecords() const reactions: ReactionObject[] = useMemo(() => { @@ -37,7 +37,7 @@ export const MessageReactions = ({ name, message_reactions, updateMessages }: { }, [message_reactions]) return ( - + {reactions.map((reaction) => { return ( ) })} - + ) } @@ -61,8 +61,6 @@ interface ReactionButtonProps { } const ReactionButton = ({ reaction, onReactionClick, currentUser, allUsers }: ReactionButtonProps) => { const { reaction: emoji, users, count } = reaction - const bgColor = useColorModeValue('white', 'gray.700') - const activeBorderColor = useColorModeValue('gray.200', 'gray.600') const onClick = useCallback(() => { onReactionClick(emoji) @@ -76,17 +74,17 @@ const ReactionButton = ({ reaction, onReactionClick, currentUser, allUsers }: Re }, [allUsers, count, currentUser, reaction, users]) return ( - - - {emoji} {count} - + + + + {emoji} {count} + + ) } \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/DateTooltip.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/DateTooltip.tsx new file mode 100644 index 000000000..348a37649 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/DateTooltip.tsx @@ -0,0 +1,31 @@ +import { DateMonthAtHourMinuteAmPm, HourMinuteAmPm } from '@/utils/dateConversions' +import { Tooltip, Text, Link } from '@radix-ui/themes' +export const DateTooltip = ({ timestamp }: { timestamp: string }) => { + return ( + }> + + + + + + + ) +} + +export const DateTooltipShort = ({ timestamp }: { timestamp: string }) => { + return ( + }> + + + + + ) +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/FileMessage.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/FileMessage.tsx new file mode 100644 index 000000000..1b40034d0 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/FileMessage.tsx @@ -0,0 +1,145 @@ +import { FileExtensionIcon } from "../../../../../utils/layout/FileExtensionIcon" +import { FileMessage } from "../../../../../../../types/Messaging/Message" +import { getFileExtension, getFileName, isVideoFile } from "../../../../../utils/operations" +import { UserFields } from "@/utils/users/UserListProvider" +import { Box, Button, Dialog, Flex, IconButton, Link, Text } from "@radix-ui/themes" +import { BoxProps } from "@radix-ui/themes/dist/cjs/components/box" +import { BiDownload, BiLink, BiShow } from "react-icons/bi" +import { useToast } from "@/hooks/useToast" +import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog" +import { DateMonthAtHourMinuteAmPm } from "@/utils/dateConversions" +import { clsx } from "clsx" + +interface FileMessageBlockProps extends BoxProps { + message: FileMessage, + user?: UserFields, +} + +export const FileMessageBlock = ({ message, user, ...props }: FileMessageBlockProps) => { + + const fileExtension = getFileExtension(message.file) + + const fileName = getFileName(message.file) + + const isVideo = isVideoFile(fileExtension) + + const isPDF = fileExtension === 'pdf' + const { toast } = useToast() + + const copyLink = () => { + if (message.file.startsWith('http') || message.file.startsWith('https')) { + navigator.clipboard.writeText(message.file) + } + else { + navigator.clipboard.writeText(window.location.origin + message.file) + } + + toast({ + title: 'Link copied', + duration: 800, + variant: 'accent' + }) + } + + return + + {isVideo ? + {fileName} + + : + + + + + + {fileName} + + + + {isPDF && } + + + + + + + + + + + } + + + +} + + +const PDFPreviewButton = ({ message, user }: { + message: FileMessage, + user?: UserFields, +}) => { + + const fileName = getFileName(message.file) + + return + + + + + + + + {fileName} + {user?.full_name ?? message.owner} on + + + + + + + + + + + + +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/ImageMessage.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/ImageMessage.tsx new file mode 100644 index 000000000..488b1abfe --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/ImageMessage.tsx @@ -0,0 +1,90 @@ +import { getFileName } from '@/utils/operations' +import { ImageMessage } from '../../../../../../../types/Messaging/Message' +import { Box, Button, Dialog, Flex, Link } from '@radix-ui/themes' +import { useState } from 'react' +import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' +import { BiDownload } from 'react-icons/bi' +import { UserFields } from '@/utils/users/UserListProvider' +import { DateMonthAtHourMinuteAmPm } from '@/utils/dateConversions' + +interface ImageMessageProps { + message: ImageMessage, + user?: UserFields, +} + +export const ImageMessageBlock = ({ message, user }: ImageMessageProps) => { + + const [isOpen, setIsOpen] = useState(false) + // Show skeleton loader when image is loading + + const height = message.thumbnail_height ?? '200' + const width = message.thumbnail_width ?? '300' + + const fileName = getFileName(message.file) + return ( + + {fileName} + setIsOpen(true)} + style={{ + height: height + 'px', + width: width + 'px', + }}> + + {/* Absolute positioned skeleton loader */} + + + + + + {`Image + + + + {fileName} + {user?.full_name ?? message.owner} on + + {`Image + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Blockquote.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Blockquote.tsx new file mode 100644 index 000000000..b579d247d --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Blockquote.tsx @@ -0,0 +1,19 @@ +import { Blockquote } from '@radix-ui/themes'; +import TiptapBlockquote from '@tiptap/extension-blockquote' +import { NodeViewRendererProps, NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react"; + +export const CustomBlockquote = TiptapBlockquote.extend({ + addNodeView() { + return ReactNodeViewRenderer(BlockquoteRenderer) + } +}) + +const BlockquoteRenderer = ({ node }: NodeViewRendererProps) => { + return ( + +
+ {node.textContent} +
+
+ ); +}; \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Bold.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Bold.tsx new file mode 100644 index 000000000..120721f45 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Bold.tsx @@ -0,0 +1,14 @@ +import TiptapBold from '@tiptap/extension-bold' +import { mergeAttributes } from "@tiptap/react"; + +export const CustomBold = TiptapBold.extend({ + renderHTML({ HTMLAttributes }) { + return [ + "strong", + mergeAttributes(HTMLAttributes, { + class: 'rt-Strong' + }), // mergeAttributes is a exported function from @tiptap/core + 0, + ]; + }, +}) \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Italic.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Italic.tsx new file mode 100644 index 000000000..cf00cde08 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Italic.tsx @@ -0,0 +1,13 @@ +import { mergeAttributes } from "@tiptap/react"; +import Italic from '@tiptap/extension-italic'; +export const CustomItalic = Italic.extend({ + renderHTML({ HTMLAttributes }) { + return [ + "em", + mergeAttributes(HTMLAttributes, { + class: 'rt-Em' + }), // mergeAttributes is a exported function from @tiptap/core + 0, + ]; + }, +}) \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Link.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Link.tsx new file mode 100644 index 000000000..c8360a022 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Link.tsx @@ -0,0 +1,78 @@ +import { Box, Flex, Text } from '@radix-ui/themes'; +import TiptapLink from '@tiptap/extension-link' +import { Editor, mergeAttributes } from "@tiptap/react"; +import { useFrappeGetCall } from 'frappe-react-sdk'; + +export const CustomLink = TiptapLink.extend({ + renderHTML({ HTMLAttributes }) { + return [ + "a", + mergeAttributes(HTMLAttributes, { + class: 'rt-Text rt-reset rt-Link rt-underline-auto' + }), // mergeAttributes is a exported function from @tiptap/core + 0, + ]; + }, +}).configure({ + protocols: ['mailto', 'https', 'http'] +}) + +export type LinkPreviewDetails = { + title: string, + description: string, + image: string, + force_title: string, + absolute_image: string, + site_name: string +} + +export const LinkPreview = ({ editor }: { editor: Editor | null }) => { + + const href = editor?.getAttributes('link').href + + const { data, isLoading } = useFrappeGetCall<{ message: LinkPreviewDetails[] }>('raven.api.preview_links.get_preview_link', { + urls: JSON.stringify([href]) + }, href ? undefined : null, { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + shouldRetryOnError: false, + }) + + if (!href) return null + + return + + {data?.message.map((linkPreview) => { + if (linkPreview.image || linkPreview.absolute_image) { + return + + {/* Absolute positioned skeleton loader */} + + + + + + {linkPreview.title} + + + + {linkPreview.title} + {linkPreview.site_name} + + {linkPreview.description} + + + } else { + return null + } + }) + } + + + +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/List.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/List.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Mention.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Mention.tsx new file mode 100644 index 000000000..9a8b489f8 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Mention.tsx @@ -0,0 +1,75 @@ +import { UserAvatar } from '@/components/common/UserAvatar'; +import { useGetUser } from '@/hooks/useGetUser'; +import { useIsUserActive } from '@/hooks/useIsUserActive'; +import { Flex, HoverCard, Link, Text } from '@radix-ui/themes'; +import Mention from '@tiptap/extension-mention' +import { NodeViewRendererProps, NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react"; +import { BsFillCircleFill } from 'react-icons/bs'; +import { Link as RouterLink } from 'react-router-dom'; + +export const CustomUserMention = Mention.extend({ + name: 'userMention', + addNodeView() { + return ReactNodeViewRenderer(UserMentionRenderer) + } +}) + +export const CustomChannelMention = Mention.extend({ + name: 'channelMention', + addNodeView() { + return ReactNodeViewRenderer(ChannelMentionRenderer) + } +}) + +const UserMentionRenderer = ({ node }: NodeViewRendererProps) => { + + const user = useGetUser(node.attrs.id) + const isActive = useIsUserActive(node.attrs.id) + + return ( + + + + + @{user?.full_name ?? node.attrs.label} + + + + + + + + {user?.full_name ?? node.attrs.label} + {isActive && + + Online + } + + {user && {user?.name}} + + + + + + {/* + @{node.attrs.label} + */} + + ); +}; + + + +const ChannelMentionRenderer = ({ node }: NodeViewRendererProps) => { + + // console.log(node) + return ( + + + + @{node.attrs.label} + + + + ); +}; \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx new file mode 100644 index 000000000..98a00d837 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx @@ -0,0 +1,97 @@ +import { EditorContent, EditorProvider, Extension, ReactRenderer, useEditor } from '@tiptap/react' +import { Message, TextMessage } from '../../../../../../../../types/Messaging/Message' +import { UserFields } from '@/utils/users/UserListProvider' +import { BoxProps } from '@radix-ui/themes/dist/cjs/components/box' +import { Box } from '@radix-ui/themes' +import Highlight from '@tiptap/extension-highlight' +import StarterKit from '@tiptap/starter-kit' +import css from 'highlight.js/lib/languages/css' +import js from 'highlight.js/lib/languages/javascript' +import ts from 'highlight.js/lib/languages/typescript' +import html from 'highlight.js/lib/languages/xml' +import json from 'highlight.js/lib/languages/json' +import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' +import { common, createLowlight } from 'lowlight' +import python from 'highlight.js/lib/languages/python' +import { CustomBlockquote } from './Blockquote' +import { CustomBold } from './Bold' +import { CustomUserMention } from './Mention' +import { CustomLink, LinkPreview } from './Link' +import { CustomItalic } from './Italic' +import { CustomUnderline } from './Underline' +const lowlight = createLowlight(common) + +lowlight.register('html', html) +lowlight.register('css', css) +lowlight.register('js', js) +lowlight.register('ts', ts) +lowlight.register('json', json) +lowlight.register('python', python) +interface TiptapRendererProps extends BoxProps { + message: TextMessage, + user?: UserFields, + showLinkPreview?: boolean +} + +export const TiptapRenderer = ({ message, user, showLinkPreview = true, ...props }: TiptapRendererProps) => { + + const editor = useEditor({ + content: message.text, + editable: false, + enableCoreExtensions: true, + extensions: [ + StarterKit.configure({ + heading: false, + codeBlock: false, + bold: false, + blockquote: false, + italic: false, + listItem: { + HTMLAttributes: { + class: 'ml-5 rt-Text rt-r-size-3' + } + }, + paragraph: { + HTMLAttributes: { + class: 'rt-Text rt-r-size-3' + } + } + }), + Highlight.configure({ + multicolor: true, + HTMLAttributes: { + class: 'bg-[var(--yellow-6)] dark:bg-[var(--yellow-11)] px-2 py-1' + } + }), + CustomUnderline, + CodeBlockLowlight.configure({ + lowlight + }), + CustomBlockquote, + CustomBold, + CustomUserMention, + CustomLink, + CustomItalic + // TODO: Add channel mention + // CustomChannelMention + ] + }) + + return ( + + + {showLinkPreview && } + + ) +} + +export const TruncatedTiptapRenderer = ({ message, user, showLinkPreview = false, ...props }: TiptapRendererProps) => { + + + return + + + +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Underline.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Underline.tsx new file mode 100644 index 000000000..0533664e4 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Underline.tsx @@ -0,0 +1,15 @@ +import TiptapUnderline from '@tiptap/extension-underline' +import { mergeAttributes } from "@tiptap/react"; + +export const CustomUnderline = TiptapUnderline.extend({ + renderHTML({ HTMLAttributes }) { + return [ + "span", + mergeAttributes(HTMLAttributes, { + class: 'rt-Text rt-reset rt-Link rt-underline-always text-[var(--gray-12)]', + 'data-accent-color': 'gray', + }), // mergeAttributes is a exported function from @tiptap/core + 0, + ]; + }, +}) \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/ReplyMessageBox/ReplyMessageBox.tsx b/raven-app/src/components/feature/chat/ChatMessage/ReplyMessageBox/ReplyMessageBox.tsx new file mode 100644 index 000000000..807cddec1 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/ReplyMessageBox/ReplyMessageBox.tsx @@ -0,0 +1,77 @@ +import { FileMessage, Message, TextMessage } from "../../../../../../../types/Messaging/Message" +import { Flex, Separator, Text } from "@radix-ui/themes" +import { useGetUser } from "@/hooks/useGetUser" +import { DateMonthAtHourMinuteAmPm } from "@/utils/dateConversions" +import { FileExtensionIcon } from "@/utils/layout/FileExtensionIcon" +import { getFileExtension, getFileName } from "@/utils/operations" +import { FlexProps } from "@radix-ui/themes/dist/cjs/components/flex" +import { clsx } from "clsx" +import { TruncatedTiptapRenderer } from "../Renderers/TiptapRenderer/TiptapRenderer" +import { useFrappeGetCall } from "frappe-react-sdk" +import { Loader } from "@/components/common/Loader" + +interface ReplyMessageBoxProps extends FlexProps { + message: Message +} +/** + * UI component to show the message being replied to + * @param props + * @returns + */ +export const ReplyMessageBox = ({ message, children, className, ...props }: ReplyMessageBoxProps) => { + + const user = useGetUser(message.owner) + return ( + + + + {user?.full_name ?? message.owner} + + + + + + {['File', 'Image'].includes(message.message_type) ? + + {message.message_type === 'File' && } + {message.message_type === 'Image' && {`Image} + {getFileName((message as FileMessage).file)} + + : + } + + + {children} + + ) +} + +interface ReplyMessageProps extends FlexProps { + messageID: string +} +/** + * Component to fetch the message being replied to and show it in the UI + */ +export const ReplyMessage = ({ messageID, ...props }: ReplyMessageProps) => { + + const { data, isLoading } = useFrappeGetCall('frappe.client.get_value', { + doctype: 'Raven Message', + filters: { + name: messageID + }, + fieldname: JSON.stringify(['owner', 'creation', 'message_type', 'file', 'text', 'channel_id', 'name']) + }, `reply_message_${messageID}`, { + revalidateIfStale: false, + revalidateOnFocus: false, + shouldRetryOnError: false, + revalidateOnReconnect: false + }) + + //TODO: Replace with a skeleton loader + if (isLoading) return + + if (data) return + + return null + +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx b/raven-app/src/components/feature/chat/ChatStream/ChatBoxBody.tsx similarity index 90% rename from raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx rename to raven-app/src/components/feature/chat/ChatStream/ChatBoxBody.tsx index 9de2282c1..421308d58 100644 --- a/raven-app/src/components/feature/chat/chat-history/ChatBoxBody.tsx +++ b/raven-app/src/components/feature/chat/ChatStream/ChatBoxBody.tsx @@ -13,10 +13,11 @@ import { UserContext } from "@/utils/auth/UserProvider" import useFileUpload from "../ChatInput/FileInput/useFileUpload" import { CustomFile, FileDrop } from "../../file-upload/FileDrop" import { FileListItem } from "../../file-upload/FileListItem" -import { PreviousMessageBox } from "../message-reply/PreviousMessageBox" import { useSendMessage } from "../ChatInput/useSendMessage" import { Loader } from "@/components/common/Loader" -import { Flex, Box } from "@radix-ui/themes" +import { Flex, Box, IconButton } from "@radix-ui/themes" +import { ReplyMessageBox } from "../ChatMessage/ReplyMessageBox/ReplyMessageBox" +import { BiX } from "react-icons/bi" const Tiptap = lazy(() => import("../ChatInput/Tiptap")) @@ -72,10 +73,18 @@ export const ChatBoxBody = ({ channelData }: ChatBoxBodyProps) => { const PreviousMessagePreview = ({ selectedMessage }: { selectedMessage: any }) => { if (selectedMessage) { - return + return + + + + } return null } diff --git a/raven-app/src/components/feature/chat/ChatStream/ChatHistory.tsx b/raven-app/src/components/feature/chat/ChatStream/ChatHistory.tsx new file mode 100644 index 000000000..412c2f592 --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatStream/ChatHistory.tsx @@ -0,0 +1,150 @@ +import { DividerWithText } from "../../../layout/Divider/DividerWithText"; +import { DateBlock, Message, MessageBlock, MessagesWithDate } from "../../../../../../types/Messaging/Message"; +import { ChannelHistoryFirstMessage } from "../../../layout/EmptyState/EmptyState"; +import { useCallback, useContext, useRef } from "react"; +import { Virtuoso } from 'react-virtuoso'; +import { VirtuosoRefContext } from "../../../../utils/message/VirtuosoRefProvider"; +import { ChannelListItem, DMChannelListItem } from "@/utils/channel/ChannelListProvider"; +import { Box, Flex } from "@radix-ui/themes"; +import { DateMonthYear } from "@/utils/dateConversions"; +import { MessageItem } from "../ChatMessage/MessageItem"; +import { DeleteMessageDialog, useDeleteMessage } from "../ChatMessage/MessageActions/DeleteMessage"; +import { EditMessageDialog, useEditMessage } from "../ChatMessage/MessageActions/EditMessage"; +import { FrappeConfig, FrappeContext, useSWRConfig } from "frappe-react-sdk"; + +/** + * Anatomy of a message + * + * A message "stream" is a list of messages separated by dates + * + * A date block is a simple divider with a date. Any message below the divider was sent on this date. + * Two date blocks can never be adjacent to each other. + * + * A message block can be of multiple types depending on the message type + who sent it and when. + * + * If two messages are sent by the same person within 2 minutes of each other, they are grouped together. + * The first message in such a group will have a User Avatar, Name and timestamp, and the rest will not. + * Subsequent message blocks in this 'mini-block' will only show a timestamp on hover. + * + * The message block can be of the following types: + * 1. Image - this will show a preview of the image. Clicking on it will open the image in a modal. + * 2. File - this will show a small box with the file name with actions to copy the link or download the file. + * PDF - PDF files will also have an action to open the file in a modal. + * Video - A video file will have a preview of the video. + * 3. Text - this will show the text message in a tiptap renderer. A text block can have multiple elements + * 1. Text - this will show the text as is (p, h1, h2, h3, h4, h5, h6, blockquote, li, ul, ol) + * 2. Code - this will show in a container with a quick action button to copy it + * 3. Mention - this will show the mentioned user's name (highlighted) and hovering over them should show a card with their details + * + * A message can also be a reply to another message. In this case, the message will have a small box at the top with the message content and a click will jump to the message. + * + * Every message has reactions at the very bottom. Every reaction has a count and a list of users who reacted to it. + * + * Every message will have a context menu (right click) with the following options: + * 1. Reply - this will open the reply box with the message quoted + * 2. Edit - this will open the edit box with the message content + * 3. Delete - this will delete the message + * 4. Copy - this will copy the message content + * 5. Copy Link - this will copy the message link (if file) + * 6. Send in an email + * 7. Link with document + * 8. Bookmark + * + * Every message will have a hover menu with the following options: + * 1. Reaction emojis with the frequently used emojis for that user on this channel + * 2. Reply/Edit depending on the user + * 3. Ellipsis to open the context menu + * + */ +interface ChatHistoryProps { + parsedMessages: MessagesWithDate, + replyToMessage: (message: Message) => void, + channelData: ChannelListItem | DMChannelListItem +} + +export const ChatHistory = ({ parsedMessages, replyToMessage, channelData }: ChatHistoryProps) => { + + const { virtuosoRef } = useContext(VirtuosoRefContext) + + const { call } = useContext(FrappeContext) as FrappeConfig + + const { mutate } = useSWRConfig() + + const updateMessages = useCallback(() => { + mutate(`get_messages_for_channel_${channelData.name}`) + }, [channelData.name]) + + const boxRef = useRef(null) + + const { setDeleteMessage, ...deleteProps } = useDeleteMessage() + + const { setEditMessage, ...editProps } = useEditMessage() + + const onReplyMessageClick = (messageID: string) => { + if (virtuosoRef?.current) { + call.get('raven.raven_messaging.doctype.raven_message.raven_message.get_index_of_message', { + channel_id: channelData.name, + message_id: messageID + }).then((result) => { + virtuosoRef.current?.scrollToIndex({ index: parseInt(result.message) ?? 'LAST', align: 'center', behavior: 'smooth' }) + }) + } + + } + + return ( + + } + initialTopMostItemIndex={parsedMessages.length - 1} + components={{ + Header: () => , + }} + context={{ channelData, replyToMessage, onReplyMessageClick, setDeleteMessage, updateMessages, setEditMessage }} + ref={virtuosoRef} + increaseViewportBy={300} + alignToBottom={true} + followOutput={'smooth'} + /> + + + + ) +} + +const RenderItem = ({ index, replyToMessage, updateMessages, block, onReplyMessageClick, channelData, setEditMessage, setDeleteMessage, ...props }: { + index: number, + block: MessageBlock | DateBlock, + replyToMessage: (message: Message) => void, + updateMessages: () => void, + setDeleteMessage: (message: Message) => void, + setEditMessage: (message: Message) => void, + onReplyMessageClick: (messageID: string) => void, + channelData: ChannelListItem | DMChannelListItem +}) => { + // const block = parsedMessages[index] + if (block.block_type === 'date') { + return ( + + + + ) + } + if (block.block_type === 'message') { + return ( + + + + + ) + } + return null +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx b/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx deleted file mode 100644 index 276507671..000000000 --- a/raven-app/src/components/feature/chat/chat-history/ChatHistory.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { DividerWithText } from "../../../layout/Divider/DividerWithText"; -import { DateBlock, FileMessage, Message, MessageBlock, MessagesWithDate } from "../../../../../../types/Messaging/Message"; -import { ChannelHistoryFirstMessage } from "../../../layout/EmptyState/EmptyState"; -import { useContext, useRef } from "react"; -import { ChatMessageBox } from "../chat-message/ChatMessageBox"; -import { MarkdownRenderer } from "../../markdown-viewer/MarkdownRenderer"; -import { FileMessageBlock } from "../chat-message/FileMessage"; -import { ModalTypes, useModalManager } from "../../../../hooks/useModalManager"; -import { FilePreviewModal } from "../../file-preview/FilePreviewModal"; -import { Virtuoso } from 'react-virtuoso'; -import { VirtuosoRefContext } from "../../../../utils/message/VirtuosoRefProvider"; -import { ChannelListItem, DMChannelListItem } from "@/utils/channel/ChannelListProvider"; -import { Box } from "@radix-ui/themes"; -import { DateMonthYear } from "@/utils/dateConversions"; - -interface ChatHistoryProps { - parsedMessages: MessagesWithDate, - replyToMessage?: (message: Message) => void, - channelData: ChannelListItem | DMChannelListItem -} - -export const ChatHistory = ({ parsedMessages, replyToMessage, channelData }: ChatHistoryProps) => { - - const { virtuosoRef } = useContext(VirtuosoRefContext) - - const boxRef = useRef(null) - - - const modalManager = useModalManager() - - const onFilePreviewModalOpen = (message: Partial) => { - if (message) { - modalManager.openModal(ModalTypes.FilePreview, { - file: message.file, - owner: message.owner, - creation: message.creation, - message_type: message.message_type - }) - } - } - - const renderItem = (block: DateBlock | MessageBlock) => { - if (block.block_type === 'date') { - return ( - - - - ) - } - if (block.block_type === 'message') { - return ( - - {block.data.message_type === 'Text' && } - {(block.data.message_type === 'File' || block.data.message_type === 'Image') && } - - ) - } - return null - } - - return ( - - renderItem(parsedMessages[index])} - initialTopMostItemIndex={parsedMessages.length - 1} - components={{ - Header: () => , - }} - ref={virtuosoRef} - increaseViewportBy={300} - alignToBottom={true} - followOutput={'smooth'} - /> - - - ) -} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-message/ChatMessageBox.tsx b/raven-app/src/components/feature/chat/chat-message/ChatMessageBox.tsx deleted file mode 100644 index d64779659..000000000 --- a/raven-app/src/components/feature/chat/chat-message/ChatMessageBox.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Avatar, Box, BoxProps, HStack, Stack } from "@chakra-ui/react" -import { useCallback, useState } from "react" -import { ActionsPalette } from "../../message-action-palette/ActionsPalette" -import { MessageReactions } from "./MessageReactions" -import { Message, MessageBlock } from "../../../../../../types/Messaging/Message" -import { PreviousMessageBox } from "../message-reply/PreviousMessageBox" -import { DateTooltipShort } from "./DateTooltip" -import { UserNameInMessage } from "./UserNameInMessage" -import { ChannelListItem, DMChannelListItem } from "@/utils/channel/ChannelListProvider" -import { useGetUserRecords } from "@/hooks/useGetUserRecords" -import { useSWRConfig } from "frappe-react-sdk" -import { useTheme } from "@/ThemeProvider" - -interface ChatMessageBoxProps extends BoxProps { - message: Message, - children?: React.ReactNode, - handleScrollToMessage?: (name: string, channel: string, messages: MessageBlock[]) => void, - replyToMessage?: (message: Message) => void - channelData: ChannelListItem | DMChannelListItem -} - -export const ChatMessageBox = ({ message, children, handleScrollToMessage, replyToMessage, channelData, ...props }: ChatMessageBoxProps) => { - - const { appearance } = useTheme() - const [showButtons, setShowButtons] = useState<{}>({ visibility: 'hidden' }) - const { name, owner: user, creation: timestamp, message_reactions, is_continuation, is_reply, linked_message } = message - - const users = useGetUserRecords() - - const { mutate } = useSWRConfig() - - const updateMessages = useCallback(() => { - mutate(`get_messages_for_channel_${channelData.name}`) - }, [mutate, channelData.name]) - - return ( - { - setShowButtons({ visibility: 'visible' }) - }} - onMouseLeave={e => { - setShowButtons({ visibility: 'hidden' }) - }} - {...props}> - - - {is_continuation === 0 ? - : - - } - - {is_continuation === 0 && } - {is_reply === 1 && linked_message && - - } - {children} - - - - - {message && - } - - - ) -} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-message/DateTooltip.tsx b/raven-app/src/components/feature/chat/chat-message/DateTooltip.tsx deleted file mode 100644 index 0844af318..000000000 --- a/raven-app/src/components/feature/chat/chat-message/DateTooltip.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { DateMonthAtHourMinuteAmPm, HourMinuteAmPm } from '@/utils/dateConversions' -import { Tooltip, Text } from '@radix-ui/themes' -export const DateTooltip = ({ timestamp }: { timestamp: string }) => { - return ( - }> - - - - - ) -} - -export const DateTooltipShort = ({ timestamp, showButtons }: { timestamp: string, showButtons: {} }) => { - return ( - }> - - - - - ) -} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx b/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx deleted file mode 100644 index 2e6660b56..000000000 --- a/raven-app/src/components/feature/chat/chat-message/FileMessage.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Collapse, HStack, IconButton, Link, Stack, Text, Image } from "@chakra-ui/react" -import { FileExtensionIcon } from "../../../../utils/layout/FileExtensionIcon" -import { FileMessage } from "../../../../../../types/Messaging/Message" -import { useCallback } from "react" -import { getFileExtension, getFileName } from "../../../../utils/operations" -import { BiChevronDown, BiChevronRight } from "react-icons/bi" -import { useBoolean } from "@/hooks/useBoolean" - -interface FileMessageProps extends Partial { - onFilePreviewModalOpen: ({ file, owner, creation, message_type }: Partial) => void -} - -export const FileMessageBlock = ({ file, owner, creation, message_type, onFilePreviewModalOpen }: FileMessageProps) => { - - const [showImage, { toggle }] = useBoolean(true) - - const openFile = useCallback(() => { - onFilePreviewModalOpen({ - file, - owner, - creation, - message_type - }) - }, [file, owner, creation, message_type]) - - if (message_type === 'File' && file) { - return ( - -
- {} -
- {getFileExtension(file).toLowerCase() === 'pdf' - ? - - {getFileName(file)} - - : - {getFileName(file)} - } -
- ) - } - - if (message_type === 'Image' && file) { - return ( - - - {{getFileName(file)}} - : } /> - - - - - - ) - } - - return null -} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-message/UserNameInMessage.tsx b/raven-app/src/components/feature/chat/chat-message/UserNameInMessage.tsx deleted file mode 100644 index 97708d25e..000000000 --- a/raven-app/src/components/feature/chat/chat-message/UserNameInMessage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { DateTooltip } from "./DateTooltip" -import { useGetUserRecords } from "@/hooks/useGetUserRecords" -import { useTheme } from "@/ThemeProvider" -import { Flex, Separator, Text } from "@radix-ui/themes" - -interface UserNameInMessageProps { - timestamp: string, - user: string, -} - -export const UserNameInMessage = ({ timestamp, user }: UserNameInMessageProps) => { - - const { appearance } = useTheme() - const textColor = appearance === 'light' ? 'gray.800' : 'gray.50' - - const users = useGetUserRecords() - - return ( - - - {users?.[user]?.full_name ?? user} - - - - - ) -} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/chat-space/ChannelSpace.tsx b/raven-app/src/components/feature/chat/chat-space/ChannelSpace.tsx index 4b86ab32e..8ce8386a8 100644 --- a/raven-app/src/components/feature/chat/chat-space/ChannelSpace.tsx +++ b/raven-app/src/components/feature/chat/chat-space/ChannelSpace.tsx @@ -1,6 +1,6 @@ import { Box } from '@radix-ui/themes' import { ChannelListItem } from '@/utils/channel/ChannelListProvider' -import { ChatBoxBody } from '../chat-history/ChatBoxBody' +import { ChatBoxBody } from '../ChatStream/ChatBoxBody' import { ChannelHeader } from '../../chat-header/ChannelHeader' interface ChannelSpaceProps { diff --git a/raven-app/src/components/feature/chat/chat-space/DirectMessageSpace.tsx b/raven-app/src/components/feature/chat/chat-space/DirectMessageSpace.tsx index b3d494984..2922e3c75 100644 --- a/raven-app/src/components/feature/chat/chat-space/DirectMessageSpace.tsx +++ b/raven-app/src/components/feature/chat/chat-space/DirectMessageSpace.tsx @@ -1,7 +1,7 @@ import { DMChannelListItem } from "@/utils/channel/ChannelListProvider" import { Box } from "@radix-ui/themes" import { DMChannelHeader } from "../../chat-header/DMChannelHeader" -import { ChatBoxBody } from "../chat-history/ChatBoxBody" +import { ChatBoxBody } from "../ChatStream/ChatBoxBody" import { useContext } from "react" import { ChannelMembersContext, ChannelMembersContextType } from "@/utils/channel/ChannelMembersProvider" diff --git a/raven-app/src/components/feature/chat/index.ts b/raven-app/src/components/feature/chat/index.ts index 7df2013f4..bd3f2e74b 100644 --- a/raven-app/src/components/feature/chat/index.ts +++ b/raven-app/src/components/feature/chat/index.ts @@ -1,3 +1 @@ -export { ChatInput } from './chat-input/ChatInput'; -export { ChatMessageBox } from './chat-message/ChatMessageBox'; -export { ChatHistory } from './chat-history/ChatHistory'; \ No newline at end of file +export { ChatHistory } from './ChatStream/ChatHistory'; \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx b/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx deleted file mode 100644 index b197b8750..000000000 --- a/raven-app/src/components/feature/chat/message-reply/PreviousMessageBox.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Text, Box, HStack, Stack, Center, Image, IconButton, StackDivider, LinkBox } from '@chakra-ui/react' -import { Message } from '../../../../../../types/Messaging/Message' -import { MarkdownRenderer } from '../../markdown-viewer/MarkdownRenderer' -import { getFileExtension, getFileName } from '../../../../utils/operations' -import { useContext } from 'react' -import { FileExtensionIcon } from '../../../../utils/layout/FileExtensionIcon' -import { useFrappeGetDoc, useFrappePostCall } from 'frappe-react-sdk' -import { VirtuosoRefContext } from '../../../../utils/message/VirtuosoRefProvider' -import { useNavigate } from "react-router-dom" -import { ChannelListItem, DMChannelListItem } from '@/utils/channel/ChannelListProvider' -import { UserFields } from '@/utils/users/UserListProvider' -import { useGetUserRecords } from '@/hooks/useGetUserRecords' -import { X } from 'lucide-react' -import { useTheme } from '@/ThemeProvider' -import { ErrorCallout } from '@/components/layout/AlertBanner/ErrorBanner' -import { DateMonthAtHourMinuteAmPm } from '@/utils/dateConversions' - -interface PreviousMessageBoxProps { - previous_message_id?: string, - previous_message_content?: Message, - onReplyingToMessageClose?: () => void, - channelData: ChannelListItem | DMChannelListItem, -} - -const MAX_TRUNCATED_LENGTH = 100 - -export const PreviousMessageBox = ({ previous_message_id, previous_message_content, onReplyingToMessageClose, channelData }: PreviousMessageBoxProps) => { - - const users = useGetUserRecords() - const { appearance } = useTheme() - - if (previous_message_content) { - return ( - - - - - }> - {users?.[previous_message_content.owner]?.full_name ?? previous_message_content.owner} - - - {/* message content */} - {previous_message_content.message_type === 'Text' && - - - - - {previous_message_content.text.length > MAX_TRUNCATED_LENGTH && ...} - - } - {(previous_message_content.message_type === 'Image' || previous_message_content.message_type === 'File') && - -
- {previous_message_content.message_type === 'Image' ? - File preview : -
- -
} -
- {getFileName(previous_message_content.file)} -
- } -
- - } - aria-label="Remove message" /> - -
-
- ) - } - - if (previous_message_id) return - - return null -} - -interface PreviousMessageBoxInChatProps { - previous_message_id: string, - channelData: ChannelListItem | DMChannelListItem, - users: Record -} - -const PreviousMessageBoxInChat = ({ previous_message_id, channelData, users }: PreviousMessageBoxInChatProps) => { - - const { appearance } = useTheme() - const { data, error } = useFrappeGetDoc('Raven Message', previous_message_id) - const { virtuosoRef } = useContext(VirtuosoRefContext) - const navigate = useNavigate() - - const { call, error: indexingError, reset } = useFrappePostCall<{ message: string }>("raven.raven_messaging.doctype.raven_message.raven_message.get_index_of_message") - - const handleNavigateToChannel = (channelID: string, _callback: VoidFunction) => { - navigate(`/channel/${channelID}`) - _callback() - } - - const handleScrollToMessage = (messageName: string) => { - reset() - handleNavigateToChannel(channelData?.name ?? '', async function () { - const result = await call({ - channel_id: channelData?.name, - message_id: messageName - }) - if (virtuosoRef) { - virtuosoRef.current?.scrollToIndex({ index: parseInt(result.message) ?? 'LAST', align: 'center' }) - } - }) - } - - if (indexingError) { - return - There was an error while searching for the previous message. - - } - if (error) { - return Previous message not found, this message may have been deleted. - } - if (data) { - return handleScrollToMessage(previous_message_id)} p='2' border={'1px'} borderColor={appearance === 'light' ? 'gray.400' : 'gray.600'} rounded={'md'} _hover={{ cursor: 'pointer', boxShadow: 'sm', bgColor: appearance === 'light' ? 'white' : 'black' }}> - - - - {users?.[data.owner]?.full_name ?? data.owner} - - - {/* message content */} - {data.message_type === 'Text' && - - - - - {data.text.length > MAX_TRUNCATED_LENGTH && ...} - - } - {(data.message_type === 'Image' || data.message_type === 'File') && - -
- {data.message_type === 'Image' ? - File preview : -
- -
} -
- {getFileName(data.file)} -
- } -
-
-
- } - - return null -} \ No newline at end of file diff --git a/raven-app/src/components/feature/command-palette/CommandPaletteActions.tsx b/raven-app/src/components/feature/command-palette/CommandPaletteActions.tsx index fa5f58909..507eb4403 100644 --- a/raven-app/src/components/feature/command-palette/CommandPaletteActions.tsx +++ b/raven-app/src/components/feature/command-palette/CommandPaletteActions.tsx @@ -2,7 +2,7 @@ import { Avatar, Box, Button, Center, HStack, Spinner, Stack, Text, useModalCont import { Command } from "cmdk" import { useFrappeGetCall } from "frappe-react-sdk" import { useContext, useMemo, useState } from "react" -import { MailSearch, Users } from "lucide-react" +import { LuMailSearch, LuUsers } from "react-icons/lu" import { BiSearch, BiHash, BiFile, BiMessageSquareDetail } from "react-icons/bi" import { useNavigate, useParams } from "react-router-dom" import { GetFileSearchResult } from "../../../../../types/Search/Search" @@ -10,7 +10,6 @@ import { UserContext } from "../../../utils/auth/UserProvider" import GlobalSearch from "../global-search/GlobalSearch" import { getFileExtension, getFileName } from "../../../utils/operations" import { useModalManager, ModalTypes } from "../../../hooks/useModalManager" -import { FilePreviewModal } from "../file-preview/FilePreviewModal" import { FileSearchResult } from "../global-search/FileSearch" import { UserFields } from "@/utils/users/UserListProvider" import { useCurrentChannelData } from "@/hooks/useCurrentChannelData" @@ -87,7 +86,7 @@ export const Home = ({ searchChange, input, isGlobalSearchModalOpen, children, i } }} > - + {channelData.is_direct_message ? (channelData.is_self_message ? `Find in direct messages with ${users[currentUser].first_name}` : @@ -138,7 +137,7 @@ export const Home = ({ searchChange, input, isGlobalSearchModalOpen, children, i Channels