diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index ad50ecb1ac..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,22 +0,0 @@ -node_modules -build -src/stream-emoji.json -/.cache/ -/public/static/styles.js -examples/*/src/serviceWorker.js -scripts/ -/dist/ -/lib/ -/bin/ -/include/ -/build/ -dist -*dist* -/src/styles/vendor/ -*.md -babel.i18next-extract.js -/coverage -types/index.d.ts -src/components/Channel/types.ts -src/@types/* -docusaurus/ diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index f491712911..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,230 +0,0 @@ -{ - "root": true, - "plugins": [ - "babel", - "jest-dom", - "jest", - "prettier", - "react-hooks", - "import", - "sort-destructure-keys" - ], - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:prettier/recommended", - "plugin:jest/all", - "plugin:jest-dom/recommended" - ], - // Next.js example comes with its own ESLint setup - "ignorePatterns": "examples/nextjs", - "rules": { - "array-callback-return": 2, - "arrow-body-style": 2, - "comma-dangle": 0, - "babel/no-invalid-this": 2, - "default-case": 2, - "eqeqeq": [2, "smart"], - "jest/expect-expect": 0, - "jest/no-conditional-expect": 0, - "jsx-quotes": ["error", "prefer-single"], - "linebreak-style": [2, "unix"], - "no-console": 0, - "no-mixed-spaces-and-tabs": 1, - "no-self-compare": 2, - "no-underscore-dangle": [2, { "allowAfterThis": true }], - "no-unused-vars": [1, { "ignoreRestSiblings": true }], - "no-use-before-define": 0, // can throw incorrect errors due to mismatch of @typescript-eslint versions in react-scripts and local package.json - "@typescript-eslint/no-use-before-define": 0, - "no-useless-concat": 2, - "no-var": 2, - "object-shorthand": 1, - "prefer-const": 1, - "react/jsx-sort-props": [ - "error", - { - "callbacksLast": false, - "ignoreCase": true, - "noSortAlphabetically": false, - "reservedFirst": false, - "shorthandFirst": false, - "shorthandLast": false - } - ], - "react/prop-types": 0, - "require-await": 2, - "semi": [1, "always"], - "sort-destructure-keys/sort-destructure-keys": [2, { "caseSensitive": false }], - "sort-imports": [ - "error", - { - "allowSeparatedGroups": true, - "ignoreCase": true, - "ignoreDeclarationSort": true, - "ignoreMemberSort": false, - "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] - } - ], - "sort-keys": ["error", "asc", { "caseSensitive": false, "minKeys": 2, "natural": false }], - "valid-typeof": 2, - "import/prefer-default-export": 0, - "import/extensions": [0], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, // TODO: set to false once React is in the dependencies (not devDependencies) - "optionalDependencies": false, - "peerDependencies": false - } - ], - "max-classes-per-file": 0, - "camelcase": 0, - "react-hooks/rules-of-hooks": 1, - "react-hooks/exhaustive-deps": 1, - "jest/prefer-inline-snapshots": 0, - "jest/lowercase-name": 0, - "jest/prefer-expect-assertions": 0, - "jest/no-hooks": 0, - "no-unused-expressions": "off", - "babel/no-unused-expressions": "error", - "jest/no-if": "off" - }, - "env": { - "es6": true, - "browser": true - }, - "parser": "babel-eslint", - "parserOptions": { - // "allowImportExportEverywhere": true, - "sourceType": "module", - "ecmaVersion": 2018, - "ecmaFeatures": { - "modules": true - } - }, - "settings": { - "react": { - "pragma": "React", - "version": "detect" - }, - "import/resolver": { - "alias": { - "map": [["mock-builders", "./src/mock-builders"]], - "extensions": [".js", ".jsx", ".ts", ".tsx"] - }, - "eslint-import-resolver-babel-module": {}, - "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"], - "paths": ["src"] - } - } - }, - "overrides": [ - { - "files": ["*.md"], - "rules": { - "react/jsx-no-undef": 0, - "react/react-in-jsx-scope": 0, - "semi": 0, - "no-undef": 0 - } - }, - { - "env": { - "es6": true, - "browser": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:jest/recommended", - "plugin:prettier/recommended", - "plugin:react/recommended", - "prettier/@typescript-eslint" - ], - "files": ["**/*.ts", "**/*.tsx"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "modules": true, - "jsx": true - }, - "ecmaVersion": 2018, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint", - "babel", - "prettier", - "react", - "typescript-sort-keys", - "sort-destructure-keys" - ], - "rules": { - "@typescript-eslint/explicit-module-boundary-types": 0, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-unused-vars": [1, { "ignoreRestSiblings": true }], - "@typescript-eslint/no-var-requires": 0, - "react-hooks/exhaustive-deps": 1, - "react-native/no-inline-styles": 0, - "array-callback-return": 2, - "arrow-body-style": 2, - "comma-dangle": 0, - "babel/no-invalid-this": 0, - "@typescript-eslint/no-invalid-this": 2, - "default-case": 2, - "eqeqeq": [2, "smart"], - "linebreak-style": [2, "unix"], - "jsx-quotes": ["error", "prefer-single"], - "no-console": 0, - "no-mixed-spaces-and-tabs": 1, - "no-self-compare": 2, - "no-shadow": 0, - "no-underscore-dangle": [2, { "allowAfterThis": true }], - "no-unused-vars": [1, { "ignoreRestSiblings": true }], - "no-useless-concat": 2, - "no-var": 2, - "object-shorthand": 1, - "prefer-const": 1, - "react/jsx-sort-props": [ - "error", - { - "callbacksLast": false, - "ignoreCase": true, - "noSortAlphabetically": false, - "reservedFirst": false, - "shorthandFirst": false, - "shorthandLast": false - } - ], - "react/prop-types": 0, - "require-await": 2, - "semi": [1, "always"], - "sort-destructure-keys/sort-destructure-keys": [2, { "caseSensitive": false }], - "sort-imports": [ - "error", - { - "allowSeparatedGroups": true, - "ignoreCase": true, - "ignoreDeclarationSort": true, - "ignoreMemberSort": false, - "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] - } - ], - "sort-keys": ["error", "asc", { "caseSensitive": false, "minKeys": 2, "natural": false }], - "typescript-sort-keys/interface": [ - "error", - "asc", - { "caseSensitive": false, "natural": true, "requiredFirst": true } - ], - "typescript-sort-keys/string-enum": [ - "error", - "asc", - { "caseSensitive": false, "natural": true } - ], - "valid-typeof": 2 - } - } - ] -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68051d6de3..42c9e1dd98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [16, 18] + node: [lts/*] name: Test with Node ${{ matrix.node }} steps: - uses: actions/checkout@v3 diff --git a/.prettierrc b/.prettierrc index 753a6e9d36..6a673bad73 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { "arrowParens": "always", "jsxSingleQuote": true, - "printWidth": 100, + "printWidth": 90, "singleQuote": true, "tabWidth": 2, "trailingComma": "all" diff --git a/e2e/fixtures/data/attachment.mjs b/e2e/fixtures/data/attachment.mjs index 5e468d85f7..f89dbaa540 100644 --- a/e2e/fixtures/data/attachment.mjs +++ b/e2e/fixtures/data/attachment.mjs @@ -1,4 +1,4 @@ -/* eslint-disable sort-keys */ + const smallImageAttachment = [ { type: 'image', diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..80c0b1af9f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,136 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +import reactHooksPlugin from 'eslint-plugin-react-hooks'; +import importPlugin from 'eslint-plugin-import'; +import sortDestructureKeysPlugin from 'eslint-plugin-sort-destructure-keys'; +import reactPlugin from 'eslint-plugin-react'; +import jestPlugin from 'eslint-plugin-jest'; +import jestDOMPlugin from 'eslint-plugin-jest-dom'; + +export default tseslint.config( + { + ignores: ['dist', 'src/@types', '*.{js,ts}'], + }, + { + name: 'default', + extends: [ + js.configs.recommended, + ...tseslint.configs.recommended, + reactPlugin.configs.flat.recommended, + ], + files: ['src/**/*.{js,ts,jsx,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooksPlugin, + import: importPlugin, + 'sort-destructure-keys': sortDestructureKeysPlugin, + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + camelcase: 'off', + semi: ['warn', 'always'], + eqeqeq: ['error', 'smart'], + 'array-callback-return': 'error', + 'arrow-body-style': 'error', + 'comma-dangle': 'off', + 'default-case': 'error', + 'jsx-quotes': ['error', 'prefer-single'], + 'linebreak-style': ['error', 'unix'], + 'no-console': 'off', + 'no-mixed-spaces-and-tabs': 'warn', + 'no-self-compare': 'error', + 'no-underscore-dangle': ['error', { allowAfterThis: true }], + 'no-use-before-define': 'off', + 'no-useless-concat': 'error', + 'no-var': 'error', + 'no-script-url': 'error', + 'no-continue': 'off', + 'object-shorthand': 'warn', + 'prefer-const': 'warn', + 'require-await': 'error', + 'sort-destructure-keys/sort-destructure-keys': ['error', { caseSensitive: false }], + 'sort-imports': [ + 'error', + { + allowSeparatedGroups: true, + ignoreCase: true, + ignoreDeclarationSort: true, + ignoreMemberSort: false, + memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], + }, + ], + 'sort-keys': ['error', 'asc', { caseSensitive: false, minKeys: 2, natural: false }], + 'valid-typeof': 'error', + 'max-classes-per-file': 'off', + 'no-unused-expressions': 'off', + 'import/prefer-default-export': 'off', + 'import/extensions': 'off', + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, // TODO: set to false once React is in the dependencies (not devDependencies) + optionalDependencies: false, + peerDependencies: false, + }, + ], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { ignoreRestSiblings: false, caughtErrors: 'none' }, + ], + '@typescript-eslint/no-unsafe-function-type': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + 'no-empty-function': 'off', + '@typescript-eslint/no-empty-function': 'warn', + '@typescript-eslint/no-require-imports': 'off', // TODO: remove this rule once all files are .mjs (and require is not used) + 'react/prop-types': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/display-name': 'warn', + 'react/jsx-sort-props': [ + 'error', + { + callbacksLast: false, + ignoreCase: true, + noSortAlphabetically: false, + reservedFirst: false, + shorthandFirst: false, + shorthandLast: false, + }, + ], + 'react-hooks/rules-of-hooks': 'warn', + 'react-hooks/exhaustive-deps': 'error', + }, + }, + { + name: 'jest', + files: ['src/**/__tests__/**'], + // extends: [jestDOMPlugin.configs['flat/recommended']], + plugins: { jest: jestPlugin, 'jest-dom': jestDOMPlugin }, + languageOptions: { + globals: jestPlugin.environments.globals.globals, + }, + rules: { + 'jest/expect-expect': 'off', + 'jest/no-conditional-expect': 'off', + 'jest/prefer-inline-snapshots': 'off', + 'jest/lowercase-name': 'off', + 'jest/prefer-expect-assertions': 'off', + 'jest/no-hooks': 'off', + 'jest/no-if': 'off', + 'jest/prefer-spy-on': 'warn', + 'jest-dom/prefer-in-document': 'warn', + 'jest-dom/prefer-to-have-class': 'warn', + '@typescript-eslint/no-empty-function': 'off', // explicitly disable for tests + }, + }, +); diff --git a/examples/nextjs/pages/_app.js b/examples/nextjs/pages/_app.jsx similarity index 100% rename from examples/nextjs/pages/_app.js rename to examples/nextjs/pages/_app.jsx diff --git a/examples/nextjs/pages/index.js b/examples/nextjs/pages/index.jsx similarity index 100% rename from examples/nextjs/pages/index.js rename to examples/nextjs/pages/index.jsx diff --git a/examples/vite/tsconfig.json b/examples/vite/tsconfig.json index 0042a24a1c..c4431280c3 100644 --- a/examples/vite/tsconfig.json +++ b/examples/vite/tsconfig.json @@ -12,9 +12,9 @@ "noEmit": true, "jsx": "react-jsx", "allowImportingTsExtensions": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": false, + "noUnusedParameters": false }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/package.json b/package.json index 9d8e13a6b9..af1a77c06f 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,7 @@ "@commitlint/config-conventional": "^18.4.3", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", + "@eslint/js": "^9.16.0", "@ladle/react": "^0.16.0", "@playwright/test": "^1.42.1", "@semantic-release/changelog": "^6.0.2", @@ -188,6 +189,7 @@ "@semantic-release/git": "^10.0.1", "@stream-io/rollup-plugin-node-builtins": "^2.1.5", "@stream-io/stream-chat-css": "^5.6.0", + "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/deep-equal": "^1.0.1", @@ -208,10 +210,7 @@ "@types/textarea-caret": "3.0.0", "@types/use-sync-external-store": "^0.0.6", "@types/uuid": "^8.3.0", - "@typescript-eslint/eslint-plugin": "5.62.0", - "@typescript-eslint/parser": "5.62.0", "autoprefixer": "^10.0.3", - "babel-eslint": "^10.1.0", "babel-jest": "^28.1.3", "babel-plugin-module-resolver": "^4.1.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", @@ -223,27 +222,14 @@ "emoji-mart": "^5.5.2", "esbuild": "^0.23.1", "esbuild-plugin-replace": "^1.4.0", - "eslint": "7.14.0", - "eslint-config-airbnb": "^18.2.1", - "eslint-config-prettier": "^6.15.0", - "eslint-config-react-app": "^6.0.0", - "eslint-import-resolver-alias": "^1.1.2", - "eslint-import-resolver-babel-module": "^5.2.0", - "eslint-plugin-babel": "^5.3.1", - "eslint-plugin-flowtype": "^5.2.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jest": "^24.1.3", - "eslint-plugin-jest-dom": "^3.3.0", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-react": "^7.21.5", - "eslint-plugin-react-hooks": "^4.2.0", - "eslint-plugin-sort-destructure-keys": "1.3.5", - "eslint-plugin-sort-keys-fix": "^1.1.2", - "eslint-plugin-testing-library": "^6.2.0", - "eslint-plugin-typescript-sort-keys": "^2.1.0", + "eslint": "^9.16.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jest": "^28.11.0", + "eslint-plugin-jest-dom": "^5.5.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-sort-destructure-keys": "^2.0.0", + "globals": "^15.13.0", "husky": "^8.0.3", "i18next-parser": "^6.0.0", "jest": "^29.7.0", @@ -252,24 +238,26 @@ "jsdom": "^24.1.1", "lint-staged": "^15.2.1", "moment-timezone": "^0.5.43", - "prettier": "^2.2.0", + "prettier": "^3.4.2", "react": "^19.0.0", "react-dom": "^19.0.0", "semantic-release": "^19.0.5", "stream-chat": "^8.50.0", "ts-jest": "^29.2.5", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "typescript-eslint": "^8.17.0" }, "scripts": { "build": "rm -rf dist && yarn build-translations && yarn bundle", "bundle": "concurrently ./scripts/bundle-esm.mjs ./scripts/copy-css.sh scripts/bundle-cjs.mjs", "build-translations": "i18next", "coverage": "jest --collectCoverage && codecov", - "eslint": "eslint '**/*.{js,md,ts,jsx,tsx}' --max-warnings 0", - "lint": "prettier --list-different 'src/**/*.{js,ts,tsx,md,json}' .eslintrc.json .prettierrc babel.config.js && eslint 'src/**/*.{js,ts,tsx,md}' --max-warnings 0 && yarn validate-translations", - "lint-fix": "prettier --write 'src/**/*.{js,ts,tsx,md,json}' .eslintrc.json .prettierrc babel.config.js && eslint --fix 'src/**/*.{js,ts,tsx,md}' --max-warnings 0", - "prettier": "prettier --list-different '**/*.{js,ts,tsx,md,json}' .eslintrc.json .prettierrc babel.config.js", - "prettier-fix": "prettier --write '**/*.{js,ts,tsx,md,json}' .eslintrc.json .prettierrc babel.config.js", + "lint": "yarn prettier --list-different && yarn eslint && yarn validate-translations", + "lint-fix": "yarn prettier-fix && yarn eslint-fix", + "eslint": "eslint --max-warnings 0", + "eslint-fix": "eslint --fix", + "prettier": "prettier 'src/**/*.{js,ts,jsx,tsx,md,json}' .prettierrc babel.config.js eslint.config.mjs", + "prettier-fix": "yarn prettier --write", "fix-staged": "lint-staged --config .lintstagedrc.fix.json --concurrent 1", "start": "tsc --watch --sourceMap --declarationMap", "prepare": "husky install", diff --git a/scripts/bundle-cjs.mjs b/scripts/bundle-cjs.mjs index 0faaa24322..0a037ca22e 100755 --- a/scripts/bundle-cjs.mjs +++ b/scripts/bundle-cjs.mjs @@ -4,7 +4,8 @@ import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import * as esbuild from 'esbuild'; import { replace } from 'esbuild-plugin-replace'; -import getPackageVersion from "./getPackageVersion.mjs"; +import getPackageVersion from './getPackageVersion.mjs'; +import packageJson from '../package.json' with { type: 'json' }; // import.meta.dirname is not available before Node 20 const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,12 +26,9 @@ const bundledDeps = [ 'remark-gfm', ]; -const packageJson = await import(resolve(__dirname, '../package.json'), { - assert: { type: 'json' }, -}); const deps = Object.keys({ - ...packageJson.default.dependencies, - ...packageJson.default.peerDependencies, + ...packageJson.dependencies, + ...packageJson.peerDependencies, }); const external = deps.filter((dep) => !bundledDeps.includes(dep)); @@ -46,7 +44,6 @@ const cjsBundleConfig = { sourcemap: 'linked', }; - // We build two CJS bundles: for browser and for node. The latter one can be // used e.g. during SSR (although it makes little sence to SSR chat, but still // nice for import not to break on server). @@ -56,9 +53,9 @@ const bundles = ['browser', 'node'].map((platform) => entryNames: `[dir]/[name].${platform}`, platform, plugins: [ - replace({ - '__STREAM_CHAT_REACT_VERSION__': getPackageVersion(), - }), + replace({ + __STREAM_CHAT_REACT_VERSION__: getPackageVersion(), + }), ], }), ); diff --git a/scripts/getPackageVersion.mjs b/scripts/getPackageVersion.mjs index 56df46af9f..f424d70c99 100644 --- a/scripts/getPackageVersion.mjs +++ b/scripts/getPackageVersion.mjs @@ -1,13 +1,5 @@ import { execSync } from 'node:child_process'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -// import.meta.dirname is not available before Node 20 -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const packageJson = await import(resolve(__dirname, '../package.json'), { - assert: { type: 'json' }, -}); +import packageJson from '../package.json' with { type: 'json' }; // Get the latest version so that magic string __STREAM_CHAT_REACT_VERSION__ can be replaced with it in the source code (used for reporting purposes) export default function getPackageVersion() { @@ -23,7 +15,7 @@ export default function getPackageVersion() { } catch (error) { console.error(error); console.warn('Could not get latest version from git tags, falling back to package.json'); - version = packageJson.default.version; + version = packageJson.version; } } console.log(`Determined the build package version to be ${version}`); diff --git a/src/components/AIStateIndicator/AIStateIndicator.tsx b/src/components/AIStateIndicator/AIStateIndicator.tsx index 2bbe572f1b..977bd240a0 100644 --- a/src/components/AIStateIndicator/AIStateIndicator.tsx +++ b/src/components/AIStateIndicator/AIStateIndicator.tsx @@ -8,20 +8,19 @@ import { useChannelStateContext, useTranslationContext } from '../../context'; import type { DefaultStreamChatGenerics } from '../../types/types'; export type AIStateIndicatorProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { channel?: Channel; }; export const AIStateIndicator = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ channel: channelFromProps, }: AIStateIndicatorProps) => { const { t } = useTranslationContext(); - const { channel: channelFromContext } = useChannelStateContext( - 'AIStateIndicator', - ); + const { channel: channelFromContext } = + useChannelStateContext('AIStateIndicator'); const channel = channelFromProps || channelFromContext; const { aiState } = useAIState(channel); const allowedStates = { diff --git a/src/components/AIStateIndicator/hooks/useAIState.ts b/src/components/AIStateIndicator/hooks/useAIState.ts index 54e955a6f5..33d744fda8 100644 --- a/src/components/AIStateIndicator/hooks/useAIState.ts +++ b/src/components/AIStateIndicator/hooks/useAIState.ts @@ -18,7 +18,7 @@ export const AIStates = { * @returns {{ aiState: AIState }} The current AI state for the given channel. */ export const useAIState = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( channel?: Channel, ): { aiState: AIState } => { diff --git a/src/components/Attachment/Attachment.tsx b/src/components/Attachment/Attachment.tsx index f1c32c43a8..93e70adcc7 100644 --- a/src/components/Attachment/Attachment.tsx +++ b/src/components/Attachment/Attachment.tsx @@ -55,7 +55,7 @@ export const ATTACHMENT_GROUPS_ORDER = [ ] as const; export type AttachmentProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { /** The message attachments to render, see [attachment structure](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) **/ attachments: StreamAttachment[]; @@ -87,14 +87,17 @@ export type AttachmentProps< * A component used for rendering message attachments. By default, the component supports: AttachmentActions, Audio, Card, File, Gallery, Image, and Video */ export const Attachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: AttachmentProps, ) => { const { attachments } = props; - // eslint-disable-next-line react-hooks/exhaustive-deps - const groupedAttachments = useMemo(() => renderGroupedAttachments(props), [attachments]); + const groupedAttachments = useMemo( + () => renderGroupedAttachments(props), + // eslint-disable-next-line react-hooks/exhaustive-deps + [attachments], + ); return (
@@ -107,13 +110,13 @@ export const Attachment = < }; const renderGroupedAttachments = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ attachments, ...rest }: AttachmentProps): GroupedRenderedAttachment => { - const uploadedImages: StreamAttachment[] = attachments.filter((attachment) => - isUploadedImage(attachment), + const uploadedImages: StreamAttachment[] = attachments.filter( + (attachment) => isUploadedImage(attachment), ); const containers = attachments @@ -169,7 +172,7 @@ const renderGroupedAttachments = < }; const getAttachmentType = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: AttachmentProps['attachments'][number], ): keyof typeof CONTAINER_MAP => { diff --git a/src/components/Attachment/AttachmentActions.tsx b/src/components/Attachment/AttachmentActions.tsx index 72fef69829..ec323edfb0 100644 --- a/src/components/Attachment/AttachmentActions.tsx +++ b/src/components/Attachment/AttachmentActions.tsx @@ -7,7 +7,7 @@ import type { ActionHandlerReturnType } from '../Message/hooks/useActionHandler' import type { DefaultStreamChatGenerics } from '../../types/types'; export type AttachmentActionsProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Attachment & { /** A list of actions */ actions: Action[]; @@ -20,7 +20,7 @@ export type AttachmentActionsProps< }; const UnMemoizedAttachmentActions = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: AttachmentActionsProps, ) => { diff --git a/src/components/Attachment/AttachmentContainer.tsx b/src/components/Attachment/AttachmentContainer.tsx index 94e73d179a..cb003a4236 100644 --- a/src/components/Attachment/AttachmentContainer.tsx +++ b/src/components/Attachment/AttachmentContainer.tsx @@ -30,13 +30,13 @@ import type { import type { Attachment } from 'stream-chat'; export type AttachmentContainerProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { attachment: Attachment | GalleryAttachment; componentType: AttachmentComponentType; }; export const AttachmentWithinContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ attachment, children, @@ -50,8 +50,8 @@ export const AttachmentWithinContainer = < componentType === 'card' && !attachment?.image_url && !attachment?.thumb_url ? 'no-image' : attachment?.actions?.length - ? 'actions' - : ''; + ? 'actions' + : ''; } const classNames = clsx( @@ -59,7 +59,8 @@ export const AttachmentWithinContainer = < { [`str-chat__message-attachment--${componentType}`]: componentType, [`str-chat__message-attachment--${attachment?.type}`]: attachment?.type, - [`str-chat__message-attachment--${componentType}--${extra}`]: componentType && extra, + [`str-chat__message-attachment--${componentType}--${extra}`]: + componentType && extra, 'str-chat__message-attachment--svg-image': isSvgAttachment(attachment), 'str-chat__message-attachment-with-actions': extra === 'actions', }, @@ -69,7 +70,7 @@ export const AttachmentWithinContainer = < }; export const AttachmentActionsContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ actionHandler, attachment, @@ -108,7 +109,7 @@ function getCssDimensionsVariables(url: string) { } export const GalleryContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ attachment, Gallery = DefaultGallery, @@ -150,7 +151,7 @@ export const GalleryContainer = < }; export const ImageContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: RenderAttachmentProps, ) => { @@ -194,7 +195,7 @@ export const ImageContainer = < }; export const CardContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: RenderAttachmentProps, ) => { @@ -220,7 +221,7 @@ export const CardContainer = < }; export const FileContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ attachment, File = DefaultFile, @@ -234,7 +235,7 @@ export const FileContainer = < ); }; export const AudioContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ attachment, Audio = DefaultAudio, @@ -247,11 +248,11 @@ export const AudioContainer = < ); export const VoiceRecordingContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ attachment, - VoiceRecording = DefaultVoiceRecording, isQuoted, + VoiceRecording = DefaultVoiceRecording, }: RenderAttachmentProps) => (
@@ -261,18 +262,17 @@ export const VoiceRecordingContainer = < ); export const MediaContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: RenderAttachmentProps, ) => { const { attachment, Media = ReactPlayer } = props; const componentType = 'media'; - const { shouldGenerateVideoThumbnail, videoAttachmentSizeHandler } = useChannelStateContext(); + const { shouldGenerateVideoThumbnail, videoAttachmentSizeHandler } = + useChannelStateContext(); const videoElement = useRef(null); - const [ - attachmentConfiguration, - setAttachmentConfiguration, - ] = useState(); + const [attachmentConfiguration, setAttachmentConfiguration] = + useState(); useLayoutEffect(() => { if (videoElement.current && videoAttachmentSizeHandler) { @@ -319,7 +319,7 @@ export const MediaContainer = < }; export const UnsupportedAttachmentContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ attachment, UnsupportedAttachment = DefaultUnsupportedAttachment, diff --git a/src/components/Attachment/Audio.tsx b/src/components/Attachment/Audio.tsx index baff2d7ef6..c9b76a1000 100644 --- a/src/components/Attachment/Audio.tsx +++ b/src/components/Attachment/Audio.tsx @@ -8,14 +8,14 @@ import { useAudioController } from './hooks/useAudioController'; import type { DefaultStreamChatGenerics } from '../../types/types'; export type AudioProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { // fixme: rename og to attachment og: Attachment; }; const UnMemoizedAudio = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: AudioProps, ) => { diff --git a/src/components/Attachment/Card.tsx b/src/components/Attachment/Card.tsx index 93d7e39aaa..b69605593a 100644 --- a/src/components/Attachment/Card.tsx +++ b/src/components/Attachment/Card.tsx @@ -41,8 +41,14 @@ const UnableToRenderCard = ({ type }: { type?: CardProps['type'] }) => { ); }; -const SourceLink = ({ author_name, url }: Pick & { url: string }) => ( -
+const SourceLink = ({ + author_name, + url, +}: Pick & { url: string }) => ( +
{ let visual = null; if (asset_url && type === 'video') { visual = ( - + ); } else if (image) { visual = ( @@ -104,7 +116,9 @@ const CardContent = (props: CardContentProps) => { ) : (
{url && } - {title &&
{title}
} + {title && ( +
{title}
+ )} {text &&
{text}
}
)} @@ -139,9 +153,13 @@ export const CardAudio = ({ )}
{url && } - {title &&
{title}
} + {title && ( +
{title}
+ )} {text && ( -
{text}
+
+ {text} +
)}
@@ -158,7 +176,8 @@ const UnMemoizedCard = (props: CardProps) => { const dimensions: { height?: string; width?: string } = {}; if (type === 'giphy' && typeof giphy !== 'undefined') { - const giphyVersion = giphy[giphyVersionName as keyof NonNullable]; + const giphyVersion = + giphy[giphyVersionName as keyof NonNullable]; image = giphyVersion.url; dimensions.height = giphyVersion.height; dimensions.width = giphyVersion.width; @@ -169,7 +188,9 @@ const UnMemoizedCard = (props: CardProps) => { } return ( -
+
diff --git a/src/components/Attachment/FileAttachment.tsx b/src/components/Attachment/FileAttachment.tsx index 0e66e74a73..7380e11af8 100644 --- a/src/components/Attachment/FileAttachment.tsx +++ b/src/components/Attachment/FileAttachment.tsx @@ -7,13 +7,13 @@ import { DownloadButton, FileSizeIndicator } from './components'; import type { DefaultStreamChatGenerics } from '../../types/types'; export type FileAttachmentProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { attachment: Attachment; }; const UnMemoizedFileAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ attachment, }: FileAttachmentProps) => ( @@ -21,7 +21,10 @@ const UnMemoizedFileAttachment = <
-
+
{attachment.title}
diff --git a/src/components/Attachment/UnsupportedAttachment.tsx b/src/components/Attachment/UnsupportedAttachment.tsx index e738ec8fed..eb81a2beca 100644 --- a/src/components/Attachment/UnsupportedAttachment.tsx +++ b/src/components/Attachment/UnsupportedAttachment.tsx @@ -5,19 +5,22 @@ import type { Attachment } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../types/types'; export type UnsupportedAttachmentProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { attachment: Attachment; }; export const UnsupportedAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ attachment, }: UnsupportedAttachmentProps) => { const { t } = useTranslationContext('UnsupportedAttachment'); return ( -
+
= Pick, 'attachment'> & { /** An array of fractional numeric values of playback speed to override the defaults (1.0, 1.5, 2.0) */ playbackRates?: number[]; }; -export const VoiceRecordingPlayer = ({ attachment, playbackRates }: VoiceRecordingPlayerProps) => { +export const VoiceRecordingPlayer = ({ + attachment, + playbackRates, +}: VoiceRecordingPlayerProps) => { const { t } = useTranslationContext('VoiceRecordingPlayer'); const { asset_url, @@ -66,10 +74,17 @@ export const VoiceRecordingPlayer = ({ attachment, playbackRates }: VoiceRecordi {attachment.duration ? ( displayDuration(displayedDuration) ) : ( - + )}
- +
@@ -86,7 +101,7 @@ export const VoiceRecordingPlayer = ({ attachment, playbackRates }: VoiceRecordi }; export type QuotedVoiceRecordingProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick, 'attachment'>; export const QuotedVoiceRecording = ({ attachment }: QuotedVoiceRecordingProps) => { @@ -109,7 +124,10 @@ export const QuotedVoiceRecording = ({ attachment }: QuotedVoiceRecordingProps) {attachment.duration ? ( displayDuration(attachment.duration) ) : ( - + )}
@@ -120,7 +138,7 @@ export const QuotedVoiceRecording = ({ attachment }: QuotedVoiceRecordingProps) }; export type VoiceRecordingProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { /** The attachment object from the message's attachment list. */ attachment: Attachment; diff --git a/src/components/Attachment/__tests__/Attachment.test.js b/src/components/Attachment/__tests__/Attachment.test.js index ee24ee0148..29a289bc9b 100644 --- a/src/components/Attachment/__tests__/Attachment.test.js +++ b/src/components/Attachment/__tests__/Attachment.test.js @@ -28,7 +28,9 @@ const Media = (props) =>
{props.customTestId const AttachmentActions = () =>
; const Image = (props) =>
{props.customTestId}
; const File = (props) =>
{props.customTestId}
; -const Gallery = (props) =>
{props.customTestId}
; +const Gallery = (props) => ( +
{props.customTestId}
+); const ATTACHMENTS = { scraped: { @@ -102,12 +104,15 @@ describe('attachment', () => { ${cases.file.attachments} | ${cases.file.case} | ${cases.file.renderedComponent} | ${cases.file.testId} ${cases.gallery.attachments} | ${cases.gallery.case} | ${cases.gallery.renderedComponent} | ${cases.gallery.testId} ${cases.image.attachments} | ${cases.image.case} | ${cases.image.renderedComponent} | ${cases.image.testId} - `('should render $renderedComponent component for $case', async ({ attachments, testId }) => { - renderComponent({ attachments }); - await waitFor(() => { - expect(screen.getByTestId(testId)).toBeInTheDocument(); - }); - }); + `( + 'should render $renderedComponent component for $case', + async ({ attachments, testId }) => { + renderComponent({ attachments }); + await waitFor(() => { + expect(screen.getByTestId(testId)).toBeInTheDocument(); + }); + }, + ); it.each(SUPPORTED_VIDEO_FORMATS.map((f) => [f]))( 'should render Media component for video of %s mime-type attachment', @@ -213,7 +218,6 @@ describe('attachment', () => { ], }); await waitFor(() => { - /* eslint-disable jest-dom/prefer-in-document */ const Card = queryAllByTestId('card-attachment'); expect(Card).toHaveLength(3); diff --git a/src/components/Attachment/__tests__/AttachmentActions.test.js b/src/components/Attachment/__tests__/AttachmentActions.test.js index 9fb11d6fc5..d3074d2e0e 100644 --- a/src/components/Attachment/__tests__/AttachmentActions.test.js +++ b/src/components/Attachment/__tests__/AttachmentActions.test.js @@ -48,7 +48,6 @@ describe('AttachmentActions', () => { fireEvent.click(getByTestId(actions[1].name)); await waitFor(() => { - // eslint-disable-next-line jest/prefer-called-with expect(actionHandler).toHaveBeenCalledTimes(2); }); }); diff --git a/src/components/Attachment/__tests__/Audio.test.js b/src/components/Attachment/__tests__/Audio.test.js index ff53883253..b88b201091 100644 --- a/src/components/Attachment/__tests__/Audio.test.js +++ b/src/components/Attachment/__tests__/Audio.test.js @@ -15,7 +15,7 @@ const originalConsoleError = console.error; jest.spyOn(console, 'error').mockImplementationOnce((...errorOrTextorArg) => { const msg = Array.isArray(errorOrTextorArg) ? errorOrTextorArg[0] - : errorOrTextorArg.message ?? errorOrTextorArg; + : (errorOrTextorArg.message ?? errorOrTextorArg); if (msg.match('Not implemented')) return; originalConsoleError(...errorOrTextorArg); }); @@ -70,7 +70,9 @@ describe('Audio', () => { .spyOn(HTMLDivElement.prototype, 'getBoundingClientRect') .mockImplementationOnce(() => ({ width: 120, x: 0 })); - jest.spyOn(HTMLAudioElement.prototype, 'currentTime', 'set').mockImplementationOnce(() => {}); + jest + .spyOn(HTMLAudioElement.prototype, 'currentTime', 'set') + .mockImplementationOnce(() => {}); jest.spyOn(HTMLAudioElement.prototype, 'duration', 'get').mockReturnValue(120); act(() => { @@ -171,7 +173,10 @@ describe('Audio', () => { jest.advanceTimersByTime(2000); await waitFor(() => { expect(audioPauseMock).toHaveBeenCalledWith(); - expect(addNotificationSpy).toHaveBeenCalledWith('Failed to play the recording', 'error'); + expect(addNotificationSpy).toHaveBeenCalledWith( + 'Failed to play the recording', + 'error', + ); }); jest.useRealTimers(); @@ -185,8 +190,12 @@ describe('Audio', () => { og: AUDIO, }); const audio = container.querySelector('audio'); - const audioPlayMock = jest.spyOn(audio, 'play').mockRejectedValueOnce(new Error(errorText)); - const audioCanPlayTypeMock = jest.spyOn(audio, 'canPlayType').mockReturnValue('maybe'); + const audioPlayMock = jest + .spyOn(audio, 'play') + .mockRejectedValueOnce(new Error(errorText)); + const audioCanPlayTypeMock = jest + .spyOn(audio, 'canPlayType') + .mockReturnValue('maybe'); expect(await playButton()).toBeInTheDocument(); expect(await pauseButton()).not.toBeInTheDocument(); @@ -230,8 +239,12 @@ describe('Audio', () => { it('should show the correct progress', async () => { const { container } = renderComponent({ og: AUDIO }); - jest.spyOn(HTMLAudioElement.prototype, 'duration', 'get').mockImplementationOnce(() => 100); - jest.spyOn(HTMLAudioElement.prototype, 'currentTime', 'get').mockImplementationOnce(() => 50); + jest + .spyOn(HTMLAudioElement.prototype, 'duration', 'get') + .mockImplementationOnce(() => 100); + jest + .spyOn(HTMLAudioElement.prototype, 'currentTime', 'get') + .mockImplementationOnce(() => 50); const audioElement = container.querySelector('audio'); fireEvent.timeUpdate(audioElement); diff --git a/src/components/Attachment/__tests__/Card.test.js b/src/components/Attachment/__tests__/Card.test.js index 826f0a87d7..5582dd599f 100644 --- a/src/components/Attachment/__tests__/Card.test.js +++ b/src/components/Attachment/__tests__/Card.test.js @@ -142,7 +142,8 @@ describe('Card', () => { thumb_url: undefined, }, props: 'asset and neither og image URL is available', - render: 'content part with title and text only and without the header part of the Card', + render: + 'content part with title and text only and without the header part of the Card', }, ); } else if (type === 'video') { diff --git a/src/components/Attachment/__tests__/VoiceRecording.test.js b/src/components/Attachment/__tests__/VoiceRecording.test.js index ac77046427..fb145257f5 100644 --- a/src/components/Attachment/__tests__/VoiceRecording.test.js +++ b/src/components/Attachment/__tests__/VoiceRecording.test.js @@ -16,7 +16,9 @@ const attachment = generateVoiceRecordingAttachment(); window.ResizeObserver = ResizeObserverMock; -jest.spyOn(HTMLDivElement.prototype, 'getBoundingClientRect').mockReturnValue({ width: 120 }); +jest + .spyOn(HTMLDivElement.prototype, 'getBoundingClientRect') + .mockReturnValue({ width: 120 }); const clickPlay = async () => { await act(async () => { @@ -57,7 +59,9 @@ describe('VoiceRecordingPlayer', () => { afterAll(jest.restoreAllMocks); it('should not render the component if asset_url is missing', () => { - const { container } = renderComponent({ attachment: { ...attachment, asset_url: undefined } }); + const { container } = renderComponent({ + attachment: { ...attachment, asset_url: undefined }, + }); expect(container).toBeEmptyDOMElement(); }); it('should render title if present', () => { @@ -65,7 +69,9 @@ describe('VoiceRecordingPlayer', () => { expect(getByTestId('voice-recording-title')).toHaveTextContent(attachment.title); }); it('should render fallback title if attachment title not present', () => { - const { getByTestId } = renderComponent({ attachment: { ...attachment, title: undefined } }); + const { getByTestId } = renderComponent({ + attachment: { ...attachment, title: undefined }, + }); expect(getByTestId('voice-recording-title')).toHaveTextContent(FALLBACK_TITLE); }); @@ -129,8 +135,12 @@ describe('VoiceRecordingPlayer', () => { it('should show the correct progress', async () => { const { container } = renderComponent({ attachment }); - jest.spyOn(HTMLAudioElement.prototype, 'duration', 'get').mockImplementationOnce(() => 100); - jest.spyOn(HTMLAudioElement.prototype, 'currentTime', 'get').mockImplementationOnce(() => 50); + jest + .spyOn(HTMLAudioElement.prototype, 'duration', 'get') + .mockImplementationOnce(() => 100); + jest + .spyOn(HTMLAudioElement.prototype, 'currentTime', 'get') + .mockImplementationOnce(() => 50); const audioElement = container.querySelector('audio'); fireEvent.timeUpdate(audioElement); diff --git a/src/components/Attachment/__tests__/WaveProgressBar.test.js b/src/components/Attachment/__tests__/WaveProgressBar.test.js index 91be6ab093..85dcc78af3 100644 --- a/src/components/Attachment/__tests__/WaveProgressBar.test.js +++ b/src/components/Attachment/__tests__/WaveProgressBar.test.js @@ -28,21 +28,28 @@ describe('WaveProgressBar', () => { it('is not rendered if no space available', () => { getBoundingClientRect.mockReturnValueOnce({ width: 0 }); - render(); + render( + , + ); expect(screen.queryByTestId(BAR_ROOT_TEST_ID)).not.toBeInTheDocument(); }); it('renders with default number of bars', () => { render(); const root = screen.getByTestId(BAR_ROOT_TEST_ID); - expect(root.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-gap-width')).toBe( - '1px', - ); + expect( + root.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-gap-width'), + ).toBe('1px'); const bars = screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID); expect( bars.every( (b) => - b.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-width') === '2px', + b.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-width') === + '2px', ), ).toBeTruthy(); expect(screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID)).toHaveLength(40); @@ -58,14 +65,15 @@ describe('WaveProgressBar', () => { />, ); const root = screen.getByTestId(BAR_ROOT_TEST_ID); - expect(root.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-gap-width')).toBe( - '5px', - ); + expect( + root.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-gap-width'), + ).toBe('5px'); const bars = screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID); expect( bars.every( (b) => - b.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-width') === '3px', + b.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-width') === + '3px', ), ).toBeTruthy(); expect(bars).toHaveLength(15); @@ -80,14 +88,15 @@ describe('WaveProgressBar', () => { activeObserver.cb([{ contentRect: { width: 21 } }]); }); const root = screen.getByTestId(BAR_ROOT_TEST_ID); - expect(root.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-gap-width')).toBe( - '1px', - ); + expect( + root.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-gap-width'), + ).toBe('1px'); const bars = screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID); expect( bars.every( (b) => - b.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-width') === '2px', + b.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-width') === + '2px', ), ).toBeTruthy(); expect(screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID)).toHaveLength(7); @@ -102,7 +111,11 @@ describe('WaveProgressBar', () => { it('is rendered with zero progress by default if waveform data is available', () => { const { container } = render( - , + , ); expect(container).toMatchSnapshot(); expect(screen.getByTestId(PROGRESS_INDICATOR_TEST_ID)).toBeInTheDocument(); diff --git a/src/components/Attachment/__tests__/audioSampling.test.js b/src/components/Attachment/__tests__/audioSampling.test.js index f018ad5a0d..8d677f6899 100644 --- a/src/components/Attachment/__tests__/audioSampling.test.js +++ b/src/components/Attachment/__tests__/audioSampling.test.js @@ -15,7 +15,9 @@ describe('amplitude sampling', () => { }); it('should return original values if the original sample size equals the target', () => { - expect(upSample(originalSample, originalSample.length)).toHaveLength(originalSample.length); + expect(upSample(originalSample, originalSample.length)).toHaveLength( + originalSample.length, + ); }); it('should fill each bucket to reach the target sample size', () => { @@ -35,7 +37,9 @@ describe('amplitude sampling', () => { }); it('should return original values if the original sample size equals the target', () => { - expect(downSample(originalSample, originalSample.length)).toHaveLength(originalSample.length); + expect(downSample(originalSample, originalSample.length)).toHaveLength( + originalSample.length, + ); }); it('should return a mean of original values if the target output size is 1', () => { diff --git a/src/components/Attachment/attachment-sizing.tsx b/src/components/Attachment/attachment-sizing.tsx index 440961d6f6..2806d67e89 100644 --- a/src/components/Attachment/attachment-sizing.tsx +++ b/src/components/Attachment/attachment-sizing.tsx @@ -1,7 +1,10 @@ import type { Attachment } from 'stream-chat'; import * as linkify from 'linkifyjs'; -export const getImageAttachmentConfiguration = (attachment: Attachment, element: HTMLElement) => { +export const getImageAttachmentConfiguration = ( + attachment: Attachment, + element: HTMLElement, +) => { let newUrl = undefined; const urlToTest = attachment.image_url || attachment.thumb_url || ''; @@ -60,7 +63,10 @@ const getSizingRestrictions = (url: URL, htmlElement: HTMLElement) => { const cssSizeRestriction = getCSSSizeRestrictions(htmlElement); let resizeDimensions: { height: number; width: number } | undefined; - if ((cssSizeRestriction.maxHeight || cssSizeRestriction.height) && cssSizeRestriction.maxWidth) { + if ( + (cssSizeRestriction.maxHeight || cssSizeRestriction.height) && + cssSizeRestriction.maxWidth + ) { resizeDimensions = getResizeDimensions( originalHeight, originalWidth, @@ -87,7 +93,9 @@ const getResizeDimensions = ( const getCSSSizeRestrictions = (htmlElement: HTMLElement) => { const computedStylesheet = getComputedStyle(htmlElement); - const height = getValueRepresentationOfCSSProperty(computedStylesheet.getPropertyValue('height')); + const height = getValueRepresentationOfCSSProperty( + computedStylesheet.getPropertyValue('height'), + ); const maxHeight = getValueRepresentationOfCSSProperty( computedStylesheet.getPropertyValue('max-height'), ); @@ -112,7 +120,10 @@ const getValueRepresentationOfCSSProperty = (property: string) => { return isNaN(number) ? undefined : number; }; -const addResizingParamsToUrl = (resizeDimensions: { height: number; width: number }, url: URL) => { +const addResizingParamsToUrl = ( + resizeDimensions: { height: number; width: number }, + url: URL, +) => { url.searchParams.set('h', resizeDimensions.height.toString()); url.searchParams.set('w', resizeDimensions.width.toString()); }; diff --git a/src/components/Attachment/audioSampling.ts b/src/components/Attachment/audioSampling.ts index ff8b7638d8..d9ece97cfb 100644 --- a/src/components/Attachment/audioSampling.ts +++ b/src/components/Attachment/audioSampling.ts @@ -4,8 +4,8 @@ export const resampleWaveformData = (waveformData: number[], amplitudesCount: nu waveformData.length === amplitudesCount ? waveformData : waveformData.length > amplitudesCount - ? downSample(waveformData, amplitudesCount) - : upSample(waveformData, amplitudesCount); + ? downSample(waveformData, amplitudesCount) + : upSample(waveformData, amplitudesCount); /** * The downSample function uses the Largest-Triangle-Three-Buckets (LTTB) algorithm. @@ -42,14 +42,21 @@ export function downSample(data: number[], targetOutputSize: number): number[] { currentPointIndex < nextBucketStartIndex; currentPointIndex++ ) { - const countUnitsBetweenAtoB = Math.abs(currentPointIndex - currentBucketStartIndex) + 1; + const countUnitsBetweenAtoB = + Math.abs(currentPointIndex - currentBucketStartIndex) + 1; const countUnitsBetweenBtoC = countUnitsBetweenAtoC - countUnitsBetweenAtoB; const currentPointValue = data[currentPointIndex]; triangleArea = triangleAreaHeron( - triangleBase(Math.abs(previousBucketRefPoint - currentPointValue), countUnitsBetweenAtoB), + triangleBase( + Math.abs(previousBucketRefPoint - currentPointValue), + countUnitsBetweenAtoB, + ), triangleBase(Math.abs(currentPointValue - nextBucketMean), countUnitsBetweenBtoC), - triangleBase(Math.abs(previousBucketRefPoint - nextBucketMean), countUnitsBetweenAtoC), + triangleBase( + Math.abs(previousBucketRefPoint - nextBucketMean), + countUnitsBetweenAtoC, + ), ); if (triangleArea > maxArea) { @@ -72,8 +79,13 @@ const triangleAreaHeron = (a: number, b: number, c: number) => { return Math.sqrt(s * (s - a) * (s - b) * (s - c)); }; const triangleBase = (a: number, b: number) => Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)); -const mean = (values: number[]) => values.reduce((acc, value) => acc + value, 0) / values.length; -const getNextBucketMean = (data: number[], currentBucketIndex: number, bucketSize: number) => { +const mean = (values: number[]) => + values.reduce((acc, value) => acc + value, 0) / values.length; +const getNextBucketMean = ( + data: number[], + currentBucketIndex: number, + bucketSize: number, +) => { const nextBucketStartIndex = Math.floor(currentBucketIndex * bucketSize) + 1; let nextNextBucketStartIndex = Math.floor((currentBucketIndex + 1) * bucketSize) + 1; nextNextBucketStartIndex = @@ -88,7 +100,9 @@ export const upSample = (values: number[], targetSize: number) => { } if (values.length > targetSize) { - console.warn('Requested to extend the waveformData that is longer than the target list size'); + console.warn( + 'Requested to extend the waveformData that is longer than the target list size', + ); return values; } diff --git a/src/components/Attachment/components/FileSizeIndicator.tsx b/src/components/Attachment/components/FileSizeIndicator.tsx index a9f26e55e7..5fdc0da421 100644 --- a/src/components/Attachment/components/FileSizeIndicator.tsx +++ b/src/components/Attachment/components/FileSizeIndicator.tsx @@ -11,7 +11,10 @@ type FileSizeIndicatorProps = { maximumFractionDigits?: number; }; -export const FileSizeIndicator = ({ fileSize, maximumFractionDigits }: FileSizeIndicatorProps) => { +export const FileSizeIndicator = ({ + fileSize, + maximumFractionDigits, +}: FileSizeIndicatorProps) => { if (!(fileSize && Number.isFinite(Number(fileSize)))) return null; return ( diff --git a/src/components/Attachment/components/ProgressBar.tsx b/src/components/Attachment/components/ProgressBar.tsx index 1e5d5fd10a..20a17b3d16 100644 --- a/src/components/Attachment/components/ProgressBar.tsx +++ b/src/components/Attachment/components/ProgressBar.tsx @@ -8,7 +8,10 @@ export type ProgressBarProps = { export const ProgressBar = ({ className, onClick, progress }: ProgressBarProps) => (
; export type GalleryAttachment< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { images: Attachment[]; type: 'gallery'; }; export type RenderAttachmentProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Omit, 'attachments'> & { attachment: Attachment; }; export type RenderGalleryProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Omit, 'attachments'> & { attachment: GalleryAttachment; }; export const isLocalAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: UnknownType, ): attachment is LocalAttachment => !!(attachment.localMetadata as LocalAttachment)?.id; export const isScrapedContent = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment, ) => attachment.og_scrape_url || attachment.title_link; export const isUploadedImage = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment, ) => attachment.type === 'image' && !isScrapedContent(attachment); export const isLocalImageAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment | LocalAttachment, ): attachment is LocalImageAttachment => isUploadedImage(attachment) && isLocalAttachment(attachment); export const isGalleryAttachmentType = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( output: Attachment | GalleryAttachment, ): output is GalleryAttachment => Array.isArray(output.images); export const isAudioAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment | LocalAttachment, ) => attachment.type === 'audio'; export const isLocalAudioAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment | LocalAttachment, ): attachment is LocalAudioAttachment => isAudioAttachment(attachment) && isLocalAttachment(attachment); export const isVoiceRecordingAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment | LocalAttachment, ): attachment is VoiceRecordingAttachment => attachment.type === 'voiceRecording'; export const isLocalVoiceRecordingAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment | LocalAttachment, ): attachment is LocalVoiceRecordingAttachment => isVoiceRecordingAttachment(attachment) && isLocalAttachment(attachment); export const isFileAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment | LocalAttachment, ) => @@ -109,22 +114,23 @@ export const isFileAttachment = < ); export const isLocalFileAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment | LocalAttachment, ): attachment is LocalFileAttachment => isFileAttachment(attachment) && isLocalAttachment(attachment); export const isMediaAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment | LocalAttachment, ) => - (attachment.mime_type && SUPPORTED_VIDEO_FORMATS.indexOf(attachment.mime_type) !== -1) || + (attachment.mime_type && + SUPPORTED_VIDEO_FORMATS.indexOf(attachment.mime_type) !== -1) || attachment.type === 'video'; export const isLocalMediaAttachment = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( attachment: Attachment | LocalAttachment, ): attachment is LocalVideoAttachment => @@ -135,7 +141,10 @@ export const isSvgAttachment = (attachment: Attachment) => { return filename.toLowerCase().endsWith('.svg'); }; -export const divMod = (num: number, divisor: number) => [Math.floor(num / divisor), num % divisor]; +export const divMod = (num: number, divisor: number) => [ + Math.floor(num / divisor), + num % divisor, +]; export const displayDuration = (totalSeconds?: number) => { if (!totalSeconds || totalSeconds < 0) return '00:00'; diff --git a/src/components/AutoCompleteTextarea/Item.jsx b/src/components/AutoCompleteTextarea/Item.jsx index 19cf35b047..4945d682d5 100644 --- a/src/components/AutoCompleteTextarea/Item.jsx +++ b/src/components/AutoCompleteTextarea/Item.jsx @@ -13,7 +13,10 @@ export const Item = React.forwardRef(function Item(props, innerRef) { } = props; const handleSelect = useCallback(() => onSelectHandler(item), [item, onSelectHandler]); - const handleClick = useCallback((event) => onClickHandler(event, item), [item, onClickHandler]); + const handleClick = useCallback( + (event) => onClickHandler(event, item), + [item, onClickHandler], + ); return (
  • - values.findIndex((value) => (value.id ? value.id === item.id : value.name === item.name)), + values.findIndex((value) => + value.id ? value.id === item.id : value.name === item.name, + ), [values], ); @@ -98,7 +100,10 @@ export const List = ({ }); } - if ((event.key === 'Enter' || event.key === 'Tab') && selectedItemIndex !== undefined) { + if ( + (event.key === 'Enter' || event.key === 'Tab') && + selectedItemIndex !== undefined + ) { handleClick(event, values[selectedItemIndex]); } @@ -134,7 +139,10 @@ export const List = ({ [propValue, selectionEnd, currentTrigger], ); - const restructuredValues = useMemo(() => values.map(restructureItem), [values, restructureItem]); + const restructuredValues = useMemo( + () => values.map(restructureItem), + [values, restructureItem], + ); return (
      diff --git a/src/components/AutoCompleteTextarea/Textarea.jsx b/src/components/AutoCompleteTextarea/Textarea.jsx index 0dfd5a2b84..65356bc327 100644 --- a/src/components/AutoCompleteTextarea/Textarea.jsx +++ b/src/components/AutoCompleteTextarea/Textarea.jsx @@ -179,7 +179,11 @@ export class ReactTextareaAutocomplete extends React.Component { showCommandsList, showMentionsList, } = this.props; - const { currentTrigger: stateTrigger, selectionEnd, value: textareaValue } = this.state; + const { + currentTrigger: stateTrigger, + selectionEnd, + value: textareaValue, + } = this.state; const currentTrigger = showCommandsList ? '/' : showMentionsList ? '@' : stateTrigger; @@ -194,7 +198,9 @@ export class ReactTextareaAutocomplete extends React.Component { return startToken + token.length; default: if (!Number.isInteger(position)) { - throw new Error('RTA: caretPosition should be "start", "next", "end" or number.'); + throw new Error( + 'RTA: caretPosition should be "start", "next", "end" or number.', + ); } return position; @@ -204,13 +210,14 @@ export class ReactTextareaAutocomplete extends React.Component { const textToModify = showCommandsList ? '/' : showMentionsList - ? '@' - : textareaValue.slice(0, selectionEnd); + ? '@' + : textareaValue.slice(0, selectionEnd); const startOfTokenPosition = textToModify.lastIndexOf(currentTrigger); // we add space after emoji is selected if a caret position is next - const newTokenString = newToken.caretPosition === 'next' ? `${newToken.text} ` : newToken.text; + const newTokenString = + newToken.caretPosition === 'next' ? `${newToken.text} ` : newToken.text; const newCaretPosition = computeCaretPosition( newToken.caretPosition, @@ -450,7 +457,6 @@ export class ReactTextareaAutocomplete extends React.Component { 'value', ]; - // eslint-disable-next-line for (const prop in props) { if (notSafe.includes(prop)) delete props[prop]; } @@ -467,7 +473,8 @@ export class ReactTextareaAutocomplete extends React.Component { }; _changeHandler = (e) => { - const { minChar, movePopupAsYouType, onCaretPositionChange, onChange, trigger } = this.props; + const { minChar, movePopupAsYouType, onCaretPositionChange, onChange, trigger } = + this.props; const { left, top } = this.state; const textarea = e.target; @@ -499,7 +506,8 @@ export class ReactTextareaAutocomplete extends React.Component { lastToken = tokenMatch && tokenMatch[tokenMatch.length - 1].trim(); - currentTrigger = (lastToken && Object.keys(trigger).find((a) => a === lastToken[0])) || null; + currentTrigger = + (lastToken && Object.keys(trigger).find((a) => a === lastToken[0])) || null; } /* @@ -636,7 +644,9 @@ export class ReactTextareaAutocomplete extends React.Component { triggerProps.component = showCommandsList ? CommandItem : UserItem; triggerProps.currentTrigger = showCommandsList ? '/' : '@'; - triggerProps.getTextToReplace = this._getTextToReplace(showCommandsList ? '/' : '@'); + triggerProps.getTextToReplace = this._getTextToReplace( + showCommandsList ? '/' : '@', + ); triggerProps.getSelectedItem = this._getItemOnSelect(showCommandsList ? '/' : '@'); triggerProps.selectionEnd = 1; triggerProps.value = showCommandsList ? '/' : '@'; diff --git a/src/components/AutoCompleteTextarea/utils.js b/src/components/AutoCompleteTextarea/utils.js index 97e323b8d4..54a32d8011 100644 --- a/src/components/AutoCompleteTextarea/utils.js +++ b/src/components/AutoCompleteTextarea/utils.js @@ -11,11 +11,13 @@ export function defaultScrollToItem(container, item) { const actualScrollTop = container.scrollTop; const itemOffsetTop = item.offsetTop; - if (itemOffsetTop < actualScrollTop + containerHight && actualScrollTop < itemOffsetTop) { + if ( + itemOffsetTop < actualScrollTop + containerHight && + actualScrollTop < itemOffsetTop + ) { return; } - // eslint-disable-next-line container.scrollTop = itemOffsetTop; } @@ -34,7 +36,9 @@ export const triggerPropsCheck = ({ trigger }) => { const [triggerChar, settings] = triggers[i]; if (typeof triggerChar !== 'string' || triggerChar.length !== 1) { - return Error('Invalid prop trigger. Keys of the object has to be string / one character.'); + return Error( + 'Invalid prop trigger. Keys of the object has to be string / one character.', + ); } // $FlowFixMe diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index 4c8ab20059..e64fb3efc0 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -9,7 +9,7 @@ import { getWholeChar } from '../../utils'; import type { DefaultStreamChatGenerics } from '../../types/types'; export type AvatarProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { /** Custom root element class that will be merged with the default class */ className?: string; @@ -29,7 +29,7 @@ export type AvatarProps< * A round avatar image with fallback to username's first letter */ export const Avatar = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: AvatarProps, ) => { @@ -75,7 +75,10 @@ export const Avatar = < ) : ( <> {!!initials.length && ( -
      +
      {initials}
      )} diff --git a/src/components/Avatar/ChannelAvatar.tsx b/src/components/Avatar/ChannelAvatar.tsx index eab6b088f9..25e64fb1cf 100644 --- a/src/components/Avatar/ChannelAvatar.tsx +++ b/src/components/Avatar/ChannelAvatar.tsx @@ -3,11 +3,11 @@ import { Avatar, AvatarProps, GroupAvatar, GroupAvatarProps } from './index'; import type { DefaultStreamChatGenerics } from '../../types'; export type ChannelAvatarProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Partial & AvatarProps; export const ChannelAvatar = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ groupChannelDisplayInfo, image, @@ -16,7 +16,9 @@ export const ChannelAvatar = < ...sharedProps }: ChannelAvatarProps) => { if (groupChannelDisplayInfo) { - return ; + return ( + + ); } return ; }; diff --git a/src/components/Avatar/GroupAvatar.tsx b/src/components/Avatar/GroupAvatar.tsx index 89a127d880..a188f6b9e5 100644 --- a/src/components/Avatar/GroupAvatar.tsx +++ b/src/components/Avatar/GroupAvatar.tsx @@ -3,7 +3,10 @@ import React from 'react'; import { Avatar, AvatarProps } from './Avatar'; import { GroupChannelDisplayInfo } from '../ChannelPreview'; -export type GroupAvatarProps = Pick & { +export type GroupAvatarProps = Pick< + AvatarProps, + 'className' | 'onClick' | 'onMouseOver' +> & { /** Mapping of image URLs to names which initials will be used as fallbacks in case image assets fail to load. */ groupChannelDisplayInfo: GroupChannelDisplayInfo; }; diff --git a/src/components/Avatar/__tests__/Avatar.test.js b/src/components/Avatar/__tests__/Avatar.test.js index 4e0805a4bb..1c82df14cf 100644 --- a/src/components/Avatar/__tests__/Avatar.test.js +++ b/src/components/Avatar/__tests__/Avatar.test.js @@ -9,7 +9,7 @@ const AVATAR_ROOT_TEST_ID = 'avatar'; const AVATAR_FALLBACK_TEST_ID = 'avatar-fallback'; const AVATAR_IMG_TEST_ID = 'avatar-img'; -afterEach(cleanup); // eslint-disable-line +afterEach(cleanup); describe('Avatar', () => { it('should render component with default props', () => { @@ -61,7 +61,9 @@ describe('Avatar', () => { it('should render initials as alt and title', () => { const name = 'Cherry Blossom'; - const { getByAltText, getByTitle } = render(); + const { getByAltText, getByTitle } = render( + , + ); expect(getByTitle(name)).toBeInTheDocument(); expect(getByAltText(name[0])).toBeInTheDocument(); @@ -94,7 +96,9 @@ describe('Avatar', () => { }); it('should render fallback initials on img error', () => { - const { getByTestId, queryByTestId } = render(); + const { getByTestId, queryByTestId } = render( + , + ); const img = getByTestId(AVATAR_IMG_TEST_ID); expect(img).toBeInTheDocument(); @@ -105,7 +109,9 @@ describe('Avatar', () => { }); it('should render new img on props change for errored img', () => { - const { getByTestId, queryByTestId, rerender } = render(); + const { getByTestId, queryByTestId, rerender } = render( + , + ); fireEvent.error(getByTestId(AVATAR_IMG_TEST_ID)); expect(queryByTestId(AVATAR_IMG_TEST_ID)).not.toBeInTheDocument(); diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index 1d68d1b22b..0ef286dec3 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -100,7 +100,7 @@ import { useThreadContext } from '../Threads'; import { CHANNEL_CONTAINER_ID } from './constants'; type ChannelPropsForwardedToComponentContext< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick< ComponentContextValue, | 'Attachment' @@ -166,7 +166,7 @@ type ChannelPropsForwardedToComponentContext< >; const isUserResponseArray = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( output: string[] | UserResponse[], ): output is UserResponse[] => @@ -174,7 +174,7 @@ const isUserResponseArray = < export type ChannelProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, - V extends CustomTrigger = CustomTrigger + V extends CustomTrigger = CustomTrigger, > = ChannelPropsForwardedToComponentContext & { /** List of accepted file types */ acceptedFiles?: string[]; @@ -255,7 +255,7 @@ export type ChannelProps< }; const ChannelContainer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ children, className: additionalClassName, @@ -275,7 +275,7 @@ const ChannelContainer = < const UnMemoizedChannel = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, - V extends CustomTrigger = CustomTrigger + V extends CustomTrigger = CustomTrigger, >( props: PropsWithChildren>, ) => { @@ -286,9 +286,8 @@ const UnMemoizedChannel = < LoadingIndicator = DefaultLoadingIndicator, } = props; - const { channel: contextChannel, channelsQueryState } = useChatContext( - 'Channel', - ); + const { channel: contextChannel, channelsQueryState } = + useChatContext('Channel'); const channel = propsChannel || contextChannel; @@ -317,7 +316,7 @@ const UnMemoizedChannel = < const ChannelInner = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, - V extends CustomTrigger = CustomTrigger + V extends CustomTrigger = CustomTrigger, >( props: PropsWithChildren< ChannelProps & { @@ -360,12 +359,8 @@ const ChannelInner = < [propChannelQueryOptions], ); - const { - client, - customClasses, - latestMessageDatesByChannels, - mutes, - } = useChatContext('Channel'); + const { client, customClasses, latestMessageDatesByChannels, mutes } = + useChatContext('Channel'); const { t } = useTranslationContext('Channel'); const chatContainerClass = getChatContainerClass(customClasses?.chatContainer); const windowsEmojiClass = useImageFlagEmojisOnWindowsClass(); @@ -374,7 +369,8 @@ const ChannelInner = < const [channelConfig, setChannelConfig] = useState(channel.getConfig()); const [notifications, setNotifications] = useState([]); const [quotedMessage, setQuotedMessage] = useState>(); - const [channelUnreadUiState, _setChannelUnreadUiState] = useState(); + const [channelUnreadUiState, _setChannelUnreadUiState] = + useState(); const notificationTimeouts = useRef>([]); @@ -457,7 +453,14 @@ const ChannelInner = < 500, { leading: true, trailing: false }, ), - [activeUnreadHandler, channel, channelConfig, doMarkReadRequest, setChannelUnreadUiState, t], + [ + activeUnreadHandler, + channel, + channelConfig, + doMarkReadRequest, + setChannelUnreadUiState, + t, + ], ); const handleEvent = async (event: Event) => { @@ -469,7 +472,8 @@ const ChannelInner = < }); } - if (event.type === 'user.watching.start' || event.type === 'user.watching.stop') return; + if (event.type === 'user.watching.start' || event.type === 'user.watching.stop') + return; if (event.type === 'typing.start' || event.type === 'typing.stop') { return dispatch({ channel, type: 'setTyping' }); @@ -480,10 +484,15 @@ const ChannelInner = < } if (event.type === 'message.new') { - const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel; + const mainChannelUpdated = + !event.message?.parent_id || event.message?.show_in_channel; if (mainChannelUpdated) { - if (document.hidden && channelConfig?.read_events && !channel.muteStatus().muted) { + if ( + document.hidden && + channelConfig?.read_events && + !channel.muteStatus().muted + ) { const unread = channel.countUnread(lastRead.current); if (activeUnreadHandler) { @@ -560,7 +569,8 @@ const ChannelInner = < if (typeof member === 'string') { userId = member; } else if (typeof member === 'object') { - const { user, user_id } = member as ChannelMemberResponse; + const { user, user_id } = + member as ChannelMemberResponse; userId = user_id || user?.id; } if (userId) { @@ -658,13 +668,21 @@ const ChannelInner = < ); const loadMore = async (limit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE) => { - if (!online.current || !window.navigator.onLine || !channel.state.messagePagination.hasPrev) + if ( + !online.current || + !window.navigator.onLine || + !channel.state.messagePagination.hasPrev + ) return 0; // prevent duplicate loading events... const oldestMessage = state?.messages?.[0]; - if (state.loadingMore || state.loadingMoreNewer || oldestMessage?.status !== 'received') { + if ( + state.loadingMore || + state.loadingMoreNewer || + oldestMessage?.status !== 'received' + ) { return 0; } @@ -691,7 +709,11 @@ const ChannelInner = < }; const loadMoreNewer = async (limit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE) => { - if (!online.current || !window.navigator.onLine || !channel.state.messagePagination.hasNext) + if ( + !online.current || + !window.navigator.onLine || + !channel.state.messagePagination.hasNext + ) return 0; const newestMessage = state?.messages?.[state?.messages?.length - 1]; @@ -722,170 +744,195 @@ const ChannelInner = < return queryResponse.messages.length; }; - const clearHighlightedMessageTimeoutId = useRef | null>(null); + const clearHighlightedMessageTimeoutId = useRef | null>( + null, + ); - const jumpToMessage: ChannelActionContextValue['jumpToMessage'] = useCallback( - async ( - messageId, - messageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, - highlightDuration = DEFAULT_HIGHLIGHT_DURATION, - ) => { - dispatch({ loadingMore: true, type: 'setLoadingMore' }); - await channel.state.loadMessageIntoState(messageId, undefined, messageLimit); + const jumpToMessage: ChannelActionContextValue['jumpToMessage'] = + useCallback( + async ( + messageId, + messageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, + highlightDuration = DEFAULT_HIGHLIGHT_DURATION, + ) => { + dispatch({ loadingMore: true, type: 'setLoadingMore' }); + await channel.state.loadMessageIntoState(messageId, undefined, messageLimit); + + loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); + dispatch({ + hasMoreNewer: channel.state.messagePagination.hasNext, + highlightedMessageId: messageId, + type: 'jumpToMessageFinished', + }); + + if (clearHighlightedMessageTimeoutId.current) { + clearTimeout(clearHighlightedMessageTimeoutId.current); + } + clearHighlightedMessageTimeoutId.current = setTimeout(() => { + clearHighlightedMessageTimeoutId.current = null; + dispatch({ type: 'clearHighlightedMessage' }); + }, highlightDuration); + }, + [channel, loadMoreFinished], + ); + + const jumpToLatestMessage: ChannelActionContextValue['jumpToLatestMessage'] = + useCallback(async () => { + await channel.state.loadMessageIntoState('latest'); loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); dispatch({ - hasMoreNewer: channel.state.messagePagination.hasNext, - highlightedMessageId: messageId, - type: 'jumpToMessageFinished', + type: 'jumpToLatestMessage', }); + }, [channel, loadMoreFinished]); - if (clearHighlightedMessageTimeoutId.current) { - clearTimeout(clearHighlightedMessageTimeoutId.current); - } + const jumpToFirstUnreadMessage: ChannelActionContextValue['jumpToFirstUnreadMessage'] = + useCallback( + async ( + queryMessageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, + highlightDuration = DEFAULT_HIGHLIGHT_DURATION, + ) => { + if (!channelUnreadUiState?.unread_messages) return; + let lastReadMessageId = channelUnreadUiState?.last_read_message_id; + let firstUnreadMessageId = channelUnreadUiState?.first_unread_message_id; + let isInCurrentMessageSet = false; + + if (firstUnreadMessageId) { + const result = findInMsgSetById(firstUnreadMessageId, channel.state.messages); + isInCurrentMessageSet = result.index !== -1; + } else if (lastReadMessageId) { + const result = findInMsgSetById(lastReadMessageId, channel.state.messages); + isInCurrentMessageSet = !!result.target; + firstUnreadMessageId = + result.index > -1 ? channel.state.messages[result.index + 1]?.id : undefined; + } else { + const lastReadTimestamp = channelUnreadUiState.last_read.getTime(); + const { index: lastReadMessageIndex, target: lastReadMessage } = + findInMsgSetByDate( + channelUnreadUiState.last_read, + channel.state.messages, + true, + ); + + if (lastReadMessage) { + firstUnreadMessageId = channel.state.messages[lastReadMessageIndex + 1]?.id; + isInCurrentMessageSet = !!firstUnreadMessageId; + lastReadMessageId = lastReadMessage.id; + } else { + dispatch({ loadingMore: true, type: 'setLoadingMore' }); + let messages; + try { + messages = ( + await channel.query( + { + messages: { + created_at_around: channelUnreadUiState.last_read.toISOString(), + limit: queryMessageLimit, + }, + }, + 'new', + ) + ).messages; + } catch (e) { + addNotification(t('Failed to jump to the first unread message'), 'error'); + loadMoreFinished( + channel.state.messagePagination.hasPrev, + channel.state.messages, + ); + return; + } - clearHighlightedMessageTimeoutId.current = setTimeout(() => { - clearHighlightedMessageTimeoutId.current = null; - dispatch({ type: 'clearHighlightedMessage' }); - }, highlightDuration); - }, - [channel, loadMoreFinished], - ); + const firstMessageWithCreationDate = messages.find((msg) => msg.created_at); + if (!firstMessageWithCreationDate) { + addNotification(t('Failed to jump to the first unread message'), 'error'); + loadMoreFinished( + channel.state.messagePagination.hasPrev, + channel.state.messages, + ); + return; + } + const firstMessageTimestamp = new Date( + firstMessageWithCreationDate.created_at as string, + ).getTime(); + if (lastReadTimestamp < firstMessageTimestamp) { + // whole channel is unread + firstUnreadMessageId = firstMessageWithCreationDate.id; + } else { + const result = findInMsgSetByDate(channelUnreadUiState.last_read, messages); + lastReadMessageId = result.target?.id; + } + loadMoreFinished( + channel.state.messagePagination.hasPrev, + channel.state.messages, + ); + } + } - const jumpToLatestMessage: ChannelActionContextValue['jumpToLatestMessage'] = useCallback(async () => { - await channel.state.loadMessageIntoState('latest'); - loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); - dispatch({ - type: 'jumpToLatestMessage', - }); - }, [channel, loadMoreFinished]); + if (!firstUnreadMessageId && !lastReadMessageId) { + addNotification(t('Failed to jump to the first unread message'), 'error'); + return; + } - const jumpToFirstUnreadMessage: ChannelActionContextValue['jumpToFirstUnreadMessage'] = useCallback( - async ( - queryMessageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, - highlightDuration = DEFAULT_HIGHLIGHT_DURATION, - ) => { - if (!channelUnreadUiState?.unread_messages) return; - let lastReadMessageId = channelUnreadUiState?.last_read_message_id; - let firstUnreadMessageId = channelUnreadUiState?.first_unread_message_id; - let isInCurrentMessageSet = false; - - if (firstUnreadMessageId) { - const result = findInMsgSetById(firstUnreadMessageId, channel.state.messages); - isInCurrentMessageSet = result.index !== -1; - } else if (lastReadMessageId) { - const result = findInMsgSetById(lastReadMessageId, channel.state.messages); - isInCurrentMessageSet = !!result.target; - firstUnreadMessageId = - result.index > -1 ? channel.state.messages[result.index + 1]?.id : undefined; - } else { - const lastReadTimestamp = channelUnreadUiState.last_read.getTime(); - const { index: lastReadMessageIndex, target: lastReadMessage } = findInMsgSetByDate( - channelUnreadUiState.last_read, - channel.state.messages, - true, - ); - - if (lastReadMessage) { - firstUnreadMessageId = channel.state.messages[lastReadMessageIndex + 1]?.id; - isInCurrentMessageSet = !!firstUnreadMessageId; - lastReadMessageId = lastReadMessage.id; - } else { + if (!isInCurrentMessageSet) { dispatch({ loadingMore: true, type: 'setLoadingMore' }); - let messages; try { - messages = ( - await channel.query( - { - messages: { - created_at_around: channelUnreadUiState.last_read.toISOString(), - limit: queryMessageLimit, - }, - }, - 'new', - ) - ).messages; + const targetId = (firstUnreadMessageId ?? lastReadMessageId) as string; + await channel.state.loadMessageIntoState( + targetId, + undefined, + queryMessageLimit, + ); + /** + * if the index of the last read message on the page is beyond the half of the page, + * we have arrived to the oldest page of the channel + */ + const indexOfTarget = channel.state.messages.findIndex( + (message) => message.id === targetId, + ) as number; + loadMoreFinished( + channel.state.messagePagination.hasPrev, + channel.state.messages, + ); + firstUnreadMessageId = + firstUnreadMessageId ?? channel.state.messages[indexOfTarget + 1]?.id; } catch (e) { addNotification(t('Failed to jump to the first unread message'), 'error'); - loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); - return; - } - - const firstMessageWithCreationDate = messages.find((msg) => msg.created_at); - if (!firstMessageWithCreationDate) { - addNotification(t('Failed to jump to the first unread message'), 'error'); - loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); + loadMoreFinished( + channel.state.messagePagination.hasPrev, + channel.state.messages, + ); return; } - const firstMessageTimestamp = new Date( - firstMessageWithCreationDate.created_at as string, - ).getTime(); - if (lastReadTimestamp < firstMessageTimestamp) { - // whole channel is unread - firstUnreadMessageId = firstMessageWithCreationDate.id; - } else { - const result = findInMsgSetByDate(channelUnreadUiState.last_read, messages); - lastReadMessageId = result.target?.id; - } - loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); } - } - if (!firstUnreadMessageId && !lastReadMessageId) { - addNotification(t('Failed to jump to the first unread message'), 'error'); - return; - } - - if (!isInCurrentMessageSet) { - dispatch({ loadingMore: true, type: 'setLoadingMore' }); - try { - const targetId = (firstUnreadMessageId ?? lastReadMessageId) as string; - await channel.state.loadMessageIntoState(targetId, undefined, queryMessageLimit); - /** - * if the index of the last read message on the page is beyond the half of the page, - * we have arrived to the oldest page of the channel - */ - const indexOfTarget = channel.state.messages.findIndex( - (message) => message.id === targetId, - ) as number; - loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); - firstUnreadMessageId = - firstUnreadMessageId ?? channel.state.messages[indexOfTarget + 1]?.id; - } catch (e) { + if (!firstUnreadMessageId) { addNotification(t('Failed to jump to the first unread message'), 'error'); - loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); return; } - } + if (!channelUnreadUiState.first_unread_message_id) + _setChannelUnreadUiState({ + ...channelUnreadUiState, + first_unread_message_id: firstUnreadMessageId, + last_read_message_id: lastReadMessageId, + }); - if (!firstUnreadMessageId) { - addNotification(t('Failed to jump to the first unread message'), 'error'); - return; - } - if (!channelUnreadUiState.first_unread_message_id) - _setChannelUnreadUiState({ - ...channelUnreadUiState, - first_unread_message_id: firstUnreadMessageId, - last_read_message_id: lastReadMessageId, + dispatch({ + hasMoreNewer: channel.state.messagePagination.hasNext, + highlightedMessageId: firstUnreadMessageId, + type: 'jumpToMessageFinished', }); - dispatch({ - hasMoreNewer: channel.state.messagePagination.hasNext, - highlightedMessageId: firstUnreadMessageId, - type: 'jumpToMessageFinished', - }); - - if (clearHighlightedMessageTimeoutId.current) { - clearTimeout(clearHighlightedMessageTimeoutId.current); - } + if (clearHighlightedMessageTimeoutId.current) { + clearTimeout(clearHighlightedMessageTimeoutId.current); + } - clearHighlightedMessageTimeoutId.current = setTimeout(() => { - clearHighlightedMessageTimeoutId.current = null; - dispatch({ type: 'clearHighlightedMessage' }); - }, highlightDuration); - }, - [addNotification, channel, loadMoreFinished, t, channelUnreadUiState], - ); + clearHighlightedMessageTimeoutId.current = setTimeout(() => { + clearHighlightedMessageTimeoutId.current = null; + dispatch({ type: 'clearHighlightedMessage' }); + }, highlightDuration); + }, + [addNotification, channel, loadMoreFinished, t, channelUnreadUiState], + ); const deleteMessage = useCallback( async ( @@ -911,7 +958,10 @@ const ChannelInner = < updatedMessage: MessageToSend | StreamMessage, ) => { // add the message to the local channel state - channel.state.addMessageSorted(updatedMessage as MessageResponse, true); + channel.state.addMessageSorted( + updatedMessage as MessageResponse, + true, + ); dispatch({ channel, @@ -937,7 +987,8 @@ const ChannelInner = < id, mentioned_users: mentions, parent_id, - quoted_message_id: parent_id === quotedMessage?.parent_id ? quotedMessage?.id : undefined, + quoted_message_id: + parent_id === quotedMessage?.parent_id ? quotedMessage?.id : undefined, text, ...customMessageData, } as Message; @@ -960,7 +1011,9 @@ const ChannelInner = < } } - const responseTimestamp = new Date(messageResponse?.message?.updated_at || 0).getTime(); + const responseTimestamp = new Date( + messageResponse?.message?.updated_at || 0, + ).getTime(); const existingMessageTimestamp = existingMessage?.updated_at?.getTime() || 0; const responseIsTheNewest = responseTimestamp > existingMessageTimestamp; @@ -977,13 +1030,14 @@ const ChannelInner = < }); } - if (quotedMessage && parent_id === quotedMessage?.parent_id) setQuotedMessage(undefined); + if (quotedMessage && parent_id === quotedMessage?.parent_id) + setQuotedMessage(undefined); } catch (error) { // error response isn't usable so needs to be stringified then parsed const stringError = JSON.stringify(error); - const parsedError = (stringError - ? JSON.parse(stringError) - : {}) as ErrorFromResponse; + const parsedError = ( + stringError ? JSON.parse(stringError) : {} + ) as ErrorFromResponse; // Handle the case where the message already exists // (typically, when retrying to send a message). @@ -1010,7 +1064,7 @@ const ChannelInner = < }); thread?.upsertReplyLocally({ - // @ts-expect-error + // @ts-expect-error message type mismatch message: { ...message, error: parsedError, @@ -1049,7 +1103,7 @@ const ChannelInner = < }; thread?.upsertReplyLocally({ - // @ts-expect-error + // @ts-expect-error message type mismatch message: messagePreview, }); @@ -1067,7 +1121,9 @@ const ChannelInner = < if (message.attachments) { // remove scraped attachments added during the message composition in MessageInput to prevent sync issues - message.attachments = message.attachments.filter((attachment) => !attachment.og_scrape_url); + message.attachments = message.attachments.filter( + (attachment) => !attachment.og_scrape_url, + ); } await doSendMessage(message); @@ -1110,7 +1166,9 @@ const ChannelInner = < debounce( ( threadHasMore: boolean, - threadMessages: Array['formatMessage']>>, + threadMessages: Array< + ReturnType['formatMessage']> + >, ) => { dispatch({ threadHasMore, @@ -1144,7 +1202,10 @@ const ChannelInner = < limit, }); - const threadHasMoreMessages = hasMoreMessagesProbably(queryResponse.messages.length, limit); + const threadHasMoreMessages = hasMoreMessagesProbably( + queryResponse.messages.length, + limit, + ); const newThreadMessages = channel.state.threads[parentId] || []; // next set loadingMore to false so we can start asking for more data @@ -1172,7 +1233,8 @@ const ChannelInner = < enrichURLForPreview: props.enrichURLForPreview, findURLFn: enrichURLForPreviewConfig?.findURLFn, giphyVersion: props.giphyVersion || 'fixed_height', - imageAttachmentSizeHandler: props.imageAttachmentSizeHandler || getImageAttachmentConfiguration, + imageAttachmentSizeHandler: + props.imageAttachmentSizeHandler || getImageAttachmentConfiguration, maxNumberOfFiles, multipleUploads, mutes, @@ -1180,59 +1242,62 @@ const ChannelInner = < onLinkPreviewDismissed: enrichURLForPreviewConfig?.onLinkPreviewDismissed, quotedMessage, shouldGenerateVideoThumbnail: props.shouldGenerateVideoThumbnail || true, - videoAttachmentSizeHandler: props.videoAttachmentSizeHandler || getVideoAttachmentConfiguration, + videoAttachmentSizeHandler: + props.videoAttachmentSizeHandler || getVideoAttachmentConfiguration, watcher_count: state.watcherCount, }); - const channelActionContextValue: ChannelActionContextValue = useMemo( - () => ({ - addNotification, - closeThread, - deleteMessage, - dispatch, - editMessage, - jumpToFirstUnreadMessage, - jumpToLatestMessage, - jumpToMessage, - loadMore, - loadMoreNewer, - loadMoreThread, - markRead, - onMentionsClick: onMentionsHoverOrClick, - onMentionsHover: onMentionsHoverOrClick, - openThread, - removeMessage, - retrySendMessage, - sendMessage, - setChannelUnreadUiState, - setQuotedMessage, - skipMessageDataMemoization, - updateMessage, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - channel.cid, - deleteMessage, - enrichURLForPreviewConfig?.findURLFn, - enrichURLForPreviewConfig?.onLinkPreviewDismissed, - loadMore, - loadMoreNewer, - markRead, - quotedMessage, - jumpToFirstUnreadMessage, - jumpToMessage, - jumpToLatestMessage, - setChannelUnreadUiState, - ], - ); + const channelActionContextValue: ChannelActionContextValue = + useMemo( + () => ({ + addNotification, + closeThread, + deleteMessage, + dispatch, + editMessage, + jumpToFirstUnreadMessage, + jumpToLatestMessage, + jumpToMessage, + loadMore, + loadMoreNewer, + loadMoreThread, + markRead, + onMentionsClick: onMentionsHoverOrClick, + onMentionsHover: onMentionsHoverOrClick, + openThread, + removeMessage, + retrySendMessage, + sendMessage, + setChannelUnreadUiState, + setQuotedMessage, + skipMessageDataMemoization, + updateMessage, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + channel.cid, + deleteMessage, + enrichURLForPreviewConfig?.findURLFn, + enrichURLForPreviewConfig?.onLinkPreviewDismissed, + loadMore, + loadMoreNewer, + markRead, + quotedMessage, + jumpToFirstUnreadMessage, + jumpToMessage, + jumpToLatestMessage, + setChannelUnreadUiState, + ], + ); - // @ts-expect-error + // @ts-expect-error message type mismatch const componentContextValue: Partial = useMemo( () => ({ Attachment: props.Attachment, AttachmentPreviewList: props.AttachmentPreviewList, AttachmentSelector: props.AttachmentSelector, - AttachmentSelectorInitiationButtonContents: props.AttachmentSelectorInitiationButtonContents, + AttachmentSelectorInitiationButtonContents: + props.AttachmentSelectorInitiationButtonContents, AudioRecorder: props.AudioRecorder, AutocompleteSuggestionItem: props.AutocompleteSuggestionItem, AutocompleteSuggestionList: props.AutocompleteSuggestionList, @@ -1388,7 +1453,9 @@ const ChannelInner = <
      {dragAndDropWindow && ( - {children} + + {children} + )} {!dragAndDropWindow && <>{children}}
      diff --git a/src/components/Channel/__tests__/Channel.test.js b/src/components/Channel/__tests__/Channel.test.js index 4668c5b16a..f107077fcf 100644 --- a/src/components/Channel/__tests__/Channel.test.js +++ b/src/components/Channel/__tests__/Channel.test.js @@ -97,7 +97,11 @@ const messages = Array.from({ length: 25 }, (_, i) => const pinnedMessages = [generateMessage({ pinned: true, user })]; const renderComponent = async (props = {}, callback = () => {}) => { - const { channel: channelFromProps, chatClient: chatClientFromProps, ...channelProps } = props; + const { + channel: channelFromProps, + chatClient: chatClientFromProps, + ...channelProps + } = props; let result; await act(() => { result = render( @@ -133,7 +137,8 @@ describe('Channel', () => { const { messages: channelMessages } = useChannelStateContext(); return channelMessages.map( - ({ id, status, text }) => status !== 'failed' &&
      {text}
      , + ({ id, status, text }) => + status !== 'failed' &&
      {text}
      , ); }; @@ -206,6 +211,7 @@ describe('Channel', () => { it('should render empty channel container if channel does not have cid', async () => { const { channel } = await initClient(); const childrenContent = 'Channel children'; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { cid, ...channelWithoutCID } = channel; const { asFragment } = render( { }, }} > -
      {loadingText}
      }>{childrenContent}
      +
      {loadingText}
      }> + {childrenContent} +
      , ); await waitFor(() => expect(screen.getByText(loadingText)).toBeInTheDocument()); @@ -314,7 +322,9 @@ describe('Channel', () => { it('should set hasMore state to false if the initial channel query returns less messages than the default initial page size', async () => { const { channel, chatClient } = await initClient(); - useMockedApis(chatClient, [queryChannelWithNewMessages([generateMessage()], channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages([generateMessage()], channel), + ]); let hasMore; await renderComponent({ channel, chatClient }, ({ hasMore: contextHasMore }) => { hasMore = contextHasMore; @@ -364,7 +374,9 @@ describe('Channel', () => { it('should set hasMore state to false if the initial channel query returns less messages than the custom query channels options message limit', async () => { const { channel, chatClient } = await initClient(); - useMockedApis(chatClient, [queryChannelWithNewMessages([generateMessage()], channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages([generateMessage()], channel), + ]); let hasMore; const channelQueryOptions = { messages: { limit: 10 }, @@ -385,7 +397,10 @@ describe('Channel', () => { const { channel, chatClient } = await initClient(); const equalCount = 10; useMockedApis(chatClient, [ - queryChannelWithNewMessages(Array.from({ length: equalCount }, generateMessage), channel), + queryChannelWithNewMessages( + Array.from({ length: equalCount }, generateMessage), + channel, + ), ]); let hasMore; const channelQueryOptions = { @@ -477,7 +492,10 @@ describe('Channel', () => { await renderComponent({ channel, chatClient }); await waitFor(() => - expect(clientOnSpy).toHaveBeenCalledWith('connection.recovered', expect.any(Function)), + expect(clientOnSpy).toHaveBeenCalledWith( + 'connection.recovered', + expect.any(Function), + ), ); }); @@ -635,22 +653,25 @@ describe('Channel', () => { const threadMessage = messages[0]; let threadHasAlreadyBeenOpened = false; - await renderComponent({ channel, chatClient }, ({ closeThread, openThread, thread }) => { - if (!thread) { - // if there is no open thread - if (!threadHasAlreadyBeenOpened) { - // and we haven't opened one before, open a thread - openThread(threadMessage, { preventDefault: () => null }); - threadHasAlreadyBeenOpened = true; + await renderComponent( + { channel, chatClient }, + ({ closeThread, openThread, thread }) => { + if (!thread) { + // if there is no open thread + if (!threadHasAlreadyBeenOpened) { + // and we haven't opened one before, open a thread + openThread(threadMessage, { preventDefault: () => null }); + threadHasAlreadyBeenOpened = true; + } else { + // if we opened it ourselves before, it means the thread was successfully closed + threadHasClosed = true; + } } else { - // if we opened it ourselves before, it means the thread was successfully closed - threadHasClosed = true; + // if a thread is open, close it. + closeThread({ preventDefault: () => null }); } - } else { - // if a thread is open, close it. - closeThread({ preventDefault: () => null }); - } - }); + }, + ); await waitFor(() => expect(threadHasClosed).toBe(true)); }); @@ -733,7 +754,9 @@ describe('Channel', () => { ({ loadMore, messages: contextMessages }) => { if (!contextMessages.find((message) => message.id === newMessages[0].id)) { // Our new message is not yet passed as part of channel context. Call loadMore and mock API response to include it. - useMockedApis(chatClient, [queryChannelWithNewMessages(newMessages, channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages(newMessages, channel), + ]); loadMore(limit); } else { // If message has been added, update checker so we can verify it happened. @@ -766,7 +789,9 @@ describe('Channel', () => { ({ hasMore, loadMore, messages: contextMessages }) => { if (!contextMessages.find((message) => message.id === newMessages[0].id)) { // Our new message is not yet passed as part of channel context. Call loadMore and mock API response to include it. - useMockedApis(chatClient, [queryChannelWithNewMessages(newMessages, channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages(newMessages, channel), + ]); loadMore(limit); } else { // If message has been added, set our checker variable, so we can verify if hasMore is false. @@ -789,7 +814,9 @@ describe('Channel', () => { ({ hasMore, loadMore, messages: contextMessages }) => { if (!contextMessages.some((message) => message.id === newMessages[0].id)) { // Our new messages are not yet passed as part of channel context. Call loadMore and mock API response to include it. - useMockedApis(chatClient, [queryChannelWithNewMessages(newMessages, channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages(newMessages, channel), + ]); loadMore(limit); } else { // If message has been added, set our checker variable so we can verify if hasMore is true. @@ -818,7 +845,9 @@ describe('Channel', () => { it('should not load the second page, if the previous query has returned less then default limit messages', async () => { const { channel, chatClient } = await initClient(); const firstPageOfMessages = [generateMessage()]; - useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageOfMessages, channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages(firstPageOfMessages, channel), + ]); let queryNextPageSpy; let contextMessageCount; await renderComponent( @@ -834,7 +863,12 @@ describe('Channel', () => { expect(queryNextPageSpy).not.toHaveBeenCalled(); expect(chatClient.axiosInstance.post).toHaveBeenCalledTimes(1); expect(chatClient.axiosInstance.post.mock.calls[0][1]).toMatchObject( - expect.objectContaining({ data: {}, presence: false, state: true, watch: false }), + expect.objectContaining({ + data: {}, + presence: false, + state: true, + watch: false, + }), ); expect(contextMessageCount).toBe(firstPageOfMessages.length); }); @@ -848,7 +882,9 @@ describe('Channel', () => { const secondPageMessages = Array.from({ length: 15 }, (_, i) => generateMessage({ created_at: new Date((i + 1) * 100000) }), ); - useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageMessages, channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages(firstPageMessages, channel), + ]); let queryNextPageSpy; let contextMessageCount; await renderComponent( @@ -856,7 +892,9 @@ describe('Channel', () => { ({ loadMore, messages: contextMessages }) => { queryNextPageSpy = jest.spyOn(channel, 'query'); contextMessageCount = contextMessages.length; - useMockedApis(chatClient, [queryChannelWithNewMessages(secondPageMessages, channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages(secondPageMessages, channel), + ]); loadMore(); }, ); @@ -878,7 +916,9 @@ describe('Channel', () => { watchers: { limit: 100 }, }), ); - expect(contextMessageCount).toBe(firstPageMessages.length + secondPageMessages.length); + expect(contextMessageCount).toBe( + firstPageMessages.length + secondPageMessages.length, + ); }); }); it('should not load the second page, if the previous query has returned less then custom limit messages', async () => { @@ -887,7 +927,9 @@ describe('Channel', () => { messages: { limit: 10 }, }; const firstPageOfMessages = [generateMessage()]; - useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageOfMessages, channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages(firstPageOfMessages, channel), + ]); let queryNextPageSpy; let contextMessageCount; await renderComponent( @@ -926,7 +968,9 @@ describe('Channel', () => { const secondPageMessages = Array.from({ length: equalCount - 1 }, (_, i) => generateMessage({ created_at: new Date((i + 1) * 100000) }), ); - useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageMessages, channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages(firstPageMessages, channel), + ]); let queryNextPageSpy; let contextMessageCount; @@ -935,7 +979,9 @@ describe('Channel', () => { ({ loadMore, messages: contextMessages }) => { queryNextPageSpy = jest.spyOn(channel, 'query'); contextMessageCount = contextMessages.length; - useMockedApis(chatClient, [queryChannelWithNewMessages(secondPageMessages, channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages(secondPageMessages, channel), + ]); loadMore(channelQueryOptions.messages.limit); }, ); @@ -963,7 +1009,9 @@ describe('Channel', () => { watchers: { limit: channelQueryOptions.messages.limit }, }), ); - expect(contextMessageCount).toBe(firstPageMessages.length + secondPageMessages.length); + expect(contextMessageCount).toBe( + firstPageMessages.length + secondPageMessages.length, + ); }); }); }); @@ -973,18 +1021,27 @@ describe('Channel', () => { const last_read = new Date(1000); const last_read_message_id = 'X'; const first_unread_message_id = 'Y'; - const lastReadMessage = generateMessage({ created_at: last_read, id: last_read_message_id }); + const lastReadMessage = generateMessage({ + created_at: last_read, + id: last_read_message_id, + }); const firstUnreadMessage = generateMessage({ id: first_unread_message_id }); const currentMessageSetLastReadLoadedFirstUnreadNotLoaded = [ generateMessage({ created_at: new Date(100) }), lastReadMessage, ]; - const currentMessageSetLastReadFirstUnreadLoaded = [lastReadMessage, firstUnreadMessage]; + const currentMessageSetLastReadFirstUnreadLoaded = [ + lastReadMessage, + firstUnreadMessage, + ]; const currentMessageSetLastReadNotLoadedFirstUnreadLoaded = [ firstUnreadMessage, generateMessage(), ]; - const currentMessageSetFirstUnreadLastReadNotLoaded = [generateMessage(), generateMessage()]; + const currentMessageSetFirstUnreadLastReadNotLoaded = [ + generateMessage(), + generateMessage(), + ]; const errorNotificationText = 'Failed to jump to the first unread message'; const ownReadStateBase = { last_read, @@ -1045,7 +1102,10 @@ describe('Channel', () => { let highlightedMessageId; await renderComponent( { channel, chatClient }, - ({ highlightedMessageId: highlightedMessageIdContext, jumpToFirstUnreadMessage }) => { + ({ + highlightedMessageId: highlightedMessageIdContext, + jumpToFirstUnreadMessage, + }) => { if (hasJumped) { highlightedMessageId = highlightedMessageIdContext; return; @@ -1340,7 +1400,9 @@ describe('Channel', () => { return; } if (!channelUnreadUiState) return; - useMockedApis(chatClient, [queryChannelWithNewMessages(jumpToPage, channel)]); + useMockedApis(chatClient, [ + queryChannelWithNewMessages(jumpToPage, channel), + ]); jumpToFirstUnreadMessage(jumpToPage.length); hasJumped = true; }, @@ -1383,7 +1445,9 @@ describe('Channel', () => { children: , }, ({ sendMessage }) => { - jest.spyOn(channel, 'sendMessage').mockImplementationOnce(() => new Promise(() => {})); + jest + .spyOn(channel, 'sendMessage') + .mockImplementationOnce(() => new Promise(() => {})); if (!hasSent) sendMessage({ text: messageText }); hasSent = true; }, @@ -1482,6 +1546,7 @@ describe('Channel', () => { }); describe('delete message', () => { it('should throw error instead of calling default client.deleteMessage() function', async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { id, ...message } = generateMessage(); const { channel, chatClient } = await initClient(); const clientDeleteMessageSpy = jest.spyOn(chatClient, 'deleteMessage'); @@ -1505,10 +1570,13 @@ describe('Channel', () => { await renderComponent({ channel, chatClient }, ({ deleteMessage }) => { deleteMessage(message); }); - await waitFor(() => expect(clientDeleteMessageSpy).toHaveBeenCalledWith(message.id)); + await waitFor(() => + expect(clientDeleteMessageSpy).toHaveBeenCalledWith(message.id), + ); }); it('should throw error instead of calling custom doDeleteMessageRequest function', async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { id, ...message } = generateMessage(); const { channel, chatClient } = await initClient(); const clientDeleteMessageSpy = jest @@ -1561,7 +1629,11 @@ describe('Channel', () => { editMessage(updatedMessage); }); await waitFor(() => - expect(clientUpdateMessageSpy).toHaveBeenCalledWith(updatedMessage, undefined, undefined), + expect(clientUpdateMessageSpy).toHaveBeenCalledWith( + updatedMessage, + undefined, + undefined, + ), ); }); @@ -1577,7 +1649,11 @@ describe('Channel', () => { ); await waitFor(() => - expect(doUpdateMessageRequest).toHaveBeenCalledWith(channel.cid, messages[0], undefined), + expect(doUpdateMessageRequest).toHaveBeenCalledWith( + channel.cid, + messages[0], + undefined, + ), ); }); @@ -1611,10 +1687,15 @@ describe('Channel', () => { { channel, chatClient, children: }, ({ messages: contextMessages, retrySendMessage, sendMessage }) => { if (!hasSent) { - jest.spyOn(channel, 'sendMessage').mockImplementationOnce(() => Promise.reject()); + jest + .spyOn(channel, 'sendMessage') + .mockImplementationOnce(() => Promise.reject()); sendMessage(messageObject); hasSent = true; - } else if (!hasRetried && contextMessages.some(({ status }) => status === 'failed')) { + } else if ( + !hasRetried && + contextMessages.some(({ status }) => status === 'failed') + ) { // retry useMockedApis(chatClient, [sendMessageApi(messageObject)]); retrySendMessage(messageObject); @@ -1645,7 +1726,10 @@ describe('Channel', () => { if (!hasSent) { sendMessage(messageObject); hasSent = true; - } else if (!hasRetried && contextMessages.some(({ status }) => status === 'failed')) { + } else if ( + !hasRetried && + contextMessages.some(({ status }) => status === 'failed') + ) { // retry useMockedApis(chatClient, [sendMessageApi(generateMessage(messageObject))]); retrySendMessage(messageObject); @@ -1701,7 +1785,12 @@ describe('Channel', () => { }; }; - const createChannelEventDispatcher = (body, client, channel, type = 'message.new') => + const createChannelEventDispatcher = ( + body, + client, + channel, + type = 'message.new', + ) => createOneTimeEventDispatcher( { type, @@ -1714,7 +1803,11 @@ describe('Channel', () => { it('should eventually pass down a message when a message.new event is triggered on the channel', async () => { const { channel, chatClient } = await initClient(); const message = generateMessage({ user }); - const dispatchMessageEvent = createChannelEventDispatcher({ message }, chatClient, channel); + const dispatchMessageEvent = createChannelEventDispatcher( + { message }, + chatClient, + channel, + ); const { findByText } = await renderComponent( { @@ -1785,8 +1878,12 @@ describe('Channel', () => { ); await waitFor(async () => { - expect(await queryByText(oldText, undefined, { timeout: 100 })).not.toBeInTheDocument(); - expect(await queryByText(newText, undefined, { timeout: 100 })).toBeInTheDocument(); + expect( + await queryByText(oldText, undefined, { timeout: 100 }), + ).not.toBeInTheDocument(); + expect( + await queryByText(newText, undefined, { timeout: 100 }), + ).toBeInTheDocument(); }); }); @@ -1817,8 +1914,12 @@ describe('Channel', () => { ); await waitFor(async () => { - expect(await queryByText(oldText, undefined, { timeout: 100 })).not.toBeInTheDocument(); - expect(await queryByText(newText, undefined, { timeout: 100 })).toBeInTheDocument(); + expect( + await queryByText(oldText, undefined, { timeout: 100 }), + ).not.toBeInTheDocument(); + expect( + await queryByText(newText, undefined, { timeout: 100 }), + ).toBeInTheDocument(); }); }); @@ -1827,7 +1928,11 @@ describe('Channel', () => { const markReadSpy = jest.spyOn(channel, 'markRead'); const message = generateMessage({ user: generateUser() }); - const dispatchMessageEvent = createChannelEventDispatcher({ message }, chatClient, channel); + const dispatchMessageEvent = createChannelEventDispatcher( + { message }, + chatClient, + channel, + ); await renderComponent({ channel, chatClient }, () => { dispatchMessageEvent(); @@ -1841,7 +1946,11 @@ describe('Channel', () => { const markReadSpy = jest.spyOn(channel, 'markRead'); const message = generateMessage({ user: generateUser() }); - const dispatchMessageEvent = createChannelEventDispatcher({ message }, chatClient, channel); + const dispatchMessageEvent = createChannelEventDispatcher( + { message }, + chatClient, + channel, + ); await renderComponent({ channel, chatClient }, () => { dispatchMessageEvent(); @@ -1859,7 +1968,11 @@ describe('Channel', () => { }); jest.spyOn(channel, 'countUnread').mockImplementation(() => unreadAmount); const message = generateMessage({ user: generateUser() }); - const dispatchMessageEvent = createChannelEventDispatcher({ message }, chatClient, channel); + const dispatchMessageEvent = createChannelEventDispatcher( + { message }, + chatClient, + channel, + ); await renderComponent({ channel, chatClient }, () => { dispatchMessageEvent(); @@ -1909,18 +2022,21 @@ describe('Channel', () => { channel, ); let newThreadMessageWasAdded = false; - await renderComponent({ channel, chatClient }, ({ openThread, thread, threadMessages }) => { - if (!thread) { - // first, open thread - openThread(threadMessage, { preventDefault: () => null }); - } else if (!threadMessages.some(({ id }) => id === newThreadMessage.id)) { - // then, add new thread message - // FIXME: dispatch event needs to be queued on event loop now - setTimeout(() => dispatchNewThreadMessageEvent(), 0); - } else { - newThreadMessageWasAdded = true; - } - }); + await renderComponent( + { channel, chatClient }, + ({ openThread, thread, threadMessages }) => { + if (!thread) { + // first, open thread + openThread(threadMessage, { preventDefault: () => null }); + } else if (!threadMessages.some(({ id }) => id === newThreadMessage.id)) { + // then, add new thread message + // FIXME: dispatch event needs to be queued on event loop now + setTimeout(() => dispatchNewThreadMessageEvent(), 0); + } else { + newThreadMessageWasAdded = true; + } + }, + ); await waitFor(() => expect(newThreadMessageWasAdded).toBe(true)); }); @@ -1935,9 +2051,11 @@ describe('Channel', () => { name: 'MessageList', }, { - callback: (message) => ({ openThread, thread }) => { - if (!thread) openThread(message, { preventDefault: () => null }); - }, + callback: + (message) => + ({ openThread, thread }) => { + if (!thread) openThread(message, { preventDefault: () => null }); + }, component: Thread, getFirstMessageAvatar: () => { // the first avatar is that of the ThreadHeader @@ -1955,7 +2073,11 @@ describe('Channel', () => { const dispatchUserUpdatedEvent = createChannelEventDispatcher( { type: 'user.updated', - user: { ...user, ...updatedAttribute, updated_at: new Date().toISOString() }, + user: { + ...user, + ...updatedAttribute, + updated_at: new Date().toISOString(), + }, }, chatClient, channel, @@ -2065,7 +2187,11 @@ describe('Channel', () => { }; await act(async () => { - await renderComponent({ channel: activeChannel, chatClient, children: }); + await renderComponent({ + channel: activeChannel, + chatClient, + children: , + }); }); expect(screen.queryByText(UNREAD_TEXT)).toBeInTheDocument(); diff --git a/src/components/Channel/channelState.ts b/src/components/Channel/channelState.ts index 686722efdd..b0d77ea3a8 100644 --- a/src/components/Channel/channelState.ts +++ b/src/components/Channel/channelState.ts @@ -1,11 +1,15 @@ -import type { Channel, MessageResponse, ChannelState as StreamChannelState } from 'stream-chat'; +import type { + Channel, + MessageResponse, + ChannelState as StreamChannelState, +} from 'stream-chat'; import type { ChannelState, StreamMessage } from '../../context/ChannelStateContext'; import type { DefaultStreamChatGenerics } from '../../types/types'; export type ChannelStateReducerAction< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = | { type: 'closeThread'; @@ -44,7 +48,9 @@ export type ChannelStateReducerAction< } | { threadHasMore: boolean; - threadMessages: Array['formatMessage']>>; + threadMessages: Array< + ReturnType['formatMessage']> + >; type: 'loadMoreThreadFinished'; } | { @@ -84,183 +90,189 @@ export type ChannelStateReducerAction< type: 'jumpToLatestMessage'; }; -export const makeChannelReducer = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics ->() => ( - state: ChannelState, - action: ChannelStateReducerAction, -) => { - switch (action.type) { - case 'closeThread': { - return { - ...state, - thread: null, - threadLoadingMore: false, - threadMessages: [], - }; - } +export const makeChannelReducer = + () => + ( + state: ChannelState, + action: ChannelStateReducerAction, + ) => { + switch (action.type) { + case 'closeThread': { + return { + ...state, + thread: null, + threadLoadingMore: false, + threadMessages: [], + }; + } - case 'copyMessagesFromChannel': { - const { channel, parentId } = action; - return { - ...state, - messages: [...channel.state.messages], - pinnedMessages: [...channel.state.pinnedMessages], - // copying messages from channel happens with new message - this resets the suppressAutoscroll - suppressAutoscroll: false, - threadMessages: parentId - ? { ...channel.state.threads }[parentId] || [] - : state.threadMessages, - }; - } + case 'copyMessagesFromChannel': { + const { channel, parentId } = action; + return { + ...state, + messages: [...channel.state.messages], + pinnedMessages: [...channel.state.pinnedMessages], + // copying messages from channel happens with new message - this resets the suppressAutoscroll + suppressAutoscroll: false, + threadMessages: parentId + ? { ...channel.state.threads }[parentId] || [] + : state.threadMessages, + }; + } - case 'copyStateFromChannelOnEvent': { - const { channel } = action; - return { - ...state, - members: { ...channel.state.members }, - messages: [...channel.state.messages], - pinnedMessages: [...channel.state.pinnedMessages], - read: { ...channel.state.read }, - watcherCount: channel.state.watcher_count, - watchers: { ...channel.state.watchers }, - }; - } + case 'copyStateFromChannelOnEvent': { + const { channel } = action; + return { + ...state, + members: { ...channel.state.members }, + messages: [...channel.state.messages], + pinnedMessages: [...channel.state.pinnedMessages], + read: { ...channel.state.read }, + watcherCount: channel.state.watcher_count, + watchers: { ...channel.state.watchers }, + }; + } - case 'initStateFromChannel': { - const { channel, hasMore } = action; - return { - ...state, - hasMore, - loading: false, - members: { ...channel.state.members }, - messages: [...channel.state.messages], - pinnedMessages: [...channel.state.pinnedMessages], - read: { ...channel.state.read }, - watcherCount: channel.state.watcher_count, - watchers: { ...channel.state.watchers }, - }; - } + case 'initStateFromChannel': { + const { channel, hasMore } = action; + return { + ...state, + hasMore, + loading: false, + members: { ...channel.state.members }, + messages: [...channel.state.messages], + pinnedMessages: [...channel.state.pinnedMessages], + read: { ...channel.state.read }, + watcherCount: channel.state.watcher_count, + watchers: { ...channel.state.watchers }, + }; + } - case 'jumpToLatestMessage': { - return { - ...state, - hasMoreNewer: false, - highlightedMessageId: undefined, - loading: false, - suppressAutoscroll: false, - }; - } + case 'jumpToLatestMessage': { + return { + ...state, + hasMoreNewer: false, + highlightedMessageId: undefined, + loading: false, + suppressAutoscroll: false, + }; + } - case 'jumpToMessageFinished': { - return { - ...state, - hasMoreNewer: action.hasMoreNewer, - highlightedMessageId: action.highlightedMessageId, - }; - } + case 'jumpToMessageFinished': { + return { + ...state, + hasMoreNewer: action.hasMoreNewer, + highlightedMessageId: action.highlightedMessageId, + }; + } - case 'clearHighlightedMessage': { - return { - ...state, - highlightedMessageId: undefined, - }; - } + case 'clearHighlightedMessage': { + return { + ...state, + highlightedMessageId: undefined, + }; + } - case 'loadMoreFinished': { - const { hasMore, messages } = action; - return { - ...state, - hasMore, - loadingMore: false, - messages, - suppressAutoscroll: false, - }; - } + case 'loadMoreFinished': { + const { hasMore, messages } = action; + return { + ...state, + hasMore, + loadingMore: false, + messages, + suppressAutoscroll: false, + }; + } - case 'loadMoreNewerFinished': { - const { hasMoreNewer, messages } = action; - return { - ...state, - hasMoreNewer, - loadingMoreNewer: false, - messages, - }; - } + case 'loadMoreNewerFinished': { + const { hasMoreNewer, messages } = action; + return { + ...state, + hasMoreNewer, + loadingMoreNewer: false, + messages, + }; + } - case 'loadMoreThreadFinished': { - const { threadHasMore, threadMessages } = action; - return { - ...state, - threadHasMore, - threadLoadingMore: false, - threadMessages, - }; - } + case 'loadMoreThreadFinished': { + const { threadHasMore, threadMessages } = action; + return { + ...state, + threadHasMore, + threadLoadingMore: false, + threadMessages, + }; + } - case 'openThread': { - const { channel, message } = action; - return { - ...state, - thread: message, - threadHasMore: true, - threadMessages: message.id ? { ...channel.state.threads }[message.id] || [] : [], - threadSuppressAutoscroll: false, - }; - } + case 'openThread': { + const { channel, message } = action; + return { + ...state, + thread: message, + threadHasMore: true, + threadMessages: message.id + ? { ...channel.state.threads }[message.id] || [] + : [], + threadSuppressAutoscroll: false, + }; + } - case 'setError': { - const { error } = action; - return { ...state, error }; - } + case 'setError': { + const { error } = action; + return { ...state, error }; + } - case 'setLoadingMore': { - const { loadingMore } = action; - // suppress the autoscroll behavior - return { ...state, loadingMore, suppressAutoscroll: loadingMore }; - } + case 'setLoadingMore': { + const { loadingMore } = action; + // suppress the autoscroll behavior + return { ...state, loadingMore, suppressAutoscroll: loadingMore }; + } - case 'setLoadingMoreNewer': { - const { loadingMoreNewer } = action; - return { ...state, loadingMoreNewer }; - } + case 'setLoadingMoreNewer': { + const { loadingMoreNewer } = action; + return { ...state, loadingMoreNewer }; + } - case 'setThread': { - const { message } = action; - return { ...state, thread: message }; - } + case 'setThread': { + const { message } = action; + return { ...state, thread: message }; + } - case 'setTyping': { - const { channel } = action; - return { - ...state, - typing: { ...channel.state.typing }, - }; - } + case 'setTyping': { + const { channel } = action; + return { + ...state, + typing: { ...channel.state.typing }, + }; + } - case 'startLoadingThread': { - return { - ...state, - threadLoadingMore: true, - threadSuppressAutoscroll: true, - }; - } + case 'startLoadingThread': { + return { + ...state, + threadLoadingMore: true, + threadSuppressAutoscroll: true, + }; + } - case 'updateThreadOnEvent': { - const { channel, message } = action; - if (!state.thread) return state; - return { - ...state, - thread: - message?.id === state.thread.id ? channel.state.formatMessage(message) : state.thread, - threadMessages: state.thread?.id ? { ...channel.state.threads }[state.thread.id] || [] : [], - }; - } + case 'updateThreadOnEvent': { + const { channel, message } = action; + if (!state.thread) return state; + return { + ...state, + thread: + message?.id === state.thread.id + ? channel.state.formatMessage(message) + : state.thread, + threadMessages: state.thread?.id + ? { ...channel.state.threads }[state.thread.id] || [] + : [], + }; + } - default: - return state; - } -}; + default: + return state; + } + }; export const initialState = { error: null, diff --git a/src/components/Channel/hooks/useChannelContainerClasses.ts b/src/components/Channel/hooks/useChannelContainerClasses.ts index f3d8477350..7a04dd808f 100644 --- a/src/components/Channel/hooks/useChannelContainerClasses.ts +++ b/src/components/Channel/hooks/useChannelContainerClasses.ts @@ -4,7 +4,7 @@ import { useChatContext } from '../../../context/ChatContext'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useImageFlagEmojisOnWindowsClass = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >() => { const { useImageFlagEmojisOnWindows } = useChatContext('Channel'); return useImageFlagEmojisOnWindows && navigator.userAgent.match(/Win/) @@ -12,10 +12,11 @@ export const useImageFlagEmojisOnWindowsClass = < : ''; }; -export const getChatContainerClass = (customClass?: string) => customClass ?? 'str-chat__container'; +export const getChatContainerClass = (customClass?: string) => + customClass ?? 'str-chat__container'; export const useChannelContainerClasses = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ customClasses, }: Pick) => { diff --git a/src/components/Channel/hooks/useCreateChannelStateContext.ts b/src/components/Channel/hooks/useCreateChannelStateContext.ts index 114b95cf34..97625703d7 100644 --- a/src/components/Channel/hooks/useCreateChannelStateContext.ts +++ b/src/components/Channel/hooks/useCreateChannelStateContext.ts @@ -7,7 +7,7 @@ import type { ChannelStateContextValue } from '../../../context/ChannelStateCont import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useCreateChannelStateContext = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( value: Omit, 'channelCapabilities'> & { channelCapabilitiesArray: string[]; @@ -19,17 +19,17 @@ export const useCreateChannelStateContext = < channel, channelCapabilitiesArray = [], channelConfig, + channelUnreadUiState, debounceURLEnrichmentMs, dragAndDropWindow, enrichURLForPreview, - giphyVersion, error, findURLFn, + giphyVersion, hasMore, hasMoreNewer, - imageAttachmentSizeHandler, - suppressAutoscroll, highlightedMessageId, + imageAttachmentSizeHandler, loading, loadingMore, maxNumberOfFiles, @@ -44,14 +44,14 @@ export const useCreateChannelStateContext = < read = {}, shouldGenerateVideoThumbnail, skipMessageDataMemoization, + suppressAutoscroll, thread, threadHasMore, threadLoadingMore, threadMessages = [], - channelUnreadUiState, videoAttachmentSizeHandler, - watcherCount, watcher_count, + watcherCount, watchers, } = value; @@ -61,7 +61,9 @@ export const useCreateChannelStateContext = < const notificationsLength = notifications.length; const readUsers = Object.values(read); const readUsersLength = readUsers.length; - const readUsersLastReads = readUsers.map(({ last_read }) => last_read.toISOString()).join(); + const readUsersLastReads = readUsers + .map(({ last_read }) => last_read.toISOString()) + .join(); const threadMessagesLength = threadMessages?.length; const channelCapabilities: Record = {}; @@ -74,7 +76,15 @@ export const useCreateChannelStateContext = < ? messages : messages .map( - ({ deleted_at, latest_reactions, pinned, reply_count, status, updated_at, user }) => + ({ + deleted_at, + latest_reactions, + pinned, + reply_count, + status, + updated_at, + user, + }) => `${deleted_at}${ latest_reactions ? latest_reactions.map(({ type }) => type).join() : '' }${pinned}${reply_count}${status}${ diff --git a/src/components/Channel/hooks/useCreateTypingContext.ts b/src/components/Channel/hooks/useCreateTypingContext.ts index ee55dfe04c..726156fee4 100644 --- a/src/components/Channel/hooks/useCreateTypingContext.ts +++ b/src/components/Channel/hooks/useCreateTypingContext.ts @@ -4,7 +4,7 @@ import type { TypingContextValue } from '../../../context/TypingContext'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useCreateTypingContext = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( value: TypingContextValue, ) => { diff --git a/src/components/Channel/hooks/useEditMessageHandler.ts b/src/components/Channel/hooks/useEditMessageHandler.ts index f5067e8870..1776d7494a 100644 --- a/src/components/Channel/hooks/useEditMessageHandler.ts +++ b/src/components/Channel/hooks/useEditMessageHandler.ts @@ -2,10 +2,13 @@ import { useChatContext } from '../../../context/ChatContext'; import type { StreamChat, UpdatedMessage } from 'stream-chat'; -import type { DefaultStreamChatGenerics, UpdateMessageOptions } from '../../../types/types'; +import type { + DefaultStreamChatGenerics, + UpdateMessageOptions, +} from '../../../types/types'; type UpdateHandler< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = ( cid: string, updatedMessage: UpdatedMessage, @@ -13,15 +16,20 @@ type UpdateHandler< ) => ReturnType['updateMessage']>; export const useEditMessageHandler = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( doUpdateMessageRequest?: UpdateHandler, ) => { const { channel, client } = useChatContext('useEditMessageHandler'); - return (updatedMessage: UpdatedMessage, options?: UpdateMessageOptions) => { + return ( + updatedMessage: UpdatedMessage, + options?: UpdateMessageOptions, + ) => { if (doUpdateMessageRequest && channel) { - return Promise.resolve(doUpdateMessageRequest(channel.cid, updatedMessage, options)); + return Promise.resolve( + doUpdateMessageRequest(channel.cid, updatedMessage, options), + ); } return client.updateMessage(updatedMessage, undefined, options); }; diff --git a/src/components/Channel/hooks/useMentionsHandlers.ts b/src/components/Channel/hooks/useMentionsHandlers.ts index ea42f6fab9..af0cc4f26b 100644 --- a/src/components/Channel/hooks/useMentionsHandlers.ts +++ b/src/components/Channel/hooks/useMentionsHandlers.ts @@ -5,18 +5,24 @@ import type { UserResponse } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export type OnMentionAction< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = (event: React.BaseSyntheticEvent, user?: UserResponse) => void; export const useMentionsHandlers = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( onMentionsHover?: OnMentionAction, onMentionsClick?: OnMentionAction, ) => useCallback( - (event: React.BaseSyntheticEvent, mentioned_users: UserResponse[]) => { - if ((!onMentionsHover && !onMentionsClick) || !(event.target instanceof HTMLElement)) { + ( + event: React.BaseSyntheticEvent, + mentioned_users: UserResponse[], + ) => { + if ( + (!onMentionsHover && !onMentionsClick) || + !(event.target instanceof HTMLElement) + ) { return; } @@ -25,7 +31,9 @@ export const useMentionsHandlers = < if (textContent[0] === '@') { const userName = textContent.replace('@', ''); - const user = mentioned_users?.find(({ id, name }) => name === userName || id === userName); + const user = mentioned_users?.find( + ({ id, name }) => name === userName || id === userName, + ); if ( onMentionsHover && @@ -35,7 +43,11 @@ export const useMentionsHandlers = < onMentionsHover(event, user); } - if (onMentionsClick && event.type === 'click' && typeof onMentionsClick === 'function') { + if ( + onMentionsClick && + event.type === 'click' && + typeof onMentionsClick === 'function' + ) { onMentionsClick(event, user); } } diff --git a/src/components/Channel/utils.ts b/src/components/Channel/utils.ts index aa7b6af1b6..76af600a1c 100644 --- a/src/components/Channel/utils.ts +++ b/src/components/Channel/utils.ts @@ -4,28 +4,30 @@ import type { ChannelState, MessageResponse } from 'stream-chat'; import type { ChannelNotifications } from '../../context/ChannelStateContext'; import type { DefaultStreamChatGenerics } from '../../types'; -export const makeAddNotifications = ( - setNotifications: Dispatch>, - notificationTimeouts: NodeJS.Timeout[], -) => (text: string, type: 'success' | 'error') => { - if (typeof text !== 'string' || (type !== 'success' && type !== 'error')) { - return; - } +export const makeAddNotifications = + ( + setNotifications: Dispatch>, + notificationTimeouts: NodeJS.Timeout[], + ) => + (text: string, type: 'success' | 'error') => { + if (typeof text !== 'string' || (type !== 'success' && type !== 'error')) { + return; + } - const id = nanoid(); + const id = nanoid(); - setNotifications((prevNotifications) => [...prevNotifications, { id, text, type }]); + setNotifications((prevNotifications) => [...prevNotifications, { id, text, type }]); - const timeout = setTimeout( - () => - setNotifications((prevNotifications) => - prevNotifications.filter((notification) => notification.id !== id), - ), - 5000, - ); + const timeout = setTimeout( + () => + setNotifications((prevNotifications) => + prevNotifications.filter((notification) => notification.id !== id), + ), + 5000, + ); - notificationTimeouts.push(timeout); -}; + notificationTimeouts.push(timeout); + }; /** * Utility function for jumpToFirstUnreadMessage @@ -33,7 +35,7 @@ export const makeAddNotifications = ( * @param msgSet */ export const findInMsgSetById = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( targetId: string, msgSet: ReturnType['formatMessage']>[], @@ -59,7 +61,7 @@ export const findInMsgSetById = < * @param exact */ export const findInMsgSetByDate = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( targetDate: Date, msgSet: @@ -73,7 +75,9 @@ export const findInMsgSetByDate = < let right = msgSet.length - 1; while (left <= right) { middle = Math.floor((right + left) / 2); - const middleTimestamp = new Date(msgSet[middle].created_at as string | Date).getTime(); + const middleTimestamp = new Date( + msgSet[middle].created_at as string | Date, + ).getTime(); const middleLeftTimestamp = msgSet[middle - 1]?.created_at && new Date(msgSet[middle - 1].created_at as string | Date).getTime(); @@ -93,7 +97,10 @@ export const findInMsgSetByDate = < else right = middle - 1; } - if (!exact || new Date(msgSet[left].created_at as string | Date).getTime() === targetTimestamp) { + if ( + !exact || + new Date(msgSet[left].created_at as string | Date).getTime() === targetTimestamp + ) { return { index: left, target: msgSet[left] }; } return { index: -1 }; diff --git a/src/components/ChannelHeader/ChannelHeader.tsx b/src/components/ChannelHeader/ChannelHeader.tsx index 05e56d9d96..e76c81a327 100644 --- a/src/components/ChannelHeader/ChannelHeader.tsx +++ b/src/components/ChannelHeader/ChannelHeader.tsx @@ -28,19 +28,20 @@ export type ChannelHeaderProps = { * The ChannelHeader component renders some basic information about a Channel. */ export const ChannelHeader = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: ChannelHeaderProps, ) => { const { Avatar = DefaultAvatar, - MenuIcon = DefaultMenuIcon, image: overrideImage, live, + MenuIcon = DefaultMenuIcon, title: overrideTitle, } = props; - const { channel, watcher_count } = useChannelStateContext('ChannelHeader'); + const { channel, watcher_count } = + useChannelStateContext('ChannelHeader'); const { openMobileNav } = useChatContext('ChannelHeader'); const { t } = useTranslationContext('ChannelHeader'); const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({ @@ -70,7 +71,9 @@ export const ChannelHeader = <

      {displayTitle}{' '} {live && ( - {t('live')} + + {t('live')} + )}

      {subtitle &&

      {subtitle}

      } diff --git a/src/components/ChannelHeader/__tests__/ChannelHeader.test.js b/src/components/ChannelHeader/__tests__/ChannelHeader.test.js index b6d10b7496..ab09f9ee13 100644 --- a/src/components/ChannelHeader/__tests__/ChannelHeader.test.js +++ b/src/components/ChannelHeader/__tests__/ChannelHeader.test.js @@ -59,7 +59,7 @@ async function renderComponent(props, channelData, channelType = 'messaging') { return renderComponentBase({ channel, client, props }); } -afterEach(cleanup); // eslint-disable-line +afterEach(cleanup); describe('ChannelHeader', () => { it('should display live label when prop live is true', async () => { @@ -69,7 +69,9 @@ describe('ChannelHeader', () => { ); const results = await axe(container); expect(results).toHaveNoViolations(); - expect(container.querySelector('.str-chat__header-livestream-livelabel')).toBeInTheDocument(); + expect( + container.querySelector('.str-chat__header-livestream-livelabel'), + ).toBeInTheDocument(); }); it("should display avatar with fallback image only if other user's name is available", async () => { @@ -170,7 +172,9 @@ describe('ChannelHeader', () => { const updatedAttribute = { name: 'new-name' }; await renderComponent(); - await waitFor(() => expect(screen.queryByText(updatedAttribute.name)).not.toBeInTheDocument()); + await waitFor(() => + expect(screen.queryByText(updatedAttribute.name)).not.toBeInTheDocument(), + ); act(() => { dispatchUserUpdatedEvent(client, { ...user2, ...updatedAttribute }); }); @@ -190,7 +194,10 @@ describe('ChannelHeader', () => { dispatchUserUpdatedEvent(client, { ...user2, ...updatedAttribute }); }); await waitFor(() => - expect(screen.getByTestId('avatar-img')).toHaveAttribute('src', updatedAttribute.image), + expect(screen.getByTestId('avatar-img')).toHaveAttribute( + 'src', + updatedAttribute.image, + ), ); }); @@ -273,8 +280,14 @@ describe('ChannelHeader', () => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages).toHaveLength(3); expect(avatarImages[0]).toHaveAttribute('src', ownUser.image); - expect(avatarImages[1]).toHaveAttribute('src', channelState.members[1].user.image); - expect(avatarImages[2]).toHaveAttribute('src', channelState.members[2].user.image); + expect(avatarImages[1]).toHaveAttribute( + 'src', + channelState.members[1].user.image, + ); + expect(avatarImages[2]).toHaveAttribute( + 'src', + channelState.members[2].user.image, + ); }); act(() => { @@ -284,8 +297,14 @@ describe('ChannelHeader', () => { await waitFor(() => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages[0]).toHaveAttribute('src', updatedAttribute.image); - expect(avatarImages[1]).toHaveAttribute('src', channelState.members[1].user.image); - expect(avatarImages[2]).toHaveAttribute('src', channelState.members[2].user.image); + expect(avatarImages[1]).toHaveAttribute( + 'src', + channelState.members[1].user.image, + ); + expect(avatarImages[2]).toHaveAttribute( + 'src', + channelState.members[2].user.image, + ); }); }); @@ -305,8 +324,14 @@ describe('ChannelHeader', () => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages).toHaveLength(3); expect(avatarImages[0]).toHaveAttribute('src', ownUser.image); - expect(avatarImages[1]).toHaveAttribute('src', channelState.members[1].user.image); - expect(avatarImages[2]).toHaveAttribute('src', channelState.members[2].user.image); + expect(avatarImages[1]).toHaveAttribute( + 'src', + channelState.members[1].user.image, + ); + expect(avatarImages[2]).toHaveAttribute( + 'src', + channelState.members[2].user.image, + ); }); act(() => { @@ -316,7 +341,10 @@ describe('ChannelHeader', () => { await waitFor(() => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages[0]).toHaveAttribute('src', ownUser.image); - expect(avatarImages[1]).toHaveAttribute('src', channelState.members[1].user.image); + expect(avatarImages[1]).toHaveAttribute( + 'src', + channelState.members[1].user.image, + ); expect(avatarImages[2]).toHaveAttribute('src', updatedAttribute.image); }); }); diff --git a/src/components/ChannelList/ChannelList.tsx b/src/components/ChannelList/ChannelList.tsx index dd60c6f42c..3b63f2ddc9 100644 --- a/src/components/ChannelList/ChannelList.tsx +++ b/src/components/ChannelList/ChannelList.tsx @@ -4,11 +4,17 @@ import clsx from 'clsx'; import { ChannelListMessenger, ChannelListMessengerProps } from './ChannelListMessenger'; import { useConnectionRecoveredListener } from './hooks/useConnectionRecoveredListener'; import { useMobileNavigation } from './hooks/useMobileNavigation'; -import { CustomQueryChannelsFn, usePaginatedChannels } from './hooks/usePaginatedChannels'; +import { + CustomQueryChannelsFn, + usePaginatedChannels, +} from './hooks/usePaginatedChannels'; import { MAX_QUERY_CHANNELS_LIMIT, moveChannelUpwards } from './utils'; import { Avatar as DefaultAvatar } from '../Avatar'; -import { ChannelPreview, ChannelPreviewUIComponentProps } from '../ChannelPreview/ChannelPreview'; +import { + ChannelPreview, + ChannelPreviewUIComponentProps, +} from '../ChannelPreview/ChannelPreview'; import { ChannelSearchProps, ChannelSearch as DefaultChannelSearch, @@ -24,21 +30,33 @@ import { NullComponent } from '../UtilityComponents'; import { ChannelListContextProvider, ChatContextValue } from '../../context'; import { useChatContext } from '../../context/ChatContext'; -import type { Channel, ChannelFilters, ChannelOptions, ChannelSort, Event } from 'stream-chat'; +import type { + Channel, + ChannelFilters, + ChannelOptions, + ChannelSort, + Event, +} from 'stream-chat'; import type { ChannelAvatarProps } from '../Avatar'; import type { TranslationContextValue } from '../../context/TranslationContext'; import type { DefaultStreamChatGenerics, PaginatorProps } from '../../types/types'; -import { useChannelListShape, usePrepareShapeHandlers } from './hooks/useChannelListShape'; +import { + useChannelListShape, + usePrepareShapeHandlers, +} from './hooks/useChannelListShape'; const DEFAULT_FILTERS = {}; const DEFAULT_OPTIONS = {}; const DEFAULT_SORT = {}; export type ChannelListProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { /** Additional props for underlying ChannelSearch component and channel search controller, [available props](https://getstream.io/chat/docs/sdk/react/utility-components/channel_search/#props) */ - additionalChannelSearchProps?: Omit, 'setChannels'>; + additionalChannelSearchProps?: Omit< + ChannelSearchProps, + 'setChannels' + >; /** * When the client receives `message.new`, `notification.message_new`, and `notification.added_to_channel` events, we automatically * push that channel to the top of the list. If the channel doesn't currently exist in the list, we grab the channel from @@ -153,13 +171,15 @@ export type ChannelListProps< watchers?: { limit?: number; offset?: number }; }; -const UnMemoizedChannelList = ( +const UnMemoizedChannelList = < + SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( props: ChannelListProps, ) => { const { additionalChannelSearchProps, - Avatar = DefaultAvatar, allowNewMessagesFromUnfilteredChannels = true, + Avatar = DefaultAvatar, channelRenderFilterFn, ChannelSearch = DefaultChannelSearch, customActiveChannel, @@ -167,9 +187,9 @@ const UnMemoizedChannelList = >, setChannels: React.Dispatch>>>, ) => { - if (!channels.length || channels.length > (options?.limit || MAX_QUERY_CHANNELS_LIMIT)) { + if ( + !channels.length || + channels.length > (options?.limit || MAX_QUERY_CHANNELS_LIMIT) + ) { return; } if (customActiveChannel) { // FIXME: this is wrong... - let customActiveChannelObject = channels.find((chan) => chan.id === customActiveChannel); + let customActiveChannelObject = channels.find( + (chan) => chan.id === customActiveChannel, + ); if (!customActiveChannelObject) { - //@ts-expect-error - [customActiveChannelObject] = await client.queryChannels({ id: customActiveChannel }); + //@ts-expect-error valid query + [customActiveChannelObject] = await client.queryChannels({ + id: customActiveChannel, + }); } if (customActiveChannelObject) { @@ -277,7 +304,9 @@ const UnMemoizedChannelList = = { /** Whether the channel query request returned an errored response */ error: ErrorFromResponse | null; @@ -28,7 +28,7 @@ export type ChannelListMessengerProps< * A preview list of channels, allowing you to select the channel you want to open */ export const ChannelListMessenger = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: PropsWithChildren>, ) => { diff --git a/src/components/ChannelList/__tests__/ChannelList.test.js b/src/components/ChannelList/__tests__/ChannelList.test.js index a7f048b1c3..87c40982f3 100644 --- a/src/components/ChannelList/__tests__/ChannelList.test.js +++ b/src/components/ChannelList/__tests__/ChannelList.test.js @@ -60,7 +60,11 @@ const channelsQueryStateMock = { * to those components might end up breaking tests for ChannelList, which will be quite painful * to debug then. */ -const ChannelPreviewComponent = ({ channel, channelUpdateCount, latestMessagePreview }) => ( +const ChannelPreviewComponent = ({ + channel, + channelUpdateCount, + latestMessagePreview, +}) => (
      {channelUpdateCount}
      {channel.data.name}
      @@ -219,7 +223,9 @@ describe('ChannelList', () => { }), filters: {}, }; - const queryChannelsMock = jest.spyOn(client, 'queryChannels').mockImplementationOnce(); + const queryChannelsMock = jest + .spyOn(client, 'queryChannels') + .mockImplementationOnce(); const { rerender } = render( @@ -294,7 +300,7 @@ describe('ChannelList', () => { // Wait for list of channels to load in DOM. await waitFor(() => { expect(getByRole('list')).toBeInTheDocument(); - // eslint-disable-next-line jest-dom/prefer-in-document + expect(queryAllByRole('listitem')).toHaveLength(1); }); const results = await axe(container); @@ -408,7 +414,9 @@ describe('ChannelList', () => { it('when queryChannels api returns no channels, `EmptyStateIndicator` should be rendered', async () => { useMockedApis(chatClient, [queryChannelsApi([])]); - const EmptyStateIndicator = () =>
      ; + const EmptyStateIndicator = () => ( +
      + ); const { container, getByTestId } = render( @@ -430,7 +438,9 @@ describe('ChannelList', () => { it('should show unique channels', async () => { useMockedApis(chatClient, [queryChannelsApi([testChannel1, testChannel2])]); - const ChannelPreview = (props) =>
      ; + const ChannelPreview = (props) => ( +
      + ); render( @@ -620,7 +630,7 @@ describe('ChannelList', () => { let channel; beforeEach(async () => { client = await getTestClientWithUser({ id: user1.id }); - // eslint-disable-next-line react-hooks/rules-of-hooks + useMockedApis(client, [getOrCreateChannelApi(mockedChannels[0])]); channel = client.channel('messaging', mockedChannels[0].id); await channel.watch(); @@ -661,7 +671,9 @@ describe('ChannelList', () => { }); await waitFor(() => { - expect(container.querySelector(SEARCH_RESULT_LIST_SELECTOR)).not.toBeInTheDocument(); + expect( + container.querySelector(SEARCH_RESULT_LIST_SELECTOR), + ).not.toBeInTheDocument(); expect(screen.queryByLabelText('Channel list')).toBeInTheDocument(); }); }); @@ -737,9 +749,13 @@ describe('ChannelList', () => { await waitFor(() => { if (clearSearchOnClickOutside) { - expect(container.querySelector(SEARCH_RESULT_LIST_SELECTOR)).not.toBeInTheDocument(); + expect( + container.querySelector(SEARCH_RESULT_LIST_SELECTOR), + ).not.toBeInTheDocument(); } else { - expect(container.querySelector(SEARCH_RESULT_LIST_SELECTOR)).toBeInTheDocument(); + expect( + container.querySelector(SEARCH_RESULT_LIST_SELECTOR), + ).toBeInTheDocument(); } }); jest.useRealTimers(); @@ -763,7 +779,9 @@ describe('ChannelList', () => { await fireEvent.click(clearButton); }); await waitFor(() => { - expect(container.querySelector(SEARCH_RESULT_LIST_SELECTOR)).not.toBeInTheDocument(); + expect( + container.querySelector(SEARCH_RESULT_LIST_SELECTOR), + ).not.toBeInTheDocument(); expect(container.querySelector(CHANNEL_LIST_SELECTOR)).toBeInTheDocument(); expect(input).toHaveValue(''); expect(input).toHaveFocus(); @@ -789,7 +807,9 @@ describe('ChannelList', () => { await fireEvent.click(returnIcon); }); await waitFor(() => { - expect(container.querySelector(SEARCH_RESULT_LIST_SELECTOR)).not.toBeInTheDocument(); + expect( + container.querySelector(SEARCH_RESULT_LIST_SELECTOR), + ).not.toBeInTheDocument(); expect(input).not.toHaveFocus(); expect(input).toHaveValue(''); expect(returnIcon).not.toBeInTheDocument(); @@ -817,10 +837,14 @@ describe('ChannelList', () => { await waitFor(() => { expect(screen.queryAllByRole('option')).toHaveLength(3); - expect(screen.queryByText(channelNotInTheList.channel.name)).not.toBeInTheDocument(); + expect( + screen.queryByText(channelNotInTheList.channel.name), + ).not.toBeInTheDocument(); }); - useMockedApis(client, [queryChannelsApi([channelNotInTheList, ...mockedChannels])]); + useMockedApis(client, [ + queryChannelsApi([channelNotInTheList, ...mockedChannels]), + ]); const input = screen.queryByTestId('search-input'); await act(() => { input.focus(); @@ -841,7 +865,9 @@ describe('ChannelList', () => { }); await waitFor(() => { - expect(screen.queryByText(channelNotInTheList.channel.name)).toBeInTheDocument(); + expect( + screen.queryByText(channelNotInTheList.channel.name), + ).toBeInTheDocument(); expect(screen.queryByTestId('return-icon')).not.toBeInTheDocument(); }); jest.useRealTimers(); @@ -895,7 +921,9 @@ describe('ChannelList', () => { }; beforeEach(() => { - useMockedApis(chatClient, [queryChannelsApi([testChannel1, testChannel2, testChannel3])]); + useMockedApis(chatClient, [ + queryChannelsApi([testChannel1, testChannel2, testChannel3]), + ]); }); it('should move channel to top of the list', async () => { @@ -918,7 +946,9 @@ describe('ChannelList', () => { const items = getAllByRole('listitem'); // Get the closes listitem to the channel that received new message. - const channelPreview = getByText(newMessage.text).closest(ROLE_LIST_ITEM_SELECTOR); + const channelPreview = getByText(newMessage.text).closest( + ROLE_LIST_ITEM_SELECTOR, + ); expect(channelPreview.isEqualNode(items[0])).toBe(true); const results = await axe(container); expect(results).toHaveNoViolations(); @@ -945,7 +975,9 @@ describe('ChannelList', () => { const items = getAllByRole('listitem'); // Get the closes listitem to the channel that received new message. - const channelPreview = getByText(newMessage.text).closest(ROLE_LIST_ITEM_SELECTOR); + const channelPreview = getByText(newMessage.text).closest( + ROLE_LIST_ITEM_SELECTOR, + ); expect(channelPreview.isEqualNode(items[2])).toBe(true); const results = await axe(container); expect(results).toHaveNoViolations(); @@ -1072,7 +1104,9 @@ describe('ChannelList', () => { useMockedApis(chatClient, [getOrCreateChannelApi(testChannel3)]); - act(() => dispatchNotificationAddedToChannelEvent(chatClient, testChannel3.channel)); + act(() => + dispatchNotificationAddedToChannelEvent(chatClient, testChannel3.channel), + ); await waitFor(() => { expect(getByTestId(testChannel3.channel.id)).toBeInTheDocument(); @@ -1100,7 +1134,9 @@ describe('ChannelList', () => { expect(getByRole('list')).toBeInTheDocument(); }); - act(() => dispatchNotificationAddedToChannelEvent(chatClient, testChannel3.channel)); + act(() => + dispatchNotificationAddedToChannelEvent(chatClient, testChannel3.channel), + ); await waitFor(() => { expect(onAddedToChannel).toHaveBeenCalledTimes(1); @@ -1118,7 +1154,9 @@ describe('ChannelList', () => { }; beforeEach(() => { - useMockedApis(chatClient, [queryChannelsApi([testChannel1, testChannel2, testChannel3])]); + useMockedApis(chatClient, [ + queryChannelsApi([testChannel1, testChannel2, testChannel3]), + ]); }); it('should remove the channel from list by default', async () => { @@ -1133,7 +1171,9 @@ describe('ChannelList', () => { }); const nodeToBeRemoved = getByTestId(testChannel3.channel.id); - act(() => dispatchNotificationRemovedFromChannel(chatClient, testChannel3.channel)); + act(() => + dispatchNotificationRemovedFromChannel(chatClient, testChannel3.channel), + ); await waitFor(() => { expect(nodeToBeRemoved).not.toBeInTheDocument(); @@ -1146,7 +1186,10 @@ describe('ChannelList', () => { const onRemovedFromChannel = jest.fn(); const { container, getByRole } = await render( - + , ); // Wait for list of channels to load in DOM. @@ -1154,7 +1197,9 @@ describe('ChannelList', () => { expect(getByRole('list')).toBeInTheDocument(); }); - act(() => dispatchNotificationRemovedFromChannel(chatClient, testChannel3.channel)); + act(() => + dispatchNotificationRemovedFromChannel(chatClient, testChannel3.channel), + ); await waitFor(() => { expect(onRemovedFromChannel).toHaveBeenCalledTimes(1); @@ -1480,7 +1525,9 @@ describe('ChannelList', () => { act(() => dispatchConnectionRecoveredEvent(chatClient)); await waitFor(() => { - expect(parseInt(getByTestId('channelUpdateCount').textContent, 10)).toBe(updateCount + 1); + expect(parseInt(getByTestId('channelUpdateCount').textContent, 10)).toBe( + updateCount + 1, + ); }); const results = await axe(container); expect(results).toHaveNoViolations(); @@ -1669,7 +1716,8 @@ describe('ChannelList', () => { const channelsToBeLoaded = Array.from({ length: 5 }, generateChannel); const channelsToBeSet = Array.from({ length: 5 }, generateChannel); const channelsToIdString = (channels) => channels.map(({ id }) => id).join(); - const channelsDataToIdString = (channels) => channels.map(({ channel: { id } }) => id).join(); + const channelsDataToIdString = (channels) => + channels.map(({ channel: { id } }) => id).join(); const ChannelListCustom = () => { const { channels, setChannels } = useChannelListContext(); @@ -1694,7 +1742,9 @@ describe('ChannelList', () => { ); await waitFor(() => { - expect(screen.getByText(channelsDataToIdString(channelsToBeLoaded))).toBeInTheDocument(); + expect( + screen.getByText(channelsDataToIdString(channelsToBeLoaded)), + ).toBeInTheDocument(); }); await act(() => { @@ -1705,7 +1755,9 @@ describe('ChannelList', () => { expect( screen.queryByText(channelsDataToIdString(channelsToBeLoaded)), ).not.toBeInTheDocument(); - expect(screen.getByText(channelsDataToIdString(channelsToBeSet))).toBeInTheDocument(); + expect( + screen.getByText(channelsDataToIdString(channelsToBeSet)), + ).toBeInTheDocument(); }); }); }); diff --git a/src/components/ChannelList/hooks/useChannelDeletedListener.ts b/src/components/ChannelList/hooks/useChannelDeletedListener.ts index 9e8db75273..f09b808e9d 100644 --- a/src/components/ChannelList/hooks/useChannelDeletedListener.ts +++ b/src/components/ChannelList/hooks/useChannelDeletedListener.ts @@ -7,7 +7,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useChannelDeletedListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( setChannels: React.Dispatch>>>, customHandler?: ( diff --git a/src/components/ChannelList/hooks/useChannelHiddenListener.ts b/src/components/ChannelList/hooks/useChannelHiddenListener.ts index e8b221a7d7..4b159dc44a 100644 --- a/src/components/ChannelList/hooks/useChannelHiddenListener.ts +++ b/src/components/ChannelList/hooks/useChannelHiddenListener.ts @@ -7,7 +7,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useChannelHiddenListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( setChannels: React.Dispatch>>>, customHandler?: ( diff --git a/src/components/ChannelList/hooks/useChannelListShape.ts b/src/components/ChannelList/hooks/useChannelListShape.ts index 0a70bbb764..8ecfcb5d0e 100644 --- a/src/components/ChannelList/hooks/useChannelListShape.ts +++ b/src/components/ChannelList/hooks/useChannelListShape.ts @@ -17,7 +17,9 @@ import { useChatContext } from '../../../context'; import { getChannel } from '../../../utils'; import { ChannelListProps } from '../ChannelList'; -type SetChannels = Dispatch[]>>; +type SetChannels = Dispatch< + SetStateAction[]> +>; type BaseParameters = { event: Event; @@ -37,44 +39,45 @@ type HandleMessageNewParameters = BaseParameters lockChannelOrder: boolean; } & Required, 'filters' | 'sort'>>; -type HandleNotificationMessageNewParameters = BaseParameters & - RepeatedParameters & { - allowNewMessagesFromUnfilteredChannels: boolean; +type HandleNotificationMessageNewParameters = + BaseParameters & + RepeatedParameters & { + allowNewMessagesFromUnfilteredChannels: boolean; + lockChannelOrder: boolean; + } & Required, 'filters' | 'sort'>>; + +type HandleNotificationRemovedFromChannelParameters = + BaseParameters & RepeatedParameters; + +type HandleNotificationAddedToChannelParameters = + BaseParameters & + RepeatedParameters & { + allowNewMessagesFromUnfilteredChannels: boolean; + lockChannelOrder: boolean; + } & Required, 'sort'>>; + +type HandleMemberUpdatedParameters = + BaseParameters & { lockChannelOrder: boolean; - } & Required, 'filters' | 'sort'>>; + } & Required, 'sort' | 'filters'>>; -type HandleNotificationRemovedFromChannelParameters< - SCG extends ExtendableGenerics -> = BaseParameters & RepeatedParameters; - -type HandleNotificationAddedToChannelParameters< - SCG extends ExtendableGenerics -> = BaseParameters & - RepeatedParameters & { - allowNewMessagesFromUnfilteredChannels: boolean; - lockChannelOrder: boolean; - } & Required, 'sort'>>; - -type HandleMemberUpdatedParameters = BaseParameters & { - lockChannelOrder: boolean; -} & Required, 'sort' | 'filters'>>; - -type HandleChannelDeletedParameters = BaseParameters & - RepeatedParameters; +type HandleChannelDeletedParameters = + BaseParameters & RepeatedParameters; type HandleChannelHiddenParameters = BaseParameters & RepeatedParameters; -type HandleChannelVisibleParameters = BaseParameters & - RepeatedParameters; +type HandleChannelVisibleParameters = + BaseParameters & RepeatedParameters; -type HandleChannelTruncatedParameters = BaseParameters & - RepeatedParameters; +type HandleChannelTruncatedParameters = + BaseParameters & RepeatedParameters; -type HandleChannelUpdatedParameters = BaseParameters & - RepeatedParameters; +type HandleChannelUpdatedParameters = + BaseParameters & RepeatedParameters; -type HandleUserPresenceChangedParameters = BaseParameters; +type HandleUserPresenceChangedParameters = + BaseParameters; const shared = ({ customHandler, @@ -218,13 +221,16 @@ export const useChannelListShapeDefaults = () => const channel = await getChannel({ client, id: event.channel.id, - members: event.channel.members?.reduce((newMembers, { user, user_id }) => { - const userId = user_id || user?.id; + members: event.channel.members?.reduce( + (newMembers, { user, user_id }) => { + const userId = user_id || user?.id; - if (userId) newMembers.push(userId); + if (userId) newMembers.push(userId); - return newMembers; - }, []), + return newMembers; + }, + [], + ), type: event.channel.type, }); @@ -251,7 +257,9 @@ export const useChannelListShapeDefaults = () => return customHandler(setChannels, event); } - setChannels((channels) => channels.filter((channel) => channel.cid !== event.channel?.cid)); + setChannels((channels) => + channels.filter((channel) => channel.cid !== event.channel?.cid), + ); }, [], ); @@ -264,7 +272,11 @@ export const useChannelListShapeDefaults = () => setChannels, sort, }: HandleMemberUpdatedParameters) => { - if (!event.member?.user || event.member.user.id !== client.userID || !event.channel_type) { + if ( + !event.member?.user || + event.member.user.id !== client.userID || + !event.channel_type + ) { return; } @@ -338,7 +350,11 @@ export const useChannelListShapeDefaults = () => ); const handleChannelVisible = useCallback( - async ({ customHandler, event, setChannels }: HandleChannelVisibleParameters) => { + async ({ + customHandler, + event, + setChannels, + }: HandleChannelVisibleParameters) => { if (typeof customHandler === 'function') { return customHandler(setChannels, event); } @@ -377,7 +393,9 @@ export const useChannelListShapeDefaults = () => } setChannels((channels) => { - const channelIndex = channels.findIndex((channel) => channel.cid === event.channel?.cid); + const channelIndex = channels.findIndex( + (channel) => channel.cid === event.channel?.cid, + ); if (channelIndex > -1 && event.channel) { const newChannels = channels; @@ -385,7 +403,8 @@ export const useChannelListShapeDefaults = () => ...event.channel, hidden: event.channel?.hidden ?? newChannels[channelIndex].data?.hidden, own_capabilities: - event.channel?.own_capabilities ?? newChannels[channelIndex].data?.own_capabilities, + event.channel?.own_capabilities ?? + newChannels[channelIndex].data?.own_capabilities, }; return [...newChannels]; @@ -452,32 +471,33 @@ export const useChannelListShapeDefaults = () => ); }; -type UseDefaultHandleChannelListShapeParameters = Required< - Pick< - ChannelListProps, - 'allowNewMessagesFromUnfilteredChannels' | 'lockChannelOrder' | 'filters' | 'sort' - > -> & - Pick< - ChannelListProps, - | 'onAddedToChannel' - | 'onChannelDeleted' - | 'onChannelHidden' - | 'onChannelTruncated' - | 'onChannelUpdated' - | 'onChannelVisible' - | 'onMessageNew' - | 'onMessageNewHandler' - | 'onRemovedFromChannel' - > & { - setChannels: SetChannels; - customHandleChannelListShape?: (data: { - // can't use ReturnType> until we upgrade prettier to at least v2.7.0 - defaults: ReturnType; - event: Event; +type UseDefaultHandleChannelListShapeParameters = + Required< + Pick< + ChannelListProps, + 'allowNewMessagesFromUnfilteredChannels' | 'lockChannelOrder' | 'filters' | 'sort' + > + > & + Pick< + ChannelListProps, + | 'onAddedToChannel' + | 'onChannelDeleted' + | 'onChannelHidden' + | 'onChannelTruncated' + | 'onChannelUpdated' + | 'onChannelVisible' + | 'onMessageNew' + | 'onMessageNewHandler' + | 'onRemovedFromChannel' + > & { setChannels: SetChannels; - }) => void; - }; + customHandleChannelListShape?: (data: { + // can't use ReturnType> until we upgrade prettier to at least v2.7.0 + defaults: ReturnType; + event: Event; + setChannels: SetChannels; + }) => void; + }; export const usePrepareShapeHandlers = ({ allowNewMessagesFromUnfilteredChannels, @@ -556,16 +576,32 @@ export const usePrepareShapeHandlers = ({ }); break; case 'channel.hidden': - defaults.handleChannelHidden({ customHandler: onChannelHidden, event, setChannels }); + defaults.handleChannelHidden({ + customHandler: onChannelHidden, + event, + setChannels, + }); break; case 'channel.visible': - defaults.handleChannelVisible({ customHandler: onChannelVisible, event, setChannels }); + defaults.handleChannelVisible({ + customHandler: onChannelVisible, + event, + setChannels, + }); break; case 'channel.truncated': - defaults.handleChannelTruncated({ customHandler: onChannelTruncated, event, setChannels }); + defaults.handleChannelTruncated({ + customHandler: onChannelTruncated, + event, + setChannels, + }); break; case 'channel.updated': - defaults.handleChannelUpdated({ customHandler: onChannelUpdated, event, setChannels }); + defaults.handleChannelUpdated({ + customHandler: onChannelUpdated, + event, + setChannels, + }); break; case 'user.presence.changed': defaults.handleUserPresenceChanged({ event, setChannels }); diff --git a/src/components/ChannelList/hooks/useChannelMembershipState.ts b/src/components/ChannelList/hooks/useChannelMembershipState.ts index fb80c07669..8f303fecde 100644 --- a/src/components/ChannelList/hooks/useChannelMembershipState.ts +++ b/src/components/ChannelList/hooks/useChannelMembershipState.ts @@ -1,4 +1,9 @@ -import type { Channel, ChannelMemberResponse, EventTypes, ExtendableGenerics } from 'stream-chat'; +import type { + Channel, + ChannelMemberResponse, + EventTypes, + ExtendableGenerics, +} from 'stream-chat'; import { useSelectedChannelState } from './useSelectedChannelState'; const selector = (c: Channel) => c.state.membership; @@ -10,6 +15,8 @@ export function useChannelMembershipState( export function useChannelMembershipState( channel?: Channel | undefined, ): ChannelMemberResponse | undefined; -export function useChannelMembershipState(channel?: Channel) { +export function useChannelMembershipState( + channel?: Channel, +) { return useSelectedChannelState({ channel, selector, stateChangeEventKeys: keys }); } diff --git a/src/components/ChannelList/hooks/useChannelTruncatedListener.ts b/src/components/ChannelList/hooks/useChannelTruncatedListener.ts index 77e7c8b5df..295b05447d 100644 --- a/src/components/ChannelList/hooks/useChannelTruncatedListener.ts +++ b/src/components/ChannelList/hooks/useChannelTruncatedListener.ts @@ -7,7 +7,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useChannelTruncatedListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( setChannels: React.Dispatch>>>, customHandler?: ( diff --git a/src/components/ChannelList/hooks/useChannelUpdatedListener.ts b/src/components/ChannelList/hooks/useChannelUpdatedListener.ts index aa6fcfda82..9159a8568f 100644 --- a/src/components/ChannelList/hooks/useChannelUpdatedListener.ts +++ b/src/components/ChannelList/hooks/useChannelUpdatedListener.ts @@ -7,7 +7,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useChannelUpdatedListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( setChannels: React.Dispatch>>>, customHandler?: ( @@ -21,7 +21,9 @@ export const useChannelUpdatedListener = < useEffect(() => { const handleEvent = (event: Event) => { setChannels((channels) => { - const channelIndex = channels.findIndex((channel) => channel.cid === event.channel?.cid); + const channelIndex = channels.findIndex( + (channel) => channel.cid === event.channel?.cid, + ); if (channelIndex > -1 && event.channel) { const newChannels = channels; @@ -29,7 +31,8 @@ export const useChannelUpdatedListener = < ...event.channel, hidden: event.channel?.hidden ?? newChannels[channelIndex].data?.hidden, own_capabilities: - event.channel?.own_capabilities ?? newChannels[channelIndex].data?.own_capabilities, + event.channel?.own_capabilities ?? + newChannels[channelIndex].data?.own_capabilities, }; return [...newChannels]; diff --git a/src/components/ChannelList/hooks/useChannelVisibleListener.ts b/src/components/ChannelList/hooks/useChannelVisibleListener.ts index cd71202921..8b4cd8c342 100644 --- a/src/components/ChannelList/hooks/useChannelVisibleListener.ts +++ b/src/components/ChannelList/hooks/useChannelVisibleListener.ts @@ -10,7 +10,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useChannelVisibleListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( setChannels: React.Dispatch>>>, customHandler?: ( diff --git a/src/components/ChannelList/hooks/useConnectionRecoveredListener.ts b/src/components/ChannelList/hooks/useConnectionRecoveredListener.ts index d54bcebc5f..4e6543893e 100644 --- a/src/components/ChannelList/hooks/useConnectionRecoveredListener.ts +++ b/src/components/ChannelList/hooks/useConnectionRecoveredListener.ts @@ -5,7 +5,7 @@ import { useChatContext } from '../../../context/ChatContext'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useConnectionRecoveredListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( forceUpdate?: () => void, ) => { diff --git a/src/components/ChannelList/hooks/useMessageNewListener.ts b/src/components/ChannelList/hooks/useMessageNewListener.ts index 548d5f22da..a8e1218ba3 100644 --- a/src/components/ChannelList/hooks/useMessageNewListener.ts +++ b/src/components/ChannelList/hooks/useMessageNewListener.ts @@ -10,7 +10,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useMessageNewListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( setChannels: React.Dispatch>>>, customHandler?: ( @@ -28,9 +28,14 @@ export const useMessageNewListener = < customHandler(setChannels, event); } else { setChannels((channels) => { - const channelInList = channels.filter((channel) => channel.cid === event.cid).length > 0; - - if (!channelInList && allowNewMessagesFromUnfilteredChannels && event.channel_type) { + const channelInList = + channels.filter((channel) => channel.cid === event.cid).length > 0; + + if ( + !channelInList && + allowNewMessagesFromUnfilteredChannels && + event.channel_type + ) { const channel = client.channel(event.channel_type, event.channel_id); return uniqBy([channel, ...channels], 'cid'); } diff --git a/src/components/ChannelList/hooks/useNotificationAddedToChannelListener.ts b/src/components/ChannelList/hooks/useNotificationAddedToChannelListener.ts index 520d580ef0..bc73978ed7 100644 --- a/src/components/ChannelList/hooks/useNotificationAddedToChannelListener.ts +++ b/src/components/ChannelList/hooks/useNotificationAddedToChannelListener.ts @@ -10,7 +10,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useNotificationAddedToChannelListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( setChannels: React.Dispatch>>>, customHandler?: ( @@ -19,7 +19,9 @@ export const useNotificationAddedToChannelListener = < ) => void, allowNewMessagesFromUnfilteredChannels = true, ) => { - const { client } = useChatContext('useNotificationAddedToChannelListener'); + const { client } = useChatContext( + 'useNotificationAddedToChannelListener', + ); useEffect(() => { const handleEvent = async (event: Event) => { diff --git a/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts b/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts index 6fd242b7da..274581247d 100644 --- a/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts +++ b/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts @@ -10,7 +10,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useNotificationMessageNewListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( setChannels: React.Dispatch>>>, customHandler?: ( @@ -19,7 +19,9 @@ export const useNotificationMessageNewListener = < ) => void, allowNewMessagesFromUnfilteredChannels = true, ) => { - const { client } = useChatContext('useNotificationMessageNewListener'); + const { client } = useChatContext( + 'useNotificationMessageNewListener', + ); useEffect(() => { const handleEvent = async (event: Event) => { diff --git a/src/components/ChannelList/hooks/useNotificationRemovedFromChannelListener.ts b/src/components/ChannelList/hooks/useNotificationRemovedFromChannelListener.ts index 00a5d3db22..4781855727 100644 --- a/src/components/ChannelList/hooks/useNotificationRemovedFromChannelListener.ts +++ b/src/components/ChannelList/hooks/useNotificationRemovedFromChannelListener.ts @@ -7,7 +7,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useNotificationRemovedFromChannelListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( setChannels: React.Dispatch>>>, customHandler?: ( @@ -24,7 +24,9 @@ export const useNotificationRemovedFromChannelListener = < if (customHandler && typeof customHandler === 'function') { customHandler(setChannels, event); } else { - setChannels((channels) => channels.filter((channel) => channel.cid !== event.channel?.cid)); + setChannels((channels) => + channels.filter((channel) => channel.cid !== event.channel?.cid), + ); } }; diff --git a/src/components/ChannelList/hooks/usePaginatedChannels.ts b/src/components/ChannelList/hooks/usePaginatedChannels.ts index 6a27d0a309..7ee6656279 100644 --- a/src/components/ChannelList/hooks/usePaginatedChannels.ts +++ b/src/components/ChannelList/hooks/usePaginatedChannels.ts @@ -3,7 +3,13 @@ import uniqBy from 'lodash.uniqby'; import { MAX_QUERY_CHANNELS_LIMIT } from '../utils'; -import type { Channel, ChannelFilters, ChannelOptions, ChannelSort, StreamChat } from 'stream-chat'; +import type { + Channel, + ChannelFilters, + ChannelOptions, + ChannelSort, + StreamChat, +} from 'stream-chat'; import { useChatContext } from '../../../context/ChatContext'; @@ -14,10 +20,13 @@ import { DEFAULT_INITIAL_CHANNEL_PAGE_SIZE } from '../../../constants/limits'; const RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS = 5000; const MIN_RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS = 2000; -type AllowedQueryType = Extract; +type AllowedQueryType = Extract< + ChannelsQueryState['queryInProgress'], + 'reload' | 'load-more' +>; export type CustomQueryChannelParams< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { currentChannels: Array>; queryType: AllowedQueryType; @@ -26,11 +35,11 @@ export type CustomQueryChannelParams< }; export type CustomQueryChannelsFn< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = (params: CustomQueryChannelParams) => Promise; export const usePaginatedChannels = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( client: StreamChat, filters: ChannelFilters, @@ -53,7 +62,7 @@ export const usePaginatedChannels = < const recoveryThrottleInterval = recoveryThrottleIntervalMs < MIN_RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS ? MIN_RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS - : recoveryThrottleIntervalMs ?? RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS; + : (recoveryThrottleIntervalMs ?? RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS); // memoize props const filterString = useMemo(() => JSON.stringify(filters), [filters]); const sortString = useMemo(() => JSON.stringify(sort), [sort]); @@ -85,7 +94,11 @@ export const usePaginatedChannels = < ...options, }; - const channelQueryResponse = await client.queryChannels(filters, sort || {}, newOptions); + const channelQueryResponse = await client.queryChannels( + filters, + sort || {}, + newOptions, + ); const newChannels = queryType === 'reload' @@ -115,7 +128,11 @@ export const usePaginatedChannels = < ? now - lastRecoveryTimestamp.current : 0; - if (!isFirstRecovery && timeElapsedSinceLastRecoveryMs < recoveryThrottleInterval && !error) { + if ( + !isFirstRecovery && + timeElapsedSinceLastRecoveryMs < recoveryThrottleInterval && + !error + ) { return; } diff --git a/src/components/ChannelList/hooks/useSelectedChannelState.ts b/src/components/ChannelList/hooks/useSelectedChannelState.ts index bf1e677303..3100e6ffa1 100644 --- a/src/components/ChannelList/hooks/useSelectedChannelState.ts +++ b/src/components/ChannelList/hooks/useSelectedChannelState.ts @@ -17,8 +17,8 @@ export function useSelectedChannelState(_: { }): O | undefined; export function useSelectedChannelState({ channel, - stateChangeEventKeys = ['all'], selector, + stateChangeEventKeys = ['all'], }: { selector: (channel: Channel) => O; channel?: Channel; diff --git a/src/components/ChannelList/hooks/useUserPresenceChangedListener.ts b/src/components/ChannelList/hooks/useUserPresenceChangedListener.ts index 771942caae..9d876f994a 100644 --- a/src/components/ChannelList/hooks/useUserPresenceChangedListener.ts +++ b/src/components/ChannelList/hooks/useUserPresenceChangedListener.ts @@ -7,7 +7,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useUserPresenceChangedListener = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( setChannels: React.Dispatch>>>, ) => { diff --git a/src/components/ChannelList/utils.ts b/src/components/ChannelList/utils.ts index 5c38301094..2a867d2dd2 100644 --- a/src/components/ChannelList/utils.ts +++ b/src/components/ChannelList/utils.ts @@ -1,12 +1,19 @@ import uniqBy from 'lodash.uniqby'; -import type { Channel, ChannelSort, ChannelSortBase, ExtendableGenerics } from 'stream-chat'; +import type { + Channel, + ChannelSort, + ChannelSortBase, + ExtendableGenerics, +} from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../types/types'; import type { ChannelListProps } from './ChannelList'; export const MAX_QUERY_CHANNELS_LIMIT = 30; -type MoveChannelUpParams = { +type MoveChannelUpParams< + SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = { channels: Array>; cid: string; activeChannel?: Channel; @@ -15,7 +22,9 @@ type MoveChannelUpParams({ +export const moveChannelUp = < + SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ activeChannel, channels, cid, @@ -56,7 +65,9 @@ export function findLastPinnedChannelIndex({ return lastPinnedChannelIndex; } -type MoveChannelUpwardsParams = { +type MoveChannelUpwardsParams< + SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = { channels: Array>; channelToMove: Channel; sort: ChannelSort; @@ -68,7 +79,7 @@ type MoveChannelUpwardsParams({ channels, channelToMove, @@ -181,7 +192,9 @@ export const shouldConsiderArchivedChannels = ( /** * Returns `true` only if `pinned_at` property is of type `string` within `membership` object. */ -export const isChannelPinned = (channel: Channel) => { +export const isChannelPinned = ( + channel: Channel, +) => { if (!channel) return false; const membership = channel.state.membership; @@ -192,7 +205,9 @@ export const isChannelPinned = (channel: Channel /** * Returns `true` only if `archived_at` property is of type `string` within `membership` object. */ -export const isChannelArchived = (channel: Channel) => { +export const isChannelArchived = ( + channel: Channel, +) => { if (!channel) return false; const membership = channel.state.membership; diff --git a/src/components/ChannelPreview/ChannelPreview.tsx b/src/components/ChannelPreview/ChannelPreview.tsx index a25bd333e0..b6bc89738a 100644 --- a/src/components/ChannelPreview/ChannelPreview.tsx +++ b/src/components/ChannelPreview/ChannelPreview.tsx @@ -8,7 +8,10 @@ import { getLatestMessagePreview as defaultGetLatestMessagePreview } from './uti import { ChatContextValue, useChatContext } from '../../context/ChatContext'; import { useTranslationContext } from '../../context/TranslationContext'; -import { MessageDeliveryStatus, useMessageDeliveryStatus } from './hooks/useMessageDeliveryStatus'; +import { + MessageDeliveryStatus, + useMessageDeliveryStatus, +} from './hooks/useMessageDeliveryStatus'; import type { Channel, Event } from 'stream-chat'; @@ -19,7 +22,7 @@ import type { TranslationContextValue } from '../../context/TranslationContext'; import type { DefaultStreamChatGenerics } from '../../types/types'; export type ChannelPreviewUIComponentProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = ChannelPreviewProps & { /** If the component's channel is the active (selected) Channel */ active?: boolean; @@ -42,7 +45,7 @@ export type ChannelPreviewUIComponentProps< }; export type ChannelPreviewProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { /** Comes from either the `channelRenderFilterFn` or `usePaginatedChannels` call from [ChannelList](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelList/ChannelList.tsx) */ channel: Channel; @@ -72,15 +75,15 @@ export type ChannelPreviewProps< }; export const ChannelPreview = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: ChannelPreviewProps, ) => { const { channel, - Preview = ChannelPreviewMessenger, channelUpdateCount, getLatestMessagePreview = defaultGetLatestMessagePreview, + Preview = ChannelPreviewMessenger, } = props; const { channel: activeChannel, @@ -144,7 +147,9 @@ export const ChannelPreview = < refreshUnreadCount(); const handleEvent = () => { - setLastMessage(channel.state.latestMessages[channel.state.latestMessages.length - 1]); + setLastMessage( + channel.state.latestMessages[channel.state.latestMessages.length - 1], + ); refreshUnreadCount(); }; diff --git a/src/components/ChannelPreview/ChannelPreviewMessenger.tsx b/src/components/ChannelPreview/ChannelPreviewMessenger.tsx index ed93d7585e..389c7bd140 100644 --- a/src/components/ChannelPreview/ChannelPreviewMessenger.tsx +++ b/src/components/ChannelPreview/ChannelPreviewMessenger.tsx @@ -8,7 +8,7 @@ import type { DefaultStreamChatGenerics } from '../../types/types'; import type { ChannelPreviewUIComponentProps } from './ChannelPreview'; const UnMemoizedChannelPreviewMessenger = < - SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: ChannelPreviewUIComponentProps, ) => { @@ -27,9 +27,8 @@ const UnMemoizedChannelPreviewMessenger = < watchers, } = props; - const { - ChannelPreviewActionButtons = DefaultChannelPreviewActionButtons, - } = useComponentContext(); + const { ChannelPreviewActionButtons = DefaultChannelPreviewActionButtons } = + useComponentContext(); const channelPreviewButton = useRef(null); @@ -78,7 +77,10 @@ const UnMemoizedChannelPreviewMessenger = < {displayTitle}
      {!!unread && ( -
      +
      {unread}
      )} diff --git a/src/components/ChannelPreview/__tests__/ChannelPreview.test.js b/src/components/ChannelPreview/__tests__/ChannelPreview.test.js index 035ac4bb5b..5380279d03 100644 --- a/src/components/ChannelPreview/__tests__/ChannelPreview.test.js +++ b/src/components/ChannelPreview/__tests__/ChannelPreview.test.js @@ -98,7 +98,6 @@ describe('ChannelPreview', () => { [c0, c1] = await client.queryChannels({}, {}); }); - // eslint-disable-next-line jest/expect-expect it('should mark channel as read, when set as active channel', async () => { // Mock the countUnread function on channel, to return 10. c0.countUnread = () => 10; @@ -670,8 +669,14 @@ describe('ChannelPreview', () => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages).toHaveLength(3); expect(avatarImages[0]).toHaveAttribute('src', ownUser.image); - expect(avatarImages[1]).toHaveAttribute('src', channelState.members[1].user.image); - expect(avatarImages[2]).toHaveAttribute('src', channelState.members[2].user.image); + expect(avatarImages[1]).toHaveAttribute( + 'src', + channelState.members[1].user.image, + ); + expect(avatarImages[2]).toHaveAttribute( + 'src', + channelState.members[2].user.image, + ); }); act(() => { @@ -681,8 +686,14 @@ describe('ChannelPreview', () => { await waitFor(() => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages[0]).toHaveAttribute('src', updatedAttribute.image); - expect(avatarImages[1]).toHaveAttribute('src', channelState.members[1].user.image); - expect(avatarImages[2]).toHaveAttribute('src', channelState.members[2].user.image); + expect(avatarImages[1]).toHaveAttribute( + 'src', + channelState.members[1].user.image, + ); + expect(avatarImages[2]).toHaveAttribute( + 'src', + channelState.members[2].user.image, + ); }); }); @@ -702,8 +713,14 @@ describe('ChannelPreview', () => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages).toHaveLength(3); expect(avatarImages[0]).toHaveAttribute('src', ownUser.image); - expect(avatarImages[1]).toHaveAttribute('src', channelState.members[1].user.image); - expect(avatarImages[2]).toHaveAttribute('src', channelState.members[2].user.image); + expect(avatarImages[1]).toHaveAttribute( + 'src', + channelState.members[1].user.image, + ); + expect(avatarImages[2]).toHaveAttribute( + 'src', + channelState.members[2].user.image, + ); }); act(() => { @@ -713,7 +730,10 @@ describe('ChannelPreview', () => { await waitFor(() => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages[0]).toHaveAttribute('src', ownUser.image); - expect(avatarImages[1]).toHaveAttribute('src', channelState.members[1].user.image); + expect(avatarImages[1]).toHaveAttribute( + 'src', + channelState.members[1].user.image, + ); expect(avatarImages[2]).toHaveAttribute('src', updatedAttribute.image); }); }); diff --git a/src/components/ChannelPreview/__tests__/ChannelPreviewMessenger.test.js b/src/components/ChannelPreview/__tests__/ChannelPreviewMessenger.test.js index c523e1edcf..6dfcda5d7e 100644 --- a/src/components/ChannelPreview/__tests__/ChannelPreviewMessenger.test.js +++ b/src/components/ChannelPreview/__tests__/ChannelPreviewMessenger.test.js @@ -74,7 +74,6 @@ describe('ChannelPreviewMessenger', () => { fireEvent.click(getByTestId(PREVIEW_TEST_ID)); await waitFor(() => { - // eslint-disable-next-line jest/prefer-called-with expect(setActiveChannel).toHaveBeenCalledTimes(1); expect(setActiveChannel).toHaveBeenCalledWith(channel, {}); }); diff --git a/src/components/ChannelPreview/__tests__/utils.test.js b/src/components/ChannelPreview/__tests__/utils.test.js index f254cb811c..fb2722d33e 100644 --- a/src/components/ChannelPreview/__tests__/utils.test.js +++ b/src/components/ChannelPreview/__tests__/utils.test.js @@ -60,7 +60,9 @@ describe('ChannelPreview utils', () => { describe('getDisplayTitle', () => { it('should return channel name, if it exists', async () => { const name = nanoid(); - const channel = await getQueriedChannelInstance(generateChannel({ channel: { name } })); + const channel = await getQueriedChannelInstance( + generateChannel({ channel: { name } }), + ); expect(getDisplayTitle(channel, chatClient.user)).toBe(name); }); @@ -69,7 +71,10 @@ describe('ChannelPreview utils', () => { const otherUser = generateUser(); const channel = await getQueriedChannelInstance( generateChannel({ - members: [generateMember({ user: otherUser }), generateMember({ user: clientUser })], + members: [ + generateMember({ user: otherUser }), + generateMember({ user: clientUser }), + ], }), ); expect(getDisplayTitle(channel, chatClient.user)).toBe(otherUser.name); @@ -79,7 +84,9 @@ describe('ChannelPreview utils', () => { describe('getDisplayImage', () => { it('should return channel image, if it exists', async () => { const image = nanoid(); - const channel = await getQueriedChannelInstance(generateChannel({ channel: { image } })); + const channel = await getQueriedChannelInstance( + generateChannel({ channel: { image } }), + ); expect(getDisplayImage(channel, chatClient.user)).toBe(image); }); @@ -88,7 +95,10 @@ describe('ChannelPreview utils', () => { const otherUser = generateUser(); const channel = await getQueriedChannelInstance( generateChannel({ - members: [generateMember({ user: otherUser }), generateMember({ user: clientUser })], + members: [ + generateMember({ user: otherUser }), + generateMember({ user: clientUser }), + ], }), ); expect(getDisplayImage(channel, chatClient.user)).toBe(otherUser.image); diff --git a/src/components/ChannelPreview/hooks/__tests__/useMessageDeliveryStatus.test.js b/src/components/ChannelPreview/hooks/__tests__/useMessageDeliveryStatus.test.js index 94a44f8b19..c716fa492a 100644 --- a/src/components/ChannelPreview/hooks/__tests__/useMessageDeliveryStatus.test.js +++ b/src/components/ChannelPreview/hooks/__tests__/useMessageDeliveryStatus.test.js @@ -1,6 +1,9 @@ import React from 'react'; import { renderHook } from '@testing-library/react'; -import { MessageDeliveryStatus, useMessageDeliveryStatus } from '../useMessageDeliveryStatus'; +import { + MessageDeliveryStatus, + useMessageDeliveryStatus, +} from '../useMessageDeliveryStatus'; import { ChatContext } from '../../../../context'; import { dispatchMessageDeletedEvent, @@ -44,7 +47,9 @@ const renderComponent = ({ channel, client, lastMessage }) => { {children} ); - return renderHook(() => useMessageDeliveryStatus({ channel, lastMessage }), { wrapper }); + return renderHook(() => useMessageDeliveryStatus({ channel, lastMessage }), { + wrapper, + }); }; describe('Message delivery status', () => { diff --git a/src/components/ChannelPreview/hooks/useChannelPreviewInfo.ts b/src/components/ChannelPreview/hooks/useChannelPreviewInfo.ts index 336b7eb768..f1fbb80cc1 100644 --- a/src/components/ChannelPreview/hooks/useChannelPreviewInfo.ts +++ b/src/components/ChannelPreview/hooks/useChannelPreviewInfo.ts @@ -6,7 +6,9 @@ import { useChatContext } from '../../../context'; import type { DefaultStreamChatGenerics } from '../../../types/types'; -export type ChannelPreviewInfoParams = { +export type ChannelPreviewInfoParams< + StreamChatGenerics extends DefaultStreamChatGenerics, +> = { channel: Channel; /** Manually set the image to render, defaults to the Channel image */ overrideImage?: string; @@ -15,7 +17,7 @@ export type ChannelPreviewInfoParams( props: ChannelPreviewInfoParams, ) => { diff --git a/src/components/ChannelPreview/hooks/useIsChannelMuted.ts b/src/components/ChannelPreview/hooks/useIsChannelMuted.ts index cda462f77e..2e9c471979 100644 --- a/src/components/ChannelPreview/hooks/useIsChannelMuted.ts +++ b/src/components/ChannelPreview/hooks/useIsChannelMuted.ts @@ -7,7 +7,7 @@ import type { Channel } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useIsChannelMuted = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( channel: Channel, ) => { diff --git a/src/components/ChannelPreview/hooks/useMessageDeliveryStatus.ts b/src/components/ChannelPreview/hooks/useMessageDeliveryStatus.ts index d7c63aa34b..8d8e3c3f70 100644 --- a/src/components/ChannelPreview/hooks/useMessageDeliveryStatus.ts +++ b/src/components/ChannelPreview/hooks/useMessageDeliveryStatus.ts @@ -12,7 +12,7 @@ export enum MessageDeliveryStatus { } type UseMessageStatusParamsChannelPreviewProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { channel: Channel; /** The last message received in a channel */ @@ -20,7 +20,7 @@ type UseMessageStatusParamsChannelPreviewProps< }; export const useMessageDeliveryStatus = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ channel, lastMessage, @@ -45,12 +45,12 @@ export const useMessageDeliveryStatus = < ? new Date(lastMessage.created_at) : lastMessage.created_at; - const channelReadByOthersAfterLastMessageUpdate = Object.values(channel.state.read).some( - ({ last_read: channelLastMarkedReadDate, user }) => { - const ignoreOwnReadStatus = client.user && user.id !== client.user.id; - return ignoreOwnReadStatus && lastMessageCreatedAtDate < channelLastMarkedReadDate; - }, - ); + const channelReadByOthersAfterLastMessageUpdate = Object.values( + channel.state.read, + ).some(({ last_read: channelLastMarkedReadDate, user }) => { + const ignoreOwnReadStatus = client.user && user.id !== client.user.id; + return ignoreOwnReadStatus && lastMessageCreatedAtDate < channelLastMarkedReadDate; + }); setMessageDeliveryStatus( channelReadByOthersAfterLastMessageUpdate @@ -79,7 +79,8 @@ export const useMessageDeliveryStatus = < useEffect(() => { if (!isOwnMessage(lastMessage)) return; const handleMarkRead = (event: Event) => { - if (event.user?.id !== client.user?.id) setMessageDeliveryStatus(MessageDeliveryStatus.READ); + if (event.user?.id !== client.user?.id) + setMessageDeliveryStatus(MessageDeliveryStatus.READ); }; channel.on('message.read', handleMarkRead); diff --git a/src/components/ChannelPreview/icons.tsx b/src/components/ChannelPreview/icons.tsx index 7ef798b1c3..04507fdaa0 100644 --- a/src/components/ChannelPreview/icons.tsx +++ b/src/components/ChannelPreview/icons.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/display-name */ import React from 'react'; import { ComponentPropsWithoutRef } from 'react'; diff --git a/src/components/ChannelPreview/utils.tsx b/src/components/ChannelPreview/utils.tsx index 4b79ddd0ba..492b4293f9 100644 --- a/src/components/ChannelPreview/utils.tsx +++ b/src/components/ChannelPreview/utils.tsx @@ -9,17 +9,20 @@ import type { TranslationContextValue } from '../../context/TranslationContext'; import type { DefaultStreamChatGenerics } from '../../types/types'; import { ChatContextValue } from '../../context'; -export const renderPreviewText = (text: string) => {text}; +export const renderPreviewText = (text: string) => ( + {text} +); const getLatestPollVote = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( latestVotesByOption: Record[]>, ) => { let latestVote: PollVote | undefined; for (const optionVotes of Object.values(latestVotesByOption)) { optionVotes.forEach((vote) => { - if (latestVote && new Date(latestVote.updated_at) >= new Date(vote.created_at)) return; + if (latestVote && new Date(latestVote.updated_at) >= new Date(vote.created_at)) + return; latestVote = vote; }); } @@ -28,14 +31,15 @@ const getLatestPollVote = < }; export const getLatestMessagePreview = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( channel: Channel, t: TranslationContextValue['t'], userLanguage: TranslationContextValue['userLanguage'] = 'en', isMessageAIGenerated?: ChatContextValue['isMessageAIGenerated'], ): string | JSX.Element => { - const latestMessage = channel.state.latestMessages[channel.state.latestMessages.length - 1]; + const latestMessage = + channel.state.latestMessages[channel.state.latestMessages.length - 1]; const previewTextToRender = latestMessage?.i18n?.[`${userLanguage}_text` as `${TranslationLanguages}_text`] || @@ -55,7 +59,7 @@ export const getLatestMessagePreview = < const createdBy = poll.created_by?.id === channel.getClient().userID ? t('You') - : poll.created_by?.name ?? t('Poll'); + : (poll.created_by?.name ?? t('Poll')); return t('📊 {{createdBy}} created: {{ pollName}}', { createdBy, pollName: poll.name, @@ -64,7 +68,8 @@ export const getLatestMessagePreview = < const latestVote = getLatestPollVote( poll.latest_votes_by_option as Record[]>, ); - const option = latestVote && poll.options.find((opt) => opt.id === latestVote.option_id); + const option = + latestVote && poll.options.find((opt) => opt.id === latestVote.option_id); if (option && latestVote) { return t('📊 {{votedBy}} voted: {{pollOptionText}}', { @@ -72,7 +77,7 @@ export const getLatestMessagePreview = < votedBy: latestVote?.user?.id === channel.getClient().userID ? t('You') - : latestVote.user?.name ?? t('Poll'), + : (latestVote.user?.name ?? t('Poll')), }); } } @@ -98,7 +103,7 @@ export const getLatestMessagePreview = < export type GroupChannelDisplayInfo = { image?: string; name?: string }[]; export const getGroupChannelDisplayInfo = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( channel: Channel, ): GroupChannelDisplayInfo | undefined => { @@ -116,7 +121,7 @@ export const getGroupChannelDisplayInfo = < }; const getChannelDisplayInfo = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( info: 'name' | 'image', channel: Channel, @@ -130,14 +135,14 @@ const getChannelDisplayInfo = < }; export const getDisplayTitle = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( channel: Channel, currentUser?: UserResponse, ) => getChannelDisplayInfo('name', channel, currentUser); export const getDisplayImage = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( channel: Channel, currentUser?: UserResponse, diff --git a/src/components/ChannelSearch/ChannelSearch.tsx b/src/components/ChannelSearch/ChannelSearch.tsx index d6229db6f7..24b6a3be37 100644 --- a/src/components/ChannelSearch/ChannelSearch.tsx +++ b/src/components/ChannelSearch/ChannelSearch.tsx @@ -1,7 +1,10 @@ import clsx from 'clsx'; import React from 'react'; -import { ChannelSearchControllerParams, useChannelSearch } from './hooks/useChannelSearch'; +import { + ChannelSearchControllerParams, + useChannelSearch, +} from './hooks/useChannelSearch'; import type { AdditionalSearchBarProps, SearchBarProps } from './SearchBar'; import { SearchBar as DefaultSearchBar } from './SearchBar'; @@ -22,7 +25,7 @@ export type AdditionalChannelSearchProps = { }; export type ChannelSearchProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = AdditionalSearchBarProps & AdditionalSearchInputProps & AdditionalSearchResultsProps & @@ -30,7 +33,7 @@ export type ChannelSearchProps< ChannelSearchControllerParams; const UnMemoizedChannelSearch = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: ChannelSearchProps, ) => { @@ -44,11 +47,11 @@ const UnMemoizedChannelSearch = < SearchBar = DefaultSearchBar, SearchEmpty, SearchInput = DefaultSearchInput, - SearchLoading, SearchInputIcon, + SearchLoading, SearchResultItem, - SearchResultsList, SearchResultsHeader, + SearchResultsList, ...channelSearchParams } = props; @@ -70,7 +73,9 @@ const UnMemoizedChannelSearch = <
      0, }, @@ -118,4 +123,6 @@ const UnMemoizedChannelSearch = < * Clicking on a list item will navigate you into a channel with the selected user. It can be used * on its own or added to the ChannelList component by setting the `showChannelSearch` prop to true. */ -export const ChannelSearch = React.memo(UnMemoizedChannelSearch) as typeof UnMemoizedChannelSearch; +export const ChannelSearch = React.memo( + UnMemoizedChannelSearch, +) as typeof UnMemoizedChannelSearch; diff --git a/src/components/ChannelSearch/SearchBar.tsx b/src/components/ChannelSearch/SearchBar.tsx index 0451a4c382..735ae91a6a 100644 --- a/src/components/ChannelSearch/SearchBar.tsx +++ b/src/components/ChannelSearch/SearchBar.tsx @@ -65,7 +65,9 @@ export type AdditionalSearchBarProps = { SearchInputIcon?: React.ComponentType; }; -export type SearchBarProps = AdditionalSearchBarProps & SearchBarController & SearchInputProps; +export type SearchBarProps = AdditionalSearchBarProps & + SearchBarController & + SearchInputProps; // todo: add context menu control logic export const SearchBar = (props: SearchBarProps) => { @@ -142,7 +144,11 @@ export const SearchBar = (props: SearchBarProps) => { const closeAppMenu = useCallback(() => setMenuIsOpen(false), []); return ( -
      +
      {inputIsFocused ? ( { }; export type SearchResultsHeaderProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick, 'results'>; const DefaultSearchResultsHeader = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ results, }: SearchResultsHeaderProps) => { @@ -43,15 +43,18 @@ const DefaultSearchResultsHeader = < }; export type SearchResultsListProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Required< - Pick, 'results' | 'SearchResultItem' | 'selectResult'> + Pick< + SearchResultsProps, + 'results' | 'SearchResultItem' | 'selectResult' + > > & { focusedUser?: number; }; const DefaultSearchResultsList = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: SearchResultsListProps, ) => { @@ -73,7 +76,7 @@ const DefaultSearchResultsList = < }; export type SearchResultItemProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick, 'selectResult'> & { index: number; result: ChannelOrUserResponse; @@ -81,7 +84,7 @@ export type SearchResultItemProps< }; const DefaultSearchResultItem = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: SearchResultItemProps, ) => { @@ -135,7 +138,10 @@ const ResultsContainer = ({ return (
      {children}
      @@ -143,15 +149,17 @@ const ResultsContainer = ({ }; export type SearchResultsController< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { results: Array>; searching: boolean; - selectResult: (result: ChannelOrUserResponse) => Promise | void; + selectResult: ( + result: ChannelOrUserResponse, + ) => Promise | void; }; export type AdditionalSearchResultsProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { /** Display search results as an absolutely positioned popup, defaults to false and shows inline */ popupResults?: boolean; @@ -168,22 +176,23 @@ export type AdditionalSearchResultsProps< }; export type SearchResultsProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics -> = AdditionalSearchResultsProps & SearchResultsController; + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = AdditionalSearchResultsProps & + SearchResultsController; export const SearchResults = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: SearchResultsProps, ) => { const { popupResults, results, - searching, SearchEmpty = DefaultSearchEmpty, - SearchResultsHeader = DefaultSearchResultsHeader, + searching, SearchLoading, SearchResultItem = DefaultSearchResultItem, + SearchResultsHeader = DefaultSearchResultsHeader, SearchResultsList = DefaultSearchResultsList, selectResult, } = props; diff --git a/src/components/ChannelSearch/__tests__/ChannelSearch.test.js b/src/components/ChannelSearch/__tests__/ChannelSearch.test.js index 1ef5ceaede..c113abdb12 100644 --- a/src/components/ChannelSearch/__tests__/ChannelSearch.test.js +++ b/src/components/ChannelSearch/__tests__/ChannelSearch.test.js @@ -85,7 +85,9 @@ describe('ChannelSearch', () => { it('starts with "searching" flag disabled', async () => { await renderSearch(); - expect(screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR)).not.toBeInTheDocument(); + expect( + screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR), + ).not.toBeInTheDocument(); }); it('sets "searching" flag on first typing stroke', async () => { @@ -93,7 +95,9 @@ describe('ChannelSearch', () => { await act(() => { typeText(typedText); }); - expect(screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR)).toBeInTheDocument(); + expect( + screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR), + ).toBeInTheDocument(); }); it('removes "searching" flag upon deleting the last character', async () => { @@ -101,11 +105,15 @@ describe('ChannelSearch', () => { await act(() => { typeText(typedText); }); - expect(screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR)).toBeInTheDocument(); + expect( + screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR), + ).toBeInTheDocument(); await act(() => { typeText(''); }); - expect(screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR)).not.toBeInTheDocument(); + expect( + screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR), + ).not.toBeInTheDocument(); }); it('removes "searching" flag upon setting search results', async () => { @@ -116,14 +124,18 @@ describe('ChannelSearch', () => { await act(() => { typeText(typedText); }); - expect(screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR)).toBeInTheDocument(); + expect( + screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR), + ).toBeInTheDocument(); await act(() => { jest.advanceTimersByTime(1000); }); await waitFor(() => { - expect(screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR)).not.toBeInTheDocument(); + expect( + screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR), + ).not.toBeInTheDocument(); }); jest.useRealTimers(); }); @@ -146,7 +158,10 @@ describe('ChannelSearch', () => { expect(client.queryUsers).toHaveBeenCalledWith( expect.objectContaining({ - $or: [{ id: { $autocomplete: typedText } }, { name: { $autocomplete: typedText } }], + $or: [ + { id: { $autocomplete: typedText } }, + { name: { $autocomplete: typedText } }, + ], }), { id: 1 }, { limit }, @@ -169,7 +184,10 @@ describe('ChannelSearch', () => { jest.spyOn(client, 'queryUsers').mockResolvedValue({ users: [...otherUsers, user] }); jest.spyOn(client, 'queryChannels').mockResolvedValue([channelResponseData]); - const { typeText } = await renderSearch({ client, props: { searchForChannels: true } }); + const { typeText } = await renderSearch({ + client, + props: { searchForChannels: true }, + }); await act(() => { typeText(typedText); }); @@ -276,8 +294,12 @@ describe('ChannelSearch', () => { }); expect(client.queryUsers).toHaveBeenCalledTimes(1); - expect(screen.queryByTestId(TEST_ID.CHANNEL_SEARCH_RESULTS_HEADER)).not.toBeInTheDocument(); - expect(screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR)).not.toBeInTheDocument(); + expect( + screen.queryByTestId(TEST_ID.CHANNEL_SEARCH_RESULTS_HEADER), + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR), + ).not.toBeInTheDocument(); jest.useRealTimers(); }); @@ -300,8 +322,12 @@ describe('ChannelSearch', () => { }); expect(client.queryUsers).toHaveBeenCalledTimes(1); - expect(screen.queryByTestId(TEST_ID.CHANNEL_SEARCH_RESULTS_HEADER)).not.toBeInTheDocument(); - expect(screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR)).not.toBeInTheDocument(); + expect( + screen.queryByTestId(TEST_ID.CHANNEL_SEARCH_RESULTS_HEADER), + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId(TEST_ID.SEARCH_IN_PROGRESS_INDICATOR), + ).not.toBeInTheDocument(); jest.useRealTimers(); }); @@ -333,7 +359,10 @@ describe('ChannelSearch', () => { expect(client.queryUsers).toHaveBeenCalledTimes(1); expect(client.queryUsers).toHaveBeenCalledWith( expect.objectContaining({ - $or: [{ id: { $autocomplete: textToQuery } }, { name: { $autocomplete: textToQuery } }], + $or: [ + { id: { $autocomplete: textToQuery } }, + { name: { $autocomplete: textToQuery } }, + ], }), { id: 1 }, { limit: 8 }, @@ -373,7 +402,10 @@ describe('ChannelSearch', () => { expect(client.queryUsers).toHaveBeenCalledTimes(1); expect(client.queryUsers).toHaveBeenCalledWith( expect.objectContaining({ - $or: [{ id: { $autocomplete: textToQuery } }, { name: { $autocomplete: textToQuery } }], + $or: [ + { id: { $autocomplete: textToQuery } }, + { name: { $autocomplete: textToQuery } }, + ], }), { id: 1 }, { limit: 8 }, diff --git a/src/components/ChannelSearch/__tests__/SearchBar.test.js b/src/components/ChannelSearch/__tests__/SearchBar.test.js index ad23f71079..f434333eeb 100644 --- a/src/components/ChannelSearch/__tests__/SearchBar.test.js +++ b/src/components/ChannelSearch/__tests__/SearchBar.test.js @@ -43,7 +43,7 @@ describe('SearchBar', () => { beforeEach(async () => { const user = generateUser(); client = await getTestClientWithUser({ id: user.id }); - useMockedApis(client, [queryUsersApi([user])]); // eslint-disable-line react-hooks/rules-of-hooks + useMockedApis(client, [queryUsersApi([user])]); }); it.each([ diff --git a/src/components/ChannelSearch/__tests__/SearchResults.test.js b/src/components/ChannelSearch/__tests__/SearchResults.test.js index 47bf988b1c..85152b8e0b 100644 --- a/src/components/ChannelSearch/__tests__/SearchResults.test.js +++ b/src/components/ChannelSearch/__tests__/SearchResults.test.js @@ -6,7 +6,11 @@ import { SearchResults } from '../SearchResults'; import { ChatProvider } from '../../../context/ChatContext'; -import { createClientWithChannel, generateChannel, generateUser } from '../../../mock-builders'; +import { + createClientWithChannel, + generateChannel, + generateUser, +} from '../../../mock-builders'; const SEARCH_RESULT_LIST_SELECTOR = '.str-chat__channel-search-result-list'; diff --git a/src/components/ChannelSearch/hooks/useChannelSearch.ts b/src/components/ChannelSearch/hooks/useChannelSearch.ts index 7a44f7a9d1..9139e71647 100644 --- a/src/components/ChannelSearch/hooks/useChannelSearch.ts +++ b/src/components/ChannelSearch/hooks/useChannelSearch.ts @@ -22,19 +22,23 @@ import type { SearchResultsController } from '../SearchResults'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export type ChannelSearchFunctionParams< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { setQuery: React.Dispatch>; - setResults: React.Dispatch[]>>; + setResults: React.Dispatch< + React.SetStateAction[]> + >; setSearching: React.Dispatch>; }; export type SearchController< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics -> = SearchInputController & SearchBarController & SearchResultsController; + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = SearchInputController & + SearchBarController & + SearchResultsController; export type SearchQueryParams< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { channelFilters?: { filters?: ChannelFilters; @@ -51,7 +55,7 @@ export type SearchQueryParams< }; export type ChannelSearchParams< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { /** The type of channel to create on user result select, defaults to `messaging` */ channelType?: string; @@ -84,14 +88,14 @@ export type ChannelSearchParams< }; export type ChannelSearchControllerParams< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = ChannelSearchParams & { /** Set the array of channels displayed in the ChannelList */ setChannels?: React.Dispatch>>>; }; export const useChannelSearch = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ channelType = 'messaging', clearSearchOnClickOutside = true, @@ -106,11 +110,14 @@ export const useChannelSearch = < searchQueryParams, setChannels, }: ChannelSearchControllerParams): SearchController => { - const { client, setActiveChannel } = useChatContext('useChannelSearch'); + const { client, setActiveChannel } = + useChatContext('useChannelSearch'); const [inputIsFocused, setInputIsFocused] = useState(false); const [query, setQuery] = useState(''); - const [results, setResults] = useState>>([]); + const [results, setResults] = useState< + Array> + >([]); const [searching, setSearching] = useState(false); const searchQueryPromiseInProgress = useRef(false); @@ -154,7 +161,6 @@ export const useChannelSearch = < document.addEventListener('click', clickListener); return () => document.removeEventListener('click', clickListener); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [disabled, inputIsFocused, query, exitSearch, clearSearchOnClickOutside]); useEffect(() => { @@ -191,7 +197,9 @@ export const useChannelSearch = < setActiveChannel(result); selectedChannel = result; } else { - const newChannel = client.channel(channelType, { members: [client.userID, result.id] }); + const newChannel = client.channel(channelType, { + members: [client.userID, result.id], + }); await newChannel.watch(); setActiveChannel(newChannel); @@ -203,7 +211,14 @@ export const useChannelSearch = < } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [clearSearchOnClickOutside, client, exitSearch, onSelectResult, setActiveChannel, setChannels], + [ + clearSearchOnClickOutside, + client, + exitSearch, + onSelectResult, + setActiveChannel, + setChannels, + ], ); const getChannels = useCallback( @@ -211,13 +226,14 @@ export const useChannelSearch = < if (!searchForChannels && !searchForUsers) return; let results: ChannelOrUserResponse[] = []; const promises: Array< - Promise[]> | Promise> + | Promise[]> + | Promise> > = []; try { if (searchForChannels) { promises.push( client.queryChannels( - // @ts-expect-error + // @ts-expect-error valid query { members: { $in: [client.userID] }, name: { $autocomplete: text }, @@ -232,7 +248,7 @@ export const useChannelSearch = < if (searchForUsers) { promises.push( client.queryUsers( - // @ts-expect-error + // @ts-expect-error valid query { $or: [{ id: { $autocomplete: text } }, { name: { $autocomplete: text } }], ...searchQueryParams?.userFilters?.filters, @@ -279,10 +295,10 @@ export const useChannelSearch = < ); // eslint-disable-next-line react-hooks/exhaustive-deps - const scheduleGetChannels = useCallback(debounce(getChannels, searchDebounceIntervalMs), [ - getChannels, - searchDebounceIntervalMs, - ]); + const scheduleGetChannels = useCallback( + debounce(getChannels, searchDebounceIntervalMs), + [getChannels, searchDebounceIntervalMs], + ); const onSearch = useCallback( (event: React.ChangeEvent) => { diff --git a/src/components/ChannelSearch/icons.tsx b/src/components/ChannelSearch/icons.tsx index 20fab9a298..d841b79ef0 100644 --- a/src/components/ChannelSearch/icons.tsx +++ b/src/components/ChannelSearch/icons.tsx @@ -36,7 +36,13 @@ export const ReturnIcon = () => ( ); export const XIcon = () => ( - + = Channel | UserResponse; export const isChannel = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( output: ChannelOrUserResponse, -): output is Channel => (output as Channel).cid != null; +): output is Channel => + (output as Channel).cid != null; diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index b908ee47bf..f65b6a1ff5 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -15,7 +15,7 @@ import type { DefaultStreamChatGenerics } from '../../types/types'; import type { MessageContextValue } from '../../context'; export type ChatProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { /** The StreamChat client object */ client: StreamChat; @@ -44,7 +44,7 @@ export type ChatProps< * as it provides the ChatContext. */ export const Chat = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: PropsWithChildren>, ) => { diff --git a/src/components/Chat/hooks/useChannelsQueryState.ts b/src/components/Chat/hooks/useChannelsQueryState.ts index 46329978c1..0a7fc6fb7a 100644 --- a/src/components/Chat/hooks/useChannelsQueryState.ts +++ b/src/components/Chat/hooks/useChannelsQueryState.ts @@ -16,7 +16,8 @@ export interface ChannelsQueryState { export const useChannelsQueryState = (): ChannelsQueryState => { const [error, setError] = useState | null>(null); - const [queryInProgress, setQueryInProgress] = useState('uninitialized'); + const [queryInProgress, setQueryInProgress] = + useState('uninitialized'); return { error, diff --git a/src/components/Chat/hooks/useChat.ts b/src/components/Chat/hooks/useChat.ts index 168c4c76e3..d7fcedaea8 100644 --- a/src/components/Chat/hooks/useChat.ts +++ b/src/components/Chat/hooks/useChat.ts @@ -8,12 +8,18 @@ import { SupportedTranslations, } from '../../../i18n'; -import type { AppSettingsAPIResponse, Channel, Event, Mute, StreamChat } from 'stream-chat'; +import type { + AppSettingsAPIResponse, + Channel, + Event, + Mute, + StreamChat, +} from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export type UseChatParams< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { client: StreamChat; defaultLanguage?: SupportedTranslations; @@ -22,7 +28,7 @@ export type UseChatParams< }; export const useChat = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ client, defaultLanguage = 'en', @@ -45,7 +51,9 @@ export const useChat = < const closeMobileNav = () => setNavOpen(false); const openMobileNav = () => setTimeout(() => setNavOpen(true), 100); - const appSettings = useRef> | null>(null); + const appSettings = useRef> | null>( + null, + ); const getAppSettings = () => { if (appSettings.current) { @@ -91,7 +99,9 @@ export const useChat = < if (!userLanguage) { const browserLanguage = window.navigator.language.slice(0, 2); // just get language code, not country-specific version - userLanguage = isLanguageSupported(browserLanguage) ? browserLanguage : defaultLanguage; + userLanguage = isLanguageSupported(browserLanguage) + ? browserLanguage + : defaultLanguage; } const streami18n = i18nInstance || new Streami18n({ language: userLanguage }); diff --git a/src/components/Chat/hooks/useCreateChatClient.ts b/src/components/Chat/hooks/useCreateChatClient.ts index f98af175a3..cefc95fe68 100644 --- a/src/components/Chat/hooks/useCreateChatClient.ts +++ b/src/components/Chat/hooks/useCreateChatClient.ts @@ -38,9 +38,11 @@ export const useCreateChatClient = (apiKey, undefined, cachedOptions); let didUserConnectInterrupt = false; - const connectionPromise = client.connectUser(cachedUserData, tokenOrProvider).then(() => { - if (!didUserConnectInterrupt) setChatClient(client); - }); + const connectionPromise = client + .connectUser(cachedUserData, tokenOrProvider) + .then(() => { + if (!didUserConnectInterrupt) setChatClient(client); + }); return () => { didUserConnectInterrupt = true; diff --git a/src/components/Chat/hooks/useCreateChatContext.ts b/src/components/Chat/hooks/useCreateChatContext.ts index d9cec2b0ba..5423628479 100644 --- a/src/components/Chat/hooks/useCreateChatContext.ts +++ b/src/components/Chat/hooks/useCreateChatContext.ts @@ -4,7 +4,7 @@ import type { ChatContextValue } from '../../../context/ChatContext'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useCreateChatContext = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( value: ChatContextValue, ) => { diff --git a/src/components/ChatAutoComplete/ChatAutoComplete.tsx b/src/components/ChatAutoComplete/ChatAutoComplete.tsx index 7c5f2c5123..eab63c1aa5 100644 --- a/src/components/ChatAutoComplete/ChatAutoComplete.tsx +++ b/src/components/ChatAutoComplete/ChatAutoComplete.tsx @@ -10,30 +10,38 @@ import { useComponentContext } from '../../context/ComponentContext'; import type { CommandResponse, UserResponse } from 'stream-chat'; import type { TriggerSettings } from '../MessageInput/DefaultTriggerProvider'; -import type { CustomTrigger, DefaultStreamChatGenerics, UnknownType } from '../../types/types'; +import type { + CustomTrigger, + DefaultStreamChatGenerics, + UnknownType, +} from '../../types/types'; import { EmojiSearchIndex, EmojiSearchIndexResult } from '../MessageInput'; type ObjectUnion = T[keyof T]; export type SuggestionCommand< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = CommandResponse; export type SuggestionUser< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = UserResponse; -export type SuggestionEmoji = EmojiSearchIndexResult & T; +export type SuggestionEmoji = + EmojiSearchIndexResult & T; export type SuggestionItem< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, - T extends UnknownType = UnknownType -> = SuggestionUser | SuggestionCommand | SuggestionEmoji; + T extends UnknownType = UnknownType, +> = + | SuggestionUser + | SuggestionCommand + | SuggestionEmoji; // FIXME: entity type is wrong, fix export type SuggestionItemProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, - T extends UnknownType = UnknownType + T extends UnknownType = UnknownType, > = { className: string; component: React.ComponentType<{ @@ -59,41 +67,41 @@ export interface SuggestionHeaderProps { export type SuggestionListProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, - V extends CustomTrigger = CustomTrigger -> = ObjectUnion< - { - [key in keyof TriggerSettings]: { - component: TriggerSettings[key]['component']; - currentTrigger: string; - dropdownScroll: (element: HTMLDivElement) => void; - getSelectedItem: - | ((item: Parameters[key]['output']>[0]) => void) - | null; - getTextToReplace: ( - item: Parameters[key]['output']>[0], - ) => { - caretPosition: 'start' | 'end' | 'next' | number; - text: string; - key?: string; - }; - Header: React.ComponentType; - onSelect: (newToken: { - caretPosition: 'start' | 'end' | 'next' | number; - text: string; - }) => void; - selectionEnd: number; - SuggestionItem: React.ComponentType; - values: Parameters< - Parameters[key]['dataProvider']>[2] - >[0]; - className?: string; - itemClassName?: string; - itemStyle?: React.CSSProperties; - style?: React.CSSProperties; - value?: string; + V extends CustomTrigger = CustomTrigger, +> = ObjectUnion<{ + [key in keyof TriggerSettings]: { + component: TriggerSettings[key]['component']; + currentTrigger: string; + dropdownScroll: (element: HTMLDivElement) => void; + getSelectedItem: + | (( + item: Parameters[key]['output']>[0], + ) => void) + | null; + getTextToReplace: ( + item: Parameters[key]['output']>[0], + ) => { + caretPosition: 'start' | 'end' | 'next' | number; + text: string; + key?: string; }; - } ->; + Header: React.ComponentType; + onSelect: (newToken: { + caretPosition: 'start' | 'end' | 'next' | number; + text: string; + }) => void; + selectionEnd: number; + SuggestionItem: React.ComponentType; + values: Parameters< + Parameters[key]['dataProvider']>[2] + >[0]; + className?: string; + itemClassName?: string; + itemStyle?: React.CSSProperties; + style?: React.CSSProperties; + value?: string; + }; +}>; export type ChatAutoCompleteProps = { /** Override the default disabled state of the underlying `textarea` component. */ @@ -120,7 +128,7 @@ export type ChatAutoCompleteProps = { const UnMemoizedChatAutoComplete = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, - V extends CustomTrigger = CustomTrigger + V extends CustomTrigger = CustomTrigger, >( props: ChatAutoCompleteProps, ) => { @@ -131,7 +139,12 @@ const UnMemoizedChatAutoComplete = < const { t } = useTranslationContext('ChatAutoComplete'); const messageInput = useMessageInputContext('ChatAutoComplete'); - const { cooldownRemaining, disabled, emojiSearchIndex, textareaRef: innerRef } = messageInput; + const { + cooldownRemaining, + disabled, + emojiSearchIndex, + textareaRef: innerRef, + } = messageInput; const placeholder = props.placeholder || t('Type your message'); diff --git a/src/components/ChatAutoComplete/__tests__/ChatAutocomplete.test.js b/src/components/ChatAutoComplete/__tests__/ChatAutocomplete.test.js index d5055a1629..6d94894d36 100644 --- a/src/components/ChatAutoComplete/__tests__/ChatAutocomplete.test.js +++ b/src/components/ChatAutoComplete/__tests__/ChatAutocomplete.test.js @@ -52,7 +52,8 @@ const renderComponent = async ( messageInputContextOverrides = {}, activeChannel = channel, ) => { - const placeholderText = props.placeholder === null ? null : props.placeholder || 'placeholder'; + const placeholderText = + props.placeholder === null ? null : props.placeholder || 'placeholder'; const OverrideMessageInputContext = ({ children }) => { const currentContext = useMessageInputContext(); @@ -61,7 +62,9 @@ const renderComponent = async ( ...messageInputContextOverrides, }; return ( - {children} + + {children} + ); }; @@ -147,7 +150,10 @@ describe('ChatAutoComplete', () => { }); it('should give preference to cooldown value over the prop disabled', async () => { - await renderComponent({ disabled: false, placeholder: null }, { cooldownRemaining: 10 }); + await renderComponent( + { disabled: false, placeholder: null }, + { cooldownRemaining: 10 }, + ); expect(screen.queryByPlaceholderText('Placeholder')).not.toBeInTheDocument(); const textarea = screen.getByTestId('message-input'); expect(textarea).toBeDisabled(); @@ -236,7 +242,6 @@ describe('ChatAutoComplete', () => { typeText(userAutocompleteText); const userText = await queryAllByText(user.name); - // eslint-disable-next-line jest-dom/prefer-in-document expect(userText).toHaveLength(0); }); diff --git a/src/components/ChatView/ChatView.tsx b/src/components/ChatView/ChatView.tsx index 7ff684c675..014731c87c 100644 --- a/src/components/ChatView/ChatView.tsx +++ b/src/components/ChatView/ChatView.tsx @@ -10,10 +10,10 @@ import type { PropsWithChildren } from 'react'; import type { Thread, ThreadManagerState } from 'stream-chat'; import clsx from 'clsx'; -const availableChatViews = ['channels', 'threads'] as const; +type ChatView = 'channels' | 'threads'; type ChatViewContextValue = { - activeChatView: typeof availableChatViews[number]; + activeChatView: ChatView; setActiveChatView: (cv: ChatViewContextValue['activeChatView']) => void; }; @@ -23,9 +23,8 @@ const ChatViewContext = createContext({ }); export const ChatView = ({ children }: PropsWithChildren) => { - const [activeChatView, setActiveChatView] = useState( - 'channels', - ); + const [activeChatView, setActiveChatView] = + useState('channels'); const { theme } = useChatContext(); @@ -60,9 +59,8 @@ export const useThreadsViewContext = () => useContext(ThreadsViewContext); const ThreadsView = ({ children }: PropsWithChildren) => { const { activeChatView } = useContext(ChatViewContext); - const [activeThread, setActiveThread] = useState( - undefined, - ); + const [activeThread, setActiveThread] = + useState(undefined); const value = useMemo(() => ({ activeThread, setActiveThread }), [activeThread]); diff --git a/src/components/CommandItem/CommandItem.tsx b/src/components/CommandItem/CommandItem.tsx index 9b53eb7efa..08ad4b07b8 100644 --- a/src/components/CommandItem/CommandItem.tsx +++ b/src/components/CommandItem/CommandItem.tsx @@ -25,4 +25,6 @@ const UnMemoizedCommandItem = (props: PropsWithChildren) => { ); }; -export const CommandItem = React.memo(UnMemoizedCommandItem) as typeof UnMemoizedCommandItem; +export const CommandItem = React.memo( + UnMemoizedCommandItem, +) as typeof UnMemoizedCommandItem; diff --git a/src/components/CommandItem/__tests__/CommandItem.test.js b/src/components/CommandItem/__tests__/CommandItem.test.js index 41f8195a22..df962b7384 100644 --- a/src/components/CommandItem/__tests__/CommandItem.test.js +++ b/src/components/CommandItem/__tests__/CommandItem.test.js @@ -5,7 +5,7 @@ import '@testing-library/jest-dom'; import { CommandItem } from '../CommandItem'; -afterEach(cleanup); // eslint-disable-line +afterEach(cleanup); describe('commandItem', () => { it('should render component with empty entity', () => { diff --git a/src/components/DateSeparator/DateSeparator.tsx b/src/components/DateSeparator/DateSeparator.tsx index f4bbb3a50b..daf75a6cb2 100644 --- a/src/components/DateSeparator/DateSeparator.tsx +++ b/src/components/DateSeparator/DateSeparator.tsx @@ -56,4 +56,6 @@ const UnMemoizedDateSeparator = (props: DateSeparatorProps) => { /** * A simple date separator between messages. */ -export const DateSeparator = React.memo(UnMemoizedDateSeparator) as typeof UnMemoizedDateSeparator; +export const DateSeparator = React.memo( + UnMemoizedDateSeparator, +) as typeof UnMemoizedDateSeparator; diff --git a/src/components/DateSeparator/__tests__/DateSeparator.test.js b/src/components/DateSeparator/__tests__/DateSeparator.test.js index 22926d7af3..e5669b48b5 100644 --- a/src/components/DateSeparator/__tests__/DateSeparator.test.js +++ b/src/components/DateSeparator/__tests__/DateSeparator.test.js @@ -12,7 +12,7 @@ import { Streami18n } from '../../../i18n'; Dayjs.extend(calendar); -afterEach(cleanup); // eslint-disable-line +afterEach(cleanup); const DATE_SEPARATOR_TEST_ID = 'date-separator'; const dateMock = 'the date'; @@ -106,7 +106,8 @@ describe('DateSeparator', () => { chatProps: { i18nInstance: new Streami18n({ translationsForLanguage: { - 'timestamp/DateSeparator': '{{ timestamp | timestampFormatter(calendar: false) }}', + 'timestamp/DateSeparator': + '{{ timestamp | timestampFormatter(calendar: false) }}', }, }), }, @@ -144,7 +145,8 @@ describe('DateSeparator', () => { chatProps: { i18nInstance: new Streami18n({ translationsForLanguage: { - 'timestamp/DateSeparator': '{{ timestamp | timestampFormatter(calendar: false) }}', + 'timestamp/DateSeparator': + '{{ timestamp | timestampFormatter(calendar: false) }}', }, }), }, diff --git a/src/components/Dialog/DialogManager.ts b/src/components/Dialog/DialogManager.ts index caa14e7f38..5ec28036b2 100644 --- a/src/components/Dialog/DialogManager.ts +++ b/src/components/Dialog/DialogManager.ts @@ -47,10 +47,13 @@ export class DialogManager { } get openDialogCount() { - return Object.values(this.state.getLatestValue().dialogsById).reduce((count, dialog) => { - if (dialog.isOpen) return count + 1; - return count; - }, 0); + return Object.values(this.state.getLatestValue().dialogsById).reduce( + (count, dialog) => { + if (dialog.isOpen) return count + 1; + return count; + }, + 0, + ); } getOrCreate({ id }: GetOrCreateDialogParams) { @@ -120,7 +123,9 @@ export class DialogManager { } closeAll() { - Object.values(this.state.getLatestValue().dialogsById).forEach((dialog) => dialog.close()); + Object.values(this.state.getLatestValue().dialogsById).forEach((dialog) => + dialog.close(), + ); } toggle(params: GetOrCreateDialogParams, closeAll = false) { diff --git a/src/components/Dialog/DialogMenu.tsx b/src/components/Dialog/DialogMenu.tsx index 9fe04f9986..33f7fcf09e 100644 --- a/src/components/Dialog/DialogMenu.tsx +++ b/src/components/Dialog/DialogMenu.tsx @@ -3,7 +3,11 @@ import clsx from 'clsx'; export type DialogMenuButtonProps = ComponentProps<'button'>; -export const DialogMenuButton = ({ children, className, ...props }: DialogMenuButtonProps) => ( +export const DialogMenuButton = ({ + children, + className, + ...props +}: DialogMenuButtonProps) => (
      {children}
      diff --git a/src/components/Dialog/FormDialog.tsx b/src/components/Dialog/FormDialog.tsx index d9eacd9d37..9908b7301c 100644 --- a/src/components/Dialog/FormDialog.tsx +++ b/src/components/Dialog/FormDialog.tsx @@ -36,7 +36,9 @@ type FormValue> = { }; export const FormDialog = < - F extends FormValue> = FormValue> + F extends FormValue> = FormValue< + Record + >, >({ className, close, @@ -55,7 +57,9 @@ export const FormDialog = < return acc as F; }); - const handleChange = useCallback>( + const handleChange = useCallback< + ChangeEventHandler + >( (event) => { const fieldId = event.target.id; const fieldConfig = fields[fieldId]; @@ -113,7 +117,9 @@ export const FormDialog = <
      {fieldConfig.label && (