Merge branch 'proofcheck-on-send' into 'dev'
Attachment refactoring - initial proofcheck on send See merge request keanuapp/keanuapp-weblite!350
This commit is contained in:
commit
4648e4969c
50 changed files with 3519 additions and 1645 deletions
|
|
@ -16,7 +16,7 @@
|
||||||
<link rel="apple-touch-icon" href="./icons/icon-384x384.png" sizes="384x384" />
|
<link rel="apple-touch-icon" href="./icons/icon-384x384.png" sizes="384x384" />
|
||||||
<link rel="apple-touch-icon" href="./icons/icon-512x512.png" sizes="512x512" />
|
<link rel="apple-touch-icon" href="./icons/icon-512x512.png" sizes="512x512" />
|
||||||
<link rel="manifest" href="./manifest.json" />
|
<link rel="manifest" href="./manifest.json" />
|
||||||
<script src="./lottie-player.js"></script>
|
<script src="/lottie-player.js" type="module"></script>
|
||||||
<style>
|
<style>
|
||||||
#loader {
|
#loader {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
||||||
621
package-lock.json
generated
621
package-lock.json
generated
|
|
@ -1,13 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "keanuapp-weblite",
|
"name": "keanuapp-weblite",
|
||||||
"version": "0.1.42",
|
"version": "0.1.49",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "keanuapp-weblite",
|
"name": "keanuapp-weblite",
|
||||||
"version": "0.1.42",
|
"version": "0.1.49",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@guardianproject/proofmode": "^0.3.2",
|
||||||
"@matrix-org/olm": "^3.2.12",
|
"@matrix-org/olm": "^3.2.12",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
|
|
@ -26,7 +27,7 @@
|
||||||
"linkify-html": "^4.1.0",
|
"linkify-html": "^4.1.0",
|
||||||
"linkifyjs": "^4.1.0",
|
"linkifyjs": "^4.1.0",
|
||||||
"material-design-icons-iconfont": "^6.7.0",
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
"matrix-js-sdk": "^37.5.0",
|
"matrix-js-sdk": "^37.6.0",
|
||||||
"md-gum-polyfill": "^1.0.0",
|
"md-gum-polyfill": "^1.0.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"pretty-bytes": "^5.6.0",
|
"pretty-bytes": "^5.6.0",
|
||||||
|
|
@ -36,6 +37,7 @@
|
||||||
"recordrtc": "^5.6.2",
|
"recordrtc": "^5.6.2",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
|
"threads": "^1.7.0",
|
||||||
"tiny-emitter": "^2.1.0",
|
"tiny-emitter": "^2.1.0",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
|
@ -52,6 +54,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
"@rollup/plugin-commonjs": "^28.0.3",
|
||||||
|
"@types/aes-js": "^3.1.4",
|
||||||
"@types/jszip": "^3.4.0",
|
"@types/jszip": "^3.4.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||||
"@vue/compiler-sfc": "^3.5.13",
|
"@vue/compiler-sfc": "^3.5.13",
|
||||||
|
|
@ -62,10 +65,13 @@
|
||||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||||
"sass": "^1.86.0",
|
"sass": "^1.86.0",
|
||||||
"sass-loader": "^10",
|
"sass-loader": "^10",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
"unplugin-vue-components": "^28.4.1",
|
"unplugin-vue-components": "^28.4.1",
|
||||||
"vite": "^6.2.2",
|
"vite": "^6.2.2",
|
||||||
"vite-plugin-static-copy": "^2.3.0",
|
"vite-plugin-static-copy": "^2.3.0",
|
||||||
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
"vite-plugin-vuetify": "^2.1.1",
|
"vite-plugin-vuetify": "^2.1.1",
|
||||||
|
"vite-plugin-wasm": "^3.4.1",
|
||||||
"vue-cli-plugin-vuetify": "^2.5.8"
|
"vue-cli-plugin-vuetify": "^2.5.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -935,6 +941,12 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@guardianproject/proofmode": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@guardianproject/proofmode/-/proofmode-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-p71l7hheUoAWYbq/t1WoP94n6Ug9PUnapNtUKytvY688+NgeFHjL7Uc8X/K+Li3ikztfm0kM30q5nbAOJU14Fw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||||
|
|
@ -1490,6 +1502,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/plugin-virtual": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/pluginutils": {
|
"node_modules/@rollup/pluginutils": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
||||||
|
|
@ -1786,6 +1816,239 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/core": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/counter": "^0.1.3",
|
||||||
|
"@swc/types": "^0.1.21"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/swc"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@swc/core-darwin-arm64": "1.11.24",
|
||||||
|
"@swc/core-darwin-x64": "1.11.24",
|
||||||
|
"@swc/core-linux-arm-gnueabihf": "1.11.24",
|
||||||
|
"@swc/core-linux-arm64-gnu": "1.11.24",
|
||||||
|
"@swc/core-linux-arm64-musl": "1.11.24",
|
||||||
|
"@swc/core-linux-x64-gnu": "1.11.24",
|
||||||
|
"@swc/core-linux-x64-musl": "1.11.24",
|
||||||
|
"@swc/core-win32-arm64-msvc": "1.11.24",
|
||||||
|
"@swc/core-win32-ia32-msvc": "1.11.24",
|
||||||
|
"@swc/core-win32-x64-msvc": "1.11.24"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/helpers": ">=0.5.17"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/helpers": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-darwin-arm64": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-darwin-x64": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-arm64-musl": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-x64-gnu": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-x64-musl": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-win32-x64-msvc": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/counter": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@swc/types": {
|
||||||
|
"version": "0.1.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz",
|
||||||
|
"integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/counter": "^0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/aes-js": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/aes-js/-/aes-js-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-v3D66IptpUqh+pHKVNRxY8yvp2ESSZXe0rTzsGdzUhEwag7ljVfgCllkWv2YgiYXDhWFBrEywll4A5JToyTNFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
|
|
@ -2723,6 +2986,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/callsites": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/camelcase": {
|
"node_modules/camelcase": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
|
@ -3174,7 +3446,6 @@
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
|
|
@ -3811,6 +4082,16 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esm": {
|
||||||
|
"version": "3.2.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
|
||||||
|
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
|
||||||
|
|
@ -4655,6 +4936,18 @@
|
||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-observable": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-plain-object": {
|
"node_modules/is-plain-object": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||||
|
|
@ -5032,9 +5325,9 @@
|
||||||
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
||||||
},
|
},
|
||||||
"node_modules/matrix-js-sdk": {
|
"node_modules/matrix-js-sdk": {
|
||||||
"version": "37.5.0",
|
"version": "37.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-37.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-37.6.0.tgz",
|
||||||
"integrity": "sha512-5tyuAi5hnKud1UkVq8Z2/3c22hWGELBZzErJPZkE6Hju2uGUfGtrIx6uj6puv0ZjvsUU3X6Qgm8vdReKO1PGig==",
|
"integrity": "sha512-OdqZGqSarksiesHovQngeVcu7+fEkJUDk0pNX/LZg+HYZfyMrKgX3fb7WNhqcBW6kuTYGwkBvY5/LpE2AFabXw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
|
|
@ -5054,7 +5347,7 @@
|
||||||
"uuid": "11"
|
"uuid": "11"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=22.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/matrix-widget-api": {
|
"node_modules/matrix-widget-api": {
|
||||||
|
|
@ -5202,7 +5495,6 @@
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
|
|
@ -5277,6 +5569,12 @@
|
||||||
"webpack": "^4.0.0 || ^5.0.0"
|
"webpack": "^4.0.0 || ^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/observable-fns": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/oidc-client-ts": {
|
"node_modules/oidc-client-ts": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.2.0.tgz",
|
||||||
|
|
@ -5394,15 +5692,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/parent-module/node_modules/callsites": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/parse-asn1": {
|
"node_modules/parse-asn1": {
|
||||||
"version": "5.1.7",
|
"version": "5.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz",
|
||||||
|
|
@ -6643,12 +6932,40 @@
|
||||||
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/threads": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/threads/-/threads-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"callsites": "^3.1.0",
|
||||||
|
"debug": "^4.2.0",
|
||||||
|
"is-observable": "^2.1.0",
|
||||||
|
"observable-fns": "^0.6.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/andywer/threads.js?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"tiny-worker": ">= 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tiny-emitter": {
|
"node_modules/tiny-emitter": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
|
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-worker": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"esm": "^3.2.25"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.13",
|
"version": "0.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||||
|
|
@ -6716,6 +7033,20 @@
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ufo": {
|
"node_modules/ufo": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
|
||||||
|
|
@ -7081,6 +7412,35 @@
|
||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-top-level-await": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-r/DtuvHrSqUVk23XpG2cl8gjt1aATMG5cjExXL1BUTcSNab6CzkcPua9BPEc9fuTP5UpwClCxUe3+dNGL0yrgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/plugin-virtual": "^3.0.2",
|
||||||
|
"@swc/core": "^1.10.16",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": ">=2.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite-plugin-top-level-await/node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite-plugin-vuetify": {
|
"node_modules/vite-plugin-vuetify": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.1.tgz",
|
||||||
|
|
@ -7101,6 +7461,16 @@
|
||||||
"vuetify": "^3.0.0"
|
"vuetify": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-wasm": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^2 || ^3 || ^4 || ^5 || ^6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite/node_modules/fdir": {
|
"node_modules/vite/node_modules/fdir": {
|
||||||
"version": "6.4.4",
|
"version": "6.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||||
|
|
@ -8152,6 +8522,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@guardianproject/proofmode": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@guardianproject/proofmode/-/proofmode-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-p71l7hheUoAWYbq/t1WoP94n6Ug9PUnapNtUKytvY688+NgeFHjL7Uc8X/K+Li3ikztfm0kM30q5nbAOJU14Fw=="
|
||||||
|
},
|
||||||
"@humanwhocodes/config-array": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||||
|
|
@ -8421,6 +8796,13 @@
|
||||||
"magic-string": "^0.30.3"
|
"magic-string": "^0.30.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@rollup/plugin-virtual": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"@rollup/pluginutils": {
|
"@rollup/pluginutils": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
||||||
|
|
@ -8560,6 +8942,117 @@
|
||||||
"integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==",
|
"integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"@swc/core": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@swc/core-darwin-arm64": "1.11.24",
|
||||||
|
"@swc/core-darwin-x64": "1.11.24",
|
||||||
|
"@swc/core-linux-arm-gnueabihf": "1.11.24",
|
||||||
|
"@swc/core-linux-arm64-gnu": "1.11.24",
|
||||||
|
"@swc/core-linux-arm64-musl": "1.11.24",
|
||||||
|
"@swc/core-linux-x64-gnu": "1.11.24",
|
||||||
|
"@swc/core-linux-x64-musl": "1.11.24",
|
||||||
|
"@swc/core-win32-arm64-msvc": "1.11.24",
|
||||||
|
"@swc/core-win32-ia32-msvc": "1.11.24",
|
||||||
|
"@swc/core-win32-x64-msvc": "1.11.24",
|
||||||
|
"@swc/counter": "^0.1.3",
|
||||||
|
"@swc/types": "^0.1.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@swc/core-darwin-arm64": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/core-darwin-x64": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/core-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/core-linux-arm64-gnu": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/core-linux-arm64-musl": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/core-linux-x64-gnu": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/core-linux-x64-musl": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/core-win32-arm64-msvc": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/core-win32-ia32-msvc": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/core-win32-x64-msvc": {
|
||||||
|
"version": "1.11.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz",
|
||||||
|
"integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/counter": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@swc/types": {
|
||||||
|
"version": "0.1.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz",
|
||||||
|
"integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@swc/counter": "^0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/aes-js": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/aes-js/-/aes-js-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-v3D66IptpUqh+pHKVNRxY8yvp2ESSZXe0rTzsGdzUhEwag7ljVfgCllkWv2YgiYXDhWFBrEywll4A5JToyTNFA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/eslint": {
|
"@types/eslint": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
|
|
@ -9285,6 +9778,11 @@
|
||||||
"get-intrinsic": "^1.3.0"
|
"get-intrinsic": "^1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"callsites": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
|
||||||
|
},
|
||||||
"camelcase": {
|
"camelcase": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
|
@ -9628,7 +10126,6 @@
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
"devOptional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
}
|
}
|
||||||
|
|
@ -10073,6 +10570,12 @@
|
||||||
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
|
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"esm": {
|
||||||
|
"version": "3.2.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
|
||||||
|
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"espree": {
|
"espree": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
|
||||||
|
|
@ -10651,6 +11154,11 @@
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
|
"is-observable": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw=="
|
||||||
|
},
|
||||||
"is-plain-object": {
|
"is-plain-object": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||||
|
|
@ -10930,9 +11438,9 @@
|
||||||
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
||||||
},
|
},
|
||||||
"matrix-js-sdk": {
|
"matrix-js-sdk": {
|
||||||
"version": "37.5.0",
|
"version": "37.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-37.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-37.6.0.tgz",
|
||||||
"integrity": "sha512-5tyuAi5hnKud1UkVq8Z2/3c22hWGELBZzErJPZkE6Hju2uGUfGtrIx6uj6puv0ZjvsUU3X6Qgm8vdReKO1PGig==",
|
"integrity": "sha512-OdqZGqSarksiesHovQngeVcu7+fEkJUDk0pNX/LZg+HYZfyMrKgX3fb7WNhqcBW6kuTYGwkBvY5/LpE2AFabXw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm": "^14.0.1",
|
"@matrix-org/matrix-sdk-crypto-wasm": "^14.0.1",
|
||||||
|
|
@ -11072,8 +11580,7 @@
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"nanoid": {
|
"nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
|
|
@ -11118,6 +11625,11 @@
|
||||||
"schema-utils": "^3.0.0"
|
"schema-utils": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"observable-fns": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg=="
|
||||||
|
},
|
||||||
"oidc-client-ts": {
|
"oidc-client-ts": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.2.0.tgz",
|
||||||
|
|
@ -11198,14 +11710,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"callsites": "^3.0.0"
|
"callsites": "^3.0.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"callsites": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"parse-asn1": {
|
"parse-asn1": {
|
||||||
|
|
@ -12045,11 +12549,32 @@
|
||||||
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"threads": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/threads/-/threads-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==",
|
||||||
|
"requires": {
|
||||||
|
"callsites": "^3.1.0",
|
||||||
|
"debug": "^4.2.0",
|
||||||
|
"is-observable": "^2.1.0",
|
||||||
|
"observable-fns": "^0.6.1",
|
||||||
|
"tiny-worker": ">= 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tiny-emitter": {
|
"tiny-emitter": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
||||||
},
|
},
|
||||||
|
"tiny-worker": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"esm": "^3.2.25"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tinyglobby": {
|
"tinyglobby": {
|
||||||
"version": "0.2.13",
|
"version": "0.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||||
|
|
@ -12090,6 +12615,12 @@
|
||||||
"prelude-ls": "^1.2.1"
|
"prelude-ls": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"typescript": {
|
||||||
|
"version": "5.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
|
"devOptional": true
|
||||||
|
},
|
||||||
"ufo": {
|
"ufo": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
|
||||||
|
|
@ -12302,6 +12833,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vite-plugin-top-level-await": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-r/DtuvHrSqUVk23XpG2cl8gjt1aATMG5cjExXL1BUTcSNab6CzkcPua9BPEc9fuTP5UpwClCxUe3+dNGL0yrgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@rollup/plugin-virtual": "^3.0.2",
|
||||||
|
"@swc/core": "^1.10.16",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"vite-plugin-vuetify": {
|
"vite-plugin-vuetify": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.1.tgz",
|
||||||
|
|
@ -12313,6 +12863,13 @@
|
||||||
"upath": "^2.0.1"
|
"upath": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vite-plugin-wasm": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "keanuapp-weblite",
|
"name": "keanuapp-weblite",
|
||||||
"version": "0.1.50",
|
"version": "0.1.51",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"create-sticker-config": "node ./create_sticker_config.js $1"
|
"create-sticker-config": "node ./create_sticker_config.js $1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@guardianproject/proofmode": "^0.3.2",
|
||||||
"@matrix-org/olm": "^3.2.12",
|
"@matrix-org/olm": "^3.2.12",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
|
|
@ -27,7 +28,7 @@
|
||||||
"linkify-html": "^4.1.0",
|
"linkify-html": "^4.1.0",
|
||||||
"linkifyjs": "^4.1.0",
|
"linkifyjs": "^4.1.0",
|
||||||
"material-design-icons-iconfont": "^6.7.0",
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
"matrix-js-sdk": "^37.5.0",
|
"matrix-js-sdk": "^37.6.0",
|
||||||
"md-gum-polyfill": "^1.0.0",
|
"md-gum-polyfill": "^1.0.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"pretty-bytes": "^5.6.0",
|
"pretty-bytes": "^5.6.0",
|
||||||
|
|
@ -37,6 +38,7 @@
|
||||||
"recordrtc": "^5.6.2",
|
"recordrtc": "^5.6.2",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
|
"threads": "^1.7.0",
|
||||||
"tiny-emitter": "^2.1.0",
|
"tiny-emitter": "^2.1.0",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
|
@ -53,6 +55,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
"@rollup/plugin-commonjs": "^28.0.3",
|
||||||
|
"@types/aes-js": "^3.1.4",
|
||||||
"@types/jszip": "^3.4.0",
|
"@types/jszip": "^3.4.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||||
"@vue/compiler-sfc": "^3.5.13",
|
"@vue/compiler-sfc": "^3.5.13",
|
||||||
|
|
@ -63,10 +66,13 @@
|
||||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||||
"sass": "^1.86.0",
|
"sass": "^1.86.0",
|
||||||
"sass-loader": "^10",
|
"sass-loader": "^10",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
"unplugin-vue-components": "^28.4.1",
|
"unplugin-vue-components": "^28.4.1",
|
||||||
"vite": "^6.2.2",
|
"vite": "^6.2.2",
|
||||||
"vite-plugin-static-copy": "^2.3.0",
|
"vite-plugin-static-copy": "^2.3.0",
|
||||||
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
"vite-plugin-vuetify": "^2.1.1",
|
"vite-plugin-vuetify": "^2.1.1",
|
||||||
|
"vite-plugin-wasm": "^3.4.1",
|
||||||
"vue-cli-plugin-vuetify": "^2.5.8"
|
"vue-cli-plugin-vuetify": "^2.5.8"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "keanuapp-weblite",
|
"name": "keanuapp-weblite",
|
||||||
"version": "0.1.49",
|
"version": "0.1.50",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"create-sticker-config": "node ./create_sticker_config.js $1"
|
"create-sticker-config": "node ./create_sticker_config.js $1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@guardianproject/proofmode": "^0.3.2",
|
||||||
"@matrix-org/olm": "^3.2.12",
|
"@matrix-org/olm": "^3.2.12",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
|
|
@ -27,7 +28,7 @@
|
||||||
"linkify-html": "^4.1.0",
|
"linkify-html": "^4.1.0",
|
||||||
"linkifyjs": "^4.1.0",
|
"linkifyjs": "^4.1.0",
|
||||||
"material-design-icons-iconfont": "^6.7.0",
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
"matrix-js-sdk": "^37.5.0",
|
"matrix-js-sdk": "^37.6.0",
|
||||||
"md-gum-polyfill": "^1.0.0",
|
"md-gum-polyfill": "^1.0.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"pretty-bytes": "^5.6.0",
|
"pretty-bytes": "^5.6.0",
|
||||||
|
|
@ -37,6 +38,7 @@
|
||||||
"recordrtc": "^5.6.2",
|
"recordrtc": "^5.6.2",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
|
"threads": "^1.7.0",
|
||||||
"tiny-emitter": "^2.1.0",
|
"tiny-emitter": "^2.1.0",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
|
@ -53,6 +55,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
"@rollup/plugin-commonjs": "^28.0.3",
|
||||||
|
"@types/aes-js": "^3.1.4",
|
||||||
"@types/jszip": "^3.4.0",
|
"@types/jszip": "^3.4.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||||
"@vue/compiler-sfc": "^3.5.13",
|
"@vue/compiler-sfc": "^3.5.13",
|
||||||
|
|
@ -63,10 +66,13 @@
|
||||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||||
"sass": "^1.86.0",
|
"sass": "^1.86.0",
|
||||||
"sass-loader": "^10",
|
"sass-loader": "^10",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
"unplugin-vue-components": "^28.4.1",
|
"unplugin-vue-components": "^28.4.1",
|
||||||
"vite": "^6.2.2",
|
"vite": "^6.2.2",
|
||||||
"vite-plugin-static-copy": "^2.3.0",
|
"vite-plugin-static-copy": "^2.3.0",
|
||||||
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
"vite-plugin-vuetify": "^2.1.1",
|
"vite-plugin-vuetify": "^2.1.1",
|
||||||
|
"vite-plugin-wasm": "^3.4.1",
|
||||||
"vue-cli-plugin-vuetify": "^2.5.8"
|
"vue-cli-plugin-vuetify": "^2.5.8"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,6 @@ $poll-hilite-color: #6360f0;
|
||||||
$poll-hilite-color-bg: #d6d5fc;
|
$poll-hilite-color-bg: #d6d5fc;
|
||||||
$alert-bg-color: #FF3300;
|
$alert-bg-color: #FF3300;
|
||||||
|
|
||||||
$min-touch-target: 48px;
|
$min-touch-target: 48px;
|
||||||
|
$large-button-height: $min-touch-target;
|
||||||
|
$small-button-height: 36px;
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
/* full bleed */
|
/* full bleed */
|
||||||
padding: 0 0 0 0;
|
padding: 0 0 0 0;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
.v-image, video {
|
.v-img, video {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +126,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make all images 'cover' */
|
/* Make all images 'cover' */
|
||||||
.v-image__image {
|
.v-img__image {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
@use "@/assets/css/variables" as *;
|
@use "@/assets/css/variables" as *;
|
||||||
@use "@/assets/css/main.scss" as *;
|
@use "@/assets/css/main.scss" as *;
|
||||||
@use "@/assets/css/vendors/v-emoji-picker" as *;
|
@use "@/assets/css/vendors/v-emoji-picker" as *;
|
||||||
@use "@/assets/css/filedrop.scss" as *;
|
|
||||||
@use "@/assets/css/channel.scss" as *;
|
@use "@/assets/css/channel.scss" as *;
|
||||||
@use "sass:map";
|
@use "sass:map";
|
||||||
|
|
||||||
|
|
@ -368,7 +367,7 @@ body {
|
||||||
|
|
||||||
.scroll-to-end {
|
.scroll-to-end {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: 102px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
&.reversed {
|
&.reversed {
|
||||||
top: 120px;
|
top: 120px;
|
||||||
|
|
@ -575,12 +574,13 @@ body {
|
||||||
}
|
}
|
||||||
.bubble.image-bubble {
|
.bubble.image-bubble {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 70%;
|
width: 70%;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.v-image,
|
.v-img,
|
||||||
video {
|
video {
|
||||||
border-radius: 10px 10px 0 10px;
|
border-radius: 10px 10px 0 10px;
|
||||||
[dir="rtl"] & {
|
[dir="rtl"] & {
|
||||||
|
|
@ -1617,4 +1617,4 @@ body {
|
||||||
.zip {
|
.zip {
|
||||||
color: #1d1d1d;
|
color: #1d1d1d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +173,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
.v-image {
|
.v-img {
|
||||||
flex: 0 0 14px;
|
flex: 0 0 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,28 @@
|
||||||
@use "@/assets/css/variables" as *;
|
@use "@/assets/css/variables" as *;
|
||||||
|
|
||||||
$large-button-height: $min-touch-target;
|
$background: #000000;
|
||||||
$small-button-height: 36px;
|
$backgroundSection: #181719;
|
||||||
|
$backgroundHilite: #383739;
|
||||||
|
$text: #ffffff;
|
||||||
|
$hiliteColor: #4642f1;
|
||||||
|
|
||||||
.file-drop-root {
|
.send-attachments {
|
||||||
$hiliteColor: #4642f1;
|
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
|
z-index: 10;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--v-background-color);
|
background-color: $background;
|
||||||
color: var(--v-foreground-color);
|
color: $text;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
.file-drop-title {
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 11.54 * $chat-text-size;
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 140%;
|
|
||||||
letter-spacing: 0.34px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-top: 13px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
.background {
|
|
||||||
width: 100%;
|
|
||||||
height: 50%;
|
|
||||||
background-color: #181719;
|
|
||||||
&.drop-target {
|
|
||||||
background-color: #383739;
|
|
||||||
}
|
|
||||||
border-radius: 19px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-format-info {
|
|
||||||
opacity: 0.6;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 11 * $chat-text-size;
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
line-height: 117%;
|
|
||||||
letter-spacing: 0.4px;
|
|
||||||
margin-top: 13px;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-btn {
|
.v-btn {
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
@ -72,6 +35,7 @@ $small-button-height: 36px;
|
||||||
height: $small-button-height !important;
|
height: $small-button-height !important;
|
||||||
margin-top: $chat-standard-padding-xs;
|
margin-top: $chat-standard-padding-xs;
|
||||||
margin-bottom: $chat-standard-padding-xs;
|
margin-bottom: $chat-standard-padding-xs;
|
||||||
|
|
||||||
&.large {
|
&.large {
|
||||||
padding: 16px 23px;
|
padding: 16px 23px;
|
||||||
height: $large-button-height;
|
height: $large-button-height;
|
||||||
|
|
@ -79,44 +43,69 @@ $small-button-height: 36px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
.back-button {
|
||||||
color: rgba(white, 80%) !important;
|
position: absolute;
|
||||||
}
|
top: 16px;
|
||||||
textarea::placeholder {
|
left: 16px;
|
||||||
color: rgba(white, 80%) !important;
|
margin: 0;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-wrapper {
|
.info-button {
|
||||||
width: 100%;
|
position: absolute;
|
||||||
flex: 0 0 100%;
|
top: 16px;
|
||||||
overflow-y: auto;
|
right: 16px;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.file-drop-current-item {
|
|
||||||
|
.send-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
color: rgba($text, 80%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea::placeholder {
|
||||||
|
color: rgba($text, 80%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-attachments__selecting__current-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 70%;
|
flex: 1 1 100%;
|
||||||
background-color: #181719;
|
background-color: $backgroundSection;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
&.drop-target {
|
&.drop-target {
|
||||||
background-color: #383739;
|
background-color: $backgroundHilite;
|
||||||
}
|
}
|
||||||
|
|
||||||
border-radius: 19px;
|
border-radius: 19px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
.v-image {
|
|
||||||
|
.v-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filename {
|
.filename {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-drop-thumbnail-container {
|
.file-drop-thumbnail-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 13px 20px 15px 20px;
|
padding: 13px 20px 15px 20px;
|
||||||
height: 74px;
|
flex: 0 0 74px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
@ -126,9 +115,30 @@ $small-button-height: 36px;
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbar for IE, Edge and Firefox */
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none; /* Firefox */
|
/* IE and Edge */
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
|
|
||||||
|
.v-badge {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-badge__badge {
|
||||||
|
top: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
padding: 0;
|
||||||
|
min-width: 12px;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.file-drop-thumbnail {
|
.file-drop-thumbnail {
|
||||||
width: 46px;
|
width: 46px;
|
||||||
|
|
@ -139,21 +149,26 @@ $small-button-height: 36px;
|
||||||
border: 2px solid white;
|
border: 2px solid white;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.current {
|
&.current {
|
||||||
border: 2px solid #4642f1;
|
border: 2px solid #4642f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.noborder {
|
&.noborder {
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
}
|
}
|
||||||
.v-image {
|
|
||||||
|
.v-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|
||||||
.add,
|
.add,
|
||||||
.remove {
|
.remove {
|
||||||
|
color: $text;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
@ -162,11 +177,13 @@ $small-button-height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.v-icon {
|
.v-icon {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 15.75px;
|
height: 15.75px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove {
|
.remove {
|
||||||
// Slight background to make visible
|
// Slight background to make visible
|
||||||
background-color: rgba(black, 0.2);
|
background-color: rgba(black, 0.2);
|
||||||
|
|
@ -177,38 +194,58 @@ $small-button-height: 36px;
|
||||||
.file-drop-section {
|
.file-drop-section {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
background-color: #181719;
|
background-color: $backgroundSection;
|
||||||
border-radius: 19px;
|
border-radius: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-drop-input-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.file-drop-input-container,
|
.file-drop-input-container,
|
||||||
.file-drop-sending-input-container,
|
.file-drop-sending-input-container,
|
||||||
.file-drop-sent-input-container {
|
.file-drop-sent-input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 20%;
|
min-height: 100px;
|
||||||
background-color: #181719;
|
background-color: $backgroundSection;
|
||||||
border-radius: 19px;
|
border-radius: 19px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
.input-area-text {
|
|
||||||
flex: 0 0 auto;
|
.input-container__buttons {
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
padding: 16px 18px;
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
.v-btn {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&>*:not(:first-child) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-area-text {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
|
||||||
|
.v-field {
|
||||||
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInStackItem {
|
@keyframes fadeInStackItem {
|
||||||
from {opacity: 0;}
|
from {
|
||||||
to {opacity: 1;}
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sending
|
// Sending
|
||||||
|
|
@ -219,49 +256,63 @@ $small-button-height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.no-items {
|
.no-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-drop-stack-item {
|
.file-drop-stack-item {
|
||||||
transform: rotate(-4.4deg);
|
transform: rotate(-4.4deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 21 * $chat-text-size;
|
font-size: 21 * $chat-text-size;
|
||||||
font-family: "Poppins", sans-serif;
|
font-family: "Poppins",
|
||||||
|
sans-serif;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.34px;
|
letter-spacing: 0.34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.items-sent {
|
.items-sent {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
div, .v-icon {
|
|
||||||
|
div,
|
||||||
|
.v-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
.v-icon, .v-icon__component {
|
|
||||||
|
.v-icon,
|
||||||
|
.v-icon__component {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
height: 30%;
|
height: 30%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-drop-stack-item {
|
.file-drop-stack-item {
|
||||||
background: #3a3a3c;
|
background: #3a3a3c;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
.v-image {
|
|
||||||
|
.v-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.direct {
|
&.direct {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.animated {
|
&.animated {
|
||||||
animation-name: fadeInStackItem;
|
animation-name: fadeInStackItem;
|
||||||
animation-fill-mode: both;
|
animation-fill-mode: both;
|
||||||
|
|
@ -283,20 +334,23 @@ $small-button-height: 36px;
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbar for IE, Edge and Firefox */
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none; /* Firefox */
|
/* IE and Edge */
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
|
|
||||||
.file-drop-sending-item {
|
.file-drop-sending-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #242424;
|
background-color: $backgroundSection;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: linear-gradient(0deg, #26242b 0%, #26242b 100%), #fff;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
.v-image {
|
|
||||||
|
.v-img {
|
||||||
width: $min-touch-target;
|
width: $min-touch-target;
|
||||||
height: $min-touch-target;
|
height: $min-touch-target;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
@ -304,18 +358,22 @@ $small-button-height: 36px;
|
||||||
flex: 0 0 $min-touch-target;
|
flex: 0 0 $min-touch-target;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.filename {
|
.filename {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 18px;
|
top: 18px;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-progress-linear {
|
.v-progress-linear {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-drop-cancel {
|
.file-drop-cancel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
|
|
@ -336,13 +394,15 @@ $small-button-height: 36px;
|
||||||
.v-progress-circular {
|
.v-progress-circular {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
background: linear-gradient(0deg, #000 0%, #000 100%), #4642f1;
|
|
||||||
|
background: linear-gradient(0deg, #000 0%, #000 100%),
|
||||||
|
#4642f1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-drop-files-sent {
|
.file-drop-files-sent {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: #fff;
|
color: $text;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 21 * $chat-text-size;
|
font-size: 21 * $chat-text-size;
|
||||||
font-family: "Poppins", sans-serif;
|
font-family: "Poppins", sans-serif;
|
||||||
|
|
@ -353,10 +413,12 @@ $small-button-height: 36px;
|
||||||
|
|
||||||
.file-drop-sent-input-container {
|
.file-drop-sent-input-container {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
.v-btn {
|
.v-btn {
|
||||||
right: unset;
|
right: unset;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
background: linear-gradient(0deg, #000 0%, #000 100%), #4642f1;
|
background: linear-gradient(0deg, #000 0%, #000 100%), #4642f1;
|
||||||
|
|
||||||
&.close {
|
&.close {
|
||||||
right: 8px;
|
right: 8px;
|
||||||
left: unset;
|
left: unset;
|
||||||
|
|
@ -365,3 +427,28 @@ $small-button-height: 36px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachment-info {
|
||||||
|
text-align: start;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.attachment-info__size {
|
||||||
|
white-space: pre;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 36px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-info__size__filename {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c2pa-badge {
|
||||||
|
overflow: hidden;
|
||||||
|
.v-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/assets/icons/ic_cr.vue
Normal file
45
src/assets/icons/ic_cr.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
version="1.0"
|
||||||
|
id="katman_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
width="24px"
|
||||||
|
height="24px"
|
||||||
|
viewBox="0 0 841.89 595.28"
|
||||||
|
style="enable-background: new 0 0 841.89 595.28"
|
||||||
|
xml:space="preserve"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
class="st1"
|
||||||
|
d="M207.73,297.41c0-117.82,95.44-213.26,213.26-213.26c117.82,0,213.26,95.44,213.26,213.26v213.26H420.99
|
||||||
|
C303.17,510.67,207.73,415.23,207.73,297.41z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="st2"
|
||||||
|
d="M617.82,297.41v196.84H420.99c-108.77,0-196.84-88.07-196.84-196.84s88.07-196.84,196.84-196.84
|
||||||
|
S617.82,188.64,617.82,297.41z M182.97,297.41c0-131.38,106.63-238.01,238.01-238.01S659,166.03,659,297.41v238.01H420.99
|
||||||
|
C289.6,535.42,182.97,428.79,182.97,297.41z M277.7,306.93c0,49.03,33.08,90.45,85.68,90.45c43.32,0,72.59-28.56,79.73-65.93
|
||||||
|
h-42.84c-5.47,17.14-19.28,27.61-36.89,27.61c-26.66,0-44.03-20.95-44.03-52.12s17.38-52.12,44.03-52.12
|
||||||
|
c17.14,0,30.7,9.76,36.42,25.94h43.08c-7.62-36.42-36.65-64.26-79.5-64.26C310.55,216.49,277.7,257.9,277.7,306.93z
|
||||||
|
M500.48,221.25h-40.46v171.61h42.13v-89.49c0-16.9,4.76-27.85,12.85-34.75c7.14-6.43,16.42-9.76,31.66-9.76h10.71V219.1H546.9
|
||||||
|
c-22.14,0-36.89,8.09-46.41,20.47v-18.57V221.25z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<style type="text/css" scoped>
|
||||||
|
.st1 {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
.st2 {
|
||||||
|
fill-rule: evenodd;
|
||||||
|
clip-rule: evenodd;
|
||||||
|
fill: #141414;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
<v-btn id="btn-rewind" :disabled="!info || info.loading" @click.stop="rewind" icon>
|
<v-btn id="btn-rewind" :disabled="!info || info.loading" @click.stop="rewind" icon>
|
||||||
<v-icon size="28">$vuetify.icons.rewind</v-icon>
|
<v-icon size="28">$vuetify.icons.rewind</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-progress-circular v-if="info && info.loading" :value="info.loadPercent" @click.stop="pause" size="36" width="2" style="margin:26px"></v-progress-circular>
|
<v-progress-circular v-if="info && info.loading" :model-value="info.loadPercent" @click.stop="pause" size="36" width="2" style="margin:26px"></v-progress-circular>
|
||||||
<v-btn v-else-if="info && info.playing" id="btn-pause" @click.stop="pause" icon>
|
<v-btn v-else-if="info && info.playing" id="btn-pause" @click.stop="pause" icon>
|
||||||
<v-icon size="56">$vuetify.icons.pause_circle</v-icon>
|
<v-icon size="56">$vuetify.icons.pause_circle</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,14 @@
|
||||||
<VoiceRecorder class="audio-layout" v-if="useVoiceMode" :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
<VoiceRecorder class="audio-layout" v-if="useVoiceMode" :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
||||||
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />
|
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />
|
||||||
|
|
||||||
<FileDropLayout class="file-drop-root" v-if="useFileModeNonAdmin" :room="room"
|
<SendAttachmentsLayout
|
||||||
v-on:pick-file="showAttachmentPicker()"
|
v-if="room && useFileModeNonAdmin"
|
||||||
v-on:add-file="addAttachment($event)"
|
:room="room"
|
||||||
v-on:remove-file="currentFileInputs.splice($event, 1)"
|
v-on:pick-file="showAttachmentPicker(false)"
|
||||||
v-on:reset="resetAttachments"
|
v-on:add-files="(files) => addAttachments(files)"
|
||||||
:attachments="currentFileInputs"
|
:batch="uploadBatch"
|
||||||
v-on:close="closeFileMode"
|
v-on:close="closeFileMode"
|
||||||
|
:showBackButton="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="!useVoiceMode && !useFileModeNonAdmin" :class="{'chat-content': true, 'flex-grow-1': true, 'flex-shrink-1': true, 'invisible': !initialLoadDone}" ref="chatContainer"
|
<div v-if="!useVoiceMode && !useFileModeNonAdmin" :class="{'chat-content': true, 'flex-grow-1': true, 'flex-shrink-1': true, 'invisible': !initialLoadDone}" ref="chatContainer"
|
||||||
|
|
@ -89,7 +90,7 @@
|
||||||
see below. Otherwise things like context menus won't work as designed.
|
see below. Otherwise things like context menus won't work as designed.
|
||||||
-->
|
-->
|
||||||
<component :is="event.component" :room="room" :originalEvent="event" :nextEvent="event.nextDisplayedEvent"
|
<component :is="event.component" :room="room" :originalEvent="event" :nextEvent="event.nextDisplayedEvent"
|
||||||
:timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
|
:timelineSet="timelineSet" v-on:send-quick-reaction="sendQuickReaction"
|
||||||
:componentFn="componentForEvent"
|
:componentFn="componentForEvent"
|
||||||
v-on:context-menu="showContextMenuForEvent({event: event, anchor: $event.anchor})"
|
v-on:context-menu="showContextMenuForEvent({event: event, anchor: $event.anchor})"
|
||||||
v-on:own-avatar-clicked="viewProfile"
|
v-on:own-avatar-clicked="viewProfile"
|
||||||
|
|
@ -116,14 +117,13 @@
|
||||||
|
|
||||||
<NoHistoryRoomWelcomeHeader v-if="showNoHistoryRoomWelcomeHeader" />
|
<NoHistoryRoomWelcomeHeader v-if="showNoHistoryRoomWelcomeHeader" />
|
||||||
|
|
||||||
<!-- "Scroll to end"-button -->
|
|
||||||
<v-btn v-if="!useVoiceMode" icon="arrow_downward" :class="{'scroll-to-end': true, 'reversed': reverseOrder}" v-show="showScrollToEnd" theme="dark" size="x-small" elevation="0" color="black"
|
|
||||||
@click.stop="scrollToEndOfTimeline">
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- "Scroll to end"-button -->
|
||||||
|
<v-btn v-if="!useVoiceMode" icon="arrow_downward" :class="{'scroll-to-end': true, 'reversed': reverseOrder}" v-show="showScrollToEnd" theme="dark" size="x-small" elevation="0" color="black"
|
||||||
|
@click.stop="scrollToEndOfTimeline">
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
<!-- Input area -->
|
<!-- Input area -->
|
||||||
<v-container v-if="!useVoiceMode && !useFileModeNonAdmin && room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
|
<v-container v-if="!useVoiceMode && !useFileModeNonAdmin && room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
|
||||||
<div :class="[replyToEvent ? 'iput-area-inner-box' : '']">
|
<div :class="[replyToEvent ? 'iput-area-inner-box' : '']">
|
||||||
|
|
@ -213,7 +213,7 @@
|
||||||
|
|
||||||
<v-col v-if="!$config.disableMediaSharing" class="input-area-button text-center flex-grow-0 flex-shrink-1">
|
<v-col v-if="!$config.disableMediaSharing" class="input-area-button text-center flex-grow-0 flex-shrink-1">
|
||||||
<label icon flat ref="attachmentLabel">
|
<label icon flat ref="attachmentLabel">
|
||||||
<v-btn icon @click="showAttachmentPicker"
|
<v-btn icon @click="() => showAttachmentPicker(true)"
|
||||||
:disabled="attachButtonDisabled">
|
:disabled="attachButtonDisabled">
|
||||||
<v-icon size="36">add_circle_outline</v-icon>
|
<v-icon size="36">add_circle_outline</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
@ -229,80 +229,14 @@
|
||||||
<input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)"
|
<input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)"
|
||||||
accept="image/*,audio/*,video/*,.mp3,.mp4,.wav,.m4a,.pdf,application/pdf,.apk,application/vnd.android.package-archive,.ipa,.zip,application/zip,application/x-zip-compressed,multipart/x-zip" class="d-none" multiple/>
|
accept="image/*,audio/*,video/*,.mp3,.mp4,.wav,.m4a,.pdf,application/pdf,.apk,application/vnd.android.package-archive,.ipa,.zip,application/zip,application/x-zip-compressed,multipart/x-zip" class="d-none" multiple/>
|
||||||
|
|
||||||
<div v-if="currentFileInputsDialog && !useFileModeNonAdmin">
|
<SendAttachmentsLayout
|
||||||
<v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.display.smAndUp ? '50%' : '85%'" persistent scrollable>
|
v-if="uploadBatch && uploadBatch.attachments.length > 0 && !useFileModeNonAdmin"
|
||||||
<v-card class="ma-0 pa-0">
|
:room="room"
|
||||||
<v-card-text v-if="!currentFileInputs.length">
|
v-on:pick-file="showAttachmentPicker(false)"
|
||||||
{{ this.$t("message.preparing_to_upload")}}
|
v-on:add-files="(files) => addAttachments(files)"
|
||||||
<v-progress-linear
|
:batch="uploadBatch"
|
||||||
indeterminate
|
v-on:close="() => { uploadBatch = undefined }"
|
||||||
class="mb-0"
|
/>
|
||||||
></v-progress-linear>
|
|
||||||
</v-card-text>
|
|
||||||
<template v-else>
|
|
||||||
<v-card-title>
|
|
||||||
<div v-if="currentSendErrorExceededFile" class="text-red">{{ currentSendErrorExceededFile }}</div>
|
|
||||||
<span v-else> {{ $t('message.send_attachements_dialog_title') }} </span>
|
|
||||||
</v-card-title>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<template v-if="imageFiles && imageFiles.length">
|
|
||||||
<v-card-title v-if="imageFiles.length > 1"> {{ $t('message.images') }} </v-card-title>
|
|
||||||
<v-card-text :class="{'ma-0 pa-2' : true, 'd-flex flex-wrap justify-center': imageFiles.length > 1}">
|
|
||||||
<div :class="{'col-4': imageFiles.length > 1}" v-for="(currentImageInput, id) in imageFiles" :key="id">
|
|
||||||
<div style="position: relative">
|
|
||||||
<v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
|
|
||||||
contain class="current-image-input-path" />
|
|
||||||
<v-progress-linear :style="{ position: 'absolute', left: '0', right: '0', bottom: '0', opacity: currentImageInput.sendInfo ? '1' : '0' }" :value="currentImageInput.sendInfo ? currentImageInput.sendInfo.progress : 0"></v-progress-linear>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
|
||||||
{{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span>
|
|
||||||
<span v-else-if="currentImageInput && currentImageInput.dimensions">
|
|
||||||
{{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
|
||||||
({{ formatBytes(currentImageInput.scaledSize) }})
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
({{ formatBytes(currentImageInput.actualSize) }})
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
|
|
||||||
v-model="currentImageInput.useScaled" :disabled="currentImageInput && currentImageInput.sendInfo !== undefined" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
</template>
|
|
||||||
<template v-if="Array.isArray(currentFileInputs) && currentFileInputs.length">
|
|
||||||
<v-card-title v-if="nonImageFiles.length > 1">{{ $t('message.files') }}</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<div v-for="(currentImageInputPath, id) in currentFileInputs" :key="id">
|
|
||||||
<div v-if="!currentImageInputPath.type.includes('image/')">
|
|
||||||
<span> {{ $t('message.file') }}: {{ currentImageInputPath.name }}</span>
|
|
||||||
<span> ({{ formatBytes(currentImageInputPath.size) }})</span>
|
|
||||||
<v-progress-linear :style="{ opacity: currentImageInputPath.sendInfo ? '1' : '0' }" :value="currentImageInputPath.sendInfo ? currentImageInputPath.sendInfo.progress : 0"></v-progress-linear>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
</template>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-textarea v-if="showAttachmentCaptionInput" v-model="attachmentCaption" ref="attachmentCaption" color="black" background-color="transparent"
|
|
||||||
variant="solo" full-width auto-grow rows="1" no-resize hide-details :placeholder="$t('file_mode.add_a_message')"></v-textarea>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer>
|
|
||||||
<div v-if="currentSendError">{{ currentSendError }}</div>
|
|
||||||
</v-spacer>
|
|
||||||
<v-btn color="primary" variant="text" @click="cancelSendAttachment" id="btn-attachment-cancel" :disabled="sendingStatus != sendStatuses.SENDING && sendingStatus != sendStatuses.INITIAL">
|
|
||||||
{{ $t("menu.cancel") }}
|
|
||||||
</v-btn>
|
|
||||||
<v-btn id="btn-attachment-send" color="primary" variant="text" @click="sendAttachment(attachmentCaption)"
|
|
||||||
v-if="currentSendShowSendButton" :disabled="currentSendShowSendButton && sendingStatus != sendStatuses.INITIAL">{{ $t("menu.send") }}</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</template>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MessageOperationsBottomSheet ref="messageOperationsSheet">
|
<MessageOperationsBottomSheet ref="messageOperationsSheet">
|
||||||
<EmojiPicker ref="emojiPicker"
|
<EmojiPicker ref="emojiPicker"
|
||||||
|
|
@ -385,9 +319,8 @@ import BottomSheet from "./BottomSheet.vue";
|
||||||
import imageResize from "image-resize";
|
import imageResize from "image-resize";
|
||||||
import CreatePollDialog from "./CreatePollDialog.vue";
|
import CreatePollDialog from "./CreatePollDialog.vue";
|
||||||
import chatMixin, { ROOM_READ_MARKER_EVENT_PLACEHOLDER } from "./chatMixin";
|
import chatMixin, { ROOM_READ_MARKER_EVENT_PLACEHOLDER } from "./chatMixin";
|
||||||
import sendAttachmentsMixin from "./sendAttachmentsMixin";
|
|
||||||
import AudioLayout from "./AudioLayout.vue";
|
import AudioLayout from "./AudioLayout.vue";
|
||||||
import FileDropLayout from "./file_mode/FileDropLayout";
|
import SendAttachmentsLayout from "./file_mode/SendAttachmentsLayout.vue";
|
||||||
import roomTypeMixin from "./roomTypeMixin";
|
import roomTypeMixin from "./roomTypeMixin";
|
||||||
import roomMembersMixin from "./roomMembersMixin";
|
import roomMembersMixin from "./roomMembersMixin";
|
||||||
import PurgeRoomDialog from "../components/PurgeRoomDialog";
|
import PurgeRoomDialog from "../components/PurgeRoomDialog";
|
||||||
|
|
@ -401,6 +334,9 @@ import 'vue3-emoji-picker/css';
|
||||||
import emitter from 'tiny-emitter/instance';
|
import emitter from 'tiny-emitter/instance';
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
import timerIcon from '@/assets/icons/ic_timer.svg';
|
import timerIcon from '@/assets/icons/ic_timer.svg';
|
||||||
|
import proofmode from "../plugins/proofmode.js";
|
||||||
|
import C2PABadge from "./c2pa/C2PABadge.vue";
|
||||||
|
import { consoleWarn } from "vuetify/lib/util/console.mjs";
|
||||||
|
|
||||||
const READ_RECEIPT_TIMEOUT = 5000; /* How long a message must have been visible before the read marker is updated */
|
const READ_RECEIPT_TIMEOUT = 5000; /* How long a message must have been visible before the read marker is updated */
|
||||||
const WINDOW_BUFFER_SIZE = 0.3; /** Relative window height of when we start paginating. Always keep this much loaded before and after our scroll position! */
|
const WINDOW_BUFFER_SIZE = 0.3; /** Relative window height of when we start paginating. Always keep this much loaded before and after our scroll position! */
|
||||||
|
|
@ -432,7 +368,7 @@ ScrollPosition.prototype.prepareFor = function (direction) {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Chat",
|
name: "Chat",
|
||||||
mixins: [chatMixin, roomTypeMixin, sendAttachmentsMixin, roomMembersMixin],
|
mixins: [chatMixin, roomTypeMixin, roomMembersMixin],
|
||||||
components: {
|
components: {
|
||||||
ChatHeader,
|
ChatHeader,
|
||||||
ChatHeaderPrivate,
|
ChatHeaderPrivate,
|
||||||
|
|
@ -447,14 +383,15 @@ export default {
|
||||||
BottomSheet,
|
BottomSheet,
|
||||||
CreatePollDialog,
|
CreatePollDialog,
|
||||||
AudioLayout,
|
AudioLayout,
|
||||||
FileDropLayout,
|
SendAttachmentsLayout,
|
||||||
UserProfileDialog,
|
UserProfileDialog,
|
||||||
PurgeRoomDialog,
|
PurgeRoomDialog,
|
||||||
WelcomeHeaderChannelUser,
|
WelcomeHeaderChannelUser,
|
||||||
MessageErrorHandler,
|
MessageErrorHandler,
|
||||||
MessageOperationsChannel,
|
MessageOperationsChannel,
|
||||||
RoomExport,
|
RoomExport,
|
||||||
EmojiPicker
|
EmojiPicker,
|
||||||
|
C2PABadge
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -473,11 +410,7 @@ export default {
|
||||||
timelineWindowPaginating: false,
|
timelineWindowPaginating: false,
|
||||||
|
|
||||||
scrollPosition: null,
|
scrollPosition: null,
|
||||||
currentFileInputs: null,
|
uploadBatch: undefined,
|
||||||
currentSendShowSendButton: true,
|
|
||||||
currentSendError: null,
|
|
||||||
currentSendErrorExceededFile: null,
|
|
||||||
attachmentCaption: undefined,
|
|
||||||
showEmojiPicker: false,
|
showEmojiPicker: false,
|
||||||
selectedEvent: null,
|
selectedEvent: null,
|
||||||
editedEvent: null,
|
editedEvent: null,
|
||||||
|
|
@ -593,35 +526,12 @@ export default {
|
||||||
return (contentArr && contentArr.length > 0) ? contentArr[0].replace(/^> (<.*> )?/g, "") : "";
|
return (contentArr && contentArr.length > 0) ? contentArr[0].replace(/^> (<.*> )?/g, "") : "";
|
||||||
},
|
},
|
||||||
heartEmoji() {
|
heartEmoji() {
|
||||||
return this.$refs.emojiPicker.mapEmojis["Symbols"].find(({ aliases }) => aliases.includes('heart')).data;
|
return "❤️";
|
||||||
},
|
},
|
||||||
compActiveMember() {
|
compActiveMember() {
|
||||||
const currentUserId= this.selectedEvent?.sender.userId || this.$matrix.currentUserId
|
const currentUserId= this.selectedEvent?.sender.userId || this.$matrix.currentUserId
|
||||||
return this.joinedAndInvitedMembers.find(({userId}) => userId === currentUserId)
|
return this.joinedAndInvitedMembers.find(({userId}) => userId === currentUserId)
|
||||||
},
|
},
|
||||||
nonImageFiles() {
|
|
||||||
return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file?.type.includes("image/"))
|
|
||||||
},
|
|
||||||
imageFiles() {
|
|
||||||
return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => file?.type.includes("image/"))
|
|
||||||
},
|
|
||||||
isCurrentFileInputsAnArray() {
|
|
||||||
return Array.isArray(this.currentFileInputs)
|
|
||||||
},
|
|
||||||
showAttachmentCaptionInput() {
|
|
||||||
// IFF we are sending one PDF, add option to set caption.
|
|
||||||
const imageFiles = this.imageFiles || [];
|
|
||||||
const otherFiles = this.nonImageFiles || [];
|
|
||||||
return imageFiles.length == 0 && otherFiles.length == 1 && (otherFiles[0].type === "application/pdf" || (otherFiles[0].name || "").endsWith(".pdf"));
|
|
||||||
},
|
|
||||||
currentFileInputsDialog: {
|
|
||||||
get() {
|
|
||||||
return this.isCurrentFileInputsAnArray
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
this.currentFileInputs = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
chatContainer() {
|
chatContainer() {
|
||||||
const container = this.$refs.chatContainer;
|
const container = this.$refs.chatContainer;
|
||||||
if (this.useVoiceMode) {
|
if (this.useVoiceMode) {
|
||||||
|
|
@ -1331,7 +1241,7 @@ export default {
|
||||||
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
|
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
|
||||||
const element = document.querySelector(sel);
|
const element = document.querySelector(sel);
|
||||||
if (element) {
|
if (element) {
|
||||||
this.onLayoutChange(fn, element);
|
this.onLayoutChange({action: fn, element: element});
|
||||||
} else {
|
} else {
|
||||||
fn();
|
fn();
|
||||||
}
|
}
|
||||||
|
|
@ -1361,7 +1271,7 @@ export default {
|
||||||
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
|
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
|
||||||
const element = document.querySelector(sel);
|
const element = document.querySelector(sel);
|
||||||
if (element) {
|
if (element) {
|
||||||
this.onLayoutChange(fn, element);
|
this.onLayoutChange({action: fn, element: element});
|
||||||
} else {
|
} else {
|
||||||
fn();
|
fn();
|
||||||
}
|
}
|
||||||
|
|
@ -1470,146 +1380,38 @@ export default {
|
||||||
/**
|
/**
|
||||||
* Show attachment picker to select file
|
* Show attachment picker to select file
|
||||||
*/
|
*/
|
||||||
showAttachmentPicker() {
|
showAttachmentPicker(reset) {
|
||||||
|
if (reset) {
|
||||||
|
this.uploadBatch?.cancel();
|
||||||
|
this.uploadBatch = null;
|
||||||
|
}
|
||||||
this.$refs.attachment.click();
|
this.$refs.attachment.click();
|
||||||
},
|
},
|
||||||
|
|
||||||
optimizeImage(evt,file) {
|
addAttachment(file) {
|
||||||
let fileObj = {}
|
|
||||||
fileObj.image = evt.target.result;
|
|
||||||
fileObj.dimensions = null;
|
|
||||||
fileObj.type = file.type;
|
|
||||||
fileObj.actualSize = file.size;
|
|
||||||
fileObj.actualFile = file
|
|
||||||
try {
|
|
||||||
const buffer = Uint8Array.from(window.atob(evt.target.result.replace(/^data[^,]+,/,'')), v => v.charCodeAt(0));
|
|
||||||
fileObj.dimensions = imageSize(buffer);
|
|
||||||
|
|
||||||
// Need to resize?
|
|
||||||
const w = fileObj.dimensions.width;
|
|
||||||
const h = fileObj.dimensions.height;
|
|
||||||
if (w > 640 || h > 640) {
|
|
||||||
var aspect = w / h;
|
|
||||||
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
|
|
||||||
var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
|
|
||||||
imageResize(evt.target.result, {
|
|
||||||
format: "png",
|
|
||||||
width: newWidth,
|
|
||||||
height: newHeight,
|
|
||||||
outputType: "blob",
|
|
||||||
})
|
|
||||||
.then((img) => {
|
|
||||||
fileObj["scaled"] =
|
|
||||||
new File([img], file.name, {
|
|
||||||
type: img.type,
|
|
||||||
lastModified: Date.now(),
|
|
||||||
});
|
|
||||||
fileObj["useScaled"] = true;
|
|
||||||
fileObj["scaledSize"] = img.size;
|
|
||||||
fileObj["scaledDimensions"] = {
|
|
||||||
width: newWidth,
|
|
||||||
height: newHeight,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("Resize failed:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to get image dimensions: " + error);
|
|
||||||
}
|
|
||||||
return fileObj
|
|
||||||
},
|
|
||||||
handleFileReader(file) {
|
|
||||||
if (file) {
|
if (file) {
|
||||||
let optimizedFileObj;
|
if (!this.uploadBatch) {
|
||||||
var reader = new FileReader();
|
this.uploadBatch = this.$matrix.attachmentManager.createUpload(this.room);
|
||||||
reader.onload = (evt) => {
|
}
|
||||||
if (file.type.startsWith("image/")) {
|
this.uploadBatch?.addAttachment(this.$matrix.attachmentManager.createAttachment(file));
|
||||||
optimizedFileObj = this.optimizeImage(evt, file)
|
|
||||||
} else {
|
|
||||||
optimizedFileObj = file
|
|
||||||
}
|
|
||||||
this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, optimizedFileObj] : [optimizedFileObj];
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle picked attachment
|
* Handle picked attachment
|
||||||
*/
|
*/
|
||||||
handlePickedAttachment(event) {
|
handlePickedAttachment(event) {
|
||||||
this.currentFileInputs = []
|
this.addAttachments(Object.values(event.target.files));
|
||||||
const uploadedFiles = Object.values(event.target.files);
|
},
|
||||||
|
|
||||||
this.$matrix.matrixClient.getMediaConfig().then((config) => {
|
addAttachments(files) {
|
||||||
const configUploadSize = config["m.upload.size"];
|
files.forEach(file => this.addAttachment(file));
|
||||||
const configFormattedUploadSize = this.formatBytes(configUploadSize);
|
|
||||||
|
|
||||||
uploadedFiles.every(file => {
|
|
||||||
if (configUploadSize && file.size > configUploadSize) {
|
|
||||||
this.currentSendError = this.$t("message.upload_file_too_large");
|
|
||||||
this.currentSendErrorExceededFile = this.$t("message.upload_exceeded_file_limit", { configFormattedUploadSize });
|
|
||||||
this.currentSendShowSendButton = false;
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
this.currentSendShowSendButton = true;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadedFiles.forEach(file => this.handleFileReader(file));
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showStickerPicker() {
|
showStickerPicker() {
|
||||||
this.$refs.stickerPickerSheet.open();
|
this.$refs.stickerPickerSheet.open();
|
||||||
},
|
},
|
||||||
|
|
||||||
sendAttachment(withText) {
|
|
||||||
this.$refs.attachment.value = null;
|
|
||||||
if (this.isCurrentFileInputsAnArray) {
|
|
||||||
const text = withText || "";
|
|
||||||
const promise = this.sendAttachments(text, this.currentFileInputs);
|
|
||||||
promise.then(() => {
|
|
||||||
this.sendingAttachments = [];
|
|
||||||
this.currentFileInputs = null;
|
|
||||||
this.attachmentCaption = undefined;
|
|
||||||
this.sendingStatus = this.sendStatuses.INITIAL;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err.name === "AbortError" || err === "Abort") {
|
|
||||||
this.currentSendError = null;
|
|
||||||
this.currentSendErrorExceededFile = null;
|
|
||||||
} else {
|
|
||||||
this.currentSendError = err.LocaleString();
|
|
||||||
this.currentSendErrorExceededFile = err.LocaleString();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelSendAttachment() {
|
|
||||||
this.$refs.attachment.value = null;
|
|
||||||
if (this.sendingStatus != this.sendStatuses.INITIAL) {
|
|
||||||
this.cancelSendAttachments();
|
|
||||||
}
|
|
||||||
this.currentFileInputs = null;
|
|
||||||
this.attachmentCaption = undefined;
|
|
||||||
this.currentSendError = null;
|
|
||||||
this.currentSendErrorExceededFile = null;
|
|
||||||
this.sendingStatus = this.sendStatuses.INITIAL;
|
|
||||||
},
|
|
||||||
|
|
||||||
addAttachment(file) {
|
|
||||||
this.handleFileReader(null, file);
|
|
||||||
},
|
|
||||||
|
|
||||||
resetAttachments() {
|
|
||||||
this.cancelSendAttachment();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by message components that need to change their layout. This will avoid "jumping" in the UI, because
|
* Called by message components that need to change their layout. This will avoid "jumping" in the UI, because
|
||||||
* we remember scroll position, apply the layout change, then restore the scroll.
|
* we remember scroll position, apply the layout change, then restore the scroll.
|
||||||
|
|
@ -1618,7 +1420,8 @@ export default {
|
||||||
* @param {} action A function that performs desired layout changes.
|
* @param {} action A function that performs desired layout changes.
|
||||||
* @param {*} element Root element for the chat message.
|
* @param {*} element Root element for the chat message.
|
||||||
*/
|
*/
|
||||||
onLayoutChange(action, element) {
|
onLayoutChange(event) {
|
||||||
|
const { action, element } = event;
|
||||||
if (!element || !element.parentElement || this.useVoiceMode || this.useFileModeNonAdmin) {
|
if (!element || !element.parentElement || this.useVoiceMode || this.useFileModeNonAdmin) {
|
||||||
action();
|
action();
|
||||||
return
|
return
|
||||||
|
|
@ -2043,18 +1846,22 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
onVoiceRecording(event) {
|
onVoiceRecording(event) {
|
||||||
this.currentSendShowSendButton = false;
|
const batch = this.$matrix.attachmentManager.createUpload(this.room);
|
||||||
this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, event.file] : [event.file];
|
batch.addAttachment(this.$matrix.attachmentManager.createAttachment(event.file));
|
||||||
var text = undefined;
|
var text = undefined;
|
||||||
if (this.currentInput && this.currentInput.length > 0) {
|
if (this.currentInput && this.currentInput.length > 0) {
|
||||||
text = this.currentInput;
|
text = this.currentInput;
|
||||||
this.currentInput = "";
|
this.currentInput = "";
|
||||||
}
|
}
|
||||||
this.sendAttachment(text);
|
batch.send(text)
|
||||||
this.showRecorder = false;
|
.then(() => {
|
||||||
|
this.showRecorder = false;
|
||||||
// Log event
|
// Log event
|
||||||
this.$analytics.event("Audio", "Voice message sent");
|
this.$analytics.event("Audio", "Voice message sent");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to send voice message", err);
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
closeRoomWelcomeHeader() {
|
closeRoomWelcomeHeader() {
|
||||||
|
|
@ -2130,7 +1937,8 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
closeFileMode() {
|
closeFileMode() {
|
||||||
this.resetAttachments();
|
this.uploadBatch?.cancel();
|
||||||
|
this.uploadBatch = undefined;
|
||||||
this.$matrix.leaveRoomAndNavigate(this.room.roomId)
|
this.$matrix.leaveRoomAndNavigate(this.room.roomId)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log("Error leaving", err);
|
console.log("Error leaving", err);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
<div v-if="creatingRoom" class="text-center">
|
<div v-if="creatingRoom" class="text-center">
|
||||||
{{ creatingRoomStatus }}
|
{{ creatingRoomStatus }}
|
||||||
<v-progress-circular v-if="creatingRoom" :indeterminate="creatingRoomProgress == null"
|
<v-progress-circular v-if="creatingRoom" :indeterminate="creatingRoomProgress == null"
|
||||||
:value="creatingRoomProgress" color="primary" width="2" size="20"></v-progress-circular>
|
:model-value="creatingRoomProgress" color="primary" width="2" size="20"></v-progress-circular>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ $t("getlink.next") }}</span>
|
<span v-else>{{ $t("getlink.next") }}</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
<div v-if="creatingRoom" class="text-center">
|
<div v-if="creatingRoom" class="text-center">
|
||||||
{{ creatingRoomStatus }}
|
{{ creatingRoomStatus }}
|
||||||
<v-progress-circular v-if="creatingRoom" :indeterminate="creatingRoomProgress == null"
|
<v-progress-circular v-if="creatingRoom" :indeterminate="creatingRoomProgress == null"
|
||||||
:value="creatingRoomProgress" color="primary" width="2" size="20"></v-progress-circular>
|
:model-value="creatingRoomProgress" color="primary" width="2" size="20"></v-progress-circular>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ $t("getlink.next") }}</span>
|
<span v-else>{{ $t("getlink.next") }}</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
<div v-if="creatingRoom && !enterRoomDialog" class="text-center">
|
<div v-if="creatingRoom && !enterRoomDialog" class="text-center">
|
||||||
{{ creatingRoomStatus }}
|
{{ creatingRoomStatus }}
|
||||||
<v-progress-circular v-if="creatingRoom" :indeterminate="creatingRoomProgress == null"
|
<v-progress-circular v-if="creatingRoom" :indeterminate="creatingRoomProgress == null"
|
||||||
:value="creatingRoomProgress" color="primary" width="2" size="20"></v-progress-circular>
|
:model-value="creatingRoomProgress" color="primary" width="2" size="20"></v-progress-circular>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ $t("getlink.next") }}</span>
|
<span v-else>{{ $t("getlink.next") }}</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
|
||||||
46
src/components/ImageWithProgress.vue
Normal file
46
src/components/ImageWithProgress.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<v-img class="image-with-progress" v-bind="{...$props, ...$attrs}">
|
||||||
|
<LoadProgress class="image-with-progress__progress" v-if="loadingProgress != undefined && loadingProgress >= 0 && loadingProgress < 100" :percentage="loadingProgress" />
|
||||||
|
</v-img>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import User from "../models/user";
|
||||||
|
import util from "../plugins/utils";
|
||||||
|
import rememberMeMixin from "./rememberMeMixin";
|
||||||
|
import * as sdk from "matrix-js-sdk";
|
||||||
|
import logoMixin from "./logoMixin";
|
||||||
|
import LoadProgress from "./LoadProgress.vue";
|
||||||
|
import { VImg } from "vuetify/components/VImg";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ImageWithProgress",
|
||||||
|
extends: VImg,
|
||||||
|
components: { LoadProgress },
|
||||||
|
props: {
|
||||||
|
loadingProgress: {
|
||||||
|
type: Number,
|
||||||
|
default: function () {
|
||||||
|
return -1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-with-progress {
|
||||||
|
position: relative;
|
||||||
|
.image-with-progress__progress {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
color: white;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
28
src/components/LoadProgress.vue
Normal file
28
src/components/LoadProgress.vue
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<v-progress-circular :rotate="360" :width="3" :model-value="percentage" color="white" class="ma-2">
|
||||||
|
{{ percentage }}
|
||||||
|
</v-progress-circular>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import User from "../models/user";
|
||||||
|
import util from "../plugins/utils";
|
||||||
|
import rememberMeMixin from "./rememberMeMixin";
|
||||||
|
import * as sdk from "matrix-js-sdk";
|
||||||
|
import logoMixin from "./logoMixin";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "LoadProgress",
|
||||||
|
props: {
|
||||||
|
percentage: {
|
||||||
|
type: Number,
|
||||||
|
default: function () {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
:rotate="360"
|
:rotate="360"
|
||||||
v-else
|
v-else
|
||||||
:width="3"
|
:width="3"
|
||||||
:value="loadValue"
|
:model-value="loadValue"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
{{ loadValue }}
|
{{ loadValue }}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
:rotate="360"
|
:rotate="360"
|
||||||
v-else
|
v-else
|
||||||
:width="3"
|
:width="3"
|
||||||
:value="loadValue"
|
:model-value="loadValue"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
{{ loadValue }}
|
{{ loadValue }}
|
||||||
|
|
|
||||||
34
src/components/c2pa/C2PABadge.vue
Normal file
34
src/components/c2pa/C2PABadge.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="hasC2PA" class="c2pa-badge">
|
||||||
|
<v-icon>$vuetify.icons.ic_cr</v-icon>
|
||||||
|
<span>This image contains C2PA data</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasExif" class="c2pa-badge">
|
||||||
|
<v-icon>camera_marker</v-icon>
|
||||||
|
<span>This image contains EXIF data</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
proof?: {
|
||||||
|
name?: string;
|
||||||
|
json?: string;
|
||||||
|
integrity?: { pgp?: any; c2pa?: any; exif?: any; opentimestamps?: any };
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const hasC2PA = computed(() => {
|
||||||
|
return props.proof?.integrity?.c2pa !== undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasExif = computed(() => {
|
||||||
|
return props.proof?.integrity?.exif !== undefined;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@/assets/css/chat.scss" as *;
|
||||||
|
</style>
|
||||||
|
|
@ -7,7 +7,7 @@ import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue";
|
||||||
import MessageIncomingVideo from "./messages/MessageIncomingVideo.vue";
|
import MessageIncomingVideo from "./messages/MessageIncomingVideo.vue";
|
||||||
import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue";
|
import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue";
|
||||||
import MessageIncomingPoll from "./messages/MessageIncomingPoll.vue";
|
import MessageIncomingPoll from "./messages/MessageIncomingPoll.vue";
|
||||||
import MessageIncomingThread from "./messages/MessageIncomingThread.vue";
|
import MessageIncomingThread from "./messages/composition/MessageIncomingThread.vue";
|
||||||
import MessageOutgoingText from "./messages/MessageOutgoingText";
|
import MessageOutgoingText from "./messages/MessageOutgoingText";
|
||||||
import MessageOutgoingFile from "./messages/MessageOutgoingFile";
|
import MessageOutgoingFile from "./messages/MessageOutgoingFile";
|
||||||
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
|
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
|
||||||
|
|
@ -15,7 +15,7 @@ import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue";
|
||||||
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
|
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
|
||||||
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
|
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
|
||||||
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
|
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
|
||||||
import MessageOutgoingThread from "./messages/MessageOutgoingThread.vue";
|
import MessageOutgoingThread from "./messages/composition/MessageOutgoingThread.vue";
|
||||||
import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport";
|
import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport";
|
||||||
import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport";
|
import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport";
|
||||||
import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport";
|
import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport";
|
||||||
|
|
@ -61,7 +61,7 @@ import roomTypeMixin from "./roomTypeMixin";
|
||||||
export const ROOM_READ_MARKER_EVENT_PLACEHOLDER = { getId: () => "ROOM_READ_MARKER", getTs: () => Date.now() };
|
export const ROOM_READ_MARKER_EVENT_PLACEHOLDER = { getId: () => "ROOM_READ_MARKER", getTs: () => Date.now() };
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [ roomDisplayOptionsMixin, roomTypeMixin ],
|
mixins: [roomDisplayOptionsMixin, roomTypeMixin],
|
||||||
components: {
|
components: {
|
||||||
ChatHeader,
|
ChatHeader,
|
||||||
MessageIncomingText,
|
MessageIncomingText,
|
||||||
|
|
@ -105,7 +105,7 @@ export default {
|
||||||
StickerPickerBottomSheet,
|
StickerPickerBottomSheet,
|
||||||
BottomSheet,
|
BottomSheet,
|
||||||
CreatePollDialog,
|
CreatePollDialog,
|
||||||
ReadMarker
|
ReadMarker,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
debugging() {
|
debugging() {
|
||||||
|
|
@ -132,7 +132,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
dateForEvent(event) {
|
dateForEvent(event) {
|
||||||
return util.formatDay(event.getTs());
|
return util.formatDay(event.getTs());
|
||||||
},
|
},
|
||||||
|
|
||||||
componentForEvent(event, isForExport = false) {
|
componentForEvent(event, isForExport = false) {
|
||||||
|
|
@ -180,18 +180,18 @@ export default {
|
||||||
|
|
||||||
case "m.room.message":
|
case "m.room.message":
|
||||||
if (event.getSender() != this.$matrix.currentUserId) {
|
if (event.getSender() != this.$matrix.currentUserId) {
|
||||||
if (event.isRedacted()) {
|
if (event.isRedacted()) {
|
||||||
// Redacted thread, show as text (and hide all media)!
|
// Redacted thread, show as text (and hide all media)!
|
||||||
if (event.getUnsigned().redacted_because.content.reason == "redactedThread") {
|
if (event.getUnsigned().redacted_because.content.reason == "redactedThread") {
|
||||||
return MessageIncomingText;
|
return MessageIncomingText;
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
if (event.isMxThread) {
|
return null;
|
||||||
// Incoming thread, e.g. a file drop!
|
}
|
||||||
return isForExport ? MessageIncomingThreadExport : MessageIncomingThread;
|
if (event.isMxThread) {
|
||||||
}
|
// Incoming thread, e.g. a file drop!
|
||||||
if (event.getContent().msgtype == "m.image") {
|
return isForExport ? MessageIncomingThreadExport : MessageIncomingThread;
|
||||||
|
}
|
||||||
|
if (event.getContent().msgtype == "m.image") {
|
||||||
// For SVG, make downloadable
|
// For SVG, make downloadable
|
||||||
if (
|
if (
|
||||||
event.getContent().info &&
|
event.getContent().info &&
|
||||||
|
|
@ -340,19 +340,27 @@ export default {
|
||||||
return MessageOutgoingPoll;
|
return MessageOutgoingPoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
case STATE_EVENT_ROOM_DELETION_NOTICE: {
|
case STATE_EVENT_ROOM_DELETION_NOTICE:
|
||||||
// Custom event for notice 30 seconds before a room is deleted/purged.
|
{
|
||||||
const deletionNotices = this.room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETION_NOTICE);
|
// Custom event for notice 30 seconds before a room is deleted/purged.
|
||||||
if (deletionNotices && deletionNotices.length > 0 && deletionNotices[deletionNotices.length - 1] == event) {
|
const deletionNotices = this.room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETION_NOTICE);
|
||||||
// This is the latest/last one. Look at the status flag. Show nothing if it is "cancel".
|
if (deletionNotices && deletionNotices.length > 0 && deletionNotices[deletionNotices.length - 1] == event) {
|
||||||
if (event.getContent().status != "cancel") {
|
// This is the latest/last one. Look at the status flag. Show nothing if it is "cancel".
|
||||||
return RoomDeletionNotice;
|
if (event.getContent().status != "cancel") {
|
||||||
|
return RoomDeletionNotice;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "m.room.encrypted":
|
case "m.room.encrypted":
|
||||||
return event.getSender() != this.$matrix.currentUserId ? MessageIncomingText : MessageOutgoingText
|
if (event.isRedacted()) {
|
||||||
|
// Redacted thread, show as text (and hide all media)!
|
||||||
|
if (event.getUnsigned().redacted_because.content.reason == "redactedThread") {
|
||||||
|
return MessageOutgoingText;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return event.getSender() != this.$matrix.currentUserId ? MessageIncomingText : MessageOutgoingText;
|
||||||
}
|
}
|
||||||
return this.debugging ? DebugEvent : null;
|
return this.debugging ? DebugEvent : null;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
50
src/components/file_mode/AttachmentInfo.vue
Normal file
50
src/components/file_mode/AttachmentInfo.vue
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<div class="attachment-info">
|
||||||
|
<div class="attachment-info__size">
|
||||||
|
<span v-if="attachment.scaledFile && attachment.useScaled && attachment.scaledDimensions">
|
||||||
|
{{ attachment.scaledDimensions.width }} x {{ attachment.scaledDimensions.height }}</span
|
||||||
|
>
|
||||||
|
<span v-else-if="attachment.dimensions">
|
||||||
|
{{ attachment.dimensions.width }} x {{ attachment.dimensions.height }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="attachment.scaledFile && attachment.useScaled">
|
||||||
|
({{ formatBytes(attachment.scaledFile.size) }})
|
||||||
|
</span>
|
||||||
|
<span v-else> ({{ formatBytes(attachment.file.size) }}) </span>
|
||||||
|
|
||||||
|
<span class="attachment-info__size__filename" v-if="attachment.src && attachment.file.name">
|
||||||
|
- {{ attachment.file.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-switch
|
||||||
|
v-if="attachment.scaledFile"
|
||||||
|
:label="$t('message.scale_image')"
|
||||||
|
v-model="attachment.useScaled"
|
||||||
|
:disabled="attachment.sendInfo !== undefined"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<C2PABadge :proof="attachment.proof" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Attachment } from "../../models/attachment";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
||||||
|
import C2PABadge from '../c2pa/C2PABadge.vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
attachment: Attachment;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number) => {
|
||||||
|
return prettyBytes(bytes);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@/assets/css/chat.scss" as *;
|
||||||
|
@use "@/assets/css/sendattachments.scss" as *;
|
||||||
|
</style>
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-bind="{ ...$props, ...$attrs }">
|
|
||||||
<!-- No attachments view -->
|
|
||||||
<template v-if="!attachments || attachments.length == 0">
|
|
||||||
<div>
|
|
||||||
<v-icon>$vuetify.icons.ic_lock</v-icon>
|
|
||||||
<div class="file-drop-title">{{ $t("file_mode.secure_file_send") }}</div>
|
|
||||||
</div>
|
|
||||||
<div :class="{ 'background': true, 'drop-target': dropTarget }" @drop.prevent="filesDropped"
|
|
||||||
@dragover.prevent="dropTarget = true" @dragleave.prevent="dropTarget = false"
|
|
||||||
@dragenter.prevent="dropTarget = true">
|
|
||||||
<v-btn @click="$emit('pick-file')" class="large">{{ $t("file_mode.choose_files") }}</v-btn>
|
|
||||||
<div class="file-format-info">{{ $t("file_mode.any_file_format_accepted") }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- ATTACHMENT SELECTION MODE -->
|
|
||||||
<template v-if="attachments && attachments.length > 0 && status == mainStatuses.SELECTING">
|
|
||||||
<div class="attachment-wrapper" ref="attachmentWrapper">
|
|
||||||
<div :class="{ 'file-drop-current-item': true, 'drop-target': dropTarget }" @drop.prevent="filesDropped"
|
|
||||||
@dragover.prevent="dropTarget = true" @dragleave.prevent="dropTarget = false"
|
|
||||||
@dragenter.prevent="dropTarget = true">
|
|
||||||
<v-img v-if="currentItemHasImagePreview" :src="attachments[currentItemIndex].image" />
|
|
||||||
<div v-else class="filename">{{ attachments[currentItemIndex].name }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="file-drop-thumbnail-container">
|
|
||||||
<div :class="{ 'file-drop-thumbnail': true, 'clickable': true, 'current': id == currentItemIndex }"
|
|
||||||
@click="currentItemIndex = id" v-for="(currentImageInput, id) in attachments" :key="id">
|
|
||||||
<v-img v-if="currentImageInput && currentImageInput.image" :src="currentImageInput.image" />
|
|
||||||
<div v-if="currentItemIndex == id" class="remove clickable" @click.stop="$emit('remove-file', id)">
|
|
||||||
<v-icon>$vuetify.icons.ic_trash</v-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="file-drop-thumbnail noborder">
|
|
||||||
<div class="add clickable" @click.stop="$emit('pick-file')">
|
|
||||||
+
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="file-drop-input-container">
|
|
||||||
<v-textarea ref="input" full-width variant="solo" flat auto-grow v-model="messageInput" no-resize class="input-area-text"
|
|
||||||
rows="1" :placeholder="$t('file_mode.add_a_message')" hide-details background-color="transparent"
|
|
||||||
v-on:keydown.enter.prevent="() => {
|
|
||||||
sendCurrentTextMessage();
|
|
||||||
}
|
|
||||||
" />
|
|
||||||
<v-btn @click="sendAll" :disabled="!attachments || attachments.length == 0">{{ $t("menu.send") }}</v-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- ATTACHMENT SENDING/SENT MODE -->
|
|
||||||
<template
|
|
||||||
v-if="attachments && attachments.length > 0 && (status == mainStatuses.SENDING || status == mainStatuses.SENT)">
|
|
||||||
<div class="attachment-wrapper">
|
|
||||||
<div class="file-drop-sent-stack" ref="stackContainer">
|
|
||||||
<div v-if="status == mainStatuses.SENDING && attachmentsSentCount == 0" class="no-items">
|
|
||||||
<div class="file-drop-stack-item direct" :style="stackItemTransform(null, -1)"></div>
|
|
||||||
<div>{{ $t('file_mode.sending_progress') }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-else v-for="(info, index) in attachmentsSent" :key="info.id" class="file-drop-stack-item animated"
|
|
||||||
:style="stackItemTransform(info, index)">
|
|
||||||
<v-img v-if="info.preview" :src="info.preview" />
|
|
||||||
</div>
|
|
||||||
<div v-if="status == mainStatuses.SENT" class="items-sent" :style="stackItemTransform(null, -1)">
|
|
||||||
<v-icon>$vuetify.icons.ic_check_circle</v-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Middle section -->
|
|
||||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-container">
|
|
||||||
<div class="file-drop-sending-item" v-for="(info, index) in attachmentsSending" :key="index">
|
|
||||||
<v-img v-if="info.preview" :src="info.preview" />
|
|
||||||
<div v-else class="filename">{{ info.attachment.name }}</div>
|
|
||||||
<v-progress-linear :value="info.progress"></v-progress-linear>
|
|
||||||
<div class="file-drop-cancel clickable" @click.stop="cancelSendAttachmentItem(info)">
|
|
||||||
<v-icon size="14" color="white">close</v-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sending-container">
|
|
||||||
<div class="file-drop-files-sent">{{ $t((this.messageInput && this.messageInput.length > 0) ?
|
|
||||||
"file_mode.files_sent_with_note" : "file_mode.files_sent", attachmentsSent.length) }}</div>
|
|
||||||
<div class="file-drop-section">
|
|
||||||
<v-textarea disabled full-width variant="solo" flat auto-grow v-model="messageInput" no-resize class="input-area-text"
|
|
||||||
rows="1" hide-details background-color="transparent" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bottom section -->
|
|
||||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-input-container">
|
|
||||||
<v-textarea disabled full-width variant="solo" flat auto-grow v-model="messageInput" no-resize class="input-area-text"
|
|
||||||
rows="1" :placeholder="$t('file_mode.add_a_message')" hide-details background-color="transparent" />
|
|
||||||
<v-btn>{{ $t("file_mode.sending") }}<v-progress-circular indeterminate size="18" width="2"
|
|
||||||
color="#4642F1"></v-progress-circular></v-btn>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sent-input-container">
|
|
||||||
<v-btn @click.stop="reset">{{ $t("file_mode.send_more_files") }}</v-btn>
|
|
||||||
<v-btn class="close" @click.stop="close">{{ $t("file_mode.close") }}</v-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import messageMixin from "../messages/messageMixin";
|
|
||||||
import sendAttachmentsMixin from "../sendAttachmentsMixin";
|
|
||||||
import prettyBytes from "pretty-bytes";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mixins: [messageMixin, sendAttachmentsMixin],
|
|
||||||
components: {},
|
|
||||||
props: {
|
|
||||||
attachments: {
|
|
||||||
type: Array,
|
|
||||||
default: function () {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currentItemIndex: 0,
|
|
||||||
messageInput: "",
|
|
||||||
mainStatuses: Object.freeze({
|
|
||||||
SELECTING: 0,
|
|
||||||
SENDING: 1,
|
|
||||||
SENT: 2,
|
|
||||||
}),
|
|
||||||
status: 0,
|
|
||||||
dropTarget: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
document.body.classList.add("dark");
|
|
||||||
this.$audioPlayer.setAutoplay(false);
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
document.body.classList.remove("dark");
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
currentItemHasImagePreview() {
|
|
||||||
return this.currentItemIndex >= 0 && this.currentItemIndex < this.attachments.length &&
|
|
||||||
this.attachments[this.currentItemIndex].image
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
attachments(newValue, oldValue) {
|
|
||||||
// Added or removed?
|
|
||||||
if (newValue && oldValue && newValue.length > oldValue.length) {
|
|
||||||
this.currentItemIndex = oldValue.length;
|
|
||||||
} else if (newValue) {
|
|
||||||
this.currentItemIndex = newValue.length - 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
messageInput() {
|
|
||||||
this.scrollToBottom();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
filesDropped(e) {
|
|
||||||
this.dropTarget = false;
|
|
||||||
let droppedFiles = e.dataTransfer.files;
|
|
||||||
if (!droppedFiles) return;
|
|
||||||
([...droppedFiles]).forEach(f => {
|
|
||||||
this.$emit('add-file', f);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
scrollToBottom() {
|
|
||||||
const el = this.$refs.attachmentWrapper;
|
|
||||||
if (el) {
|
|
||||||
// Ugly - need to wait until input is auto-sized, THEN scroll to bottom.
|
|
||||||
//
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
el.scrollTop = el.scrollHeight
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formatBytes(bytes) {
|
|
||||||
return prettyBytes(bytes);
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
this.$emit('reset');
|
|
||||||
this.sendingAttachments = [];
|
|
||||||
this.status = this.mainStatuses.SELECTING;
|
|
||||||
this.messageInput = "";
|
|
||||||
this.currentItemIndex = 0;
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
sendAll() {
|
|
||||||
this.status = this.mainStatuses.SENDING;
|
|
||||||
this.sendAttachments((this.messageInput && this.messageInput.length > 0) ? this.messageInput : this.$t('file_mode.files'), this.attachments)
|
|
||||||
.then(() => {
|
|
||||||
this.status = this.mainStatuses.SENT;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
stackItemTransform(item, index) {
|
|
||||||
const size = 0.6 * (this.$refs.stackContainer ? Math.min(this.$refs.stackContainer.clientWidth, this.$refs.stackContainer.clientHeight) : 176);
|
|
||||||
let transform = ""
|
|
||||||
if (item != null && index != -1) {
|
|
||||||
transform = "transform: rotate(" + item.randomRotation + "deg) translate(" + item.randomTranslationX + "px," + item.randomTranslationY + "px); z-index:" + (index + 2) + ";";
|
|
||||||
}
|
|
||||||
return transform + "width:" + size + "px;height:" + size + "px;border-radius:" + (size / 8) + "px";
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use "@/assets/css/chat.scss" as *;
|
|
||||||
</style>
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<div class="file-drop-thumbnail-container">
|
<div class="file-drop-thumbnail-container">
|
||||||
<div :class="{ 'file-drop-thumbnail': true, 'clickable': true, 'current': id == currentItemIndex }"
|
<div :class="{ 'file-drop-thumbnail': true, 'clickable': true, 'current': id == currentItemIndex }"
|
||||||
@click="currentItemIndex = id" v-for="(currentImageInput, id) in items" :key="id">
|
@click="currentItemIndex = id" v-for="(currentImageInput, id) in items" :key="id">
|
||||||
<v-img v-if="currentImageInput && currentImageInput.src" :src="currentImageInput.src" />
|
<v-img v-if="currentImageInput" :src="currentImageInput.thumbnail ? currentImageInput.thumbnail : currentImageInput.src" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
331
src/components/file_mode/SendAttachmentsLayout.vue
Normal file
331
src/components/file_mode/SendAttachmentsLayout.vue
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
<template>
|
||||||
|
<div v-bind="{ ...$attrs }" class="send-attachments">
|
||||||
|
<v-btn
|
||||||
|
v-if="showBackButton"
|
||||||
|
class="back-button clickable"
|
||||||
|
icon="arrow_back"
|
||||||
|
size="default"
|
||||||
|
elevation="0"
|
||||||
|
color="black"
|
||||||
|
@click.stop="close"
|
||||||
|
:disabled="backButtonDisabled"
|
||||||
|
></v-btn>
|
||||||
|
|
||||||
|
<!-- ATTACHMENT SELECTION MODE -->
|
||||||
|
<template v-if="status == mainStatuses.SELECTING">
|
||||||
|
<div
|
||||||
|
:class="{ 'send-attachments__selecting__current-item': true, 'drop-target': dropTarget }"
|
||||||
|
@drop.prevent="filesDropped"
|
||||||
|
@dragover.prevent="dropTarget = true"
|
||||||
|
@dragleave.prevent="dropTarget = false"
|
||||||
|
@dragenter.prevent="dropTarget = true"
|
||||||
|
>
|
||||||
|
<v-img
|
||||||
|
v-if="currentAttachment && currentAttachment.src && currentAttachment.status === 'loaded'"
|
||||||
|
:src="currentAttachment.src"
|
||||||
|
/>
|
||||||
|
<div v-else-if="currentAttachment" class="filename">
|
||||||
|
<div>{{ currentAttachment.file.name }}</div>
|
||||||
|
<div v-if="currentAttachment.status === 'loading'" style="font-size: 0.7em; opacity: 0.7">
|
||||||
|
{{ $t("message.preparing_to_upload") }}
|
||||||
|
<v-progress-linear indeterminate class="mb-0"></v-progress-linear>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
class="info-button clickable"
|
||||||
|
icon="information"
|
||||||
|
size="default"
|
||||||
|
elevation="0"
|
||||||
|
color="black"
|
||||||
|
@click.stop="showInformation"
|
||||||
|
></v-btn>
|
||||||
|
</div>
|
||||||
|
<div class="file-drop-thumbnail-container">
|
||||||
|
<v-tooltip location="top" v-for="(attachment, index) in batch.attachments" :key="index">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-badge :model-value="batch.isTooLarge(attachment)" color="error">
|
||||||
|
<template v-slot:badge><span v-bind="props"> </span></template>
|
||||||
|
<div
|
||||||
|
:class="{ 'file-drop-thumbnail': true, clickable: true, current: index == currentItemIndex }"
|
||||||
|
@click="currentItemIndex = index"
|
||||||
|
>
|
||||||
|
<v-img v-if="attachment && attachment.src" :src="attachment.src" />
|
||||||
|
<div
|
||||||
|
v-if="currentItemIndex == index"
|
||||||
|
class="remove clickable"
|
||||||
|
@click.stop="batch.removeAttachment(attachment)"
|
||||||
|
>
|
||||||
|
<v-icon>$vuetify.icons.ic_trash</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-badge>
|
||||||
|
</template>
|
||||||
|
<span>{{ $t("message.upload_file_too_large") }}</span>
|
||||||
|
</v-tooltip>
|
||||||
|
<div class="file-drop-thumbnail noborder">
|
||||||
|
<div class="add clickable" @click.stop="$emit('pick-file')">+</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-drop-input-container">
|
||||||
|
<v-textarea
|
||||||
|
ref="input"
|
||||||
|
full-width
|
||||||
|
variant="solo"
|
||||||
|
flat
|
||||||
|
auto-grow
|
||||||
|
v-model="messageInput"
|
||||||
|
no-resize
|
||||||
|
class="input-area-text"
|
||||||
|
rows="1"
|
||||||
|
:placeholder="$t('file_mode.add_a_message')"
|
||||||
|
hide-details
|
||||||
|
background-color="transparent"
|
||||||
|
v-on:keydown.enter.prevent="
|
||||||
|
() => {
|
||||||
|
sendAll();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
class="send-button clickable"
|
||||||
|
icon="arrow_upward"
|
||||||
|
size="default"
|
||||||
|
elevation="0"
|
||||||
|
color="black"
|
||||||
|
@click.stop="sendAll"
|
||||||
|
:disabled="sendButtonDisabled"
|
||||||
|
></v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ATTACHMENT SENDING/SENT MODE -->
|
||||||
|
<template v-if="batch.attachments.length > 0 && (status == mainStatuses.SENDING || status == mainStatuses.SENT)">
|
||||||
|
<div class="file-drop-sent-stack" ref="stackContainer">
|
||||||
|
<div v-if="status == mainStatuses.SENDING && batch.attachmentsSentCount == 0" class="no-items">
|
||||||
|
<div class="file-drop-stack-item direct" :style="stackItemTransform(null, -1)"></div>
|
||||||
|
<div>{{ $t("file_mode.sending_progress") }}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
v-for="(info, index) in batch.attachmentsSent"
|
||||||
|
:key="info.file.name"
|
||||||
|
class="file-drop-stack-item animated"
|
||||||
|
:style="stackItemTransform(info, index)"
|
||||||
|
>
|
||||||
|
<v-img v-if="info.src" :src="info.src" />
|
||||||
|
</div>
|
||||||
|
<div v-if="status == mainStatuses.SENT" class="items-sent" :style="stackItemTransform(null, -1)">
|
||||||
|
<v-icon>$vuetify.icons.ic_check_circle</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle section -->
|
||||||
|
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-container">
|
||||||
|
<div class="file-drop-sending-item" v-for="attachment in batch.attachmentsSending" :key="attachment.file.name">
|
||||||
|
<v-img v-if="attachment.src" :src="attachment.src" />
|
||||||
|
<div v-else class="filename">{{ attachment.file.name }}</div>
|
||||||
|
<v-progress-linear :model-value="attachment.sendInfo?.progress ?? 0"></v-progress-linear>
|
||||||
|
<div class="file-drop-cancel clickable" @click.stop="batch.cancelSendAttachment(attachment)">
|
||||||
|
<v-icon size="14" color="white">close</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sending-container">
|
||||||
|
<div class="file-drop-files-sent">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
messageInput && messageInput.length > 0 ? "file_mode.files_sent_with_note" : "file_mode.files_sent",
|
||||||
|
batch.attachmentsSent.length
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="file-drop-section">
|
||||||
|
<v-textarea
|
||||||
|
disabled
|
||||||
|
full-width
|
||||||
|
variant="solo"
|
||||||
|
flat
|
||||||
|
auto-grow
|
||||||
|
v-model="messageInput"
|
||||||
|
no-resize
|
||||||
|
class="input-area-text"
|
||||||
|
rows="1"
|
||||||
|
hide-details
|
||||||
|
background-color="transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom section -->
|
||||||
|
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-input-container">
|
||||||
|
<v-textarea
|
||||||
|
disabled
|
||||||
|
full-width
|
||||||
|
variant="solo"
|
||||||
|
flat
|
||||||
|
auto-grow
|
||||||
|
v-model="messageInput"
|
||||||
|
no-resize
|
||||||
|
class="input-area-text"
|
||||||
|
rows="1"
|
||||||
|
:placeholder="$t('file_mode.add_a_message')"
|
||||||
|
hide-details
|
||||||
|
background-color="transparent"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
>{{ $t("file_mode.sending")
|
||||||
|
}}<v-progress-circular indeterminate size="18" width="2" color="#4642F1"></v-progress-circular
|
||||||
|
></v-btn>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sent-input-container">
|
||||||
|
<v-btn class="close" @click.stop="close">{{ $t("file_mode.close") }}</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-bottom-sheet v-model="showAttachmentInformation" theme="dark">
|
||||||
|
<v-card class="text-center">
|
||||||
|
<v-card-text>
|
||||||
|
<AttachmentInfo :attachment="currentAttachment" />
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-bottom-sheet>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive } from "vue";
|
||||||
|
import messageMixin from "../messages/messageMixin";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import { Attachment } from "../../models/attachment";
|
||||||
|
import C2PABadge from "../c2pa/C2PABadge.vue";
|
||||||
|
import { createUploadBatch } from "../../models/attachmentManager";
|
||||||
|
import AttachmentInfo from "./AttachmentInfo.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
mixins: [messageMixin],
|
||||||
|
components: { C2PABadge, AttachmentInfo },
|
||||||
|
emits: ["pick-file", "close"],
|
||||||
|
props: {
|
||||||
|
showBackButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: function () {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
batch: {
|
||||||
|
type: Object,
|
||||||
|
default: function () {
|
||||||
|
return reactive(createUploadBatch(null, null, 0));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentItemIndex: 0,
|
||||||
|
messageInput: "",
|
||||||
|
mainStatuses: Object.freeze({
|
||||||
|
SELECTING: 0,
|
||||||
|
SENDING: 1,
|
||||||
|
SENT: 2,
|
||||||
|
}),
|
||||||
|
status: 0,
|
||||||
|
dropTarget: false,
|
||||||
|
showAttachmentInformation: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$audioPlayer.setAutoplay(false);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
backButtonDisabled() {
|
||||||
|
return this.status == this.mainStatuses.SENDING;
|
||||||
|
},
|
||||||
|
sendButtonDisabled() {
|
||||||
|
return !this.batch.canSend;
|
||||||
|
},
|
||||||
|
currentAttachment(): Attachment | undefined {
|
||||||
|
if (this.currentItemIndex >= 0 && this.currentItemIndex < this.batch.attachments.length) {
|
||||||
|
return this.batch.attachments[this.currentItemIndex];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
currentItemHasImagePreview() {
|
||||||
|
return (
|
||||||
|
this.currentItemIndex >= 0 &&
|
||||||
|
this.currentItemIndex < this.batch.attachments.length &&
|
||||||
|
this.batch.attachments[this.currentItemIndex].src
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"batch.attachments": {
|
||||||
|
handler(newValue, oldValue) {
|
||||||
|
// Added or removed?
|
||||||
|
if (newValue && oldValue && newValue.length > oldValue.length) {
|
||||||
|
this.currentItemIndex = oldValue.length;
|
||||||
|
} else if (newValue) {
|
||||||
|
this.currentItemIndex = newValue.length - 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showInformation() {
|
||||||
|
if (this.currentAttachment) {
|
||||||
|
this.showAttachmentInformation = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filesDropped(e: DragEvent) {
|
||||||
|
this.dropTarget = false;
|
||||||
|
let droppedFiles: FileList | undefined = e.dataTransfer?.files;
|
||||||
|
if (!droppedFiles) return;
|
||||||
|
for (let i = 0; i < droppedFiles.length; i++) {
|
||||||
|
const file = droppedFiles.item(i);
|
||||||
|
this.batch.addAttachment(this.$matrix.attachmentManager.createAttachment(file));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.batch.cancel();
|
||||||
|
this.status = this.mainStatuses.SELECTING;
|
||||||
|
this.messageInput = "";
|
||||||
|
this.currentItemIndex = 0;
|
||||||
|
this.$emit("close");
|
||||||
|
},
|
||||||
|
sendAll() {
|
||||||
|
this.status = this.mainStatuses.SENDING;
|
||||||
|
this.batch
|
||||||
|
.send(this.messageInput && this.messageInput.length > 0 ? this.messageInput : this.$t("file_mode.files"))
|
||||||
|
.then(() => {
|
||||||
|
this.status = this.mainStatuses.SENT;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
stackItemTransform(attachment: Attachment, index: number) {
|
||||||
|
const size =
|
||||||
|
0.6 *
|
||||||
|
(this.$refs.stackContainer
|
||||||
|
? Math.min(this.$refs.stackContainer.clientWidth, this.$refs.stackContainer.clientHeight)
|
||||||
|
: 176);
|
||||||
|
let transform = "";
|
||||||
|
if (attachment != null && attachment.sendInfo && index != -1) {
|
||||||
|
transform =
|
||||||
|
"transform: rotate(" +
|
||||||
|
attachment.sendInfo.randomRotation +
|
||||||
|
"deg) translate(" +
|
||||||
|
attachment.sendInfo.randomTranslationX +
|
||||||
|
"px," +
|
||||||
|
attachment.sendInfo.randomTranslationY +
|
||||||
|
"px); z-index:" +
|
||||||
|
(index + 2) +
|
||||||
|
";";
|
||||||
|
}
|
||||||
|
return transform + "width:" + size + "px;height:" + size + "px;border-radius:" + size / 8 + "px";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@/assets/css/chat.scss" as *;
|
||||||
|
@use "@/assets/css/sendattachments.scss" as *;
|
||||||
|
</style>
|
||||||
|
|
@ -1,113 +1,131 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="thumbnailRef">
|
<div ref="thumbnailRef" style="width: 100%;height: 100%;">
|
||||||
<v-responsive v-if="item.event.getContent().msgtype == 'm.video' && item.src" :class="{'thumbnail-item': true, 'preview': previewOnly}">
|
<v-responsive
|
||||||
<video :src="item.src" :controls="!previewOnly" class="w-100 h-100">
|
v-if="item.event.getContent().msgtype == 'm.video' && item.src"
|
||||||
{{ $t('fallbacks.video_file') }}
|
:class="{ 'thumbnail-item': true, preview: previewOnly }"
|
||||||
</video>
|
>
|
||||||
</v-responsive>
|
<video :src="item.src" :controls="!previewOnly" class="w-100 h-100">
|
||||||
<v-img v-else-if="item.event.getContent().msgtype == 'm.image' && item.src" :aspect-ratio="previewOnly ? (16 / 9) : undefined" :class="{'thumbnail-item': true, 'preview': previewOnly}" :src="item.src" :contain="!previewOnly" :cover="previewOnly" />
|
{{ $t("fallbacks.video_file") }}
|
||||||
<div v-else :class="{'thumbnail-item': true, 'preview': previewOnly, 'file-item': true}" >
|
</video>
|
||||||
<v-icon :class="fileTypeIconClass">{{ fileTypeIcon }}</v-icon>
|
</v-responsive>
|
||||||
<div class="file-name">{{ $sanitize(fileName) }}</div>
|
<ImageWithProgress
|
||||||
<div class="file-size">{{ fileSize }}</div>
|
v-else-if="item.event.getContent().msgtype == 'm.image'"
|
||||||
</div>
|
:aspect-ratio="previewOnly ? 16 / 9 : undefined"
|
||||||
|
:class="{ 'thumbnail-item': true, preview: previewOnly }"
|
||||||
|
:src="item.src ? item.src : item.thumbnail"
|
||||||
|
:contain="!previewOnly"
|
||||||
|
:cover="previewOnly"
|
||||||
|
:loadingProgress="previewOnly ? item.thumbnailProgress : item.srcProgress"
|
||||||
|
/>
|
||||||
|
<div v-else :class="{ 'thumbnail-item': true, preview: previewOnly, 'file-item': true }">
|
||||||
|
<v-icon :class="fileTypeIconClass">{{ fileTypeIcon }}</v-icon>
|
||||||
|
<div class="file-name">{{ $sanitize(fileName) }}</div>
|
||||||
|
<div class="file-size">{{ fileSize }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
import util from "../../plugins/utils";
|
import util from "../../plugins/utils";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { EventAttachment } from "../../models/eventAttachment";
|
||||||
|
import ImageWithProgress from "../ImageWithProgress";
|
||||||
|
|
||||||
export default {
|
export default defineComponent({
|
||||||
props: {
|
components: { ImageWithProgress },
|
||||||
/**
|
props: {
|
||||||
* Item is an object of { event: MXEvent, src: URL }
|
/**
|
||||||
*/
|
* Item is an object of { event: MXEvent, src: URL }
|
||||||
item: {
|
*/
|
||||||
type: Object,
|
item: {
|
||||||
default: function () {
|
type: Object as PropType<EventAttachment>,
|
||||||
return {}
|
default: function () {
|
||||||
}
|
return {};
|
||||||
},
|
},
|
||||||
previewOnly: {
|
|
||||||
type: Boolean,
|
|
||||||
default: function() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
previewOnly: {
|
||||||
fileTypeIcon() {
|
type: Boolean,
|
||||||
if (util.isFileTypeAPK(this.item.event)) {
|
default: function () {
|
||||||
if (this.item.event.isChannelMessage) {
|
return false;
|
||||||
return "$vuetify.icons.ic_channel_apk";
|
},
|
||||||
}
|
},
|
||||||
return "$vuetify.icons.ic_apk";
|
},
|
||||||
} else if (util.isFileTypeIPA(this.item.event)) {
|
computed: {
|
||||||
return "$vuetify.icons.ic_ipa";
|
fileTypeIcon() {
|
||||||
} else if (util.isFileTypePDF(this.item.event)) {
|
if (util.isFileTypeAPK(this.item.event)) {
|
||||||
if (this.item.event.isChannelMessage) {
|
if (this.item.event.isChannelMessage) {
|
||||||
return "$vuetify.icons.ic_channel_pdf";
|
return "$vuetify.icons.ic_channel_apk";
|
||||||
}
|
|
||||||
return "$vuetify.icons.ic_pdf";
|
|
||||||
} else if (util.isFileTypeZip(this.item.event)) {
|
|
||||||
return "$vuetify.icons.ic_zip";
|
|
||||||
}
|
|
||||||
return "description"
|
|
||||||
},
|
|
||||||
fileTypeIconClass() {
|
|
||||||
if (util.isFileTypeZip(this.item.event)) {
|
|
||||||
return "zip";
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
fileName() {
|
|
||||||
return util.getFileName(this.item.event);
|
|
||||||
},
|
|
||||||
fileSize() {
|
|
||||||
return util.getFileSizeFormatted(this.item.event);
|
|
||||||
}
|
}
|
||||||
|
return "$vuetify.icons.ic_apk";
|
||||||
|
} else if (util.isFileTypeIPA(this.item.event)) {
|
||||||
|
return "$vuetify.icons.ic_ipa";
|
||||||
|
} else if (util.isFileTypePDF(this.item.event)) {
|
||||||
|
if (this.item.event.isChannelMessage) {
|
||||||
|
return "$vuetify.icons.ic_channel_pdf";
|
||||||
|
}
|
||||||
|
return "$vuetify.icons.ic_pdf";
|
||||||
|
} else if (util.isFileTypeZip(this.item.event)) {
|
||||||
|
return "$vuetify.icons.ic_zip";
|
||||||
|
}
|
||||||
|
return "description";
|
||||||
},
|
},
|
||||||
methods: {
|
fileTypeIconClass() {
|
||||||
// listen for custom hammerJs singletab click to differentiate it from double click(heart animation).
|
if (util.isFileTypeZip(this.item.event)) {
|
||||||
initThumbnailHammerJs(element) {
|
return "zip";
|
||||||
const hammerInstance = util.singleOrDoubleTabRecognizer(element)
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
fileName() {
|
||||||
|
return util.getFileName(this.item.event);
|
||||||
|
},
|
||||||
|
fileSize() {
|
||||||
|
return util.getFileSizeFormatted(this.item.event);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// listen for custom hammerJs singletab click to differentiate it from double click(heart animation).
|
||||||
|
initThumbnailHammerJs(element: any) {
|
||||||
|
const hammerInstance = util.singleOrDoubleTabRecognizer(element);
|
||||||
|
|
||||||
hammerInstance.on("singletap doubletap", (ev) => {
|
hammerInstance.on("singletap doubletap", (ev: any) => {
|
||||||
if(ev.type === 'singletap') {
|
if (ev.type === "singletap") {
|
||||||
this.$emit('itemclick', { item: this.item })
|
this.$emit("itemclick", { item: this.item });
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
mounted() {
|
},
|
||||||
if(this.$refs.thumbnailRef) {
|
mounted() {
|
||||||
this.initThumbnailHammerJs(this.$refs.thumbnailRef);
|
if (this.$refs.thumbnailRef) {
|
||||||
}
|
this.initThumbnailHammerJs(this.$refs.thumbnailRef);
|
||||||
},
|
}
|
||||||
}
|
if (!this.previewOnly && this.item) {
|
||||||
|
this.item.loadSrc();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "@/assets/css/chat.scss" as *;
|
@use "@/assets/css/chat.scss" as *;
|
||||||
|
|
||||||
.thumbnail-item {
|
.thumbnail-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-item {
|
.file-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
.v-icon {
|
.v-icon {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="audio-player d-flex flex-row align-center">
|
<div class="audio-player d-flex flex-row align-center">
|
||||||
<v-progress-circular v-if="info.loading" @click.stop="pause" :value="info.loadPercent" size="24" width="2" style="margin:6px"></v-progress-circular>
|
<v-progress-circular v-if="info.loading" @click.stop="pause" :model-value="info.loadPercent" size="24" width="2" style="margin:6px"></v-progress-circular>
|
||||||
<v-btn v-else-if="info.playing" id="btn-pause" @click.stop="pause" icon><v-icon size="20">pause</v-icon></v-btn>
|
<v-btn v-else-if="info.playing" id="btn-pause" @click.stop="pause" icon><v-icon size="20">pause</v-icon></v-btn>
|
||||||
<v-btn v-else id="btn-play" @click.stop="play" icon><v-icon size="20">play_arrow</v-icon></v-btn>
|
<v-btn v-else id="btn-play" @click.stop="play" icon><v-icon size="20">play_arrow</v-icon></v-btn>
|
||||||
<div class="play-time">
|
<div class="play-time">
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<message-incoming v-bind="{...$props, ...$attrs}">
|
<message-incoming v-bind="{ ...$props, ...$attrs }">
|
||||||
<div class="bubble image-bubble" ref="imageRef">
|
<div class="bubble image-bubble" ref="imageRef">
|
||||||
<v-img
|
<ImageWithProgress
|
||||||
:aspect-ratio="16 / 9"
|
:aspect-ratio="16 / 9"
|
||||||
ref="image"
|
ref="image"
|
||||||
:src="src"
|
:src="eventAttachment.src ? eventAttachment.src : eventAttachment.thumbnail"
|
||||||
:cover="cover"
|
:cover="cover"
|
||||||
:contain="contain"
|
:contain="contain"
|
||||||
|
:loadingProgress="eventAttachment.thumbnailProgress"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<v-dialog
|
<v-dialog v-model="dialog" :width="$vuetify.display.smAndUp ? '940px' : '90%'">
|
||||||
v-model="dialog"
|
<ImageWithProgress :src="eventAttachment.src ? eventAttachment.src : eventAttachment.thumbnail" :loadingProgress="eventAttachment.srcProgress" />
|
||||||
:width="$vuetify.display.smAndUp ? '940px' : '90%'"
|
|
||||||
>
|
|
||||||
<v-img :src="src"/>
|
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</message-incoming>
|
</message-incoming>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import util from "../../plugins/utils";
|
import util from "../../plugins/utils";
|
||||||
import MessageIncoming from './MessageIncoming.vue';
|
import ImageWithProgress from "../ImageWithProgress.vue";
|
||||||
|
import MessageIncoming from "./MessageIncoming.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: MessageIncoming,
|
extends: MessageIncoming,
|
||||||
components: { MessageIncoming },
|
components: { MessageIncoming, ImageWithProgress },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
src: undefined,
|
eventAttachment: {},
|
||||||
cover: true,
|
cover: true,
|
||||||
contain: false,
|
contain: false,
|
||||||
dialog: false
|
dialog: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -39,48 +38,38 @@ export default {
|
||||||
const hammerInstance = util.singleOrDoubleTabRecognizer(element);
|
const hammerInstance = util.singleOrDoubleTabRecognizer(element);
|
||||||
|
|
||||||
hammerInstance.on("singletap doubletap", (ev) => {
|
hammerInstance.on("singletap doubletap", (ev) => {
|
||||||
if(ev.type === 'singletap') {
|
if (ev.type === "singletap") {
|
||||||
|
this.eventAttachment?.loadSrc();
|
||||||
this.dialog = true;
|
this.dialog = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
//console.log("Mounted with event:", JSON.stringify(this.event.getContent()));
|
//console.log("Mounted with event:", JSON.stringify(this.event.getContent()));
|
||||||
const width = this.$refs.image.$el.clientWidth;
|
const info = this.event.getContent().info;
|
||||||
const height = (width * 9) / 16;
|
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
|
||||||
util
|
// be stickers and small emoji type things.
|
||||||
.getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, this.event, this.$config, width, height)
|
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
|
||||||
.then((url) => {
|
this.cover = true;
|
||||||
const info = this.event.getContent().info;
|
this.contain = false;
|
||||||
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
|
} else {
|
||||||
// be stickers and small emoji type things.
|
this.cover = false;
|
||||||
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
|
this.contain = true;
|
||||||
this.cover = true;
|
}
|
||||||
this.contain = false;
|
if (this.$refs.imageRef) {
|
||||||
} else {
|
this.initMessageInImageHammerJs(this.$refs.imageRef);
|
||||||
this.cover = false;
|
}
|
||||||
this.contain = true;
|
|
||||||
}
|
this.eventAttachment = this.$matrix.attachmentManager.getEventAttachment(this.event);
|
||||||
this.src = url;
|
this.eventAttachment?.loadThumbnail();
|
||||||
if(this.$refs.imageRef) {
|
|
||||||
this.initMessageInImageHammerJs(this.$refs.imageRef);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("Failed to fetch thumbnail: ", err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
if (this.src) {
|
this.eventAttachment?.release();
|
||||||
const objectUrl = this.src;
|
|
||||||
this.src = null;
|
|
||||||
URL.revokeObjectURL(objectUrl);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "@/assets/css/chat.scss" as *;
|
@use "@/assets/css/chat.scss" as *;
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
<template>
|
|
||||||
<message-incoming v-bind="{ ...$props, ...$attrs }" v-if="items.length > 1 || event.isRedacted() || forceMultiview">
|
|
||||||
<div class="bubble">
|
|
||||||
<div class="original-message" v-if="inReplyToText">
|
|
||||||
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
|
||||||
<div
|
|
||||||
class="original-message-text"
|
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="message">
|
|
||||||
<SwipeableThumbnailsView :items="items" v-if="!event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL" v-bind="$attrs" />
|
|
||||||
<v-container v-else-if="!event.isRedacted()" fluid class="imageCollection">
|
|
||||||
<v-row wrap>
|
|
||||||
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
|
|
||||||
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
<i v-if="event.isRedacted()" class="deleted-text">
|
|
||||||
<v-icon :color="this.senderIsAdminOrModerator(this.event) ? 'white' : ''" size="small">block</v-icon>
|
|
||||||
{{ redactedBySomeoneElse(event) ? $t('message.incoming_message_deleted_text') : $t('message.outgoing_message_deleted_text')}}
|
|
||||||
</i>
|
|
||||||
<span v-html="linkify($sanitize(messageText))" v-else-if="messageText" />
|
|
||||||
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
|
|
||||||
{{ $t('message.edited') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<GalleryItemsView :originalEvent="originalEvent" :items="items" :initialItem="showItem" v-if="!!showItem" v-on:close="showItem = null" />
|
|
||||||
</message-incoming>
|
|
||||||
<component v-else-if="items.length == 1" :is="componentFn(items[0].event)"
|
|
||||||
v-bind="{...$props, ...$attrs}"
|
|
||||||
:originalEvent="items[0].event"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import MessageIncoming from "./MessageIncoming.vue";
|
|
||||||
import messageMixin from "./messageMixin";
|
|
||||||
import util, { ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE } from "../../plugins/utils";
|
|
||||||
import GalleryItemsView from '../file_mode/GalleryItemsView.vue';
|
|
||||||
import ThumbnailView from '../file_mode/ThumbnailView.vue';
|
|
||||||
import SwipeableThumbnailsView from "./channel/SwipeableThumbnailsView.vue";
|
|
||||||
import { reactive } from "vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
extends: MessageIncoming,
|
|
||||||
components: { MessageIncoming, GalleryItemsView, ThumbnailView, SwipeableThumbnailsView },
|
|
||||||
mixins: [messageMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
|
|
||||||
items: [],
|
|
||||||
showItem: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
|
|
||||||
if (!this.thread) {
|
|
||||||
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
forceMultiview() {
|
|
||||||
return this.room.displayType == ROOM_TYPE_FILE_MODE || (this.room.displayType == ROOM_TYPE_CHANNEL && this.items.length == 1 && util.isFileTypePDF(this.items[0].event));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onRelationsCreated() {
|
|
||||||
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
|
|
||||||
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
|
||||||
},
|
|
||||||
onItemClick(event) {
|
|
||||||
this.showItem = event.item;
|
|
||||||
},
|
|
||||||
processThread() {
|
|
||||||
this.$emit('layout-change', () => {
|
|
||||||
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
|
|
||||||
.filter(e => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype))
|
|
||||||
.map(e => {
|
|
||||||
let ret = reactive({
|
|
||||||
event: e,
|
|
||||||
src: null,
|
|
||||||
});
|
|
||||||
ret.promise = this.$matrix.matrixClient.decryptEventIfNeeded(e)
|
|
||||||
.then(() => util.getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, e, this.$config, 100, 100))
|
|
||||||
.then((url) => {
|
|
||||||
ret.src = url;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("Failed to fetch thumbnail: ", err);
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
}, this.$el);
|
|
||||||
},
|
|
||||||
layoutedItems() {
|
|
||||||
if (!this.items || this.items.length == 0) { return [] }
|
|
||||||
let array = this.items.slice(0);
|
|
||||||
let rows = []
|
|
||||||
while (array.length > 0) {
|
|
||||||
if (array.length >= 7) {
|
|
||||||
rows.push({ size: 6, item: array[0] });
|
|
||||||
rows.push({ size: 6, item: array[1] });
|
|
||||||
rows.push({ size: 12, item: array[2] });
|
|
||||||
rows.push({ size: 3, item: array[3] });
|
|
||||||
rows.push({ size: 3, item: array[4] });
|
|
||||||
rows.push({ size: 3, item: array[5] });
|
|
||||||
rows.push({ size: 3, item: array[6] });
|
|
||||||
array = array.slice(7);
|
|
||||||
} else if (array.length >= 3) {
|
|
||||||
rows.push({ size: 6, item: array[0] });
|
|
||||||
rows.push({ size: 6, item: array[1] });
|
|
||||||
rows.push({ size: 12, item: array[2] });
|
|
||||||
array = array.slice(3);
|
|
||||||
} else if (array.length >= 2) {
|
|
||||||
rows.push({ size: 6, item: array[0] });
|
|
||||||
rows.push({ size: 6, item: array[1] });
|
|
||||||
array = array.slice(2);
|
|
||||||
} else {
|
|
||||||
rows.push({ size: 12, item: array[0] });
|
|
||||||
array = array.slice(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rows
|
|
||||||
},
|
|
||||||
downloadAll() {
|
|
||||||
this.items.forEach(item => util.download(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, item.event));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use "@/assets/css/chat.scss" as *;
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.bubble {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.imageCollection {
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col {
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,36 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<message-outgoing v-bind="{ ...$props, ...$attrs }">
|
<message-outgoing v-bind="{ ...$props, ...$attrs }">
|
||||||
<div class="bubble image-bubble" ref="imageRef">
|
<div class="bubble image-bubble" ref="imageRef">
|
||||||
<v-img
|
<ImageWithProgress
|
||||||
:aspect-ratio="16 / 9"
|
:aspect-ratio="16 / 9"
|
||||||
ref="image"
|
ref="image"
|
||||||
:src="src"
|
:src="eventAttachment.src ? eventAttachment.src : eventAttachment.thumbnail"
|
||||||
:cover="cover"
|
:cover="cover"
|
||||||
:contain="contain"
|
:contain="contain"
|
||||||
|
:loadingProgress="eventAttachment.thumbnailProgress"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<v-dialog
|
<v-dialog v-model="dialog" :width="$vuetify.display.smAndUp ? '940px' : '90%'">
|
||||||
v-model="dialog"
|
<ImageWithProgress :src="eventAttachment.src ? eventAttachment.src : eventAttachment.thumbnail" :loadingProgress="eventAttachment.srcProgress" />
|
||||||
:width="$vuetify.display.smAndUp ? '940px' : '90%'"
|
|
||||||
>
|
|
||||||
<v-img :src="src"/>
|
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</message-outgoing>
|
</message-outgoing>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import util from "../../plugins/utils";
|
import util from "../../plugins/utils";
|
||||||
|
import ImageWithProgress from "../ImageWithProgress.vue";
|
||||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: MessageOutgoing,
|
extends: MessageOutgoing,
|
||||||
components: { MessageOutgoing },
|
components: { MessageOutgoing, ImageWithProgress },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
src: undefined,
|
eventAttachment: {},
|
||||||
cover: true,
|
cover: true,
|
||||||
contain: false,
|
contain: false,
|
||||||
dialog: false
|
dialog: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -39,47 +38,38 @@ export default {
|
||||||
const hammerInstance = util.singleOrDoubleTabRecognizer(element);
|
const hammerInstance = util.singleOrDoubleTabRecognizer(element);
|
||||||
|
|
||||||
hammerInstance.on("singletap doubletap", (ev) => {
|
hammerInstance.on("singletap doubletap", (ev) => {
|
||||||
if(ev.type === 'singletap') {
|
if (ev.type === "singletap") {
|
||||||
|
this.eventAttachment?.loadSrc();
|
||||||
this.dialog = true;
|
this.dialog = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const width = this.$refs.image.$el.clientWidth;
|
console.error("Mounted outgoing image, load thumbnail!");
|
||||||
const height = (width * 9) / 16;
|
|
||||||
util
|
const info = this.event.getContent().info;
|
||||||
.getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, this.event, this.$config, width, height)
|
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
|
||||||
.then((url) => {
|
// be stickers and small emoji type things.
|
||||||
const info = this.event.getContent().info;
|
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
|
||||||
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
|
this.cover = true;
|
||||||
// be stickers and small emoji type things.
|
this.contain = false;
|
||||||
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
|
} else {
|
||||||
this.cover = true;
|
this.cover = false;
|
||||||
this.contain = false;
|
this.contain = true;
|
||||||
} else {
|
}
|
||||||
this.cover = false;
|
if (this.$refs.imageRef) {
|
||||||
this.contain = true;
|
this.initMessageOutImageHammerJs(this.$refs.imageRef);
|
||||||
}
|
}
|
||||||
this.src = url;
|
this.eventAttachment = this.$matrix.attachmentManager.getEventAttachment(this.event);
|
||||||
if(this.$refs.imageRef) {
|
this.eventAttachment?.loadThumbnail();
|
||||||
this.initMessageOutImageHammerJs(this.$refs.imageRef);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("Failed to fetch thumbnail: ", err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
if (this.src) {
|
this.eventAttachment?.release();
|
||||||
const objectUrl = this.src;
|
|
||||||
this.src = null;
|
|
||||||
URL.revokeObjectURL(objectUrl);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "@/assets/css/chat.scss" as *;
|
@use "@/assets/css/chat.scss" as *;
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
<template>
|
|
||||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-if="items.length > 1 || event.isRedacted() || forceMultiview">
|
|
||||||
<div class="bubble">
|
|
||||||
<div class="original-message" v-if="inReplyToText">
|
|
||||||
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
|
||||||
<div
|
|
||||||
class="original-message-text"
|
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="message">
|
|
||||||
<SwipeableThumbnailsView :items="items" v-if="!event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL" v-bind="$attrs" />
|
|
||||||
<v-container v-else-if="!event.isRedacted()" fluid class="imageCollection">
|
|
||||||
<v-row wrap>
|
|
||||||
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
|
|
||||||
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
<i v-if="event.isRedacted()" class="deleted-text">
|
|
||||||
<v-icon size="small">block</v-icon>
|
|
||||||
{{ redactedBySomeoneElse(event) ? $t('message.incoming_message_deleted_text') : $t('message.outgoing_message_deleted_text')}}
|
|
||||||
</i>
|
|
||||||
<span v-html="linkify($sanitize(messageText))" v-else-if="messageText" />
|
|
||||||
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
|
|
||||||
{{ $t('message.edited') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<GalleryItemsView :originalEvent="originalEvent" :items="items" :initialItem="showItem" v-if="!!showItem" v-on:close="showItem = null" />
|
|
||||||
</message-outgoing>
|
|
||||||
<component v-else-if="items.length == 1" :is="componentFn(items[0].event)"
|
|
||||||
v-bind="{...$props, ...$attrs}"
|
|
||||||
:originalEvent="items[0].event"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
|
||||||
import messageMixin from "./messageMixin";
|
|
||||||
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
|
|
||||||
import GalleryItemsView from '../file_mode/GalleryItemsView.vue';
|
|
||||||
import ThumbnailView from '../file_mode/ThumbnailView.vue';
|
|
||||||
import SwipeableThumbnailsView from "./channel/SwipeableThumbnailsView.vue";
|
|
||||||
import { reactive } from "vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
extends: MessageOutgoing,
|
|
||||||
components: { MessageOutgoing, GalleryItemsView, ThumbnailView, SwipeableThumbnailsView },
|
|
||||||
mixins: [messageMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
|
|
||||||
items: [],
|
|
||||||
showItem: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
|
|
||||||
if (!this.thread) {
|
|
||||||
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
forceMultiview() {
|
|
||||||
return this.room.displayType == ROOM_TYPE_CHANNEL && this.items.length == 1 && util.isFileTypePDF(this.items[0].event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onRelationsCreated() {
|
|
||||||
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
|
|
||||||
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
|
||||||
},
|
|
||||||
onItemClick(event) {
|
|
||||||
this.showItem = event.item;
|
|
||||||
},
|
|
||||||
processThread() {
|
|
||||||
this.$emit('layout-change', () => {
|
|
||||||
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
|
|
||||||
.filter(e => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype))
|
|
||||||
.map(e => {
|
|
||||||
let ret = reactive({
|
|
||||||
event: e,
|
|
||||||
src: null,
|
|
||||||
});
|
|
||||||
ret.promise = this.$matrix.matrixClient.decryptEventIfNeeded(e)
|
|
||||||
.then(() => util.getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, e, this.$config, 100, 100))
|
|
||||||
.then((url) => {
|
|
||||||
ret.src = url;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("Failed to fetch thumbnail: ", err);
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
}, this.$el);
|
|
||||||
},
|
|
||||||
layoutedItems() {
|
|
||||||
if (!this.items || this.items.length == 0) { return [] }
|
|
||||||
let array = this.items.slice(0);
|
|
||||||
let rows = []
|
|
||||||
while (array.length > 0) {
|
|
||||||
if (array.length >= 7) {
|
|
||||||
rows.push({ size: 6, item: array[0] });
|
|
||||||
rows.push({ size: 6, item: array[1] });
|
|
||||||
rows.push({ size: 12, item: array[2] });
|
|
||||||
rows.push({ size: 3, item: array[3] });
|
|
||||||
rows.push({ size: 3, item: array[4] });
|
|
||||||
rows.push({ size: 3, item: array[5] });
|
|
||||||
rows.push({ size: 3, item: array[6] });
|
|
||||||
array = array.slice(7);
|
|
||||||
} else if (array.length >= 3) {
|
|
||||||
rows.push({ size: 6, item: array[0] });
|
|
||||||
rows.push({ size: 6, item: array[1] });
|
|
||||||
rows.push({ size: 12, item: array[2] });
|
|
||||||
array = array.slice(3);
|
|
||||||
} else if (array.length >= 2) {
|
|
||||||
rows.push({ size: 6, item: array[0] });
|
|
||||||
rows.push({ size: 6, item: array[1] });
|
|
||||||
array = array.slice(2);
|
|
||||||
} else {
|
|
||||||
rows.push({ size: 12, item: array[0] });
|
|
||||||
array = array.slice(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rows
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss">
|
|
||||||
@use "@/assets/css/chat.scss" as *;
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.bubble {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.imageCollection {
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col {
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.6rem;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -105,7 +105,7 @@ export default {
|
||||||
this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction');
|
this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction');
|
||||||
},
|
},
|
||||||
onClickEmoji(emoji) {
|
onClickEmoji(emoji) {
|
||||||
this.$bubble('send-quick-reaction', {reaction:emoji, event:this.event});
|
this.$emit('send-quick-reaction', {reaction:emoji, event:this.event});
|
||||||
},
|
},
|
||||||
onAddRelation(ignoredevent) {
|
onAddRelation(ignoredevent) {
|
||||||
this.processReactions();
|
this.processReactions();
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ export default {
|
||||||
this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction');
|
this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction');
|
||||||
},
|
},
|
||||||
onClickEmoji(emoji) {
|
onClickEmoji(emoji) {
|
||||||
this.$bubble('send-quick-reaction', {reaction:emoji, event:this.event});
|
this.$emit('send-quick-reaction', {reaction:emoji, event:this.event});
|
||||||
},
|
},
|
||||||
onAddRelation(ignoredevent) {
|
onAddRelation(ignoredevent) {
|
||||||
this.processReactions();
|
this.processReactions();
|
||||||
|
|
|
||||||
77
src/components/messages/composition/MessageIncoming.vue
Normal file
77
src/components/messages/composition/MessageIncoming.vue
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<!-- BASE CLASS FOR INCOMING MESSAGE -->
|
||||||
|
<div :class="messageClasses">
|
||||||
|
<div v-if="showSenderAndTime || room.displayType == ROOM_TYPE_CHANNEL" class="senderAndTime">
|
||||||
|
<div class="sender">{{ eventSenderDisplayName(event) }}</div>
|
||||||
|
<div class="time">
|
||||||
|
{{ room.displayType == ROOM_TYPE_CHANNEL ? formatTimeAgo(event?.event.origin_server_ts) : formatTime(event?.event.origin_server_ts) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<v-avatar class="avatar" ref="avatar" size="32" color="#ededed" @click.stop="otherAvatarClicked(avatar)">
|
||||||
|
<AuthedImage v-if="messageEventAvatar(event)" :src="messageEventAvatar(event)" onerror="this.style.display='none'" />
|
||||||
|
<span v-else class="text-white headline">{{
|
||||||
|
eventSenderDisplayName(event).substring(0, 1).toUpperCase()
|
||||||
|
}}</span>
|
||||||
|
</v-avatar>
|
||||||
|
<!-- SLOT FOR CONTENT -->
|
||||||
|
<span ref="messageInOutRef" class="content">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
<div class="pin-icon" v-if="isPinned"><v-icon>$vuetify.icons.ic_pin_filled</v-icon></div>
|
||||||
|
<div class="op-button" ref="opbutton" v-if="event && !event.isRedacted() && $matrix.userCanSendMessageInCurrentRoom">
|
||||||
|
<v-btn id="btn-more" icon @click.stop="showContextMenu($refs.opbutton)">
|
||||||
|
<v-icon>more_vert</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<QuickReactionsChannel v-if="room.displayType == ROOM_TYPE_CHANNEL" :event="eventForReactions" :timelineSet="timelineSet" v-bind="$attrs"/>
|
||||||
|
<QuickReactions v-else :event="eventForReactions" :timelineSet="timelineSet" v-bind="$attrs"/>
|
||||||
|
<SeenBy v-if="room.displayType != ROOM_TYPE_CHANNEL" :room="room" :event="event"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SeenBy from "../SeenBy.vue";
|
||||||
|
import { MessageEmits, MessageProps, useMessage } from "./messageMixin";
|
||||||
|
import util, { ROOM_TYPE_CHANNEL } from "@/plugins/utils";
|
||||||
|
import QuickReactions from "../QuickReactions.vue";
|
||||||
|
import QuickReactionsChannel from "../channel/QuickReactionsChannel.vue";
|
||||||
|
import AuthedImage from "../../AuthedImage.vue";
|
||||||
|
import { inject, onMounted, ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const opbutton = ref(null);
|
||||||
|
const messageInOutRef = ref(null);
|
||||||
|
const avatar = ref(null);
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const $matrix: any = inject('globalMatrix');
|
||||||
|
|
||||||
|
const props = defineProps<MessageProps>();
|
||||||
|
const emits = defineEmits<MessageEmits>();
|
||||||
|
|
||||||
|
const { room } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
event,
|
||||||
|
eventForReactions,
|
||||||
|
showSenderAndTime,
|
||||||
|
isPinned,
|
||||||
|
messageClasses,
|
||||||
|
otherAvatarClicked,
|
||||||
|
showContextMenu,
|
||||||
|
eventSenderDisplayName,
|
||||||
|
messageEventAvatar,
|
||||||
|
formatTimeAgo,
|
||||||
|
formatTime,
|
||||||
|
initMsgHammerJs,
|
||||||
|
} = useMessage($matrix, t, props, emits);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (util.isMobileOrTabletBrowser() && messageInOutRef.value && opbutton.value) {
|
||||||
|
initMsgHammerJs(messageInOutRef.value, opbutton.value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@/assets/css/chat.scss" as *;
|
||||||
|
</style>
|
||||||
220
src/components/messages/composition/MessageIncomingThread.vue
Normal file
220
src/components/messages/composition/MessageIncomingThread.vue
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
<template>
|
||||||
|
<MessageIncoming
|
||||||
|
ref="root"
|
||||||
|
v-bind="{ ...$props, ...$attrs }"
|
||||||
|
v-if="showMultiview"
|
||||||
|
>
|
||||||
|
<div class="bubble">
|
||||||
|
<div class="original-message" v-if="inReplyToText">
|
||||||
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
|
<div class="original-message-text" v-html="linkify($$sanitize(inReplyToText))" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
<SwipeableThumbnailsView
|
||||||
|
:items="items"
|
||||||
|
v-if="event && !event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
<v-container v-else-if="event && !event.isRedacted()" fluid class="imageCollection">
|
||||||
|
<v-row wrap>
|
||||||
|
<v-col v-for="{ size, item } in layoutedItems" :key="item.event.getId()" :cols="size">
|
||||||
|
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
<i v-if="event && event.isRedacted()" class="deleted-text">
|
||||||
|
<v-icon :color="senderIsAdminOrModerator(event) ? 'white' : ''" size="small">block</v-icon>
|
||||||
|
{{
|
||||||
|
redactedBySomeoneElse(event)
|
||||||
|
? $t("message.incoming_message_deleted_text")
|
||||||
|
: $t("message.outgoing_message_deleted_text")
|
||||||
|
}}
|
||||||
|
</i>
|
||||||
|
<span v-html="linkify($$sanitize(messageText))" v-else-if="messageText" />
|
||||||
|
<span class="edit-marker" v-if="event && event.replacingEventId() && !event.isRedacted()">
|
||||||
|
{{ t("message.edited") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<GalleryItemsView
|
||||||
|
:originalEvent="originalEvent"
|
||||||
|
:items="items"
|
||||||
|
:initialItem="showItem"
|
||||||
|
v-if="!!showItem"
|
||||||
|
v-on:close="showItem = undefined"
|
||||||
|
/>
|
||||||
|
</MessageIncoming>
|
||||||
|
<component
|
||||||
|
v-else-if="items.length == 1"
|
||||||
|
:is="componentFn(items[0].event)"
|
||||||
|
v-bind="{ ...$props, ...$attrs }"
|
||||||
|
:originalEvent="items[0].event"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MessageIncoming from "./MessageIncoming.vue";
|
||||||
|
import { MessageEmits, MessageProps, useMessage } from "./messageMixin";
|
||||||
|
import util, { ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE } from "@/plugins/utils";
|
||||||
|
import GalleryItemsView from "../../file_mode/GalleryItemsView.vue";
|
||||||
|
import ThumbnailView from "../../file_mode/ThumbnailView.vue";
|
||||||
|
import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.vue";
|
||||||
|
import { computed, inject, onBeforeUnmount, ref, Ref, watch } from "vue";
|
||||||
|
import { EventAttachment } from "../../../models/eventAttachment";
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const $matrix: any = inject('globalMatrix');
|
||||||
|
const $$sanitize: any = inject('globalSanitize');
|
||||||
|
|
||||||
|
const root = ref(undefined);
|
||||||
|
const emits = defineEmits<MessageEmits & {(event: "layout-change", value: {element: Element | undefined, action: () => void}): void}>();
|
||||||
|
|
||||||
|
const items: Ref<EventAttachment[]> = ref([]);
|
||||||
|
const showItem: Ref<EventAttachment | undefined> = ref(undefined);
|
||||||
|
|
||||||
|
const props = defineProps<MessageProps>();
|
||||||
|
|
||||||
|
const { room } = props;
|
||||||
|
|
||||||
|
const processThread = () => {
|
||||||
|
if (!event.value?.isRedacted()) {
|
||||||
|
emits("layout-change", {element: root.value, action: _processThread});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
event,
|
||||||
|
thread,
|
||||||
|
senderIsAdminOrModerator,
|
||||||
|
inReplyToSender,
|
||||||
|
inReplyToText,
|
||||||
|
messageText,
|
||||||
|
redactedBySomeoneElse,
|
||||||
|
linkify,
|
||||||
|
} = useMessage($matrix, t, props, emits, processThread);
|
||||||
|
|
||||||
|
watch(event, () => {
|
||||||
|
if (event.value) {
|
||||||
|
if (thread.value === undefined) {
|
||||||
|
thread.value = props.timelineSet.relations.getChildEventsForEvent(
|
||||||
|
event.value.getId() ?? "",
|
||||||
|
util.threadMessageType(),
|
||||||
|
"m.room.message"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!thread.value) {
|
||||||
|
event.value.on(MatrixEventEvent.RelationsCreated, onRelationsCreated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
event.value?.off(MatrixEventEvent.RelationsCreated, onRelationsCreated);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showMultiview = computed((): boolean => {
|
||||||
|
return props.room.displayType == ROOM_TYPE_FILE_MODE ||
|
||||||
|
items.value?.length > 1 ||
|
||||||
|
(event.value && event.value.isRedacted()) ||
|
||||||
|
(props.room.displayType == ROOM_TYPE_CHANNEL && items.value.length == 1 && util.isFileTypePDF(items.value[0].event)) ||
|
||||||
|
messageText.value?.length > 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const onRelationsCreated = () => {
|
||||||
|
if (event.value) {
|
||||||
|
thread.value = props.timelineSet.relations.getChildEventsForEvent(
|
||||||
|
event.value.getId() ?? "",
|
||||||
|
util.threadMessageType(),
|
||||||
|
"m.room.message"
|
||||||
|
);
|
||||||
|
event.value.off(MatrixEventEvent.RelationsCreated, onRelationsCreated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onItemClick = (event: any) => {
|
||||||
|
showItem.value = event.item;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _processThread = () => {
|
||||||
|
const eventItems = props.timelineSet.relations
|
||||||
|
.getAllChildEventsForEvent(event.value?.getId() ?? "")
|
||||||
|
.filter((e: MatrixEvent) => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype));
|
||||||
|
|
||||||
|
items.value = eventItems.map((e: MatrixEvent) => {
|
||||||
|
let ea = $matrix.attachmentManager.getEventAttachment(e);
|
||||||
|
if (showMultiview.value) {
|
||||||
|
ea.loadThumbnail();
|
||||||
|
}
|
||||||
|
return ea;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const layoutedItems = computed(() => {
|
||||||
|
if (!items.value || items.value.length == 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let array = items.value.slice(0);
|
||||||
|
let rows = [];
|
||||||
|
while (array.length > 0) {
|
||||||
|
if (array.length >= 7) {
|
||||||
|
rows.push({ size: 6, item: array[0] });
|
||||||
|
rows.push({ size: 6, item: array[1] });
|
||||||
|
rows.push({ size: 12, item: array[2] });
|
||||||
|
rows.push({ size: 3, item: array[3] });
|
||||||
|
rows.push({ size: 3, item: array[4] });
|
||||||
|
rows.push({ size: 3, item: array[5] });
|
||||||
|
rows.push({ size: 3, item: array[6] });
|
||||||
|
array = array.slice(7);
|
||||||
|
} else if (array.length >= 3) {
|
||||||
|
rows.push({ size: 6, item: array[0] });
|
||||||
|
rows.push({ size: 6, item: array[1] });
|
||||||
|
rows.push({ size: 12, item: array[2] });
|
||||||
|
array = array.slice(3);
|
||||||
|
} else if (array.length >= 2) {
|
||||||
|
rows.push({ size: 6, item: array[0] });
|
||||||
|
rows.push({ size: 6, item: array[1] });
|
||||||
|
array = array.slice(2);
|
||||||
|
} else {
|
||||||
|
rows.push({ size: 12, item: array[0] });
|
||||||
|
array = array.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@/assets/css/chat.scss" as *;
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bubble {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageCollection {
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
src/components/messages/composition/MessageOutgoing.vue
Normal file
95
src/components/messages/composition/MessageOutgoing.vue
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<!-- BASE CLASS FOR OUTGOING MESSAGE -->
|
||||||
|
<div :class="messageClasses">
|
||||||
|
<div class="senderAndTime">
|
||||||
|
<div class="sender" v-if="room && room.displayType == ROOM_TYPE_CHANNEL">{{ eventSenderDisplayName(event) }}</div>
|
||||||
|
<div class="time">
|
||||||
|
{{ room.displayType == ROOM_TYPE_CHANNEL ? formatTimeAgo(event?.event.origin_server_ts) : formatTime(event?.event.origin_server_ts) }}
|
||||||
|
</div>
|
||||||
|
<div class="status">{{ event?.status }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="op-button" ref="opbutton" v-if="event && !event.isRedacted() && $matrix.userCanSendMessageInCurrentRoom">
|
||||||
|
<v-btn id="btn-show-menu" icon @click.stop="showContextMenu(opbutton)">
|
||||||
|
<v-icon>more_vert</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<div class="pin-icon" v-if="isPinned"><v-icon>$vuetify.icons.ic_pin_filled</v-icon></div>
|
||||||
|
|
||||||
|
<!-- SLOT FOR CONTENT -->
|
||||||
|
<span ref="messageInOutRef" class="content">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
<v-avatar
|
||||||
|
class="avatar"
|
||||||
|
size="32"
|
||||||
|
color="#ededed"
|
||||||
|
@click.stop="ownAvatarClicked"
|
||||||
|
>
|
||||||
|
<AuthedImage v-if="userAvatar" :src="userAvatar" />
|
||||||
|
<span v-else class="text-white headline">{{ userAvatarLetter }}</span>
|
||||||
|
</v-avatar>
|
||||||
|
<QuickReactionsChannel v-if="room.displayType == ROOM_TYPE_CHANNEL" :event="eventForReactions" :timelineSet="timelineSet" v-bind="$attrs"/>
|
||||||
|
<QuickReactions v-else :event="eventForReactions" :timelineSet="timelineSet" v-bind="$attrs"/>
|
||||||
|
<SeenBy v-if="room.displayType != ROOM_TYPE_CHANNEL" :room="room" :event="event"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SeenBy from "../SeenBy.vue";
|
||||||
|
import { MessageEmits, MessageProps, useMessage } from "./messageMixin";
|
||||||
|
import util, { ROOM_TYPE_CHANNEL } from "@/plugins/utils";
|
||||||
|
import QuickReactions from "../QuickReactions.vue";
|
||||||
|
import QuickReactionsChannel from "../channel/QuickReactionsChannel.vue";
|
||||||
|
import AuthedImage from "../../AuthedImage.vue";
|
||||||
|
import { inject, onMounted, ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const opbutton = ref(null);
|
||||||
|
const messageInOutRef = ref(null);
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const $matrix: any = inject('globalMatrix');
|
||||||
|
|
||||||
|
const props = defineProps<MessageProps>();
|
||||||
|
const emits = defineEmits<MessageEmits>();
|
||||||
|
|
||||||
|
const { room } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
event,
|
||||||
|
thread,
|
||||||
|
validEvent,
|
||||||
|
eventForReactions,
|
||||||
|
showSenderAndTime,
|
||||||
|
inReplyToSender,
|
||||||
|
inReplyToEvent,
|
||||||
|
inReplyToText,
|
||||||
|
messageText,
|
||||||
|
isPinned,
|
||||||
|
messageClasses,
|
||||||
|
userAvatar,
|
||||||
|
userAvatarLetter,
|
||||||
|
ownAvatarClicked,
|
||||||
|
otherAvatarClicked,
|
||||||
|
showContextMenu,
|
||||||
|
eventSenderDisplayName,
|
||||||
|
eventStateKeyDisplayName,
|
||||||
|
messageEventAvatar,
|
||||||
|
senderIsAdminOrModerator,
|
||||||
|
redactedBySomeoneElse,
|
||||||
|
formatTimeAgo,
|
||||||
|
formatTime,
|
||||||
|
linkify,
|
||||||
|
initMsgHammerJs,
|
||||||
|
} = useMessage($matrix, t, props, emits);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (util.isMobileOrTabletBrowser() && messageInOutRef.value && opbutton.value) {
|
||||||
|
initMsgHammerJs(messageInOutRef.value, opbutton.value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@/assets/css/chat.scss" as *;
|
||||||
|
</style>
|
||||||
218
src/components/messages/composition/MessageOutgoingThread.vue
Normal file
218
src/components/messages/composition/MessageOutgoingThread.vue
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
<template>
|
||||||
|
<MessageOutgoing
|
||||||
|
ref="root"
|
||||||
|
v-bind="{ ...$props, ...$attrs }"
|
||||||
|
v-if="showMultiview"
|
||||||
|
>
|
||||||
|
<div class="bubble">
|
||||||
|
<div class="original-message" v-if="inReplyToText">
|
||||||
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
|
<div class="original-message-text" v-html="linkify($$sanitize(inReplyToText))" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
<SwipeableThumbnailsView
|
||||||
|
:items="items"
|
||||||
|
v-if="event && !event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
<v-container v-else-if="event && !event.isRedacted()" fluid class="imageCollection">
|
||||||
|
<v-row wrap>
|
||||||
|
<v-col v-for="{ size, item } in layoutedItems" :key="item.event.getId()" :cols="size">
|
||||||
|
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
<i v-if="event && event.isRedacted()" class="deleted-text">
|
||||||
|
<v-icon size="small">block</v-icon>
|
||||||
|
{{
|
||||||
|
redactedBySomeoneElse(event)
|
||||||
|
? t("message.incoming_message_deleted_text")
|
||||||
|
: t("message.outgoing_message_deleted_text")
|
||||||
|
}}
|
||||||
|
</i>
|
||||||
|
<span v-html="linkify($$sanitize(messageText))" v-else-if="messageText" />
|
||||||
|
<span class="edit-marker" v-if="event && event.replacingEventId() && !event.isRedacted()">
|
||||||
|
{{ t("message.edited") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<GalleryItemsView
|
||||||
|
:originalEvent="originalEvent"
|
||||||
|
:items="items"
|
||||||
|
:initialItem="showItem"
|
||||||
|
v-if="!!showItem"
|
||||||
|
v-on:close="showItem = undefined"
|
||||||
|
/>
|
||||||
|
</MessageOutgoing>
|
||||||
|
<component
|
||||||
|
v-else-if="items.length == 1"
|
||||||
|
:is="componentFn(items[0].event)"
|
||||||
|
v-bind="{ ...$props, ...$attrs }"
|
||||||
|
:originalEvent="items[0].event"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||||
|
import { MessageEmits, MessageProps, useMessage } from "./messageMixin";
|
||||||
|
import util, { ROOM_TYPE_CHANNEL } from "@/plugins/utils";
|
||||||
|
import GalleryItemsView from "../../file_mode/GalleryItemsView.vue";
|
||||||
|
import ThumbnailView from "../../file_mode/ThumbnailView.vue";
|
||||||
|
import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.vue";
|
||||||
|
import { computed, inject, onBeforeUnmount, ref, Ref, watch } from "vue";
|
||||||
|
import { EventAttachment } from "../../../models/eventAttachment";
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const $matrix: any = inject('globalMatrix');
|
||||||
|
const $$sanitize: any = inject('globalSanitize');
|
||||||
|
|
||||||
|
const root = ref(undefined);
|
||||||
|
const emits = defineEmits<MessageEmits & {(event: "layout-change", value: {element: Element | undefined, action: () => void}): void}>();
|
||||||
|
|
||||||
|
const items: Ref<EventAttachment[]> = ref([]);
|
||||||
|
const showItem: Ref<EventAttachment | undefined> = ref(undefined);
|
||||||
|
|
||||||
|
const props = defineProps<MessageProps>();
|
||||||
|
|
||||||
|
const { room } = props;
|
||||||
|
|
||||||
|
const processThread = () => {
|
||||||
|
if (!event.value?.isRedacted()) {
|
||||||
|
emits("layout-change", {element: root.value, action: _processThread});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
event,
|
||||||
|
thread,
|
||||||
|
inReplyToSender,
|
||||||
|
inReplyToText,
|
||||||
|
messageText,
|
||||||
|
redactedBySomeoneElse,
|
||||||
|
linkify,
|
||||||
|
} = useMessage($matrix, t, props, emits, processThread);
|
||||||
|
|
||||||
|
watch(event, () => {
|
||||||
|
if (event.value) {
|
||||||
|
if (thread.value === undefined) {
|
||||||
|
thread.value = props.timelineSet.relations.getChildEventsForEvent(
|
||||||
|
event.value.getId() ?? "",
|
||||||
|
util.threadMessageType(),
|
||||||
|
"m.room.message"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!thread.value) {
|
||||||
|
event.value.on(MatrixEventEvent.RelationsCreated, onRelationsCreated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
event.value?.off(MatrixEventEvent.RelationsCreated, onRelationsCreated);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showMultiview = computed((): boolean => {
|
||||||
|
return items.value?.length > 1 ||
|
||||||
|
(event.value && event.value.isRedacted()) ||
|
||||||
|
(props.room.displayType == ROOM_TYPE_CHANNEL && items.value.length == 1 && util.isFileTypePDF(items.value[0].event)) ||
|
||||||
|
messageText.value?.length > 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const onRelationsCreated = () => {
|
||||||
|
if (event.value) {
|
||||||
|
thread.value = props.timelineSet.relations.getChildEventsForEvent(
|
||||||
|
event.value.getId() ?? "",
|
||||||
|
util.threadMessageType(),
|
||||||
|
"m.room.message"
|
||||||
|
);
|
||||||
|
event.value.off(MatrixEventEvent.RelationsCreated, onRelationsCreated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onItemClick = (event: any) => {
|
||||||
|
showItem.value = event.item;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _processThread = () => {
|
||||||
|
const eventItems = props.timelineSet.relations
|
||||||
|
.getAllChildEventsForEvent(event.value?.getId() ?? "")
|
||||||
|
.filter((e: MatrixEvent) => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype));
|
||||||
|
|
||||||
|
items.value = eventItems.map((e: MatrixEvent) => {
|
||||||
|
let ea = $matrix.attachmentManager.getEventAttachment(e);
|
||||||
|
if (showMultiview.value) {
|
||||||
|
ea.loadThumbnail();
|
||||||
|
}
|
||||||
|
return ea;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const layoutedItems = computed(() => {
|
||||||
|
if (!items.value || items.value.length == 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let array = items.value.slice(0);
|
||||||
|
let rows = [];
|
||||||
|
while (array.length > 0) {
|
||||||
|
if (array.length >= 7) {
|
||||||
|
rows.push({ size: 6, item: array[0] });
|
||||||
|
rows.push({ size: 6, item: array[1] });
|
||||||
|
rows.push({ size: 12, item: array[2] });
|
||||||
|
rows.push({ size: 3, item: array[3] });
|
||||||
|
rows.push({ size: 3, item: array[4] });
|
||||||
|
rows.push({ size: 3, item: array[5] });
|
||||||
|
rows.push({ size: 3, item: array[6] });
|
||||||
|
array = array.slice(7);
|
||||||
|
} else if (array.length >= 3) {
|
||||||
|
rows.push({ size: 6, item: array[0] });
|
||||||
|
rows.push({ size: 6, item: array[1] });
|
||||||
|
rows.push({ size: 12, item: array[2] });
|
||||||
|
array = array.slice(3);
|
||||||
|
} else if (array.length >= 2) {
|
||||||
|
rows.push({ size: 6, item: array[0] });
|
||||||
|
rows.push({ size: 6, item: array[1] });
|
||||||
|
array = array.slice(2);
|
||||||
|
} else {
|
||||||
|
rows.push({ size: 12, item: array[0] });
|
||||||
|
array = array.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@/assets/css/chat.scss" as *;
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bubble {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageCollection {
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
384
src/components/messages/composition/messageMixin.ts
Normal file
384
src/components/messages/composition/messageMixin.ts
Normal file
|
|
@ -0,0 +1,384 @@
|
||||||
|
import * as linkify from "linkifyjs";
|
||||||
|
import linkifyHtml from "linkify-html";
|
||||||
|
import utils from "@/plugins/utils";
|
||||||
|
import Hammer from "hammerjs";
|
||||||
|
|
||||||
|
linkify.options.defaults.className = "link";
|
||||||
|
linkify.options.defaults.target = { url: "_blank" };
|
||||||
|
|
||||||
|
import { computed, onBeforeUnmount, Ref, ref, watch } from "vue";
|
||||||
|
import { EventTimelineSet, Relations, RelationsEvent } from "matrix-js-sdk";
|
||||||
|
import { KeanuEvent, KeanuRoom } from "../../../models/eventAttachment";
|
||||||
|
|
||||||
|
export interface MessageProps {
|
||||||
|
room: KeanuRoom;
|
||||||
|
originalEvent: KeanuEvent;
|
||||||
|
nextEvent: KeanuEvent | null | undefined;
|
||||||
|
timelineSet: EventTimelineSet;
|
||||||
|
componentFn: (event: KeanuEvent) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageEmits = {
|
||||||
|
(event: "ownAvatarClicked", value: { event: KeanuEvent }): void;
|
||||||
|
(event: "otherAvatarClicked", value: { event: KeanuEvent; anchor: any }): void;
|
||||||
|
(event: "contextMenu", value: { event: KeanuEvent; anchor: any }): void;
|
||||||
|
(event: "addQuickHeartReaction", value: { position: { top: string; left: string } }): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMessage = (
|
||||||
|
$matrix: any,
|
||||||
|
$t: any,
|
||||||
|
props: MessageProps,
|
||||||
|
emits: MessageEmits,
|
||||||
|
processThread?: () => void
|
||||||
|
) => {
|
||||||
|
const event: Ref<KeanuEvent | undefined> = ref(undefined);
|
||||||
|
const thread: Ref<Relations | undefined> = ref(undefined);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
thread.value = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
props.originalEvent,
|
||||||
|
(originalEvent) => {
|
||||||
|
event.value = originalEvent;
|
||||||
|
|
||||||
|
// Check not null and not {}
|
||||||
|
if (originalEvent && originalEvent.isBeingDecrypted && originalEvent.isBeingDecrypted()) {
|
||||||
|
originalEvent.getDecryptionPromise()?.then(() => {
|
||||||
|
event.value = originalEvent;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
thread,
|
||||||
|
(newValue, oldValue) => {
|
||||||
|
if (oldValue) {
|
||||||
|
oldValue.off(RelationsEvent.Add, onAddRelation);
|
||||||
|
}
|
||||||
|
if (newValue) {
|
||||||
|
newValue.on(RelationsEvent.Add, onAddRelation);
|
||||||
|
if (processThread) {
|
||||||
|
processThread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns true if event is non-null and contains data
|
||||||
|
*/
|
||||||
|
const validEvent = computed(() => {
|
||||||
|
return event.value !== undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is a thread event, we return the root here, so all reactions will land on the root event.
|
||||||
|
*/
|
||||||
|
const eventForReactions = computed(() => {
|
||||||
|
if (event.value && event.value.parentThread) {
|
||||||
|
return event.value.parentThread;
|
||||||
|
}
|
||||||
|
return event.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const incoming = computed(() => {
|
||||||
|
return event.value && event.value.getSender() != $matrix.currentUserId;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't show sender and time if the next event is within 2 minutes and also from us (= back to back messages)
|
||||||
|
*/
|
||||||
|
const showSenderAndTime = computed(() => {
|
||||||
|
if (!isPinned.value && props.nextEvent && props.nextEvent.getSender() == event.value?.getSender()) {
|
||||||
|
const ts1 = props.nextEvent.event.origin_server_ts ?? 0;
|
||||||
|
const ts2 = event.value!.event.origin_server_ts ?? 0;
|
||||||
|
return ts1 - ts2 < 2 * 60 * 1000; // less than 2 minutes
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inReplyToSender = computed(() => {
|
||||||
|
const originalEvent = validEvent.value && event.value?.replyEvent;
|
||||||
|
if (originalEvent) {
|
||||||
|
const sender = eventSenderDisplayName(originalEvent);
|
||||||
|
if (originalEvent.isThreadRoot || originalEvent.isMxThread) {
|
||||||
|
return sender || $t("message.someone");
|
||||||
|
} else {
|
||||||
|
return $t("message.user_said", { user: sender || $t("message.someone") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inReplyToEvent = computed(() => {
|
||||||
|
return event.value?.replyEvent;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inReplyToText = computed(() => {
|
||||||
|
const relatesTo = event.value?.getWireContent()["m.relates_to"];
|
||||||
|
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
||||||
|
if (inReplyToEvent.value && (inReplyToEvent.value.isThreadRoot || inReplyToEvent.value.isMxThread)) {
|
||||||
|
const children = props.timelineSet.relations
|
||||||
|
.getAllChildEventsForEvent(inReplyToEvent.value.getId()!)
|
||||||
|
.filter((e) => utils.downloadableTypes().includes(e.getContent().msgtype));
|
||||||
|
return $t("message.sent_media", { count: children.length });
|
||||||
|
}
|
||||||
|
const content = event.value?.getContent();
|
||||||
|
if (content && content.body) {
|
||||||
|
const lines = content.body.split("\n").reverse() || [];
|
||||||
|
while (lines.length && !lines[0].startsWith("> ")) lines.shift();
|
||||||
|
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||||
|
if (lines[0] === "") lines.shift();
|
||||||
|
const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, "");
|
||||||
|
if (text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inReplyToEvent.value) {
|
||||||
|
var c = inReplyToEvent.value.getContent();
|
||||||
|
return c.body;
|
||||||
|
}
|
||||||
|
// We don't have the original text (at the moment at least)
|
||||||
|
return $t("fallbacks.original_text");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageText = computed(() => {
|
||||||
|
const relatesTo = event.value?.getWireContent()["m.relates_to"];
|
||||||
|
if (event.value && relatesTo && relatesTo["m.in_reply_to"]) {
|
||||||
|
const content = event.value.getContent();
|
||||||
|
if ("body" in content) {
|
||||||
|
// Remove the new text and strip "> " from the old original text
|
||||||
|
const lines = content.body.split("\n");
|
||||||
|
while (lines.length && lines[0].startsWith("> ")) lines.shift();
|
||||||
|
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||||
|
if (lines[0] === "") lines.shift();
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return event.value?.getContent().body;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPinned = computed(() => {
|
||||||
|
return event.value && event.value.parentThread ? event.value.parentThread.isPinned : event.value?.isPinned || false;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classes to set for the message. Currently only for "messageIn"
|
||||||
|
*/
|
||||||
|
|
||||||
|
const messageClasses = computed(() => {
|
||||||
|
if (incoming.value) {
|
||||||
|
return { messageIn: true, "from-admin": senderIsAdminOrModerator(event.value), pinned: isPinned.value };
|
||||||
|
} else {
|
||||||
|
return { messageOut: true, pinned: isPinned.value };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const userAvatar = computed(() => {
|
||||||
|
if (!$matrix.userAvatar) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $matrix.matrixClient.mxcUrlToHttp(
|
||||||
|
$matrix.userAvatar,
|
||||||
|
80,
|
||||||
|
80,
|
||||||
|
"scale",
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
$matrix.useAuthedMedia
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const userAvatarLetter = computed(() => {
|
||||||
|
if (!$matrix.currentUser) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ($matrix.currentUserDisplayName || $matrix.currentUserId.substring(1)).substring(0, 1).toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onAddRelation = () => {
|
||||||
|
if (processThread) {
|
||||||
|
processThread();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ownAvatarClicked = () => {
|
||||||
|
if (event.value) {
|
||||||
|
emits("ownAvatarClicked", { event: event.value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const otherAvatarClicked = (avatarRef: any) => {
|
||||||
|
if (event.value) {
|
||||||
|
emits("otherAvatarClicked", { event: event.value, anchor: avatarRef });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showContextMenu = (buttonRef: any) => {
|
||||||
|
if (event.value) {
|
||||||
|
emits("contextMenu", { event: event.value, anchor: buttonRef });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a display name given an event.
|
||||||
|
*/
|
||||||
|
const eventSenderDisplayName = (e: KeanuEvent | undefined) => {
|
||||||
|
if (e?.getSender() === $matrix.currentUserId) {
|
||||||
|
return $t("message.you");
|
||||||
|
}
|
||||||
|
if (e && props.room) {
|
||||||
|
const sender = e.getSender();
|
||||||
|
const member = sender ? props.room.getMember(sender) : undefined;
|
||||||
|
if (member) {
|
||||||
|
return member.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e?.getContent().displayname || e?.getSender();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the case where the state_key points out a userId for an operation (e.g. membership events)
|
||||||
|
* return the display name of the affected user.
|
||||||
|
* @param event
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const eventStateKeyDisplayName = (e: KeanuEvent | undefined) => {
|
||||||
|
if (e?.getStateKey() === $matrix.currentUserId) {
|
||||||
|
return $t("message.you");
|
||||||
|
}
|
||||||
|
if (e && props.room) {
|
||||||
|
const key = e.getStateKey();
|
||||||
|
const member = key ? props.room.getMember(key) : undefined;
|
||||||
|
if (member) {
|
||||||
|
return member.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e?.getStateKey();
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageEventAvatar = (e: KeanuEvent | undefined) => {
|
||||||
|
if (e && props.room) {
|
||||||
|
const sender = e.getSender();
|
||||||
|
const member = sender ? props.room.getMember(sender) : undefined;
|
||||||
|
if (member) {
|
||||||
|
return member.getAvatarUrl(
|
||||||
|
$matrix.matrixClient.getHomeserverUrl(),
|
||||||
|
40,
|
||||||
|
40,
|
||||||
|
"scale",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
$matrix.useAuthedMedia
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the event sender has a powel level > 0, e.g. is moderator or admin of some sort.
|
||||||
|
*/
|
||||||
|
const senderIsAdminOrModerator = (e: KeanuEvent | undefined) => {
|
||||||
|
if (e && props.room) {
|
||||||
|
const sender = e.getSender();
|
||||||
|
const member = sender ? props.room.getMember(sender) : undefined;
|
||||||
|
if (member) {
|
||||||
|
return member.powerLevel > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const redactedBySomeoneElse = (e: KeanuEvent | undefined) => {
|
||||||
|
if (!e || !e.isRedacted()) return false;
|
||||||
|
const redactionEvent = e.getUnsigned().redacted_because;
|
||||||
|
if (redactionEvent) {
|
||||||
|
return redactionEvent.sender !== $matrix.currentUserId;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimeAgo = (time: number | undefined) => {
|
||||||
|
if (!time) return "";
|
||||||
|
const date = new Date();
|
||||||
|
date.setTime(time);
|
||||||
|
var ti = Math.abs(new Date().getTime() - date.getTime());
|
||||||
|
ti = ti / 1000; // Convert to seconds
|
||||||
|
let s = "";
|
||||||
|
if (ti < 60) {
|
||||||
|
s = $t("global.time.recently");
|
||||||
|
} else if (ti < 3600 && Math.round(ti / 60) < 60) {
|
||||||
|
s = $t("global.time.minutes", Math.round(ti / 60));
|
||||||
|
} else if (ti < 86400 && Math.round(ti / 60 / 60) < 24) {
|
||||||
|
s = $t("global.time.hours", Math.round(ti / 60 / 60));
|
||||||
|
} else {
|
||||||
|
s = $t("global.time.days", Math.round(ti / 60 / 60 / 24));
|
||||||
|
}
|
||||||
|
return utils.toLocalNumbers(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (time: number | undefined) => {
|
||||||
|
if (!time) return "";
|
||||||
|
return utils.formatTime(time);
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkify = (text: string) => {
|
||||||
|
return linkifyHtml(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mc: Ref<Hammer | undefined> = ref(undefined);
|
||||||
|
const mcCustom: Ref<Hammer.Manager | undefined> = ref(undefined);
|
||||||
|
|
||||||
|
const initMsgHammerJs = (element: Element, opbutton: Element) => {
|
||||||
|
mc.value = new Hammer(element);
|
||||||
|
mcCustom.value = new Hammer.Manager(element);
|
||||||
|
mcCustom.value.add(new Hammer.Tap({ event: "doubletap", taps: 2 }));
|
||||||
|
mcCustom.value.on("doubletap", (evt: Hammer.HammerInput) => {
|
||||||
|
var { top, left } = evt.target.getBoundingClientRect();
|
||||||
|
var position = { top: `${top}px`, left: `${left}px` };
|
||||||
|
emits("addQuickHeartReaction", { position });
|
||||||
|
});
|
||||||
|
mc.value.on("press", () => {
|
||||||
|
showContextMenu(opbutton);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
event,
|
||||||
|
thread,
|
||||||
|
|
||||||
|
validEvent,
|
||||||
|
eventForReactions,
|
||||||
|
showSenderAndTime,
|
||||||
|
inReplyToSender,
|
||||||
|
inReplyToEvent,
|
||||||
|
inReplyToText,
|
||||||
|
messageText,
|
||||||
|
isPinned,
|
||||||
|
messageClasses,
|
||||||
|
userAvatar,
|
||||||
|
userAvatarLetter,
|
||||||
|
ownAvatarClicked,
|
||||||
|
otherAvatarClicked,
|
||||||
|
showContextMenu,
|
||||||
|
eventSenderDisplayName,
|
||||||
|
eventStateKeyDisplayName,
|
||||||
|
messageEventAvatar,
|
||||||
|
senderIsAdminOrModerator,
|
||||||
|
redactedBySomeoneElse,
|
||||||
|
formatTimeAgo,
|
||||||
|
formatTime,
|
||||||
|
linkify,
|
||||||
|
initMsgHammerJs,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -75,8 +75,8 @@ export default {
|
||||||
}
|
}
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
newValue.on("Relations.add", this.onAddRelation);
|
newValue.on("Relations.add", this.onAddRelation);
|
||||||
|
this.processThread();
|
||||||
}
|
}
|
||||||
this.processThread();
|
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
import util from "../plugins/utils";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
sendStatuses: Object.freeze({
|
|
||||||
INITIAL: 0,
|
|
||||||
SENDING: 1,
|
|
||||||
SENT: 2,
|
|
||||||
CANCELED: 3,
|
|
||||||
FAILED: 4,
|
|
||||||
}),
|
|
||||||
sendingStatus: 0,
|
|
||||||
sendingPromise: null,
|
|
||||||
sendingRootEventId: null,
|
|
||||||
sendingAttachments: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
attachmentsSentCount() {
|
|
||||||
return this.sendingAttachments ? this.sendingAttachments.reduce((a, elem, ignoredidx, ignoredarray) => elem.status == this.sendStatuses.SENT ? a + 1 : a, 0) : 0
|
|
||||||
},
|
|
||||||
attachmentsSending() {
|
|
||||||
return this.sendingAttachments ? this.sendingAttachments.filter(elem => elem.status == this.sendStatuses.INITIAL || elem.status == this.sendStatuses.SENDING) : []
|
|
||||||
},
|
|
||||||
attachmentsSent() {
|
|
||||||
this.sortSendingAttachments();
|
|
||||||
return this.sendingAttachments ? this.sendingAttachments.filter(elem => elem.status == this.sendStatuses.SENT) : []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
sendAttachments(text, attachments) {
|
|
||||||
this.sendingStatus = this.sendStatuses.SENDING;
|
|
||||||
|
|
||||||
this.sendingAttachments = attachments.map((attachment) => {
|
|
||||||
let file = (() => {
|
|
||||||
// other than file type image
|
|
||||||
if(attachment instanceof File) {
|
|
||||||
return attachment;
|
|
||||||
} else {
|
|
||||||
if (attachment.scaled && attachment.useScaled) {
|
|
||||||
// Send scaled version of image instead!
|
|
||||||
return attachment.scaled;
|
|
||||||
} else {
|
|
||||||
// Send actual file image when not scaled!
|
|
||||||
return attachment.actualFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
let sendInfo = {
|
|
||||||
id: attachment.name,
|
|
||||||
status: this.sendStatuses.INITIAL,
|
|
||||||
statusDate: Date.now,
|
|
||||||
mediaEventId: undefined,
|
|
||||||
attachment: file,
|
|
||||||
preview: attachment.image,
|
|
||||||
progress: 0,
|
|
||||||
randomRotation: 0,
|
|
||||||
randomTranslationX: 0,
|
|
||||||
randomTranslationY: 0
|
|
||||||
};
|
|
||||||
attachment.sendInfo = sendInfo;
|
|
||||||
return sendInfo;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sendingPromise = util.sendTextMessage(this.$matrix.matrixClient, this.room.roomId, text)
|
|
||||||
.then((eventId) => {
|
|
||||||
this.sendingRootEventId = eventId;
|
|
||||||
|
|
||||||
// Use the eventId as a thread root for all the media
|
|
||||||
let promiseChain = Promise.resolve();
|
|
||||||
const getItemPromise = (index) => {
|
|
||||||
if (index < this.sendingAttachments.length) {
|
|
||||||
const item = this.sendingAttachments[index];
|
|
||||||
if (item.status !== this.sendStatuses.INITIAL) {
|
|
||||||
return getItemPromise(++index);
|
|
||||||
}
|
|
||||||
item.status = this.sendStatuses.SENDING;
|
|
||||||
const itemPromise = util.sendFile(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => {
|
|
||||||
if (loaded == total) {
|
|
||||||
item.progress = 100;
|
|
||||||
} else if (total > 0) {
|
|
||||||
item.progress = 100 * loaded / total;
|
|
||||||
}
|
|
||||||
}, eventId)
|
|
||||||
.then((mediaEventId) => {
|
|
||||||
// Look at last item rotation, flipping the sign on this, so looks more like a true stack
|
|
||||||
let signR = 1;
|
|
||||||
let signX = 1;
|
|
||||||
let signY = 1;
|
|
||||||
if (this.attachmentsSent.length > 0) {
|
|
||||||
if (this.attachmentsSent[0].randomRotation >= 0) {
|
|
||||||
signR = -1;
|
|
||||||
}
|
|
||||||
if (this.attachmentsSent[0].randomTranslationX >= 0) {
|
|
||||||
signX = -1;
|
|
||||||
}
|
|
||||||
if (this.attachmentsSent[0].randomTranslationY >= 0) {
|
|
||||||
signY = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item.randomRotation = signR * (2 + Math.random() * 10);
|
|
||||||
item.randomTranslationX = signX * Math.random() * 20;
|
|
||||||
item.randomTranslationY = signY * Math.random() * 20;
|
|
||||||
item.mediaEventId = mediaEventId;
|
|
||||||
item.status = this.sendStatuses.SENT;
|
|
||||||
item.statusDate = Date.now;
|
|
||||||
}).catch(ignorederr => {
|
|
||||||
if (item.promise.aborted) {
|
|
||||||
item.status = this.sendStatuses.CANCELED;
|
|
||||||
} else {
|
|
||||||
console.error("ERROR", ignorederr);
|
|
||||||
item.status = this.sendStatuses.FAILED;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
item.promise = itemPromise;
|
|
||||||
return itemPromise.then(() => getItemPromise(++index));
|
|
||||||
}
|
|
||||||
else return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
return promiseChain.then(() => getItemPromise(0));
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.sendingStatus = this.sendStatuses.SENT;
|
|
||||||
this.sendingRootEventId = null;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("ERROR", err);
|
|
||||||
});
|
|
||||||
return this.sendingPromise;
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelSendAttachments() {
|
|
||||||
this.sendingAttachments.toReversed().forEach(item => {
|
|
||||||
this.cancelSendAttachmentItem(item);
|
|
||||||
});
|
|
||||||
this.sendingStatus = this.sendStatuses.CANCELED;
|
|
||||||
if (this.sendingRootEventId && this.room) {
|
|
||||||
|
|
||||||
// Redact all media we already sent, plus the root event
|
|
||||||
let promises = this.sendingAttachments.filter((item) => item.mediaEventId !== undefined).map((item) => this.$matrix.matrixClient.redactEvent(this.room.roomId, item.mediaEventId, undefined, { reason: "cancel" }));
|
|
||||||
promises.push(this.$matrix.matrixClient.redactEvent(this.room.roomId, this.sendingRootEventId, undefined, { reason: "cancel" }));
|
|
||||||
Promise.allSettled(promises)
|
|
||||||
.then(() => {
|
|
||||||
console.log("Message redacted");
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("Redaction failed: ", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelSendAttachmentItem(item) {
|
|
||||||
if (item.promise && item.status != this.sendStatuses.INITIAL) {
|
|
||||||
item.promise.abort();
|
|
||||||
}
|
|
||||||
item.status = this.sendStatuses.CANCELED;
|
|
||||||
},
|
|
||||||
|
|
||||||
sortSendingAttachments() {
|
|
||||||
this.sendingAttachments.sort((a, b) => b.statusDate - a.statusDate);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
src/main.js
24
src/main.js
|
|
@ -41,27 +41,6 @@ app.use(analytics);
|
||||||
app.use(VueClipboard);
|
app.use(VueClipboard);
|
||||||
app.use(audioPlayer);
|
app.use(audioPlayer);
|
||||||
|
|
||||||
// Add bubble functionality to custom events.
|
|
||||||
// From here: https://stackoverflow.com/questions/41993508/vuejs-bubbling-custom-events
|
|
||||||
app.use((instance) => {
|
|
||||||
instance.$bubble = function $bubble(eventName, ...args) {
|
|
||||||
// Emit the event on all parent components
|
|
||||||
let component = this;
|
|
||||||
let arg = args.at(0);
|
|
||||||
let stop = false;
|
|
||||||
if (arg) {
|
|
||||||
// Add a "stopPropagation" function so that we can do v-on:<eventname>.stop="..."
|
|
||||||
arg.stopPropagation = () => {
|
|
||||||
stop = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
component.$emit(eventName, ... args);
|
|
||||||
component = component.$parent;
|
|
||||||
} while (!stop && component);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register a global custom directive called `v-blur` that prevents focus
|
// Register a global custom directive called `v-blur` that prevents focus
|
||||||
app.directive('blur', {
|
app.directive('blur', {
|
||||||
mounted: function (el) {
|
mounted: function (el) {
|
||||||
|
|
@ -182,6 +161,9 @@ app.use(i18n);
|
||||||
app.$i18n = i18n;
|
app.$i18n = i18n;
|
||||||
app.config.globalProperties.$i18n = i18n;
|
app.config.globalProperties.$i18n = i18n;
|
||||||
|
|
||||||
|
app.provide("globalT", i18n.global.t);
|
||||||
|
app.provide("globalSanitize", app.config.globalProperties.$sanitize);
|
||||||
|
|
||||||
app.use(matrix, { store: store, i18n: i18n });
|
app.use(matrix, { store: store, i18n: i18n });
|
||||||
|
|
||||||
// Set $matrix inside data store
|
// Set $matrix inside data store
|
||||||
|
|
|
||||||
71
src/models/attachment.ts
Normal file
71
src/models/attachment.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { ComputedRef, Ref } from "vue";
|
||||||
|
|
||||||
|
export class UploadPromise<Type> {
|
||||||
|
wrappedPromise: Promise<Type>;
|
||||||
|
aborted: boolean = false;
|
||||||
|
onAbort: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
|
constructor(wrappedPromise: Promise<Type>) {
|
||||||
|
this.wrappedPromise = wrappedPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
this.aborted = true;
|
||||||
|
if (this.onAbort) {
|
||||||
|
this.onAbort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
then(resolve: any, reject: any) {
|
||||||
|
this.wrappedPromise = this.wrappedPromise.then(resolve, reject);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
catch(handler: any) {
|
||||||
|
this.wrappedPromise = this.wrappedPromise.catch(handler);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AttachmentSendStatus = "initial" | "sending" | "sent" | "canceled" | "failed";
|
||||||
|
|
||||||
|
export type AttachmentSendInfo = {
|
||||||
|
status: AttachmentSendStatus;
|
||||||
|
statusDate: number; //ms
|
||||||
|
mediaEventId: string | undefined;
|
||||||
|
progress: number;
|
||||||
|
promise: UploadPromise<string> | undefined;
|
||||||
|
randomRotation: number; // For UI effects
|
||||||
|
randomTranslationX: number; // For UI effects
|
||||||
|
randomTranslationY: number; // For UI effects
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Attachment = {
|
||||||
|
status: "loading" | "loaded";
|
||||||
|
file: File;
|
||||||
|
dimensions?: { width: number; height: number };
|
||||||
|
scaledFile?: File;
|
||||||
|
scaledDimensions?: { width: number; height: number };
|
||||||
|
useScaled: boolean;
|
||||||
|
src?: string;
|
||||||
|
proof?: any;
|
||||||
|
sendInfo?: AttachmentSendInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AttachmentBatch = {
|
||||||
|
sendingStatus: Ref<"initial" | "sending" | "sent" | "canceled" | "failed">;
|
||||||
|
sendingRootEventId: Ref<string | undefined>;
|
||||||
|
sendingPromise: Ref<Promise<any> | undefined>;
|
||||||
|
attachments: Ref<Attachment[]>;
|
||||||
|
attachmentsSentCount: ComputedRef<number>;
|
||||||
|
attachmentsSending: ComputedRef<Attachment[]>;
|
||||||
|
attachmentsSent: ComputedRef<Attachment[]>;
|
||||||
|
addAttachment: (attachment: Attachment) => void;
|
||||||
|
removeAttachment: (attachment: Attachment) => void;
|
||||||
|
isTooLarge: (attachment: Attachment) => boolean;
|
||||||
|
canSend: ComputedRef<boolean>;
|
||||||
|
send: (message: string) => Promise<any>;
|
||||||
|
cancel: () => void;
|
||||||
|
cancelSendAttachment: (attachment: Attachment) => void;
|
||||||
|
};
|
||||||
|
|
||||||
517
src/models/attachmentManager.ts
Normal file
517
src/models/attachmentManager.ts
Normal file
|
|
@ -0,0 +1,517 @@
|
||||||
|
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk";
|
||||||
|
import { EventAttachment, KeanuEventExtension } from "./eventAttachment";
|
||||||
|
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
|
import { Counter, ModeOfOperation } from "aes-js";
|
||||||
|
import { Attachment, AttachmentBatch, AttachmentSendInfo } from "./attachment";
|
||||||
|
import proofmode from "../plugins/proofmode";
|
||||||
|
import imageSize from "image-size";
|
||||||
|
import imageResize from "image-resize";
|
||||||
|
import { computed, isRef, Reactive, reactive, ref, Ref } from "vue";
|
||||||
|
import utils from "@/plugins/utils";
|
||||||
|
|
||||||
|
export class AttachmentManager {
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
useAuthedMedia: boolean;
|
||||||
|
maxSizeUploads: number;
|
||||||
|
maxSizeAutoDownloads: number;
|
||||||
|
|
||||||
|
cache: Map<string | undefined, Reactive<EventAttachment>>;
|
||||||
|
|
||||||
|
constructor(matrixClient: MatrixClient, useAuthedMedia: boolean, maxSizeAutoDownloads: number) {
|
||||||
|
this.matrixClient = matrixClient;
|
||||||
|
this.useAuthedMedia = useAuthedMedia;
|
||||||
|
this.maxSizeUploads = 0;
|
||||||
|
this.maxSizeAutoDownloads = maxSizeAutoDownloads;
|
||||||
|
|
||||||
|
this.cache = new Map();
|
||||||
|
|
||||||
|
// Get max upload size
|
||||||
|
this.matrixClient
|
||||||
|
.getMediaConfig(useAuthedMedia)
|
||||||
|
.then((config) => {
|
||||||
|
this.maxSizeUploads = config["m.upload.size"] ?? 0;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public createUpload(room: Room) {
|
||||||
|
return createUploadBatch(this.matrixClient, room, this.maxSizeUploads);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createAttachment(file: File): Attachment {
|
||||||
|
let a: Attachment = {
|
||||||
|
status: "loading",
|
||||||
|
file: file,
|
||||||
|
useScaled: false,
|
||||||
|
};
|
||||||
|
const ra = reactive(a);
|
||||||
|
this.prepareUpload(ra);
|
||||||
|
return ra;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepareUpload(attachment: Attachment): Promise<Attachment> {
|
||||||
|
const file = attachment.file;
|
||||||
|
if (file.type.startsWith("image/")) {
|
||||||
|
attachment.proof = await proofmode.proofCheckFile(file);
|
||||||
|
|
||||||
|
var reader = new FileReader();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
attachment.src = (evt.target?.result as string) ?? undefined;
|
||||||
|
if (attachment.src) {
|
||||||
|
try {
|
||||||
|
const buffer = Uint8Array.from(window.atob(attachment.src.replace(/^data[^,]+,/, "")), (v) =>
|
||||||
|
v.charCodeAt(0)
|
||||||
|
);
|
||||||
|
attachment.dimensions = imageSize(buffer);
|
||||||
|
|
||||||
|
// Need to resize?
|
||||||
|
const w = attachment.dimensions.width;
|
||||||
|
const h = attachment.dimensions.height;
|
||||||
|
if (w > 640 || h > 640) {
|
||||||
|
var aspect = w / h;
|
||||||
|
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
|
||||||
|
var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
|
||||||
|
imageResize(attachment.src, {
|
||||||
|
format: "webp",
|
||||||
|
width: newWidth,
|
||||||
|
height: newHeight,
|
||||||
|
outputType: "blob",
|
||||||
|
})
|
||||||
|
.then((img) => {
|
||||||
|
attachment.scaledFile = new File([img as BlobPart], file.name, {
|
||||||
|
type: "image/webp",
|
||||||
|
lastModified: Date.now(),
|
||||||
|
});
|
||||||
|
attachment.scaledDimensions = {
|
||||||
|
width: newWidth,
|
||||||
|
height: newHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use scaled version if the image does not contain C2PA
|
||||||
|
attachment.useScaled =
|
||||||
|
attachment.scaledFile !== undefined &&
|
||||||
|
(attachment.proof === undefined ||
|
||||||
|
attachment.proof.integrity === undefined ||
|
||||||
|
attachment.proof.integrity.c2pa === undefined);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Resize failed:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get image dimensions: " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment.status = "loaded";
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEventAttachment(event: MatrixEvent & KeanuEventExtension): Reactive<EventAttachment> {
|
||||||
|
let entry = this.cache.get(event.getId());
|
||||||
|
if (entry !== undefined) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
const attachment: Reactive<EventAttachment> = reactive({
|
||||||
|
event: event,
|
||||||
|
srcProgress: -1,
|
||||||
|
thumbnailProgress: -1,
|
||||||
|
loadSrc: () => Promise.reject("Not implemented"),
|
||||||
|
loadThumbnail: () => Promise.reject("Not implemented"),
|
||||||
|
release: () => Promise.reject("Not implemented"),
|
||||||
|
});
|
||||||
|
attachment.loadSrc = () => {
|
||||||
|
if (attachment.src) {
|
||||||
|
return Promise.resolve(attachment.src);
|
||||||
|
} else if (attachment.srcPromise) {
|
||||||
|
return attachment.srcPromise;
|
||||||
|
}
|
||||||
|
attachment.srcPromise = this._loadEventAttachmentOrThumbnail(event, false, (percent) => {
|
||||||
|
attachment.srcProgress = percent;
|
||||||
|
}).then((src) => {
|
||||||
|
attachment.src = src;
|
||||||
|
return src;
|
||||||
|
});
|
||||||
|
return attachment.srcPromise;
|
||||||
|
};
|
||||||
|
attachment.loadThumbnail = () => {
|
||||||
|
if (attachment.thumbnail) {
|
||||||
|
return Promise.resolve(attachment.thumbnail);
|
||||||
|
} else if (attachment.thumbnailPromise) {
|
||||||
|
return attachment.thumbnailPromise;
|
||||||
|
}
|
||||||
|
attachment.thumbnailPromise = this._loadEventAttachmentOrThumbnail(event, true, (percent) => {
|
||||||
|
attachment.thumbnailProgress = percent;
|
||||||
|
}).then((thummbnail) => {
|
||||||
|
attachment.thumbnail = thummbnail;
|
||||||
|
return thummbnail;
|
||||||
|
});
|
||||||
|
return attachment.thumbnailPromise;
|
||||||
|
};
|
||||||
|
attachment.release = (src: boolean, thumbnail: boolean) => {
|
||||||
|
// TODO - figure out logic
|
||||||
|
if (entry) {
|
||||||
|
// TODO - abortable promises
|
||||||
|
this.cache.delete(event.getId());
|
||||||
|
if (attachment.src) {
|
||||||
|
URL.revokeObjectURL(attachment.src);
|
||||||
|
}
|
||||||
|
if (attachment.thumbnail) {
|
||||||
|
URL.revokeObjectURL(attachment.thumbnail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.cache.set(event.getId(), attachment!);
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadEventAttachmentOrThumbnail(
|
||||||
|
event: MatrixEvent & KeanuEventExtension,
|
||||||
|
thumbnail: boolean,
|
||||||
|
progress?: (percent: number) => void
|
||||||
|
): Promise<string> {
|
||||||
|
await this.matrixClient.decryptEventIfNeeded(event);
|
||||||
|
|
||||||
|
const content = event.getContent();
|
||||||
|
var url = null;
|
||||||
|
var mime = "image/png";
|
||||||
|
var file = null;
|
||||||
|
let decrypt = true;
|
||||||
|
if (thumbnail && !!content.info && !!content.info.thumbnail_url) {
|
||||||
|
url = this.matrixClient.mxcUrlToHttp(
|
||||||
|
content.info.thumbnail_url,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
this.useAuthedMedia
|
||||||
|
);
|
||||||
|
decrypt = false;
|
||||||
|
if (content.info.thumbnail_info) {
|
||||||
|
mime = content.info.thumbnail_info.mimetype;
|
||||||
|
}
|
||||||
|
} else if (content.url != null) {
|
||||||
|
url = this.matrixClient.mxcUrlToHttp(
|
||||||
|
content.url,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
this.useAuthedMedia
|
||||||
|
);
|
||||||
|
decrypt = false;
|
||||||
|
if (content.info) {
|
||||||
|
mime = content.info.mimetype;
|
||||||
|
}
|
||||||
|
} else if (thumbnail && content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) {
|
||||||
|
file = content.info.thumbnail_file;
|
||||||
|
url = this.matrixClient.mxcUrlToHttp(
|
||||||
|
file.url,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
this.useAuthedMedia
|
||||||
|
);
|
||||||
|
mime = file.mimetype;
|
||||||
|
} else if (
|
||||||
|
content.file &&
|
||||||
|
content.file.url &&
|
||||||
|
event.getContent()?.info?.size > 0 &&
|
||||||
|
event.getContent()?.info?.size < this.maxSizeAutoDownloads
|
||||||
|
) {
|
||||||
|
// No thumb, use real url
|
||||||
|
file = content.file;
|
||||||
|
url = this.matrixClient.mxcUrlToHttp(
|
||||||
|
file.url,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
this.useAuthedMedia
|
||||||
|
);
|
||||||
|
mime = file.mimetype;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url == null) {
|
||||||
|
throw new Error("No url found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let options: AxiosRequestConfig = {
|
||||||
|
responseType: "arraybuffer",
|
||||||
|
onDownloadProgress: (progressEvent) => {
|
||||||
|
if (progress) {
|
||||||
|
progress(Math.floor((progressEvent.progress ?? 0) * 100));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (this.useAuthedMedia) {
|
||||||
|
options.headers = {
|
||||||
|
Authorization: `Bearer ${this.matrixClient.getAccessToken()}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get(url, options);
|
||||||
|
const bytes = decrypt ? await this.decryptData(file, response) : { buffer: response.data };
|
||||||
|
return URL.createObjectURL(new Blob([bytes.buffer], { type: mime }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private b64toBuffer(val: any) {
|
||||||
|
const baseValue = val.replaceAll("-", "+").replaceAll("_", "/");
|
||||||
|
return Buffer.from(baseValue, "base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
private decryptData(file: any, response: AxiosResponse): Promise<Uint8Array> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const key = this.b64toBuffer(file.key.k);
|
||||||
|
const iv = this.b64toBuffer(file.iv);
|
||||||
|
const originalHash = this.b64toBuffer(file.hashes.sha256);
|
||||||
|
|
||||||
|
var aesCtr = new ModeOfOperation.ctr(key, new Counter(iv));
|
||||||
|
const data = new Uint8Array(response.data);
|
||||||
|
|
||||||
|
crypto.subtle
|
||||||
|
.digest("SHA-256", data)
|
||||||
|
.then((hash) => {
|
||||||
|
// Calculate sha256 and compare hashes
|
||||||
|
if (Buffer.compare(Buffer.from(hash), originalHash) != 0) {
|
||||||
|
reject("Hashes don't match!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var decryptedBytes = aesCtr.decrypt(data);
|
||||||
|
resolve(decryptedBytes);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject("Failed to calculate hash value");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUploadBatch = (
|
||||||
|
matrixClient: MatrixClient | null,
|
||||||
|
room: Room | null,
|
||||||
|
maxSizeUploads: number
|
||||||
|
): AttachmentBatch => {
|
||||||
|
const sendingStatus: Ref<"initial" | "sending" | "sent" | "canceled" | "failed"> = ref("initial");
|
||||||
|
const sendingRootEventId: Ref<string | undefined> = ref(undefined);
|
||||||
|
const sendingPromise: Ref<Promise<any> | undefined> = ref(undefined);
|
||||||
|
const attachments: Ref<Attachment[]> = ref([]);
|
||||||
|
|
||||||
|
const attachmentsSentCount = computed(() => {
|
||||||
|
return attachments.value.reduce((a, elem) => (elem.sendInfo?.status == "sent" ? a + 1 : a), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const attachmentsSending = computed(() => {
|
||||||
|
return attachments.value.filter((elem) => elem.sendInfo?.status == "initial" || elem.sendInfo?.status == "sending");
|
||||||
|
});
|
||||||
|
|
||||||
|
const attachmentsSent = computed(() => {
|
||||||
|
sortSendingAttachments();
|
||||||
|
return attachments.value.filter((elem) => elem.sendInfo?.status == "sent");
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortSendingAttachments = () => {
|
||||||
|
attachments.value.sort((a, b) => (b.sendInfo?.statusDate ?? 0) - (a.sendInfo?.statusDate ?? 0));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAttachment = (attachment: Attachment) => {
|
||||||
|
if (sendingStatus.value == "initial") {
|
||||||
|
attachments.value.push(attachment);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAttachment = (attachment: Attachment) => {
|
||||||
|
if (sendingStatus.value == "initial") {
|
||||||
|
attachments.value = attachments.value.filter((a) => a !== attachment);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTooLarge = (attachment: Attachment) => {
|
||||||
|
const file = attachment.scaledFile && attachment.useScaled ? attachment.scaledFile : attachment.file;
|
||||||
|
return file.size > maxSizeUploads;
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSend = computed(() => {
|
||||||
|
return attachments.value.length > 0 && !attachments.value.some((a: Attachment) => isTooLarge(a));
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
if (sendingStatus.value !== "initial" && matrixClient && room) {
|
||||||
|
attachments.value.toReversed().forEach((attachment) => {
|
||||||
|
cancelSendAttachment(attachment);
|
||||||
|
});
|
||||||
|
sendingStatus.value = "canceled";
|
||||||
|
if (sendingRootEventId.value) {
|
||||||
|
// Redact all media we already sent, plus the root event
|
||||||
|
let promises = attachments.value.reduce((val: Promise<any>[], attachment: Attachment) => {
|
||||||
|
if (attachment.sendInfo?.mediaEventId) {
|
||||||
|
val.push(
|
||||||
|
matrixClient.redactEvent(room.roomId, attachment.sendInfo!.mediaEventId, undefined, {
|
||||||
|
reason: "cancel",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}, [] as Promise<any>[]);
|
||||||
|
if (sendingRootEventId.value) {
|
||||||
|
promises.push(
|
||||||
|
matrixClient.redactEvent(room.roomId, sendingRootEventId.value, undefined, {
|
||||||
|
reason: "cancel",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Promise.allSettled(promises)
|
||||||
|
.then(() => {
|
||||||
|
console.log("Message redacted");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("Redaction failed: ", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelSendAttachment = (attachment: Attachment) => {
|
||||||
|
if (attachment.sendInfo) {
|
||||||
|
if (attachment.sendInfo.promise && attachment.sendInfo.status != "initial") {
|
||||||
|
attachment.sendInfo.promise.abort();
|
||||||
|
}
|
||||||
|
attachment.sendInfo.status = "canceled";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const send = (message: string): Promise<any> => {
|
||||||
|
if (!matrixClient || !room) return Promise.reject("Not configured");
|
||||||
|
sendingStatus.value = "sending";
|
||||||
|
attachments.value.forEach((attachment) => {
|
||||||
|
let sendInfo: AttachmentSendInfo = {
|
||||||
|
status: "initial",
|
||||||
|
statusDate: Date.now(),
|
||||||
|
mediaEventId: undefined,
|
||||||
|
progress: 0,
|
||||||
|
randomRotation: 0,
|
||||||
|
randomTranslationX: 0,
|
||||||
|
randomTranslationY: 0,
|
||||||
|
promise: undefined,
|
||||||
|
};
|
||||||
|
attachment.sendInfo = reactive(sendInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendingPromise = utils
|
||||||
|
.sendTextMessage(matrixClient, room.roomId, message)
|
||||||
|
.then((eventId: string) => {
|
||||||
|
sendingRootEventId.value = eventId;
|
||||||
|
|
||||||
|
// Use the eventId as a thread root for all the media
|
||||||
|
let promiseChain = Promise.resolve();
|
||||||
|
const getItemPromise = (index: number) => {
|
||||||
|
if (index < attachments.value.length) {
|
||||||
|
const attachment = attachments.value[index];
|
||||||
|
const item = attachment.sendInfo!;
|
||||||
|
if (item.status !== "initial") {
|
||||||
|
return getItemPromise(++index);
|
||||||
|
}
|
||||||
|
item.status = "sending";
|
||||||
|
|
||||||
|
let file = (() => {
|
||||||
|
if (attachment.scaledFile && attachment.useScaled) {
|
||||||
|
// Send scaled version of image instead!
|
||||||
|
return attachment.scaledFile;
|
||||||
|
} else {
|
||||||
|
// Send actual file image when not scaled!
|
||||||
|
return attachment.file;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const itemPromise = utils
|
||||||
|
.sendFile(
|
||||||
|
matrixClient,
|
||||||
|
room.roomId,
|
||||||
|
file,
|
||||||
|
({ loaded, total }: { loaded: number; total: number }) => {
|
||||||
|
if (loaded == total) {
|
||||||
|
item.progress = 100;
|
||||||
|
} else if (total > 0) {
|
||||||
|
item.progress = (100 * loaded) / total;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
eventId,
|
||||||
|
attachment.dimensions
|
||||||
|
)
|
||||||
|
.then((mediaEventId: string) => {
|
||||||
|
// Look at last item rotation, flipping the sign on this, so looks more like a true stack
|
||||||
|
let signR = 1;
|
||||||
|
let signX = 1;
|
||||||
|
let signY = 1;
|
||||||
|
if (attachmentsSent.value.length > 0) {
|
||||||
|
if (attachmentsSent.value[0].sendInfo!.randomRotation >= 0) {
|
||||||
|
signR = -1;
|
||||||
|
}
|
||||||
|
if (attachmentsSent.value[0].sendInfo!.randomTranslationX >= 0) {
|
||||||
|
signX = -1;
|
||||||
|
}
|
||||||
|
if (attachmentsSent.value[0].sendInfo!.randomTranslationY >= 0) {
|
||||||
|
signY = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.randomRotation = signR * (2 + Math.random() * 10);
|
||||||
|
item.randomTranslationX = signX * Math.random() * 20;
|
||||||
|
item.randomTranslationY = signY * Math.random() * 20;
|
||||||
|
item.mediaEventId = mediaEventId;
|
||||||
|
item.status = "sent";
|
||||||
|
item.statusDate = Date.now();
|
||||||
|
})
|
||||||
|
.catch((ignorederr: any) => {
|
||||||
|
if (item.promise?.aborted) {
|
||||||
|
item.status = "canceled";
|
||||||
|
} else {
|
||||||
|
console.error("ERROR", ignorederr);
|
||||||
|
item.status = "failed";
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
item.promise = itemPromise;
|
||||||
|
return itemPromise.then(() => getItemPromise(++index));
|
||||||
|
} else return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
return promiseChain.then(() => getItemPromise(0));
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
sendingStatus.value = "sent";
|
||||||
|
sendingRootEventId.value = undefined;
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
console.error("ERROR", err);
|
||||||
|
});
|
||||||
|
return sendingPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sendingStatus,
|
||||||
|
sendingRootEventId,
|
||||||
|
sendingPromise,
|
||||||
|
attachments,
|
||||||
|
attachmentsSentCount,
|
||||||
|
attachmentsSending,
|
||||||
|
attachmentsSent,
|
||||||
|
addAttachment,
|
||||||
|
removeAttachment,
|
||||||
|
isTooLarge,
|
||||||
|
canSend,
|
||||||
|
send,
|
||||||
|
cancel,
|
||||||
|
cancelSendAttachment,
|
||||||
|
};
|
||||||
|
};
|
||||||
28
src/models/eventAttachment.ts
Normal file
28
src/models/eventAttachment.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { MatrixEvent, Room } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
export type KeanuEventExtension = {
|
||||||
|
isMxThread?: boolean;
|
||||||
|
isChannelMessage?: boolean;
|
||||||
|
isPinned?: boolean;
|
||||||
|
parentThread?: MatrixEvent & KeanuEventExtension;
|
||||||
|
replyEvent?: MatrixEvent & KeanuEventExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventAttachment = {
|
||||||
|
event: MatrixEvent & KeanuEventExtension;
|
||||||
|
src?: string;
|
||||||
|
thumbnail?: string;
|
||||||
|
srcPromise?: Promise<string>;
|
||||||
|
thumbnailPromise?: Promise<string>;
|
||||||
|
srcProgress: number;
|
||||||
|
thumbnailProgress: number;
|
||||||
|
loadSrc: () => void;
|
||||||
|
loadThumbnail: () => Promise<string>;
|
||||||
|
release: (src: boolean, thumbnail: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KeanuEvent = MatrixEvent & KeanuEventExtension;
|
||||||
|
|
||||||
|
export type KeanuRoom = Room & {
|
||||||
|
displayType: "im.keanu.room_type_default" | "im.keanu.room_type_voice" | "im.keanu.room_type_file" | "im.keanu.room_type_channel" | undefined;
|
||||||
|
}
|
||||||
38
src/plugins/proofmode.ts
Normal file
38
src/plugins/proofmode.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { spawn } from "threads";
|
||||||
|
import ProofmodeWorker from './proofmodeWorker?worker'
|
||||||
|
|
||||||
|
export type ProofCheckResult = {
|
||||||
|
name?: string;
|
||||||
|
json?: string;
|
||||||
|
integrity?: { pgp?: any; c2pa?: any; exif?: any; opentimestamps?: any };
|
||||||
|
};
|
||||||
|
|
||||||
|
class ProofMode {
|
||||||
|
worker: any | undefined = undefined;
|
||||||
|
|
||||||
|
async getProofcheckWorker() {
|
||||||
|
if (this.worker) {
|
||||||
|
return this.worker;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.worker = await spawn(new ProofmodeWorker(), { timeout: 20000 });
|
||||||
|
this.worker.values().subscribe(({ type, message }: { type: string, message: string}) => {
|
||||||
|
console.log("ProofCheck:", type, message);
|
||||||
|
});
|
||||||
|
} catch (error) {}
|
||||||
|
return this.worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
async proofCheckFile(file: File): Promise<ProofCheckResult | undefined> {
|
||||||
|
try {
|
||||||
|
const worker = await this.getProofcheckWorker();
|
||||||
|
const res = await worker.checkFiles([file]);
|
||||||
|
if (res && res.files && res.files.length == 1) {
|
||||||
|
return res.files[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default new ProofMode();
|
||||||
20
src/plugins/proofmodeWorker.js
Normal file
20
src/plugins/proofmodeWorker.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Observable, Subject } from "threads/observable";
|
||||||
|
import { expose } from "threads/worker";
|
||||||
|
import { checkFiles } from "@guardianproject/proofmode";
|
||||||
|
|
||||||
|
let subject = new Subject();
|
||||||
|
|
||||||
|
const sendMessage = (type, message) => {
|
||||||
|
subject.next({ type, message });
|
||||||
|
};
|
||||||
|
|
||||||
|
const check = {
|
||||||
|
checkFiles: (files) => {
|
||||||
|
return checkFiles(files, sendMessage);
|
||||||
|
},
|
||||||
|
values: () => {
|
||||||
|
return Observable.from(subject);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expose(check);
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
|
import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
|
||||||
import imageResize from "image-resize";
|
import imageResize from "image-resize";
|
||||||
import { AutoDiscovery } from "matrix-js-sdk";
|
import { AutoDiscovery, Method } from "matrix-js-sdk";
|
||||||
import User from "../models/user";
|
import User from "../models/user";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import Hammer from "hammerjs";
|
import Hammer from "hammerjs";
|
||||||
|
|
@ -12,12 +12,8 @@ import aesjs from "aes-js";
|
||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
import duration from "dayjs/plugin/duration";
|
import duration from "dayjs/plugin/duration";
|
||||||
import i18n from "./lang";
|
import i18n from "./lang";
|
||||||
import {
|
import { toRaw, isRef, isReactive, isProxy } from "vue";
|
||||||
toRaw,
|
import { UploadPromise } from "../models/attachment";
|
||||||
isRef,
|
|
||||||
isReactive,
|
|
||||||
isProxy,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice";
|
export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice";
|
||||||
export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted";
|
export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted";
|
||||||
|
|
@ -29,6 +25,9 @@ export const ROOM_TYPE_CHANNEL = "im.keanu.room_type_channel";
|
||||||
|
|
||||||
export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type";
|
export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type";
|
||||||
|
|
||||||
|
const THUMBNAIL_MAX_WIDTH = 160;
|
||||||
|
const THUMBNAIL_MAX_HEIGHT = 160;
|
||||||
|
|
||||||
// Install extended localized format
|
// Install extended localized format
|
||||||
dayjs.extend(localizedFormat);
|
dayjs.extend(localizedFormat);
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
@ -43,32 +42,6 @@ var _browserCanRecordAudioF = function () {
|
||||||
};
|
};
|
||||||
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
||||||
|
|
||||||
class UploadPromise {
|
|
||||||
aborted = false;
|
|
||||||
onAbort = undefined;
|
|
||||||
|
|
||||||
constructor(wrappedPromise) {
|
|
||||||
this.wrappedPromise = wrappedPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
abort() {
|
|
||||||
this.aborted = true;
|
|
||||||
if (this.onAbort) {
|
|
||||||
this.onAbort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
then(resolve, reject) {
|
|
||||||
this.wrappedPromise = this.wrappedPromise.then(resolve, reject);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
catch(handler) {
|
|
||||||
this.wrappedPromise = this.wrappedPromise.catch(handler);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Util {
|
class Util {
|
||||||
threadMessageType() {
|
threadMessageType() {
|
||||||
return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread";
|
return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread";
|
||||||
|
|
@ -90,6 +63,7 @@ class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttachment(matrixClient, useAuthedMedia, event, progressCallback, asBlob = false, abortController = undefined) {
|
getAttachment(matrixClient, useAuthedMedia, event, progressCallback, asBlob = false, abortController = undefined) {
|
||||||
|
console.error("GET ATTACHMENT FOR EVENT", event.getId());
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
var url = null;
|
var url = null;
|
||||||
|
|
@ -164,94 +138,54 @@ class Util {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getThumbnail(matrixClient, useAuthedMedia, event, config, ignoredw, ignoredh) {
|
async getThumbnail(matrixClient, useAuthedMedia, event, config, ignoredw, ignoredh) {
|
||||||
return new Promise((resolve, reject) => {
|
console.error("GET THUMB FOR EVENT", event.getId());
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
var url = null;
|
var url = null;
|
||||||
var mime = "image/png";
|
var mime = "image/png";
|
||||||
var file = null;
|
var file = null;
|
||||||
let decrypt = true;
|
let decrypt = true;
|
||||||
if (content.url != null) {
|
if (!!content.info && !!content.info.thumbnail_url) {
|
||||||
url = matrixClient.mxcUrlToHttp(
|
url = matrixClient.mxcUrlToHttp(content.info.thumbnail_url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
|
||||||
content.url,
|
decrypt = false;
|
||||||
undefined,
|
if (content.info.thumbnail_info) {
|
||||||
undefined,
|
mime = content.info.thumbnail_info.mimetype;
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
useAuthedMedia
|
|
||||||
);
|
|
||||||
decrypt = false;
|
|
||||||
if (content.info) {
|
|
||||||
mime = content.info.mimetype;
|
|
||||||
}
|
|
||||||
} else if (content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) {
|
|
||||||
file = content.info.thumbnail_file;
|
|
||||||
// var width = w;
|
|
||||||
// var height = h;
|
|
||||||
// if (content.info.w < w || content.info.h < h) {
|
|
||||||
// width = content.info.w;
|
|
||||||
// height = content.info.h;
|
|
||||||
// }
|
|
||||||
// url = matrixClient.mxcUrlToHttp(
|
|
||||||
// file.url,
|
|
||||||
// width, height,
|
|
||||||
// "scale",
|
|
||||||
// true
|
|
||||||
// );
|
|
||||||
url = matrixClient.mxcUrlToHttp(
|
|
||||||
file.url,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
useAuthedMedia
|
|
||||||
);
|
|
||||||
mime = file.mimetype;
|
|
||||||
} else if (
|
|
||||||
content.file &&
|
|
||||||
content.file.url &&
|
|
||||||
this.getFileSize(event) > 0 &&
|
|
||||||
this.getFileSize(event) < config.maxSizeAutoDownloads
|
|
||||||
) {
|
|
||||||
// No thumb, use real url
|
|
||||||
file = content.file;
|
|
||||||
url = matrixClient.mxcUrlToHttp(
|
|
||||||
file.url,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
useAuthedMedia
|
|
||||||
);
|
|
||||||
mime = file.mimetype;
|
|
||||||
}
|
}
|
||||||
|
} else if (content.url != null) {
|
||||||
if (url == null) {
|
url = matrixClient.mxcUrlToHttp(content.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
|
||||||
reject("No url found!");
|
decrypt = false;
|
||||||
return;
|
if (content.info) {
|
||||||
|
mime = content.info.mimetype;
|
||||||
}
|
}
|
||||||
|
} else if (content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) {
|
||||||
|
file = content.info.thumbnail_file;
|
||||||
|
url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
|
||||||
|
mime = file.mimetype;
|
||||||
|
} else if (
|
||||||
|
content.file &&
|
||||||
|
content.file.url &&
|
||||||
|
this.getFileSize(event) > 0 &&
|
||||||
|
this.getFileSize(event) < config.maxSizeAutoDownloads
|
||||||
|
) {
|
||||||
|
// No thumb, use real url
|
||||||
|
file = content.file;
|
||||||
|
url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
|
||||||
|
mime = file.mimetype;
|
||||||
|
}
|
||||||
|
|
||||||
axios
|
if (url == null) {
|
||||||
|
throw new Error("No url found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios
|
||||||
.get(url, useAuthedMedia ? {
|
.get(url, useAuthedMedia ? {
|
||||||
responseType: "arraybuffer",
|
responseType: "arraybuffer",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${matrixClient.getAccessToken()}`,
|
Authorization: `Bearer ${matrixClient.getAccessToken()}`,
|
||||||
},
|
},
|
||||||
} : { responseType: "arraybuffer" })
|
} : { responseType: "arraybuffer" });
|
||||||
.then((response) => {
|
const bytes = decrypt ? await this.decryptIfNeeded(file, response) : { buffer: response.data };
|
||||||
return decrypt ? this.decryptIfNeeded(file, response) : Promise.resolve({ buffer: response.data });
|
return URL.createObjectURL(new Blob([bytes.buffer], { type: mime }));
|
||||||
})
|
|
||||||
.then((bytes) => {
|
|
||||||
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: mime })));
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("Download error: ", err);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b64toBuffer(val) {
|
b64toBuffer(val) {
|
||||||
|
|
@ -430,7 +364,37 @@ class Util {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot) {
|
async encryptFileAndGenerateInfo(data, mime) {
|
||||||
|
let key = Buffer.from(crypto.getRandomValues(new Uint8Array(256 / 8)));
|
||||||
|
let iv = Buffer.concat([Buffer.from(crypto.getRandomValues(new Uint8Array(8))), Buffer.alloc(8)]); // Initialization vector.
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
let aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
|
||||||
|
let encryptedBytes = aesCtr.encrypt(data);
|
||||||
|
|
||||||
|
// Calculate sha256
|
||||||
|
let hash = await crypto.subtle.digest("SHA-256", encryptedBytes);
|
||||||
|
console.error("HASH GENERATED", Buffer.from(hash));
|
||||||
|
|
||||||
|
const jwk = {
|
||||||
|
kty: "oct",
|
||||||
|
key_ops: ["encrypt", "decrypt"],
|
||||||
|
alg: "A256CTR",
|
||||||
|
k: key.toString("base64").replaceAll(/\//g, "_").replaceAll(/\+/g, "-"),
|
||||||
|
ext: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedFile = {
|
||||||
|
mimetype: mime,
|
||||||
|
key: jwk,
|
||||||
|
iv: Buffer.from(iv).toString("base64").replace(/=/g, ""),
|
||||||
|
hashes: { sha256: Buffer.from(hash).toString("base64").replace(/=/g, "") },
|
||||||
|
v: "v2",
|
||||||
|
};
|
||||||
|
return [encryptedBytes, encryptedFile];
|
||||||
|
}
|
||||||
|
|
||||||
|
sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot, dimensions) {
|
||||||
const uploadPromise = new UploadPromise(undefined);
|
const uploadPromise = new UploadPromise(undefined);
|
||||||
uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
|
uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
|
|
@ -439,131 +403,140 @@ class Util {
|
||||||
reject("Aborted");
|
reject("Aborted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fileContents = e.target.result;
|
try {
|
||||||
var data = new Uint8Array(fileContents);
|
const fileContents = e.target.result;
|
||||||
|
|
||||||
const info = {
|
var data = new Uint8Array(fileContents);
|
||||||
mimetype: file.type,
|
let thumbnailData = undefined;
|
||||||
size: file.size,
|
let thumbnailInfo = undefined;
|
||||||
};
|
|
||||||
|
|
||||||
// If audio, send duration in ms as well
|
const info = {
|
||||||
if (file.duration) {
|
mimetype: file.type,
|
||||||
info.duration = file.duration;
|
size: file.size,
|
||||||
}
|
|
||||||
|
|
||||||
var description = file.name;
|
|
||||||
var msgtype = "m.file";
|
|
||||||
if (file.type.startsWith("image/")) {
|
|
||||||
msgtype = "m.image";
|
|
||||||
} else if (file.type.startsWith("audio/")) {
|
|
||||||
msgtype = "m.audio";
|
|
||||||
} else if (file.type.startsWith("video/")) {
|
|
||||||
msgtype = "m.video";
|
|
||||||
}
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
type: file.type,
|
|
||||||
name: description,
|
|
||||||
progressHandler: onUploadProgress,
|
|
||||||
onlyContentUri: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
var messageContent = {
|
|
||||||
body: description,
|
|
||||||
info: info,
|
|
||||||
msgtype: msgtype,
|
|
||||||
};
|
|
||||||
|
|
||||||
// If thread root (an eventId) is set, add that here
|
|
||||||
if (threadRoot) {
|
|
||||||
messageContent["m.relates_to"] = {
|
|
||||||
rel_type: this.threadMessageType(),
|
|
||||||
event_id: threadRoot,
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Set filename for files
|
// If audio, send duration in ms as well
|
||||||
if (msgtype == "m.file") {
|
if (file.duration) {
|
||||||
messageContent.filename = file.name;
|
info.duration = file.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!matrixClient.isRoomEncrypted(roomId)) {
|
var description = file.name;
|
||||||
// Not encrypted.
|
var msgtype = "m.file";
|
||||||
const promise = matrixClient.uploadContent(data, opts);
|
if (file.type.startsWith("image/")) {
|
||||||
uploadPromise.onAbort = () => {
|
msgtype = "m.image";
|
||||||
matrixClient.cancelUpload(promise);
|
|
||||||
};
|
|
||||||
promise
|
|
||||||
.then((response) => {
|
|
||||||
messageContent.url = response.content_uri;
|
|
||||||
return msgtype == "m.audio" ? this.generateWaveform(fileContents, messageContent) : true;
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
resolve(result);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
return; // Don't fall through
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = Buffer.from(crypto.getRandomValues(new Uint8Array(256 / 8)));
|
// Generate thumbnail?
|
||||||
let iv = Buffer.concat([Buffer.from(crypto.getRandomValues(new Uint8Array(8))), Buffer.alloc(8)]); // Initialization vector.
|
if (dimensions) {
|
||||||
|
const w = dimensions.width;
|
||||||
// Encrypt
|
const h = dimensions.height;
|
||||||
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
|
if (w > THUMBNAIL_MAX_WIDTH || h > THUMBNAIL_MAX_HEIGHT) {
|
||||||
var encryptedBytes = aesCtr.encrypt(data);
|
var aspect = w / h;
|
||||||
data = encryptedBytes;
|
var newWidth = parseInt((w > h ? THUMBNAIL_MAX_WIDTH : THUMBNAIL_MAX_HEIGHT * aspect).toFixed());
|
||||||
|
var newHeight = parseInt((w > h ? THUMBNAIL_MAX_WIDTH / aspect : THUMBNAIL_MAX_HEIGHT).toFixed());
|
||||||
// Calculate sha256
|
const scaled = await imageResize(file, {
|
||||||
let hash = await crypto.subtle.digest("SHA-256", data);
|
format: "webp",
|
||||||
|
width: newWidth,
|
||||||
const jwk = {
|
height: newHeight,
|
||||||
kty: "oct",
|
outputType: "blob",
|
||||||
key_ops: ["encrypt", "decrypt"],
|
}).catch(() => {return Promise.resolve(undefined)});
|
||||||
alg: "A256CTR",
|
if (scaled && file.size > scaled.size) {
|
||||||
k: key.toString("base64").replaceAll(/\//g, "_").replaceAll(/\+/g, "-"),
|
thumbnailData = new Uint8Array(await scaled.arrayBuffer());
|
||||||
ext: true,
|
thumbnailInfo = {
|
||||||
};
|
mimetype: scaled.type,
|
||||||
|
size: scaled.size,
|
||||||
const encryptedFile = {
|
h: newHeight,
|
||||||
mimetype: file.type,
|
w: newWidth,
|
||||||
key: jwk,
|
};
|
||||||
iv: Buffer.from(iv).toString("base64").replace(/=/g, ""),
|
}
|
||||||
hashes: { sha256: Buffer.from(hash).toString("base64").replace(/=/g, "") },
|
}
|
||||||
v: "v2",
|
|
||||||
};
|
|
||||||
|
|
||||||
messageContent.file = encryptedFile;
|
|
||||||
|
|
||||||
// Encrypted data sent as octet-stream!
|
|
||||||
opts.type = "application/octet-stream";
|
|
||||||
|
|
||||||
const promise = matrixClient.uploadContent(data, opts);
|
|
||||||
uploadPromise.onAbort = () => {
|
|
||||||
matrixClient.cancelUpload(promise);
|
|
||||||
};
|
|
||||||
promise
|
|
||||||
.then((response) => {
|
|
||||||
if (response.error) {
|
|
||||||
return reject(response.error);
|
|
||||||
}
|
}
|
||||||
|
} else if (file.type.startsWith("audio/")) {
|
||||||
|
msgtype = "m.audio";
|
||||||
|
} else if (file.type.startsWith("video/")) {
|
||||||
|
msgtype = "m.video";
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageContent = {
|
||||||
|
body: description,
|
||||||
|
info: info,
|
||||||
|
msgtype: msgtype,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If thread root (an eventId) is set, add that here
|
||||||
|
if (threadRoot) {
|
||||||
|
messageContent["m.relates_to"] = {
|
||||||
|
rel_type: this.threadMessageType(),
|
||||||
|
event_id: threadRoot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set filename for files
|
||||||
|
if (msgtype == "m.file") {
|
||||||
|
messageContent.filename = file.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useEncryption = matrixClient.isRoomEncrypted(roomId);
|
||||||
|
|
||||||
|
const dataUploadOpts = {
|
||||||
|
type: useEncryption ? "application/octet-stream" : file.type,
|
||||||
|
name: description,
|
||||||
|
progressHandler: onUploadProgress,
|
||||||
|
onlyContentUri: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (useEncryption) {
|
||||||
|
const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(data, file.type);
|
||||||
|
messageContent.file = encryptedFile;
|
||||||
|
data = encryptedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnailData) {
|
||||||
|
messageContent.thumbnail_info = thumbnailInfo;
|
||||||
|
if (useEncryption) {
|
||||||
|
console.error("Encrypt thumb thumb");
|
||||||
|
const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(thumbnailData, file.type);
|
||||||
|
messageContent.info.thumbnail_file = encryptedFile;
|
||||||
|
thumbnailData = encryptedBytes;
|
||||||
|
}
|
||||||
|
const thumbnailUploadOpts = {
|
||||||
|
type: useEncryption ? "application/octet-stream" : file.type,
|
||||||
|
name: "thumb:" + description,
|
||||||
|
progressHandler: onUploadProgress,
|
||||||
|
onlyContentUri: false,
|
||||||
|
};
|
||||||
|
const thumbUploadPromise = matrixClient.uploadContent(thumbnailData, thumbnailUploadOpts);
|
||||||
|
uploadPromise.onAbort = () => {
|
||||||
|
matrixClient.cancelUpload(thumbUploadPromise);
|
||||||
|
};
|
||||||
|
const thumbnailResponse = await thumbUploadPromise;
|
||||||
|
if (useEncryption) {
|
||||||
|
messageContent.info.thumbnail_file.url = thumbnailResponse.content_uri;
|
||||||
|
} else {
|
||||||
|
messageContent.info.thumbnail_url = thumbnailResponse.content_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataUploadPromise = matrixClient.uploadContent(data, dataUploadOpts);
|
||||||
|
uploadPromise.onAbort = () => {
|
||||||
|
matrixClient.cancelUpload(dataUploadPromise);
|
||||||
|
};
|
||||||
|
const response = await dataUploadPromise;
|
||||||
|
if (useEncryption) {
|
||||||
messageContent.file.url = response.content_uri;
|
messageContent.file.url = response.content_uri;
|
||||||
return msgtype == "m.audio" ? this.generateWaveform(fileContents, messageContent) : true;
|
} else {
|
||||||
})
|
messageContent.url = response.content_uri;
|
||||||
.then(() => {
|
}
|
||||||
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
|
|
||||||
})
|
// Generate audio waveforms
|
||||||
.then((result) => {
|
if (msgtype == "m.audio") {
|
||||||
resolve(result);
|
this.generateWaveform(fileContents, messageContent);
|
||||||
})
|
}
|
||||||
.catch((err) => {
|
|
||||||
reject(err);
|
const result = await this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
|
||||||
});
|
resolve(result);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
reader.onerror = (err) => {
|
reader.onerror = (err) => {
|
||||||
reject(err);
|
reject(err);
|
||||||
|
|
@ -1248,5 +1221,5 @@ class Util {
|
||||||
};
|
};
|
||||||
return objectIterator(sourceObj);
|
return objectIterator(sourceObj);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
export default new Util();
|
export default new Util();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import util, { STATE_EVENT_ROOM_DELETED, STATE_EVENT_ROOM_TYPE, ROOM_TYPE_CHANNE
|
||||||
import User from "../models/user";
|
import User from "../models/user";
|
||||||
import * as LocalStorageCryptoStoreClass from "matrix-js-sdk/lib/crypto/store/localStorage-crypto-store";
|
import * as LocalStorageCryptoStoreClass from "matrix-js-sdk/lib/crypto/store/localStorage-crypto-store";
|
||||||
import rememberMeMixin from "../components/rememberMeMixin";
|
import rememberMeMixin from "../components/rememberMeMixin";
|
||||||
|
import { AttachmentManager } from "../models/attachmentManager";
|
||||||
|
|
||||||
const LocalStorageCryptoStore = LocalStorageCryptoStoreClass.LocalStorageCryptoStore;
|
const LocalStorageCryptoStore = LocalStorageCryptoStoreClass.LocalStorageCryptoStore;
|
||||||
|
|
||||||
|
|
@ -47,6 +48,7 @@ export default {
|
||||||
notificationCount: 0,
|
notificationCount: 0,
|
||||||
legacyCryptoStore: undefined,
|
legacyCryptoStore: undefined,
|
||||||
tokenRefreshPromise: undefined,
|
tokenRefreshPromise: undefined,
|
||||||
|
attachmentManager: undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -352,6 +354,9 @@ export default {
|
||||||
}
|
}
|
||||||
this.useAuthedMedia = await this.matrixClient.isVersionSupported("v1.11");
|
this.useAuthedMedia = await this.matrixClient.isVersionSupported("v1.11");
|
||||||
|
|
||||||
|
// Create the attachment manager
|
||||||
|
this.attachmentManager = new AttachmentManager(this.matrixClient, this.useAuthedMedia, this.$config.maxSizeAutoDownloads);
|
||||||
|
|
||||||
// Ready to use! Start by loading rooms.
|
// Ready to use! Start by loading rooms.
|
||||||
this.initClient();
|
this.initClient();
|
||||||
return user;
|
return user;
|
||||||
|
|
@ -1369,6 +1374,7 @@ export default {
|
||||||
const instance = matrixService.mount("#app2");
|
const instance = matrixService.mount("#app2");
|
||||||
app.config.globalProperties.$matrix = instance;
|
app.config.globalProperties.$matrix = instance;
|
||||||
app.$matrix = instance;
|
app.$matrix = instance;
|
||||||
|
app.provide("globalMatrix", instance);
|
||||||
sdk.setCryptoStoreFactory(instance.createCryptoStore.bind(instance));
|
sdk.setCryptoStoreFactory(instance.createCryptoStore.bind(instance));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,18 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true
|
"target": "es2024",
|
||||||
},
|
"moduleResolution": "bundler",
|
||||||
}
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ES2024"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
//"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/components/file_mode/ThumbnailView.vue"]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,34 +3,16 @@ import vue from "@vitejs/plugin-vue";
|
||||||
import vueJsx from "@vitejs/plugin-vue-jsx";
|
import vueJsx from "@vitejs/plugin-vue-jsx";
|
||||||
import vitePluginVuetify from "vite-plugin-vuetify";
|
import vitePluginVuetify from "vite-plugin-vuetify";
|
||||||
import { fileURLToPath, URL } from "node:url";
|
import { fileURLToPath, URL } from "node:url";
|
||||||
import Components from "unplugin-vue-components/vite";
|
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||||
import nodePolyfills from 'rollup-plugin-polyfill-node';
|
import wasm from "vite-plugin-wasm";
|
||||||
import { resolve } from "path";
|
import topLevelAwait from "vite-plugin-top-level-await";
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
|
||||||
|
|
||||||
function VuetifyResolver() {
|
|
||||||
return {
|
|
||||||
type: 'component',
|
|
||||||
resolve: (name) => {
|
|
||||||
console.log("rESOLVE", name);
|
|
||||||
if (name.match(/^V[A-Z]/) && !name.includes("VEmojiPicker"))
|
|
||||||
return { name, from: './node_modules/vuetify/components' }
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({mode}) => ({
|
export default defineConfig(({mode}) => ({
|
||||||
base: "./",
|
base: "./",
|
||||||
plugins: [
|
plugins: [
|
||||||
// commonjs({
|
wasm(),
|
||||||
// include: /node_modules/,
|
topLevelAwait(),
|
||||||
// requireReturnsDefault: 'auto', // <---- this solves default issue
|
|
||||||
// }),
|
|
||||||
// commonjs({
|
|
||||||
// exclude: ["*vuex-persist*", "*deepmerge*"]
|
|
||||||
// }),
|
|
||||||
vue({
|
vue({
|
||||||
template: {
|
template: {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
|
|
@ -40,9 +22,6 @@ export default defineConfig(({mode}) => ({
|
||||||
}),
|
}),
|
||||||
vueJsx(),
|
vueJsx(),
|
||||||
vitePluginVuetify(),
|
vitePluginVuetify(),
|
||||||
// Components({
|
|
||||||
// resolvers: [VuetifyResolver()],
|
|
||||||
// }),
|
|
||||||
viteStaticCopy({
|
viteStaticCopy({
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
|
|
@ -59,8 +38,14 @@ export default defineConfig(({mode}) => ({
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
//nodePolyfills(),
|
|
||||||
],
|
],
|
||||||
|
worker: {
|
||||||
|
format: "es",
|
||||||
|
plugins: () => [
|
||||||
|
wasm(),
|
||||||
|
topLevelAwait()
|
||||||
|
]
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.vue','.mjs', '.js', '.ts', '.jsx', '.tsx', '.json','.wasm'],
|
extensions: ['.vue','.mjs', '.js', '.ts', '.jsx', '.tsx', '.json','.wasm'],
|
||||||
alias: [
|
alias: [
|
||||||
|
|
@ -69,53 +54,8 @@ export default defineConfig(({mode}) => ({
|
||||||
{ find: "vue", replacement: fileURLToPath(new URL("./node_modules/vue/dist/vue.esm-bundler.js", import.meta.url)) },
|
{ find: "vue", replacement: fileURLToPath(new URL("./node_modules/vue/dist/vue.esm-bundler.js", import.meta.url)) },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
define: {
|
build: {
|
||||||
//global: "window",
|
assetsDir: "assets",
|
||||||
//module: {},
|
commonjsOptions: { transformMixedEsModules: true }
|
||||||
Lame: "window.Lame",
|
|
||||||
Presets: "window.Presets",
|
|
||||||
GainAnalysis: "window.GainAnalysis",
|
|
||||||
QuantizePVT: "window.QuantizePVT",
|
|
||||||
Quantize: "window.Quantize",
|
|
||||||
Takehiro: "window.Takehiro",
|
|
||||||
Reservoir: "window.Reservoir",
|
|
||||||
MPEGMode: "window.MPEGMode",
|
|
||||||
BitStream: "window.BitStream",
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
commonjsOptions: { transformMixedEsModules: true } // Change
|
|
||||||
}
|
}
|
||||||
// optimizeDeps: {
|
|
||||||
// include: ["deepmerge", "vuex-persist"],
|
|
||||||
// },
|
|
||||||
// optimizeDeps: {
|
|
||||||
// include: [
|
|
||||||
// "vuex-persist", "vue-sanitize"
|
|
||||||
// ],
|
|
||||||
// esbuildOptions:{
|
|
||||||
// plugins:[
|
|
||||||
// commonjs()
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// build: {
|
|
||||||
// commonjsOptions: {
|
|
||||||
// include: [/node_modules/],
|
|
||||||
// requireReturnsDefault: true,
|
|
||||||
// exclude: ["vuex-persist"]
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// rollupOptions: {
|
|
||||||
// //Here, we are externalizing Vue to prevent it to be bundled
|
|
||||||
// //with our library
|
|
||||||
// external: ["vue"],
|
|
||||||
// //Add this so the UMD build will recognize the global variables
|
|
||||||
// //of externalized dependencies
|
|
||||||
// output: {
|
|
||||||
// globals: {
|
|
||||||
// vue: "Vue",
|
|
||||||
// },
|
|
||||||
// exports: "named",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue