feat: Implement rich media messaging for images, files, and GIFs, and add message read/delivery status indicators.
This commit is contained in:
515
package-lock.json
generated
515
package-lock.json
generated
@@ -8,13 +8,19 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emoji-mart/data": "^1.2.1",
|
||||||
|
"@emoji-mart/react": "^1.1.1",
|
||||||
|
"@giphy/js-fetch-api": "^5.7.0",
|
||||||
|
"@giphy/react-components": "^10.1.1",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"emoji-mart": "^5.6.0",
|
||||||
"next": "16.1.0",
|
"next": "16.1.0",
|
||||||
"next-auth": "^5.0.0-beta.30",
|
"next-auth": "^5.0.0-beta.30",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-select": "^5.10.2",
|
"react-select": "^5.10.2",
|
||||||
"socket.io-client": "^4.8.3"
|
"socket.io-client": "^4.8.3",
|
||||||
|
"styled-components": "^6.3.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -341,6 +347,22 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emoji-mart/data": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@emoji-mart/react": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"emoji-mart": "^5.2",
|
||||||
|
"react": "^16.8 || ^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emotion/babel-plugin": {
|
"node_modules/@emotion/babel-plugin": {
|
||||||
"version": "11.13.5",
|
"version": "11.13.5",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||||
@@ -385,6 +407,15 @@
|
|||||||
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
|
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@emotion/is-prop-valid": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/memoize": "^0.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emotion/memoize": {
|
"node_modules/@emotion/memoize": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
|
||||||
@@ -630,6 +661,70 @@
|
|||||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@giphy/colors": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@giphy/colors/-/colors-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-CMUY2UR8Ujl/wTRVjwu9spss6Cf2qkQNATKfoWCTWQp7CXdSfPWUDepp0c0/F0n7lYphLFvD9qzOC4NSdBt7nw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@giphy/js-analytics": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@giphy/js-analytics/-/js-analytics-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-jBZG6OqyMWB6meLi8Sz3iLplXYnhkbj+DJhS4ChmRX8Y6UA7i5dbbsUN/So1s7tTjhZOvu0rxA6rWJE73S1FvQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@giphy/js-types": "*",
|
||||||
|
"@giphy/js-util": "*",
|
||||||
|
"append-query": "^2.1.0",
|
||||||
|
"throttle-debounce": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@giphy/js-fetch-api": {
|
||||||
|
"version": "5.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@giphy/js-fetch-api/-/js-fetch-api-5.7.0.tgz",
|
||||||
|
"integrity": "sha512-5W8j7QUEnWRhW2KKlRhwHNCz0HwUeW0SlFryKFrqzE5XB8kjoAwZ2GvwtLNnkpjN0cniLE+bSGAzMKcL5Bt5Vg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@giphy/js-types": "*",
|
||||||
|
"@giphy/js-util": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@giphy/js-types": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@giphy/js-types/-/js-types-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-BZYCDtYNRR7cUWkbDLB4wmm3qmWMsVCQdUiBNOfmZ3yAazCgygKJoDI/5Rq4CK5MBaOc5LVdF8viC2WtoBdaPA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@giphy/js-util": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Qt7pGh2cqiNmXLeWAgb459wK8+BuMLtIxTfg4ZksnPHPsLthiHT9hhzs2QhqUh7Pp/HOq+Cbv2etGDfnq+xiKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@giphy/js-types": "*",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@giphy/react-components": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@giphy/react-components/-/react-components-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-K5Qnipj+QEoZCMFRZ3PqxMv7FgTIP474xIUFFGFa9x6zJIx0OpilKOFqwNtJS/CtVu9VFmHNkDO/Y9xsHfdplA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@giphy/colors": "*",
|
||||||
|
"@giphy/js-analytics": "*",
|
||||||
|
"@giphy/js-fetch-api": "*",
|
||||||
|
"@giphy/js-types": "*",
|
||||||
|
"@giphy/js-util": "*",
|
||||||
|
"intersection-observer": "^0.12.2",
|
||||||
|
"react-use": "17.6.0",
|
||||||
|
"throttle-debounce": "^3.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "18 - 19",
|
||||||
|
"styled-components": ">= 5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -1719,6 +1814,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-cookie": {
|
||||||
|
"version": "2.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
|
||||||
|
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -1778,6 +1879,12 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/stylis": {
|
||||||
|
"version": "4.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz",
|
||||||
|
"integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.53.1",
|
"version": "8.53.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz",
|
||||||
@@ -2316,6 +2423,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@xobotyi/scrollbar-width": {
|
||||||
|
"version": "1.9.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
|
||||||
|
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
@@ -2372,6 +2485,15 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/append-query": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/append-query/-/append-query-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-adm0E8o1o7ay+HbkWvGIpNNeciLB/rxJ0heThHuzSSVq5zcdQ5/ZubFnUoY0imFmk6gZVghSpwoubLVtwi9EHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"extend": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
@@ -2766,6 +2888,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelize": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001765",
|
"version": "1.0.30001765",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz",
|
||||||
@@ -2855,6 +2986,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/copy-to-clipboard": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"toggle-selection": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||||
@@ -2886,6 +3026,57 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-color-keywords": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-in-js-utils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hyphenate-style-name": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-to-react-native": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"camelize": "^1.0.0",
|
||||||
|
"css-color-keywords": "^1.0.0",
|
||||||
|
"postcss-value-parser": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mdn-data": "2.0.14",
|
||||||
|
"source-map": "^0.6.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree/node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -3076,6 +3267,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-mart": {
|
||||||
|
"version": "5.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz",
|
||||||
|
"integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "9.2.2",
|
"version": "9.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
@@ -3128,6 +3325,15 @@
|
|||||||
"is-arrayish": "^0.2.1"
|
"is-arrayish": "^0.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/error-stack-parser": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"stackframe": "^1.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-abstract": {
|
"node_modules/es-abstract": {
|
||||||
"version": "1.24.1",
|
"version": "1.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
|
||||||
@@ -3747,11 +3953,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/extend": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
@@ -3798,6 +4009,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-shallow-equal": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
|
||||||
|
},
|
||||||
|
"node_modules/fastest-stable-stringify": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||||
@@ -4255,6 +4477,12 @@
|
|||||||
"react-is": "^16.7.0"
|
"react-is": "^16.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hyphenate-style-name": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -4291,6 +4519,15 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inline-style-prefixer": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-in-js-utils": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||||
@@ -4306,6 +4543,13 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/intersection-observer": {
|
||||||
|
"version": "0.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz",
|
||||||
|
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==",
|
||||||
|
"deprecated": "The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -4777,6 +5021,12 @@
|
|||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-cookie": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -5239,6 +5489,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdn-data": {
|
||||||
|
"version": "2.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||||
|
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||||
|
"license": "CC0-1.0"
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||||
@@ -5319,6 +5575,32 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/nano-css": {
|
||||||
|
"version": "5.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz",
|
||||||
|
"integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||||
|
"css-tree": "^1.1.2",
|
||||||
|
"csstype": "^3.1.2",
|
||||||
|
"fastest-stable-stringify": "^2.0.2",
|
||||||
|
"inline-style-prefixer": "^7.0.1",
|
||||||
|
"rtl-css-js": "^1.16.1",
|
||||||
|
"stacktrace-js": "^2.0.2",
|
||||||
|
"stylis": "^4.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-dom": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nano-css/node_modules/stylis": {
|
||||||
|
"version": "4.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
||||||
|
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -5797,6 +6079,12 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-value-parser": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/preact": {
|
"node_modules/preact": {
|
||||||
"version": "10.24.3",
|
"version": "10.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
||||||
@@ -5938,6 +6226,41 @@
|
|||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-universal-interface": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"tslib": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-use": {
|
||||||
|
"version": "17.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz",
|
||||||
|
"integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/js-cookie": "^2.2.6",
|
||||||
|
"@xobotyi/scrollbar-width": "^1.9.5",
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-shallow-equal": "^1.0.0",
|
||||||
|
"js-cookie": "^2.2.1",
|
||||||
|
"nano-css": "^5.6.2",
|
||||||
|
"react-universal-interface": "^0.6.2",
|
||||||
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
|
"screenfull": "^5.1.0",
|
||||||
|
"set-harmonic-interval": "^1.0.1",
|
||||||
|
"throttle-debounce": "^3.0.1",
|
||||||
|
"ts-easing": "^0.2.0",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-dom": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -5982,6 +6305,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resize-observer-polyfill": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -6032,6 +6361,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rtl-css-js": {
|
||||||
|
"version": "1.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",
|
||||||
|
"integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -6117,6 +6455,18 @@
|
|||||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/screenfull": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -6161,6 +6511,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-harmonic-interval": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/set-proto": {
|
"node_modules/set-proto": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
|
||||||
@@ -6176,6 +6535,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/shallowequal": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.34.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
@@ -6386,6 +6751,51 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/stack-generator": {
|
||||||
|
"version": "2.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
|
||||||
|
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"stackframe": "^1.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stackframe": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/stacktrace-gps": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"source-map": "0.5.6",
|
||||||
|
"stackframe": "^1.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stacktrace-gps/node_modules/source-map": {
|
||||||
|
"version": "0.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
|
||||||
|
"integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stacktrace-js": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"error-stack-parser": "^2.0.6",
|
||||||
|
"stack-generator": "^2.0.5",
|
||||||
|
"stacktrace-gps": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stop-iteration-iterator": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
@@ -6536,6 +6946,73 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/styled-components": {
|
||||||
|
"version": "6.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.11.tgz",
|
||||||
|
"integrity": "sha512-opzgceGlQ5rdZdGwf9ddLW7EM2F4L7tgsgLn6fFzQ2JgE5EVQ4HZwNkcgB1p8WfOBx1GEZP3fa66ajJmtXhSrA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/is-prop-valid": "1.4.0",
|
||||||
|
"@emotion/unitless": "0.10.0",
|
||||||
|
"@types/stylis": "4.2.7",
|
||||||
|
"css-to-react-native": "3.2.0",
|
||||||
|
"csstype": "3.2.3",
|
||||||
|
"postcss": "8.4.49",
|
||||||
|
"shallowequal": "1.1.0",
|
||||||
|
"stylis": "4.3.6",
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/styled-components"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8.0",
|
||||||
|
"react-dom": ">= 16.8.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/styled-components/node_modules/postcss": {
|
||||||
|
"version": "8.4.49",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
|
||||||
|
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.7",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/styled-components/node_modules/stylis": {
|
||||||
|
"version": "4.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
||||||
|
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/styled-jsx": {
|
"node_modules/styled-jsx": {
|
||||||
"version": "5.1.6",
|
"version": "5.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||||
@@ -6611,6 +7088,15 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/throttle-debounce": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
@@ -6654,6 +7140,12 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toggle-selection": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||||
@@ -6667,6 +7159,12 @@
|
|||||||
"typescript": ">=4.8.4"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-easing": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.15.0",
|
"version": "3.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||||
@@ -6944,6 +7442,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -9,13 +9,19 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emoji-mart/data": "^1.2.1",
|
||||||
|
"@emoji-mart/react": "^1.1.1",
|
||||||
|
"@giphy/js-fetch-api": "^5.7.0",
|
||||||
|
"@giphy/react-components": "^10.1.1",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"emoji-mart": "^5.6.0",
|
||||||
"next": "16.1.0",
|
"next": "16.1.0",
|
||||||
"next-auth": "^5.0.0-beta.30",
|
"next-auth": "^5.0.0-beta.30",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-select": "^5.10.2",
|
"react-select": "^5.10.2",
|
||||||
"socket.io-client": "^4.8.3"
|
"socket.io-client": "^4.8.3",
|
||||||
|
"styled-components": "^6.3.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
4
public/assets/icons/tick-double-blue.svg
Normal file
4
public/assets/icons/tick-double-blue.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 5.5L4.5 9L11 1" stroke="#3b82f6" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5 5.5L8.5 9L15 1" stroke="#3b82f6" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 333 B |
4
public/assets/icons/tick-double-gray.svg
Normal file
4
public/assets/icons/tick-double-gray.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 5.5L4.5 9L11 1" stroke="#00293D" stroke-opacity="0.4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5 5.5L8.5 9L15 1" stroke="#00293D" stroke-opacity="0.4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 375 B |
3
public/assets/icons/tick-single-gray.svg
Normal file
3
public/assets/icons/tick-single-gray.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 5.5L4.5 9L10 1" stroke="#00293D" stroke-opacity="0.4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 239 B |
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Footer } from '@/components/layout/Footer';
|
import { Footer } from '@/components/layout/Footer';
|
||||||
import { CommonHeader } from '@/components/layout/CommonHeader';
|
import { CommonHeader } from '@/components/layout/CommonHeader';
|
||||||
|
import { PresenceProvider } from '@/components/providers/presence-provider';
|
||||||
|
|
||||||
export default function AgentLayout({
|
export default function AgentLayout({
|
||||||
children,
|
children,
|
||||||
@@ -38,6 +39,7 @@ export default function AgentLayout({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<PresenceProvider>
|
||||||
<div className="min-h-screen bg-white flex flex-col">
|
<div className="min-h-screen bg-white flex flex-col">
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<main className="flex-1">
|
<main className="flex-1">
|
||||||
@@ -55,5 +57,6 @@ export default function AgentLayout({
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
</PresenceProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useRouter, usePathname } from 'next/navigation';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Footer } from '@/components/layout/Footer';
|
import { Footer } from '@/components/layout/Footer';
|
||||||
import { CommonHeader } from '@/components/layout/CommonHeader';
|
import { CommonHeader } from '@/components/layout/CommonHeader';
|
||||||
|
import { PresenceProvider } from '@/components/providers/presence-provider';
|
||||||
|
|
||||||
// Pages that don't require authentication
|
// Pages that don't require authentication
|
||||||
const publicPaths = [
|
const publicPaths = [
|
||||||
@@ -77,6 +78,7 @@ export default function UserLayout({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<PresenceProvider>
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{isDashboard ? (
|
{isDashboard ? (
|
||||||
<>
|
<>
|
||||||
@@ -110,5 +112,6 @@ export default function UserLayout({
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
</PresenceProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,55 @@
|
|||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
import data from '@emoji-mart/data';
|
||||||
|
import Picker from '@emoji-mart/react';
|
||||||
|
import { GiphyFetch } from '@giphy/js-fetch-api';
|
||||||
|
import { Grid } from '@giphy/react-components';
|
||||||
|
|
||||||
|
const gf = new GiphyFetch(process.env.NEXT_PUBLIC_GIPHY_API_KEY || '');
|
||||||
|
|
||||||
|
interface AttachedFile {
|
||||||
|
file: File;
|
||||||
|
preview?: string; // Object URL for image preview
|
||||||
|
messageType: 'IMAGE' | 'FILE';
|
||||||
|
}
|
||||||
|
|
||||||
interface MessageInputProps {
|
interface MessageInputProps {
|
||||||
onSend?: (message: string) => void;
|
onSend?: (message: string) => void;
|
||||||
|
onSendGif?: (gifUrl: string) => void;
|
||||||
|
onSendFile?: (file: File, messageType: 'IMAGE' | 'FILE') => void;
|
||||||
onTypingStart?: () => void;
|
onTypingStart?: () => void;
|
||||||
onTypingStop?: () => void;
|
onTypingStop?: () => void;
|
||||||
|
isUploading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MessageInput({ onSend, onTypingStart, onTypingStop }: MessageInputProps) {
|
export function MessageInput({ onSend, onSendGif, onSendFile, onTypingStart, onTypingStop, isUploading }: MessageInputProps) {
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
|
const [showGifPicker, setShowGifPicker] = useState(false);
|
||||||
|
const [gifSearchQuery, setGifSearchQuery] = useState('');
|
||||||
|
const [attachedFile, setAttachedFile] = useState<AttachedFile | null>(null);
|
||||||
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const isTypingRef = useRef(false);
|
const isTypingRef = useRef(false);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const emojiPickerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const gifPickerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const emojiButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const gifButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Handle typing indicator
|
// Handle typing indicator
|
||||||
const handleTyping = useCallback(() => {
|
const handleTyping = useCallback(() => {
|
||||||
// Start typing indicator
|
|
||||||
if (!isTypingRef.current && onTypingStart) {
|
if (!isTypingRef.current && onTypingStart) {
|
||||||
isTypingRef.current = true;
|
isTypingRef.current = true;
|
||||||
onTypingStart();
|
onTypingStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing timeout
|
|
||||||
if (typingTimeoutRef.current) {
|
if (typingTimeoutRef.current) {
|
||||||
clearTimeout(typingTimeoutRef.current);
|
clearTimeout(typingTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set timeout to stop typing indicator after 2 seconds of inactivity
|
|
||||||
typingTimeoutRef.current = setTimeout(() => {
|
typingTimeoutRef.current = setTimeout(() => {
|
||||||
if (isTypingRef.current && onTypingStop) {
|
if (isTypingRef.current && onTypingStop) {
|
||||||
isTypingRef.current = false;
|
isTypingRef.current = false;
|
||||||
@@ -48,9 +71,90 @@ export function MessageInput({ onSend, onTypingStart, onTypingStop }: MessageInp
|
|||||||
};
|
};
|
||||||
}, [onTypingStop]);
|
}, [onTypingStop]);
|
||||||
|
|
||||||
|
// Close pickers on click outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
showEmojiPicker &&
|
||||||
|
emojiPickerRef.current &&
|
||||||
|
!emojiPickerRef.current.contains(e.target as Node) &&
|
||||||
|
emojiButtonRef.current &&
|
||||||
|
!emojiButtonRef.current.contains(e.target as Node)
|
||||||
|
) {
|
||||||
|
setShowEmojiPicker(false);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
showGifPicker &&
|
||||||
|
gifPickerRef.current &&
|
||||||
|
!gifPickerRef.current.contains(e.target as Node) &&
|
||||||
|
gifButtonRef.current &&
|
||||||
|
!gifButtonRef.current.contains(e.target as Node)
|
||||||
|
) {
|
||||||
|
setShowGifPicker(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, [showEmojiPicker, showGifPicker]);
|
||||||
|
|
||||||
|
// Clean up object URL on unmount or when file changes
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (attachedFile?.preview) {
|
||||||
|
URL.revokeObjectURL(attachedFile.preview);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [attachedFile]);
|
||||||
|
|
||||||
|
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const preview = URL.createObjectURL(file);
|
||||||
|
setAttachedFile({ file, preview, messageType: 'IMAGE' });
|
||||||
|
// Reset input so same file can be selected again
|
||||||
|
e.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
setAttachedFile({ file, messageType: 'FILE' });
|
||||||
|
e.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAttachment = () => {
|
||||||
|
if (attachedFile?.preview) {
|
||||||
|
URL.revokeObjectURL(attachedFile.preview);
|
||||||
|
}
|
||||||
|
setAttachedFile(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
};
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
|
// Send attached file
|
||||||
|
if (attachedFile && onSendFile) {
|
||||||
|
if (typingTimeoutRef.current) {
|
||||||
|
clearTimeout(typingTimeoutRef.current);
|
||||||
|
}
|
||||||
|
if (isTypingRef.current && onTypingStop) {
|
||||||
|
isTypingRef.current = false;
|
||||||
|
onTypingStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSendFile(attachedFile.file, attachedFile.messageType);
|
||||||
|
removeAttachment();
|
||||||
|
setMessage('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send text message
|
||||||
if (message.trim() && onSend) {
|
if (message.trim() && onSend) {
|
||||||
// Stop typing indicator before sending
|
|
||||||
if (typingTimeoutRef.current) {
|
if (typingTimeoutRef.current) {
|
||||||
clearTimeout(typingTimeoutRef.current);
|
clearTimeout(typingTimeoutRef.current);
|
||||||
}
|
}
|
||||||
@@ -67,7 +171,7 @@ export function MessageInput({ onSend, onTypingStart, onTypingStop }: MessageInp
|
|||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSend();
|
if (!isUploading) handleSend();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,11 +180,122 @@ export function MessageInput({ onSend, onTypingStart, onTypingStop }: MessageInp
|
|||||||
handleTyping();
|
handleTyping();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEmojiSelect = (emoji: { native: string }) => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (textarea) {
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const newMessage = message.slice(0, start) + emoji.native + message.slice(end);
|
||||||
|
setMessage(newMessage);
|
||||||
|
// Restore cursor position after emoji
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + emoji.native.length;
|
||||||
|
textarea.focus();
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
setMessage((prev) => prev + emoji.native);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGifSelect = (gif: { images: { original: { url: string } } }, e: React.SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const gifUrl = gif.images.original.url;
|
||||||
|
if (onSendGif) {
|
||||||
|
onSendGif(gifUrl);
|
||||||
|
}
|
||||||
|
setShowGifPicker(false);
|
||||||
|
setGifSearchQuery('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEmojiPicker = () => {
|
||||||
|
setShowEmojiPicker((prev) => !prev);
|
||||||
|
setShowGifPicker(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleGifPicker = () => {
|
||||||
|
setShowGifPicker((prev) => !prev);
|
||||||
|
setShowEmojiPicker(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGifs = useCallback(
|
||||||
|
(offset: number) => {
|
||||||
|
if (gifSearchQuery.trim()) {
|
||||||
|
return gf.search(gifSearchQuery, { offset, limit: 10 });
|
||||||
|
}
|
||||||
|
return gf.trending({ offset, limit: 10 });
|
||||||
|
},
|
||||||
|
[gifSearchQuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
const canSend = message.trim() || attachedFile;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-[#00293d]/10 rounded-[20px] bg-white p-4">
|
<div className="border border-[#00293d]/10 rounded-[20px] bg-white p-4">
|
||||||
|
{/* Hidden file inputs */}
|
||||||
|
<input
|
||||||
|
ref={imageInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/webp,image/gif"
|
||||||
|
onChange={handleImageSelect}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Attachment preview area */}
|
||||||
|
{attachedFile && (
|
||||||
|
<div className="border border-[#00293d]/10 rounded-[15px] p-3 mb-3 bg-[#00293d]/[0.02]">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{attachedFile.messageType === 'IMAGE' && attachedFile.preview ? (
|
||||||
|
<img
|
||||||
|
src={attachedFile.preview}
|
||||||
|
alt="Preview"
|
||||||
|
className="w-[80px] h-[80px] object-cover rounded-[10px]"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-[50px] h-[50px] bg-[#e58625]/10 rounded-[10px] flex items-center justify-center flex-shrink-0">
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/attachment-icon.svg"
|
||||||
|
alt="File"
|
||||||
|
width={22}
|
||||||
|
height={12}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] truncate">
|
||||||
|
{attachedFile.file.name}
|
||||||
|
</p>
|
||||||
|
<p className="font-serif font-normal text-[12px] leading-[16px] text-[#00293D]/50">
|
||||||
|
{formatFileSize(attachedFile.file.size)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={removeAttachment}
|
||||||
|
className="p-1 hover:bg-gray-100 rounded-full transition-colors cursor-pointer flex-shrink-0"
|
||||||
|
title="Remove attachment"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/close-icon.svg"
|
||||||
|
alt="Remove"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className="opacity-50"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Text input area */}
|
{/* Text input area */}
|
||||||
<div className="border border-[#00293d]/10 rounded-[20px] p-4 mb-3">
|
<div className="border border-[#00293d]/10 rounded-[20px] p-4 mb-3">
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
value={message}
|
value={message}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@@ -93,7 +308,11 @@ export function MessageInput({ onSend, onTypingStart, onTypingStop }: MessageInp
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Attachment buttons */}
|
{/* Attachment buttons */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
|
<button
|
||||||
|
onClick={() => imageInputRef.current?.click()}
|
||||||
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||||
|
title="Attach image"
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/assets/icons/gallery-icon.svg"
|
src="/assets/icons/gallery-icon.svg"
|
||||||
alt="Add image"
|
alt="Add image"
|
||||||
@@ -101,15 +320,26 @@ export function MessageInput({ onSend, onTypingStart, onTypingStop }: MessageInp
|
|||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
|
<button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||||
|
title="Attach file"
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/assets/icons/chain-icon.svg"
|
src="/assets/icons/chain-icon.svg"
|
||||||
alt="Add link"
|
alt="Attach file"
|
||||||
width={24}
|
width={24}
|
||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
|
|
||||||
|
{/* GIF button with picker */}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
ref={gifButtonRef}
|
||||||
|
onClick={toggleGifPicker}
|
||||||
|
className={`p-1.5 rounded-lg transition-colors cursor-pointer ${showGifPicker ? 'bg-[#e58625]/20' : 'hover:bg-gray-100'}`}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/assets/icons/gif-icon.svg"
|
src="/assets/icons/gif-icon.svg"
|
||||||
alt="Add GIF"
|
alt="Add GIF"
|
||||||
@@ -117,7 +347,47 @@ export function MessageInput({ onSend, onTypingStart, onTypingStop }: MessageInp
|
|||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
|
{showGifPicker && (
|
||||||
|
<div
|
||||||
|
ref={gifPickerRef}
|
||||||
|
className="absolute bottom-full left-0 mb-2 z-50 bg-white border border-[#00293d]/10 rounded-[15px] shadow-lg overflow-hidden"
|
||||||
|
style={{ width: '360px', height: '420px' }}
|
||||||
|
>
|
||||||
|
<div className="p-3 border-b border-[#00293d]/10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={gifSearchQuery}
|
||||||
|
onChange={(e) => setGifSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search GIFs..."
|
||||||
|
className="w-full px-3 py-2 border border-[#00293d]/10 rounded-[10px] font-serif text-[14px] text-[#00293D] placeholder-[#00293D]/50 focus:outline-none focus:border-[#e58625]"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-auto" style={{ height: 'calc(100% - 56px)' }}>
|
||||||
|
<Grid
|
||||||
|
key={gifSearchQuery}
|
||||||
|
fetchGifs={fetchGifs}
|
||||||
|
width={356}
|
||||||
|
columns={2}
|
||||||
|
gutter={6}
|
||||||
|
noLink
|
||||||
|
onGifClick={handleGifSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-2 right-3 bg-white/80 rounded px-1.5 py-0.5">
|
||||||
|
<span className="font-serif text-[10px] text-[#00293D]/40">Powered by GIPHY</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Emoji button with picker */}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
ref={emojiButtonRef}
|
||||||
|
onClick={toggleEmojiPicker}
|
||||||
|
className={`p-1.5 rounded-lg transition-colors cursor-pointer ${showEmojiPicker ? 'bg-[#e58625]/20' : 'hover:bg-gray-100'}`}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/assets/icons/emoji-icon.svg"
|
src="/assets/icons/emoji-icon.svg"
|
||||||
alt="Add emoji"
|
alt="Add emoji"
|
||||||
@@ -125,20 +395,32 @@ export function MessageInput({ onSend, onTypingStart, onTypingStop }: MessageInp
|
|||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
{showEmojiPicker && (
|
||||||
|
<div ref={emojiPickerRef} className="absolute bottom-full left-0 mb-2 z-50">
|
||||||
|
<Picker
|
||||||
|
data={data}
|
||||||
|
onEmojiSelect={handleEmojiSelect}
|
||||||
|
theme="light"
|
||||||
|
previewPosition="none"
|
||||||
|
skinTonePosition="search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Send button and more options */}
|
{/* Send button and more options */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={!message.trim()}
|
disabled={!canSend || isUploading}
|
||||||
className={`px-4 py-1.5 font-serif font-normal text-[14px] leading-[19px] rounded-[15px] transition-colors cursor-pointer ${
|
className={`px-4 py-1.5 font-serif font-normal text-[14px] leading-[19px] rounded-[15px] transition-colors cursor-pointer ${
|
||||||
message.trim()
|
canSend && !isUploading
|
||||||
? 'bg-[#e58625] text-[#00293d] hover:bg-[#d47720]'
|
? 'bg-[#e58625] text-[#00293d] hover:bg-[#d47720]'
|
||||||
: 'bg-[#e58625]/50 text-[#00293d]/50 cursor-not-allowed'
|
: 'bg-[#e58625]/50 text-[#00293d]/50 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Send
|
{isUploading ? 'Uploading...' : 'Send'}
|
||||||
</button>
|
</button>
|
||||||
<button className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
|
<button className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { MessageInput } from './MessageInput';
|
|||||||
import { useMessaging } from '@/hooks/useMessaging';
|
import { useMessaging } from '@/hooks/useMessaging';
|
||||||
import { connectionRequestsService, type ConnectionRequest } from '@/services/connection-requests.service';
|
import { connectionRequestsService, type ConnectionRequest } from '@/services/connection-requests.service';
|
||||||
import { uploadService } from '@/services/upload.service';
|
import { uploadService } from '@/services/upload.service';
|
||||||
import type { Conversation, Message } from '@/types/messaging';
|
import type { Conversation, Message, MessageType } from '@/types/messaging';
|
||||||
|
|
||||||
// Helper to get a valid avatar URL (must start with http://, https://, or /)
|
// Helper to get a valid avatar URL (must start with http://, https://, or /)
|
||||||
function getValidAvatarUrl(avatar: string | null | undefined): string {
|
function getValidAvatarUrl(avatar: string | null | undefined): string {
|
||||||
@@ -110,12 +110,15 @@ export function MessagingPage() {
|
|||||||
} = useMessaging();
|
} = useMessaging();
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||||
const [connectedAgents, setConnectedAgents] = useState<ConnectedAgent[]>([]);
|
const [connectedAgents, setConnectedAgents] = useState<ConnectedAgent[]>([]);
|
||||||
const [isLoadingAgents, setIsLoadingAgents] = useState(false);
|
const [isLoadingAgents, setIsLoadingAgents] = useState(false);
|
||||||
const [isStartingConversation, setIsStartingConversation] = useState<string | null>(null);
|
const [isStartingConversation, setIsStartingConversation] = useState<string | null>(null);
|
||||||
// Store converted avatar URLs by conversation ID
|
// Store converted avatar URLs by conversation ID
|
||||||
const [conversationAvatarUrls, setConversationAvatarUrls] = useState<Record<string, string>>({});
|
const [conversationAvatarUrls, setConversationAvatarUrls] = useState<Record<string, string>>({});
|
||||||
|
// Store converted file URLs by message ID (for S3-stored images/files)
|
||||||
|
const [messageFileUrls, setMessageFileUrls] = useState<Record<string, string>>({});
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const justSwitchedConversationRef = useRef(false);
|
const justSwitchedConversationRef = useRef(false);
|
||||||
@@ -246,6 +249,39 @@ export function MessagingPage() {
|
|||||||
convertCurrentAvatar();
|
convertCurrentAvatar();
|
||||||
}, [currentConversation]);
|
}, [currentConversation]);
|
||||||
|
|
||||||
|
// Convert S3 keys to presigned download URLs for message attachments
|
||||||
|
useEffect(() => {
|
||||||
|
const convertFileUrls = async () => {
|
||||||
|
for (const msg of messages) {
|
||||||
|
if ((msg.messageType === 'IMAGE' || msg.messageType === 'FILE') && msg.fileUrl) {
|
||||||
|
// Skip if already converted
|
||||||
|
if (messageFileUrls[msg.id]) continue;
|
||||||
|
// Skip if it's already an accessible URL (e.g., Giphy GIF URLs)
|
||||||
|
if (msg.fileUrl.startsWith('http://') || msg.fileUrl.startsWith('https://')) {
|
||||||
|
setMessageFileUrls(prev => ({ ...prev, [msg.id]: msg.fileUrl! }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Convert S3 key to presigned download URL
|
||||||
|
try {
|
||||||
|
const presignedUrl = await uploadService.getPresignedDownloadUrl(msg.fileUrl);
|
||||||
|
setMessageFileUrls(prev => ({ ...prev, [msg.id]: presignedUrl }));
|
||||||
|
} catch {
|
||||||
|
// Keep original if conversion fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (messages.length > 0) {
|
||||||
|
convertFileUrls();
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Clear cached file URLs when switching conversations
|
||||||
|
useEffect(() => {
|
||||||
|
setMessageFileUrls({});
|
||||||
|
}, [currentConversation?.id]);
|
||||||
|
|
||||||
// Handle starting a new conversation with an agent
|
// Handle starting a new conversation with an agent
|
||||||
const handleStartConversation = useCallback(async (agentProfileId: string) => {
|
const handleStartConversation = useCallback(async (agentProfileId: string) => {
|
||||||
setIsStartingConversation(agentProfileId);
|
setIsStartingConversation(agentProfileId);
|
||||||
@@ -265,6 +301,35 @@ export function MessagingPage() {
|
|||||||
await sendMessage(content);
|
await sendMessage(content);
|
||||||
}, [sendMessage]);
|
}, [sendMessage]);
|
||||||
|
|
||||||
|
// Handle sending a GIF
|
||||||
|
const handleSendGif = useCallback(async (gifUrl: string) => {
|
||||||
|
await sendMessage(gifUrl, 'IMAGE' as MessageType, { fileUrl: gifUrl, mimeType: 'image/gif' });
|
||||||
|
}, [sendMessage]);
|
||||||
|
|
||||||
|
// Handle sending a file (image or document)
|
||||||
|
const handleSendFile = useCallback(async (file: File, messageType: 'IMAGE' | 'FILE') => {
|
||||||
|
setIsUploading(true);
|
||||||
|
try {
|
||||||
|
const uploaded = await uploadService.uploadMessageFile(file);
|
||||||
|
// uploaded.url is the S3 key (not a full URL)
|
||||||
|
const content = messageType === 'IMAGE' ? 'Sent an image' : 'Sent a file';
|
||||||
|
await sendMessage(
|
||||||
|
content,
|
||||||
|
messageType as MessageType,
|
||||||
|
{
|
||||||
|
fileUrl: uploaded.url,
|
||||||
|
mimeType: file.type,
|
||||||
|
fileName: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to upload file:', error);
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
}, [sendMessage]);
|
||||||
|
|
||||||
// Handle typing events
|
// Handle typing events
|
||||||
const handleTypingStart = useCallback(() => {
|
const handleTypingStart = useCallback(() => {
|
||||||
startTyping();
|
startTyping();
|
||||||
@@ -599,16 +664,64 @@ export function MessagingPage() {
|
|||||||
className={`flex ${isOwn ? 'justify-end' : 'justify-start'}`}
|
className={`flex ${isOwn ? 'justify-end' : 'justify-start'}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`max-w-[70%] rounded-[15px] px-4 py-3 ${
|
className={`max-w-[70%] rounded-[15px] ${
|
||||||
|
message.messageType === 'IMAGE' ? 'p-1' : 'px-4 py-3'
|
||||||
|
} ${
|
||||||
isOwn
|
isOwn
|
||||||
? 'bg-[#e58625] text-[#00293D]'
|
? 'bg-[#e58625] text-[#00293D]'
|
||||||
: 'bg-[#00293d]/10 text-[#00293D]'
|
: 'bg-[#00293d]/10 text-[#00293D]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{message.messageType === 'IMAGE' ? (
|
||||||
|
messageFileUrls[message.id] ? (
|
||||||
|
<img
|
||||||
|
src={messageFileUrls[message.id]}
|
||||||
|
alt={message.fileName || 'Image'}
|
||||||
|
className="rounded-[12px] max-w-full cursor-pointer"
|
||||||
|
style={{ maxHeight: '250px' }}
|
||||||
|
onClick={() => window.open(messageFileUrls[message.id], '_blank')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-[200px] h-[150px] rounded-[12px] bg-black/5 flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[#00293d]" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : message.messageType === 'FILE' ? (
|
||||||
|
<a
|
||||||
|
href={messageFileUrls[message.id] || '#'}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-3 no-underline"
|
||||||
|
>
|
||||||
|
<div className={`w-[40px] h-[40px] rounded-[8px] flex items-center justify-center flex-shrink-0 ${isOwn ? 'bg-[#00293D]/10' : 'bg-[#e58625]/10'}`}>
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/attachment-icon.svg"
|
||||||
|
alt="File"
|
||||||
|
width={20}
|
||||||
|
height={11}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-serif font-medium text-[14px] leading-[19px] truncate">
|
||||||
|
{message.fileName || message.content}
|
||||||
|
</p>
|
||||||
|
{message.fileSize && (
|
||||||
|
<p className={`font-serif font-normal text-[12px] leading-[16px] ${isOwn ? 'text-[#00293D]/70' : 'text-[#00293D]/50'}`}>
|
||||||
|
{message.fileSize < 1024
|
||||||
|
? `${message.fileSize} B`
|
||||||
|
: message.fileSize < 1024 * 1024
|
||||||
|
? `${(message.fileSize / 1024).toFixed(1)} KB`
|
||||||
|
: `${(message.fileSize / (1024 * 1024)).toFixed(1)} MB`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
<p className="font-serif font-normal text-[14px] leading-[19px]">
|
<p className="font-serif font-normal text-[14px] leading-[19px]">
|
||||||
{message.content}
|
{message.content}
|
||||||
</p>
|
</p>
|
||||||
<div className={`flex items-center gap-2 mt-1 ${isOwn ? 'justify-end' : 'justify-start'}`}>
|
)}
|
||||||
|
<div className={`flex items-center gap-2 mt-1 ${message.messageType === 'IMAGE' ? 'px-2 pb-1' : ''} ${isOwn ? 'justify-end' : 'justify-start'}`}>
|
||||||
<p
|
<p
|
||||||
className={`font-serif font-normal text-[12px] leading-[16px] ${
|
className={`font-serif font-normal text-[12px] leading-[16px] ${
|
||||||
isOwn ? 'text-[#00293D]/70' : 'text-[#00293D]/50'
|
isOwn ? 'text-[#00293D]/70' : 'text-[#00293D]/50'
|
||||||
@@ -617,8 +730,14 @@ export function MessagingPage() {
|
|||||||
{formatMessageTime(message.createdAt)}
|
{formatMessageTime(message.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
{isOwn && (
|
{isOwn && (
|
||||||
<span className={`text-[10px] ${message.status === 'READ' ? 'text-blue-500' : 'text-[#00293D]/50'}`}>
|
<span className="inline-flex items-center">
|
||||||
{message.status === 'READ' ? '✓✓' : message.status === 'DELIVERED' ? '✓✓' : '✓'}
|
{message.status === 'READ' ? (
|
||||||
|
<Image src="/assets/icons/tick-double-blue.svg" alt="Read" width={16} height={11} />
|
||||||
|
) : message.status === 'DELIVERED' ? (
|
||||||
|
<Image src="/assets/icons/tick-double-gray.svg" alt="Delivered" width={16} height={11} />
|
||||||
|
) : (
|
||||||
|
<Image src="/assets/icons/tick-single-gray.svg" alt="Sent" width={11} height={11} />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -669,8 +788,11 @@ export function MessagingPage() {
|
|||||||
{/* Message input */}
|
{/* Message input */}
|
||||||
<MessageInput
|
<MessageInput
|
||||||
onSend={handleSendMessage}
|
onSend={handleSendMessage}
|
||||||
|
onSendGif={handleSendGif}
|
||||||
|
onSendFile={handleSendFile}
|
||||||
onTypingStart={handleTypingStart}
|
onTypingStart={handleTypingStart}
|
||||||
onTypingStop={handleTypingStop}
|
onTypingStop={handleTypingStop}
|
||||||
|
isUploading={isUploading}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
166
src/components/providers/presence-provider.tsx
Normal file
166
src/components/providers/presence-provider.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { socketService } from '@/services';
|
||||||
|
|
||||||
|
// Disconnect after 3 minutes of tab being hidden
|
||||||
|
const VISIBILITY_TIMEOUT = 3 * 60 * 1000;
|
||||||
|
// Disconnect after 5 minutes of no user interaction
|
||||||
|
const IDLE_TIMEOUT = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PresenceProvider handles user online/offline presence globally.
|
||||||
|
* - Connects socket on login (marks user online on the backend)
|
||||||
|
* - Disconnects socket when tab is hidden for too long or user is idle
|
||||||
|
* - Handles auth error recovery (token refresh + reconnect)
|
||||||
|
*/
|
||||||
|
export function PresenceProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
const idleTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const visibilityTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const isIdleRef = useRef(false);
|
||||||
|
const isConnectedRef = useRef(false);
|
||||||
|
const hasInitializedRef = useRef(false);
|
||||||
|
|
||||||
|
const getAccessToken = useCallback(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return localStorage.getItem('accessToken');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const connectSocket = useCallback(async () => {
|
||||||
|
const token = getAccessToken();
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await socketService.connect(token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Presence] Socket connect failed:', error);
|
||||||
|
}
|
||||||
|
}, [getAccessToken]);
|
||||||
|
|
||||||
|
const disconnectForIdle = useCallback(() => {
|
||||||
|
if (!isConnectedRef.current) return;
|
||||||
|
isIdleRef.current = true;
|
||||||
|
socketService.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Track socket connection state
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = socketService.onConnectionChange((connected) => {
|
||||||
|
isConnectedRef.current = connected;
|
||||||
|
});
|
||||||
|
return unsubscribe;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Connect on authentication
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'authenticated' && session?.user && !hasInitializedRef.current) {
|
||||||
|
hasInitializedRef.current = true;
|
||||||
|
// Small delay to ensure TokenSync has set localStorage tokens
|
||||||
|
const timer = setTimeout(connectSocket, 500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'unauthenticated') {
|
||||||
|
hasInitializedRef.current = false;
|
||||||
|
if (isConnectedRef.current) {
|
||||||
|
socketService.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [status, session, connectSocket]);
|
||||||
|
|
||||||
|
// Handle auth errors (token expiry) — single global handler
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = socketService.onAuthError(async () => {
|
||||||
|
console.log('[Presence] Auth error, refreshing token...');
|
||||||
|
try {
|
||||||
|
const refreshToken = localStorage.getItem('refreshToken');
|
||||||
|
if (!refreshToken) return;
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1'}/auth/refresh`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ refreshToken }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok && data.success) {
|
||||||
|
localStorage.setItem('accessToken', data.data.accessToken);
|
||||||
|
localStorage.setItem('refreshToken', data.data.refreshToken);
|
||||||
|
socketService.reconnectWithToken(data.data.accessToken);
|
||||||
|
} else {
|
||||||
|
console.error('[Presence] Token refresh failed:', data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Presence] Token refresh error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return unsubscribe;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Browser visibility tracking — disconnect when tab hidden for too long
|
||||||
|
useEffect(() => {
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
visibilityTimerRef.current = setTimeout(() => {
|
||||||
|
if (document.hidden) {
|
||||||
|
disconnectForIdle();
|
||||||
|
}
|
||||||
|
}, VISIBILITY_TIMEOUT);
|
||||||
|
} else {
|
||||||
|
// Tab visible again — clear timer and reconnect if was idle
|
||||||
|
if (visibilityTimerRef.current) {
|
||||||
|
clearTimeout(visibilityTimerRef.current);
|
||||||
|
visibilityTimerRef.current = null;
|
||||||
|
}
|
||||||
|
if (isIdleRef.current) {
|
||||||
|
isIdleRef.current = false;
|
||||||
|
connectSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
if (visibilityTimerRef.current) {
|
||||||
|
clearTimeout(visibilityTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [connectSocket, disconnectForIdle]);
|
||||||
|
|
||||||
|
// User activity tracking — disconnect when idle for too long
|
||||||
|
useEffect(() => {
|
||||||
|
const resetIdleTimer = () => {
|
||||||
|
if (idleTimerRef.current) {
|
||||||
|
clearTimeout(idleTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Was idle and user became active — reconnect
|
||||||
|
if (isIdleRef.current) {
|
||||||
|
isIdleRef.current = false;
|
||||||
|
connectSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
idleTimerRef.current = setTimeout(disconnectForIdle, IDLE_TIMEOUT);
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = ['mousedown', 'mousemove', 'keydown', 'scroll', 'touchstart'];
|
||||||
|
events.forEach((e) => document.addEventListener(e, resetIdleTimer, { passive: true }));
|
||||||
|
resetIdleTimer();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
events.forEach((e) => document.removeEventListener(e, resetIdleTimer));
|
||||||
|
if (idleTimerRef.current) {
|
||||||
|
clearTimeout(idleTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [connectSocket, disconnectForIdle]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ interface UseMessagingReturn {
|
|||||||
loadConversations: () => Promise<void>;
|
loadConversations: () => Promise<void>;
|
||||||
selectConversation: (conversationId: string) => Promise<void>;
|
selectConversation: (conversationId: string) => Promise<void>;
|
||||||
startConversation: (agentProfileId: string) => Promise<Conversation>;
|
startConversation: (agentProfileId: string) => Promise<Conversation>;
|
||||||
sendMessage: (content: string, messageType?: MessageType) => Promise<void>;
|
sendMessage: (content: string, messageType?: MessageType, fileFields?: { fileUrl?: string; mimeType?: string; fileName?: string; fileSize?: number }) => Promise<void>;
|
||||||
loadMoreMessages: () => Promise<void>;
|
loadMoreMessages: () => Promise<void>;
|
||||||
startTyping: () => void;
|
startTyping: () => void;
|
||||||
stopTyping: () => void;
|
stopTyping: () => void;
|
||||||
@@ -179,12 +179,13 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
|||||||
|
|
||||||
// Send a message
|
// Send a message
|
||||||
const sendMessage = useCallback(
|
const sendMessage = useCallback(
|
||||||
async (content: string, messageType: MessageType = 'TEXT' as MessageType) => {
|
async (content: string, messageType: MessageType = 'TEXT' as MessageType, fileFields?: { fileUrl?: string; mimeType?: string; fileName?: string; fileSize?: number }) => {
|
||||||
if (!currentConversationIdRef.current) return;
|
if (!currentConversationIdRef.current) return;
|
||||||
|
|
||||||
const dto: CreateMessageDto = {
|
const dto: CreateMessageDto = {
|
||||||
content,
|
content,
|
||||||
messageType,
|
messageType,
|
||||||
|
...fileFields,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stop typing indicator
|
// Stop typing indicator
|
||||||
@@ -226,6 +227,11 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
|||||||
|
|
||||||
// Update conversation in list
|
// Update conversation in list
|
||||||
const conversationId = currentConversationIdRef.current;
|
const conversationId = currentConversationIdRef.current;
|
||||||
|
const previewText = messageType === ('IMAGE' as MessageType)
|
||||||
|
? 'Sent an image'
|
||||||
|
: messageType === ('FILE' as MessageType)
|
||||||
|
? 'Sent a file'
|
||||||
|
: content;
|
||||||
setConversations((prev) =>
|
setConversations((prev) =>
|
||||||
prev.map((c) =>
|
prev.map((c) =>
|
||||||
c.id === conversationId
|
c.id === conversationId
|
||||||
@@ -233,7 +239,7 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
|||||||
...c,
|
...c,
|
||||||
lastMessageAt: new Date().toISOString(),
|
lastMessageAt: new Date().toISOString(),
|
||||||
lastMessageText:
|
lastMessageText:
|
||||||
content.length > 255 ? content.substring(0, 252) + '...' : content,
|
previewText.length > 255 ? previewText.substring(0, 252) + '...' : previewText,
|
||||||
}
|
}
|
||||||
: c
|
: c
|
||||||
)
|
)
|
||||||
@@ -360,10 +366,17 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
|||||||
if (c.id === message.conversationId) {
|
if (c.id === message.conversationId) {
|
||||||
const isCurrentConversation =
|
const isCurrentConversation =
|
||||||
c.id === currentConversationIdRef.current;
|
c.id === currentConversationIdRef.current;
|
||||||
|
// Show friendly preview for non-text messages
|
||||||
|
let previewText = message.content.substring(0, 255);
|
||||||
|
if (message.messageType === 'IMAGE') {
|
||||||
|
previewText = 'Sent an image';
|
||||||
|
} else if (message.messageType === 'FILE') {
|
||||||
|
previewText = 'Sent a file';
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...c,
|
...c,
|
||||||
lastMessageAt: message.createdAt,
|
lastMessageAt: message.createdAt,
|
||||||
lastMessageText: message.content.substring(0, 255),
|
lastMessageText: previewText,
|
||||||
unreadCount: isCurrentConversation ? 0 : c.unreadCount + 1,
|
unreadCount: isCurrentConversation ? 0 : c.unreadCount + 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -413,38 +426,8 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle auth errors (token expired, server disconnected us)
|
// Auth error handling (token refresh + reconnect) is handled globally
|
||||||
const unsubscribeAuthError = socketService.onAuthError(async () => {
|
// by PresenceProvider — no duplicate handler needed here.
|
||||||
console.log('Socket auth error detected, refreshing token via REST...');
|
|
||||||
try {
|
|
||||||
const refreshToken = localStorage.getItem('refreshToken');
|
|
||||||
if (!refreshToken) {
|
|
||||||
console.error('No refresh token available for socket reconnection');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Refresh token using the same endpoint as the REST API interceptor
|
|
||||||
const res = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1'}/auth/refresh`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ refreshToken }),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const data = await res.json();
|
|
||||||
if (res.ok && data.success) {
|
|
||||||
const newToken = data.data.accessToken;
|
|
||||||
localStorage.setItem('accessToken', newToken);
|
|
||||||
localStorage.setItem('refreshToken', data.data.refreshToken);
|
|
||||||
console.log('Token refreshed, reconnecting socket...');
|
|
||||||
socketService.reconnectWithToken(newToken);
|
|
||||||
} else {
|
|
||||||
console.error('Token refresh failed:', data.message);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to refresh token for socket reconnection:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribeConnection();
|
unsubscribeConnection();
|
||||||
@@ -453,7 +436,6 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
|||||||
unsubscribeTypingStop();
|
unsubscribeTypingStop();
|
||||||
unsubscribeStatus();
|
unsubscribeStatus();
|
||||||
unsubscribeRead();
|
unsubscribeRead();
|
||||||
unsubscribeAuthError();
|
|
||||||
};
|
};
|
||||||
}, [updateUserStatus]);
|
}, [updateUserStatus]);
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface PresignedUrlResponse {
|
|||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadFolder = 'avatars' | 'documents' | 'properties' | 'verification';
|
type UploadFolder = 'avatars' | 'documents' | 'properties' | 'verification' | 'messages';
|
||||||
|
|
||||||
class UploadService {
|
class UploadService {
|
||||||
private basePath = '/upload';
|
private basePath = '/upload';
|
||||||
@@ -213,6 +213,39 @@ class UploadService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a message attachment (image or document) to S3
|
||||||
|
* Uses the message-presigned-url endpoint which allows USER and AGENT roles
|
||||||
|
* Returns the S3 key (not full URL) — use getPresignedDownloadUrl() to get a viewable URL
|
||||||
|
*/
|
||||||
|
async uploadMessageFile(
|
||||||
|
file: File,
|
||||||
|
onProgress?: (progress: UploadProgress) => void
|
||||||
|
): Promise<UploadedFile> {
|
||||||
|
// Step 1: Get presigned URL from backend (messages folder)
|
||||||
|
const response = await api.post<{ data: PresignedUrlResponse }>(
|
||||||
|
`${this.basePath}/message-presigned-url`,
|
||||||
|
{
|
||||||
|
filename: file.name,
|
||||||
|
contentType: file.type,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { uploadUrl, key } = response.data.data;
|
||||||
|
|
||||||
|
// Step 2: Upload directly to S3
|
||||||
|
await this.uploadToS3(uploadUrl, file, onProgress);
|
||||||
|
|
||||||
|
// Step 3: Return uploaded file info with KEY (not full URL) for database storage
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
url: key, // Store the S3 key, not the full URL
|
||||||
|
uploadedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a presigned download URL for an S3 key
|
* Get a presigned download URL for an S3 key
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user