Work on attachments

This commit is contained in:
N-Pex 2025-06-09 09:44:37 +02:00
parent ec79a33eab
commit 842c87dc96
28 changed files with 2714 additions and 798 deletions

600
package-lock.json generated
View file

@ -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",
@ -65,7 +68,9 @@
"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 +940,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 +1501,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 +1815,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 +2985,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 +3445,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 +4081,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 +4935,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 +5324,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 +5346,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 +5494,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 +5568,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 +5691,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 +6931,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",
@ -7081,6 +7397,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 +7446,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 +8507,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 +8781,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 +8927,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 +9763,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 +10111,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 +10555,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 +11139,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 +11423,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 +11565,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 +11610,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 +11695,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 +12534,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",
@ -12302,6 +12812,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 +12842,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",

View file

@ -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",
@ -66,7 +69,9 @@
"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": {

View file

@ -0,0 +1,402 @@
@use "@/assets/css/variables" as *;
$large-button-height: $min-touch-target;
$small-button-height: 36px;
$background: #ffffff;
$backgroundSection: rgba(#ededed,0.8);
$backgroundHilite: #383739;
$text: #000000;
$hiliteColor: #4642f1;
.send-attachments {
font-family: "Inter", sans-serif;
z-index: 10;
position: absolute;
top: 0px;
left: 0;
right: 0;
bottom: 0;
margin: 0;
background-color: $background;
color: $text;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 16px;
.send-attachments__title {
color: $text;
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: $background;
&.drop-target {
background-color: $backgroundHilite;
}
border-radius: 19px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.file-format-info {
opacity: 0.6;
color: $text;
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 {
font-family: "Inter", sans-serif;
font-weight: 700;
font-size: 11.54 * $chat-text-size;
line-height: 140%;
color: white;
background-color: $hiliteColor !important;
border-radius: $small-button-height * 0.5;
min-height: 0;
height: $small-button-height !important;
margin-top: $chat-standard-padding-xs;
margin-bottom: $chat-standard-padding-xs;
&.large {
padding: 16px 23px;
height: $large-button-height;
border-radius: $large-button-height * 0.5;
}
}
textarea {
color: rgba($text, 80%) !important;
}
textarea::placeholder {
color: rgba($text, 80%) !important;
}
.attachment-wrapper {
width: 100%;
flex: 0 0 100%;
overflow-y: auto;
}
.file-drop-current-item {
width: 100%;
height: 60%;
background-color: $backgroundSection;
display: flex;
&.drop-target {
background-color: $backgroundHilite;
}
border-radius: 19px;
overflow: hidden;
.v-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.filename {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
.send-attachments__current-item__info {
height: 80px;
text-align: start;
margin: 18px 20px;
padding: 0;
position: relative;
.send-attachments__current-item__info__size {
white-space: pre;
overflow: hidden;
margin-right: 36px;
text-overflow: ellipsis;
}
.send-attachments__current-item__info__size__filename {
opacity: 0.7;
font-size: 0.8em;
}
}
.file-drop-thumbnail-container {
width: 100%;
padding: 13px 20px 15px 20px;
height: 74px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
text-align: start;
/* Hide scrollbar for Chrome, Safari and Opera */
&::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
.file-drop-thumbnail {
width: 46px;
height: 46px;
border-radius: 9px;
overflow: hidden;
background-color: #242424;
border: 2px solid white;
display: inline-block;
position: relative;
&.current {
border: 2px solid #4642f1;
}
&.noborder {
border: 2px solid transparent;
}
.v-img {
width: 100%;
height: 100%;
object-fit: cover;
}
margin-right: 8px;
.add,
.remove {
color: $background;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
.v-icon {
width: 14px;
height: 15.75px;
}
}
.remove {
// Slight background to make visible
background-color: rgba(black, 0.2);
}
}
}
.file-drop-section {
margin-top: 20px;
padding: 16px 18px;
background-color: $backgroundSection;
border-radius: 19px;
}
.file-drop-input-container,
.file-drop-sending-input-container,
.file-drop-sent-input-container {
position: relative;
width: 100%;
min-height: 100px;
background-color: $backgroundSection;
border-radius: 19px;
display: flex;
flex-direction: column;
.input-area-text {
flex: 0 0 auto;
width: 100%;
margin-bottom: 50px;
padding: 16px 18px;
font-family: "Inter", sans-serif;
font-weight: 300;
}
.input-container__buttons {
position: absolute;
right: 8px;
bottom: 10px;
display: flex;
& > *:not(:first-child) {
margin-left: 8px;
}
}
}
@keyframes fadeInStackItem {
from {opacity: 0;}
to {opacity: 1;}
}
// Sending
//
.file-drop-sent-stack {
width: 100%;
height: 30%;
display: flex;
align-items: center;
justify-content: center;
.no-items {
display: flex;
align-items: center;
justify-content: center;
div {
position: absolute;
}
.file-drop-stack-item {
transform: rotate(-4.4deg);
}
color: #fff;
text-align: center;
font-size: 21 * $chat-text-size;
font-family: "Poppins", sans-serif;
font-weight: 700;
letter-spacing: 0.34px;
}
.items-sent {
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
div, .v-icon {
position: absolute;
}
.v-icon, .v-icon__component {
width: 30%;
height: 30%;
}
}
.file-drop-stack-item {
background: #3a3a3c;
position: absolute;
overflow: hidden;
opacity: 0;
.v-img {
width: 100%;
height: 100%;
object-fit: cover;
}
&.direct {
opacity: 1 !important;
}
&.animated {
animation-name: fadeInStackItem;
animation-fill-mode: both;
animation-duration: 1.5s;
}
}
}
.file-drop-sending-container {
width: 100%;
padding: 13px 0px 15px 0px;
height: 50%;
overflow-x: hidden;
overflow-y: auto;
white-space: nowrap;
text-align: start;
/* Hide scrollbar for Chrome, Safari and Opera */
&::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
.file-drop-sending-item {
width: 100%;
height: 64px;
overflow: hidden;
background-color: $backgroundSection;
border-radius: 12px;
position: relative;
padding: 8px;
.v-img {
width: $min-touch-target;
height: $min-touch-target;
border-radius: 8px;
object-fit: cover;
flex: 0 0 $min-touch-target;
margin-right: 8px;
}
margin-bottom: 8px;
display: flex;
align-items: center;
.filename {
position: absolute;
top: 18px;
left: 8px;
font-size: 0.7em;
}
.v-progress-linear {
align-self: flex-end;
}
.file-drop-cancel {
position: absolute;
right: 8px;
width: 17px;
height: 17px;
color: green !important;
background: #2e2e3b;
border-radius: 8.5px;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.file-drop-sending-input-container {
.v-btn {
.v-progress-circular {
margin-left: 8px;
}
background: linear-gradient(0deg, #000 0%, #000 100%), #4642f1;
}
}
.file-drop-files-sent {
width: 100%;
color: $text;
text-align: center;
font-size: 21 * $chat-text-size;
font-family: "Poppins", sans-serif;
font-weight: 700;
letter-spacing: 0.34px;
text-align: center;
}
.file-drop-sent-input-container {
background-color: transparent;
.v-btn {
right: unset;
left: 8px;
background: linear-gradient(0deg, #000 0%, #000 100%), #4642f1;
&.close {
right: 8px;
left: unset;
background: $hiliteColor !important;
}
}
}
}

View 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>

View file

@ -27,10 +27,10 @@
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" <FileDropLayout class="file-drop-root" v-if="useFileModeNonAdmin" :room="room"
v-on:pick-file="showAttachmentPicker()" v-on:pick-file="showAttachmentPicker(false)"
v-on:add-file="addAttachment($event)" v-on:add-files="(files) => addAttachments(files)"
v-on:remove-file="currentFileInputs.splice($event, 1)" v-on:remove-file="currentFileInputs.splice($event, 1)"
v-on:reset="resetAttachments" v-on:reset="v"
:attachments="currentFileInputs" :attachments="currentFileInputs"
v-on:close="closeFileMode" v-on:close="closeFileMode"
/> />
@ -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,7 +229,7 @@
<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"> <!-- <div v-if="currentFileInputsDialog && !useFileModeNonAdmin">
<v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.display.smAndUp ? '50%' : '85%'" persistent scrollable> <v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.display.smAndUp ? '50%' : '85%'" persistent scrollable>
<v-card class="ma-0 pa-0"> <v-card class="ma-0 pa-0">
<v-card-text v-if="!currentFileInputs.length"> <v-card-text v-if="!currentFileInputs.length">
@ -253,6 +253,7 @@
<v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image" <v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
contain class="current-image-input-path" /> 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> <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>
<C2PABadge :file="currentImageInput.actualFile" />
</div> </div>
<div> <div>
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled"> <span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
@ -302,7 +303,17 @@
</template> </template>
</v-card> </v-card>
</v-dialog> </v-dialog>
</div> </div> -->
<SendAttachmentsLayout
v-if="currentFileInputs && currentFileInputs.length > 0 && !useFileModeNonAdmin"
:room="room"
v-on:pick-file="showAttachmentPicker(false)"
v-on:add-files="(files) => addAttachments(files)"
v-on:remove-file="(index) => removeAttachment(index)"
:attachments="currentFileInputs"
v-on:close="resetAttachments"
/>
<MessageOperationsBottomSheet ref="messageOperationsSheet"> <MessageOperationsBottomSheet ref="messageOperationsSheet">
<EmojiPicker ref="emojiPicker" <EmojiPicker ref="emojiPicker"
@ -385,9 +396,10 @@ 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 sendAttachmentsMixin from "./sendAttachmentsMixin.ts";
import AudioLayout from "./AudioLayout.vue"; import AudioLayout from "./AudioLayout.vue";
import FileDropLayout from "./file_mode/FileDropLayout"; 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 +413,8 @@ 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";
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! */
@ -448,13 +462,15 @@ export default {
CreatePollDialog, CreatePollDialog,
AudioLayout, AudioLayout,
FileDropLayout, FileDropLayout,
SendAttachmentsLayout,
UserProfileDialog, UserProfileDialog,
PurgeRoomDialog, PurgeRoomDialog,
WelcomeHeaderChannelUser, WelcomeHeaderChannelUser,
MessageErrorHandler, MessageErrorHandler,
MessageOperationsChannel, MessageOperationsChannel,
RoomExport, RoomExport,
EmojiPicker EmojiPicker,
C2PABadge
}, },
data() { data() {
@ -473,7 +489,7 @@ export default {
timelineWindowPaginating: false, timelineWindowPaginating: false,
scrollPosition: null, scrollPosition: null,
currentFileInputs: null, currentFileInputs: [],
currentSendShowSendButton: true, currentSendShowSendButton: true,
currentSendError: null, currentSendError: null,
currentSendErrorExceededFile: null, currentSendErrorExceededFile: null,
@ -619,7 +635,7 @@ export default {
return this.isCurrentFileInputsAnArray return this.isCurrentFileInputsAnArray
}, },
set() { set() {
this.currentFileInputs = null this.currentFileInputs = [];
} }
}, },
chatContainer() { chatContainer() {
@ -1470,83 +1486,53 @@ export default {
/** /**
* Show attachment picker to select file * Show attachment picker to select file
*/ */
showAttachmentPicker() { showAttachmentPicker(reset) {
if (reset) {
this.resetAttachments();
}
this.$refs.attachment.click(); this.$refs.attachment.click();
}, },
optimizeImage(evt,file) { async 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; this.currentFileInputs = [... this.currentFileInputs, this.$matrix.attachmentManager.createAttachment(file)];
var reader = new FileReader(); // let optimizedFileObj;
reader.onload = (evt) => { // if (file.type.startsWith("image/")) {
if (file.type.startsWith("image/")) { // const f = await proofmode.proofCheckFile(file);
optimizedFileObj = this.optimizeImage(evt, file)
} else { // var reader = new FileReader();
optimizedFileObj = file // optimizedFileObj = await new Promise(resolve => {
} // reader.onload = evt => {
this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, optimizedFileObj] : [optimizedFileObj]; // resolve(this.optimizeImage(evt, file));
}; // }
reader.readAsDataURL(file); // reader.readAsDataURL(f);
// })
// } else {
// optimizedFileObj = file;
// }
// console.error("OPTIMIZED", optimizedFileObj);
// this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, optimizedFileObj] : [optimizedFileObj];
} }
}, },
removeAttachment(index) {
this.currentFileInputs = this.currentFileInputs.toSpliced(index, 1);
},
/** /**
* 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) {
// TODO - refactor
this.$matrix.matrixClient.getMediaConfig(this.$matrix.useAuthedMedia).then((config) => {
const configUploadSize = config["m.upload.size"]; const configUploadSize = config["m.upload.size"];
const configFormattedUploadSize = this.formatBytes(configUploadSize); const configFormattedUploadSize = this.formatBytes(configUploadSize);
uploadedFiles.every(file => { files.every(file => {
if (configUploadSize && file.size > configUploadSize) { if (configUploadSize && file.size > configUploadSize) {
this.currentSendError = this.$t("message.upload_file_too_large"); this.currentSendError = this.$t("message.upload_file_too_large");
this.currentSendErrorExceededFile = this.$t("message.upload_exceeded_file_limit", { configFormattedUploadSize }); this.currentSendErrorExceededFile = this.$t("message.upload_exceeded_file_limit", { configFormattedUploadSize });
@ -1557,9 +1543,7 @@ export default {
} }
return true; return true;
}); });
files.forEach(file => this.addAttachment(file));
uploadedFiles.forEach(file => this.handleFileReader(file));
}); });
}, },
@ -1574,9 +1558,9 @@ export default {
const promise = this.sendAttachments(text, this.currentFileInputs); const promise = this.sendAttachments(text, this.currentFileInputs);
promise.then(() => { promise.then(() => {
this.sendingAttachments = []; this.sendingAttachments = [];
this.currentFileInputs = null; this.currentFileInputs = [];
this.attachmentCaption = undefined; this.attachmentCaption = undefined;
this.sendingStatus = this.sendStatuses.INITIAL; this.sendingStatus = "initial"
}) })
.catch((err) => { .catch((err) => {
if (err.name === "AbortError" || err === "Abort") { if (err.name === "AbortError" || err === "Abort") {
@ -1592,18 +1576,14 @@ export default {
cancelSendAttachment() { cancelSendAttachment() {
this.$refs.attachment.value = null; this.$refs.attachment.value = null;
if (this.sendingStatus != this.sendStatuses.INITIAL) { if (this.sendingStatus != "initial") {
this.cancelSendAttachments(); this.cancelSendAttachments();
} }
this.currentFileInputs = null; this.currentFileInputs = [];
this.attachmentCaption = undefined; this.attachmentCaption = undefined;
this.currentSendError = null; this.currentSendError = null;
this.currentSendErrorExceededFile = null; this.currentSendErrorExceededFile = null;
this.sendingStatus = this.sendStatuses.INITIAL; this.sendingStatus = "initial";
},
addAttachment(file) {
this.handleFileReader(null, file);
}, },
resetAttachments() { resetAttachments() {
@ -2044,6 +2024,7 @@ export default {
onVoiceRecording(event) { onVoiceRecording(event) {
this.currentSendShowSendButton = false; this.currentSendShowSendButton = false;
// TODO - refactor
this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, event.file] : [event.file]; this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, event.file] : [event.file];
var text = undefined; var text = undefined;
if (this.currentInput && this.currentInput.length > 0) { if (this.currentInput && this.currentInput.length > 0) {

View file

@ -0,0 +1,44 @@
<template>
<v-img class="image-with-progress" v-bind="{...$props, ...$attrs}">
<LoadProgress class="image-with-progress__progress" v-if="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";
export default {
name: "ImageWithProgress",
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>

View 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>

View file

@ -0,0 +1,48 @@
<template>
<div v-if="show" class="c2pa-badge">
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" @click.stop="">$vuetify.icons.ic_cr</v-icon>
</template>
<span>This image contains C2PA data</span>
</v-tooltip>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
export default defineComponent({
name: "C2PABadge",
emits: [],
props: {
proof: {
type: Object as PropType<{
name?: string;
json?: string;
integrity?: { pgp?: any; c2pa?: any; exif?: any; opentimestamps?: any };
}>,
default: function () {
return undefined;
},
},
},
computed: {
show() {
console.log("PROOFCHEKDATA", this.proof);
if (this.proof) {
const {
name,
json,
integrity: { pgp, c2pa, exif, opentimestamps },
} = this.proof;
return c2pa !== undefined;
}
},
},
});
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -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() {
@ -340,7 +340,8 @@ 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. // Custom event for notice 30 seconds before a room is deleted/purged.
const deletionNotices = this.room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETION_NOTICE); const deletionNotices = this.room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETION_NOTICE);
if (deletionNotices && deletionNotices.length > 0 && deletionNotices[deletionNotices.length - 1] == event) { if (deletionNotices && deletionNotices.length > 0 && deletionNotices[deletionNotices.length - 1] == event) {
@ -352,7 +353,14 @@ export default {
} }
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;
}, },

View file

@ -38,7 +38,7 @@
</div> </div>
</div> </div>
<div class="file-drop-input-container"> <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" <v-textarea theme="dark" 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" rows="1" :placeholder="$t('file_mode.add_a_message')" hide-details background-color="transparent"
v-on:keydown.enter.prevent="() => { v-on:keydown.enter.prevent="() => {
sendCurrentTextMessage(); sendCurrentTextMessage();
@ -72,7 +72,7 @@
<div class="file-drop-sending-item" v-for="(info, index) in attachmentsSending" :key="index"> <div class="file-drop-sending-item" v-for="(info, index) in attachmentsSending" :key="index">
<v-img v-if="info.preview" :src="info.preview" /> <v-img v-if="info.preview" :src="info.preview" />
<div v-else class="filename">{{ info.attachment.name }}</div> <div v-else class="filename">{{ info.attachment.name }}</div>
<v-progress-linear :value="info.progress"></v-progress-linear> <v-progress-linear :model-value="info.progress"></v-progress-linear>
<div class="file-drop-cancel clickable" @click.stop="cancelSendAttachmentItem(info)"> <div class="file-drop-cancel clickable" @click.stop="cancelSendAttachmentItem(info)">
<v-icon size="14" color="white">close</v-icon> <v-icon size="14" color="white">close</v-icon>
</div> </div>
@ -105,7 +105,7 @@
<script> <script>
import messageMixin from "../messages/messageMixin"; import messageMixin from "../messages/messageMixin";
import sendAttachmentsMixin from "../sendAttachmentsMixin"; import sendAttachmentsMixin from "../sendAttachmentsMixin.ts";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
export default { export default {
@ -163,9 +163,7 @@ export default {
this.dropTarget = false; this.dropTarget = false;
let droppedFiles = e.dataTransfer.files; let droppedFiles = e.dataTransfer.files;
if (!droppedFiles) return; if (!droppedFiles) return;
([...droppedFiles]).forEach(f => { this.$emit('add-files', [...droppedFiles]);
this.$emit('add-file', f);
});
}, },
scrollToBottom() { scrollToBottom() {
const el = this.$refs.attachmentWrapper; const el = this.$refs.attachmentWrapper;

View file

@ -0,0 +1,322 @@
<template>
<div v-bind="{ ...$attrs }" class="send-attachments">
<div class="send-attachments__title">{{ $t("message.send_attachements_dialog_title") }}</div>
<!-- ATTACHMENT SELECTION MODE -->
<template v-if="attachments && attachments.length > 0 && status == mainStatuses.SELECTING">
<div class="attachment-wrapper" ref="attachmentWrapper" v-if="currentAttachment">
<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="currentAttachment.src && currentAttachment.status === 'loaded'" :src="currentAttachment.src" />
<div v-else 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>
</div>
<div class="send-attachments__current-item__info">
<div class="send-attachments__current-item__info__size">
<span
v-if="currentAttachment.scaledFile && currentAttachment.useScaled && currentAttachment.scaledDimensions"
>
{{ currentAttachment.scaledDimensions.width }} x {{ currentAttachment.scaledDimensions.height }}</span
>
<span v-else-if="currentAttachment.dimensions">
{{ currentAttachment.dimensions.width }} x {{ currentAttachment.dimensions.height }}
</span>
<span v-if="currentAttachment.scaledFile && currentAttachment.useScaled">
({{ formatBytes(currentAttachment.scaledFile.size) }})
</span>
<span v-else> ({{ formatBytes(currentAttachment.file.size) }}) </span>
<span class="send-attachments__current-item__info__size__filename" v-if="currentAttachment.src && currentAttachment.file.name">
- {{ currentAttachment.file.name }}
</span>
</div>
<v-switch
v-if="currentAttachment.scaledFile"
:label="$t('message.scale_image')"
v-model="currentAttachment.useScaled"
:disabled="currentAttachment.sendInfo !== undefined"
/>
<C2PABadge :proof="currentAttachment.proof" />
</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.src" :src="currentImageInput.src" />
<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="
() => {
sendAll();
}
"
/>
<div class="input-container__buttons">
<v-btn @click="close">{{ $t("menu.cancel") }}</v-btn>
<v-btn @click="sendAll" :disabled="!attachments || attachments.length == 0">{{ $t("menu.send") }}</v-btn>
</div>
</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.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 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="cancelSendAttachmentItem(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",
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>
</div>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import messageMixin from "../messages/messageMixin";
import sendAttachmentsMixin from "../sendAttachmentsMixin";
import prettyBytes from "pretty-bytes";
import type { PropType } from "vue";
import { Attachment } from "../../models/attachment";
import C2PABadge from "../c2pa/C2PABadge.vue";
export default defineComponent({
mixins: [messageMixin, sendAttachmentsMixin],
components: { C2PABadge },
emits: ["add-files", "remove-file", "pick-file", "close"],
props: {
attachments: {
type: Array as PropType<Attachment[]>,
default: function () {
return [] as Attachment[];
},
},
},
data() {
return {
currentItemIndex: 0,
messageInput: "",
mainStatuses: Object.freeze({
SELECTING: 0,
SENDING: 1,
SENT: 2,
}),
status: 0,
dropTarget: false,
};
},
mounted() {
this.$audioPlayer.setAutoplay(false);
},
computed: {
currentAttachment(): Attachment {
return this.attachments[this.currentItemIndex];
},
currentItemHasImagePreview() {
return (
this.currentItemIndex >= 0 &&
this.currentItemIndex < this.attachments.length &&
this.attachments[this.currentItemIndex].src
);
},
},
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: DragEvent) {
this.dropTarget = false;
let droppedFiles: FileList | undefined = e.dataTransfer?.files;
if (!droppedFiles) return;
this.$emit("add-files", [... droppedFiles]);
},
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: number) {
return prettyBytes(bytes);
},
close() {
this.sendingAttachments = [];
this.status = this.mainStatuses.SELECTING;
this.messageInput = "";
this.currentItemIndex = 0;
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 *;
@use "@/assets/css/sendattachments.scss" as *;
</style>

View file

@ -1,36 +1,38 @@
<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="src ? src : thumbnailSrc"
:cover="cover" :cover="cover"
:contain="contain" :contain="contain"
:loadingProgress="thumbnailProgress"
/> />
</div> </div>
<v-dialog <v-dialog v-model="dialog" :width="$vuetify.display.smAndUp ? '940px' : '90%'">
v-model="dialog" <ImageWithProgress :src="src ? src : thumbnailSrc" :loadingProgress="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, src: undefined,
thumbnailSrc: undefined,
srcProgress: -1,
thumbnailProgress: -1,
cover: true, cover: true,
contain: false, contain: false,
dialog: false dialog: false,
}; };
}, },
methods: { methods: {
@ -39,19 +41,25 @@ 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.$matrix.attachmentManager
.loadEventAttachment(
this.event,
(percent) => {
this.srcProgress = percent;
},
this
)
.catch((err) => {
console.log("Failed to fetch attachment: ", err);
});
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 height = (width * 9) / 16;
util
.getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, this.event, this.$config, width, height)
.then((url) => {
const info = this.event.getContent().info; const info = this.event.getContent().info;
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to // JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
// be stickers and small emoji type things. // be stickers and small emoji type things.
@ -62,21 +70,24 @@ export default {
this.cover = false; this.cover = false;
this.contain = true; this.contain = true;
} }
this.src = url; if (this.$refs.imageRef) {
if(this.$refs.imageRef) {
this.initMessageInImageHammerJs(this.$refs.imageRef); this.initMessageInImageHammerJs(this.$refs.imageRef);
} }
})
this.$matrix.attachmentManager
.loadEventThumbnail(
this.event,
(percent) => {
this.thumbnailProgress = percent;
},
this
)
.catch((err) => { .catch((err) => {
console.log("Failed to fetch thumbnail: ", err); console.log("Failed to fetch thumbnail: ", err);
}); });
}, },
beforeUnmount() { beforeUnmount() {
if (this.src) { this.$matrix.attachmentManager.releaseEvent(this.event);
const objectUrl = this.src;
this.src = null;
URL.revokeObjectURL(objectUrl);
}
}, },
}; };
</script> </script>

View file

@ -3,35 +3,48 @@
<div class="bubble"> <div class="bubble">
<div class="original-message" v-if="inReplyToText"> <div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div> <div class="original-message-sender">{{ inReplyToSender }}</div>
<div <div class="original-message-text" v-html="linkify($sanitize(inReplyToText))" />
class="original-message-text"
v-html="linkify($sanitize(inReplyToText))"
/>
</div> </div>
<div class="message"> <div class="message">
<SwipeableThumbnailsView :items="items" v-if="!event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL" v-bind="$attrs" /> <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-container v-else-if="!event.isRedacted()" fluid class="imageCollection">
<v-row wrap> <v-row wrap>
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size"> <v-col v-for="{ size, item } in layoutedItems()" :key="item.event.getId()" :cols="size">
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" /> <ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
<i v-if="event.isRedacted()" class="deleted-text"> <i v-if="event.isRedacted()" class="deleted-text">
<v-icon :color="this.senderIsAdminOrModerator(this.event) ? 'white' : ''" size="small">block</v-icon> <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')}} {{
redactedBySomeoneElse(event)
? $t("message.incoming_message_deleted_text")
: $t("message.outgoing_message_deleted_text")
}}
</i> </i>
<span v-html="linkify($sanitize(messageText))" v-else-if="messageText" /> <span v-html="linkify($sanitize(messageText))" v-else-if="messageText" />
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()"> <span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
{{ $t('message.edited') }} {{ $t("message.edited") }}
</span> </span>
</div> </div>
</div> </div>
<GalleryItemsView :originalEvent="originalEvent" :items="items" :initialItem="showItem" v-if="!!showItem" v-on:close="showItem = null" /> <GalleryItemsView
:originalEvent="originalEvent"
:items="items"
:initialItem="showItem"
v-if="!!showItem"
v-on:close="showItem = null"
/>
</message-incoming> </message-incoming>
<component v-else-if="items.length == 1" :is="componentFn(items[0].event)" <component
v-bind="{...$props, ...$attrs}" v-else-if="items.length == 1"
:is="componentFn(items[0].event)"
v-bind="{ ...$props, ...$attrs }"
:originalEvent="items[0].event" :originalEvent="items[0].event"
/> />
</template> </template>
@ -40,24 +53,33 @@
import MessageIncoming from "./MessageIncoming.vue"; import MessageIncoming from "./MessageIncoming.vue";
import messageMixin from "./messageMixin"; import messageMixin from "./messageMixin";
import util, { ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE } from "../../plugins/utils"; import util, { ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE } from "../../plugins/utils";
import GalleryItemsView from '../file_mode/GalleryItemsView.vue'; import GalleryItemsView from "../file_mode/GalleryItemsView.vue";
import ThumbnailView from '../file_mode/ThumbnailView.vue'; import ThumbnailView from "../file_mode/ThumbnailView.vue";
import SwipeableThumbnailsView from "./channel/SwipeableThumbnailsView.vue"; import SwipeableThumbnailsView from "./channel/SwipeableThumbnailsView.vue";
import { reactive } from "vue"; import { reactive } from "vue";
export default { export default {
extends: MessageIncoming, extends: MessageIncoming,
components: { MessageIncoming, GalleryItemsView, ThumbnailView, SwipeableThumbnailsView }, components: {
MessageIncoming,
GalleryItemsView,
ThumbnailView,
SwipeableThumbnailsView,
},
mixins: [messageMixin], mixins: [messageMixin],
data() { data() {
return { return {
ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL, ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
items: [], items: [],
showItem: null, showItem: null,
} };
}, },
mounted() { mounted() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(
this.event.getId(),
util.threadMessageType(),
"m.room.message"
);
if (!this.thread) { if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated); this.event.on("Event.relationsCreated", this.onRelationsCreated);
} }
@ -67,42 +89,66 @@ export default {
}, },
computed: { computed: {
forceMultiview() { 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)); 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: { methods: {
onRelationsCreated() { onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(
this.event.getId(),
util.threadMessageType(),
"m.room.message"
);
this.event.off("Event.relationsCreated", this.onRelationsCreated); this.event.off("Event.relationsCreated", this.onRelationsCreated);
}, },
onItemClick(event) { onItemClick(event) {
this.showItem = event.item; this.showItem = event.item;
}, },
processThread() { processThread() {
this.$emit('layout-change', () => { if (!this.event.isRedacted()) {
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()) this.$emit(
.filter(e => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype)) "layout-change",
.map(e => { () => {
const items = this.timelineSet.relations
.getAllChildEventsForEvent(this.event.getId())
.filter((e) => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype));
this.items = items.map((e) => {
let ret = reactive({ let ret = reactive({
event: e, event: e,
src: null, src: null,
}); });
ret.promise = this.$matrix.matrixClient.decryptEventIfNeeded(e) if (items.length > 1) {
.then(() => util.getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, e, this.$config, 100, 100)) ret.promise = this.$matrix.matrixClient
.decryptEventIfNeeded(e)
.then(() =>
util.getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, e, this.$config, 100, 100)
)
.then((url) => { .then((url) => {
ret.src = url; ret.src = url;
}) })
.catch((err) => { .catch((err) => {
console.log("Failed to fetch thumbnail: ", err); console.log("Failed to fetch thumbnail: ", err);
}); });
}
return ret; return ret;
}); });
}, this.$el); },
this.$el
);
}
}, },
layoutedItems() { layoutedItems() {
if (!this.items || this.items.length == 0) { return [] } if (!this.items || this.items.length == 0) {
return [];
}
let array = this.items.slice(0); let array = this.items.slice(0);
let rows = [] let rows = [];
while (array.length > 0) { while (array.length > 0) {
if (array.length >= 7) { if (array.length >= 7) {
rows.push({ size: 6, item: array[0] }); rows.push({ size: 6, item: array[0] });
@ -127,12 +173,12 @@ export default {
array = array.slice(1); array = array.slice(1);
} }
} }
return rows return rows;
}, },
downloadAll() { downloadAll() {
this.items.forEach(item => util.download(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, item.event)); this.items.forEach((item) => util.download(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, item.event));
} },
} },
}; };
</script> </script>

View file

@ -1,36 +1,38 @@
<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="src ? src : thumbnailSrc"
:cover="cover" :cover="cover"
:contain="contain" :contain="contain"
:loadingProgress="thumbnailProgress"
/> />
</div> </div>
<v-dialog <v-dialog v-model="dialog" :width="$vuetify.display.smAndUp ? '940px' : '90%'">
v-model="dialog" <ImageWithProgress :src="src ? src : thumbnailSrc" :loadingProgress="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, src: undefined,
thumbnailSrc: undefined,
srcProgress: -1,
thumbnailProgress: -1,
cover: true, cover: true,
contain: false, contain: false,
dialog: false dialog: false,
}; };
}, },
methods: { methods: {
@ -39,18 +41,24 @@ 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.$matrix.attachmentManager
.loadEventAttachment(
this.event,
(percent) => {
this.srcProgress = percent;
},
this
)
.catch((err) => {
console.log("Failed to fetch attachment: ", err);
});
this.dialog = true; this.dialog = true;
} }
}); });
} },
}, },
mounted() { mounted() {
const width = this.$refs.image.$el.clientWidth;
const height = (width * 9) / 16;
util
.getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, this.event, this.$config, width, height)
.then((url) => {
const info = this.event.getContent().info; const info = this.event.getContent().info;
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to // JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
// be stickers and small emoji type things. // be stickers and small emoji type things.
@ -61,21 +69,24 @@ export default {
this.cover = false; this.cover = false;
this.contain = true; this.contain = true;
} }
this.src = url; if (this.$refs.imageRef) {
if(this.$refs.imageRef) {
this.initMessageOutImageHammerJs(this.$refs.imageRef); this.initMessageOutImageHammerJs(this.$refs.imageRef);
} }
})
this.$matrix.attachmentManager
.loadEventThumbnail(
this.event,
(percent) => {
this.thumbnailProgress = percent;
},
this
)
.catch((err) => { .catch((err) => {
console.log("Failed to fetch thumbnail: ", err); console.log("Failed to fetch thumbnail: ", err);
}); });
}, },
beforeUnmount() { beforeUnmount() {
if (this.src) { this.$matrix.attachmentManager.releaseEvent(this.event);
const objectUrl = this.src;
this.src = null;
URL.revokeObjectURL(objectUrl);
}
}, },
}; };
</script> </script>

View file

@ -3,46 +3,46 @@
<div class="bubble"> <div class="bubble">
<div class="original-message" v-if="inReplyToText"> <div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div> <div class="original-message-sender">{{ inReplyToSender }}</div>
<div <div class="original-message-text" v-html="linkify($sanitize(inReplyToText))" />
class="original-message-text"
v-html="linkify($sanitize(inReplyToText))"
/>
</div> </div>
<div class="message"> <div class="message">
<SwipeableThumbnailsView :items="items" v-if="!event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL" v-bind="$attrs" /> <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-container v-else-if="!event.isRedacted()" fluid class="imageCollection">
<v-row wrap> <v-row wrap>
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size"> <v-col v-for="{ size, item } in layoutedItems()" :key="item.event.getId()" :cols="size">
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" /> <ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
<i v-if="event.isRedacted()" class="deleted-text"> <i v-if="event.isRedacted()" class="deleted-text">
<v-icon size="small">block</v-icon> <v-icon size="small">block</v-icon>
{{ redactedBySomeoneElse(event) ? $t('message.incoming_message_deleted_text') : $t('message.outgoing_message_deleted_text')}} {{
redactedBySomeoneElse(event)
? $t("message.incoming_message_deleted_text")
: $t("message.outgoing_message_deleted_text")
}}
</i> </i>
<span v-html="linkify($sanitize(messageText))" v-else-if="messageText" /> <span v-html="linkify($sanitize(messageText))" v-else-if="messageText" />
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()"> <span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
{{ $t('message.edited') }} {{ $t("message.edited") }}
</span> </span>
</div> </div>
</div> </div>
<GalleryItemsView :originalEvent="originalEvent" :items="items" :initialItem="showItem" v-if="!!showItem" v-on:close="showItem = null" /> <GalleryItemsView :originalEvent="originalEvent" :items="items" :initialItem="showItem" v-if="!!showItem"
v-on:close="showItem = null" />
</message-outgoing> </message-outgoing>
<component v-else-if="items.length == 1" :is="componentFn(items[0].event)" <component v-else-if="items.length == 1" :is="componentFn(items[0].event)" v-bind="{ ...$props, ...$attrs }"
v-bind="{...$props, ...$attrs}" :originalEvent="items[0].event" />
:originalEvent="items[0].event"
/>
</template> </template>
<script> <script>
import MessageOutgoing from "./MessageOutgoing.vue"; import MessageOutgoing from "./MessageOutgoing.vue";
import messageMixin from "./messageMixin"; import messageMixin from "./messageMixin";
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils"; import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
import GalleryItemsView from '../file_mode/GalleryItemsView.vue'; import GalleryItemsView from "../file_mode/GalleryItemsView.vue";
import ThumbnailView from '../file_mode/ThumbnailView.vue'; import ThumbnailView from "../file_mode/ThumbnailView.vue";
import SwipeableThumbnailsView from "./channel/SwipeableThumbnailsView.vue"; import SwipeableThumbnailsView from "./channel/SwipeableThumbnailsView.vue";
import { reactive } from "vue"; import { reactive } from "vue";
@ -55,10 +55,14 @@ export default {
ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL, ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
items: [], items: [],
showItem: null, showItem: null,
} };
}, },
mounted() { mounted() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(
this.event.getId(),
util.threadMessageType(),
"m.room.message"
);
if (!this.thread) { if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated); this.event.on("Event.relationsCreated", this.onRelationsCreated);
} }
@ -68,42 +72,65 @@ export default {
}, },
computed: { computed: {
forceMultiview() { forceMultiview() {
return this.room.displayType == ROOM_TYPE_CHANNEL && this.items.length == 1 && util.isFileTypePDF(this.items[0].event); return (
} this.room.displayType == ROOM_TYPE_CHANNEL && this.items.length == 1 && util.isFileTypePDF(this.items[0].event)
);
},
}, },
methods: { methods: {
onRelationsCreated() { onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message"); this.thread = this.timelineSet.relations.getChildEventsForEvent(
this.event.getId(),
util.threadMessageType(),
"m.room.message"
);
this.event.off("Event.relationsCreated", this.onRelationsCreated); this.event.off("Event.relationsCreated", this.onRelationsCreated);
}, },
onItemClick(event) { onItemClick(event) {
this.showItem = event.item; this.showItem = event.item;
}, },
processThread() { processThread() {
this.$emit('layout-change', () => { if (!this.event.isRedacted()) {
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()) this.$emit(
.filter(e => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype)) "layout-change",
.map(e => { () => {
const items = this.timelineSet.relations
.getAllChildEventsForEvent(this.event.getId())
.filter((e) => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype));
this.items = items.map((e) => {
let ret = reactive({ let ret = reactive({
event: e, event: e,
src: null, src: null,
}); });
ret.promise = this.$matrix.matrixClient.decryptEventIfNeeded(e) if (items.length > 1) {
.then(() => util.getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, e, this.$config, 100, 100)) // Only do if items more than one. If one, the individual component in <component> above will do the work.
//
ret.promise = this.$matrix.matrixClient
.decryptEventIfNeeded(e)
.then(() =>
util.getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, e, this.$config, 100, 100)
)
.then((url) => { .then((url) => {
ret.src = url; ret.src = url;
}) })
.catch((err) => { .catch((err) => {
console.log("Failed to fetch thumbnail: ", err); console.log("Failed to fetch thumbnail: ", err);
}); });
}
return ret; return ret;
}); });
}, this.$el); },
this.$el
);
}
}, },
layoutedItems() { layoutedItems() {
if (!this.items || this.items.length == 0) { return [] } if (!this.items || this.items.length == 0) {
return [];
}
let array = this.items.slice(0); let array = this.items.slice(0);
let rows = [] let rows = [];
while (array.length > 0) { while (array.length > 0) {
if (array.length >= 7) { if (array.length >= 7) {
rows.push({ size: 6, item: array[0] }); rows.push({ size: 6, item: array[0] });
@ -128,9 +155,9 @@ export default {
array = array.slice(1); array = array.slice(1);
} }
} }
return rows return rows;
},
}, },
}
}; };
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -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,
}, },

View file

@ -0,0 +1 @@
declare module 'sendAttachmentsMixin';

View file

@ -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);
},
}
}

View file

@ -0,0 +1,195 @@
import { defineComponent, reactive } from "vue";
import util from "../plugins/utils";
import { Attachment, AttachmentSendInfo } from "../models/attachment";
export default defineComponent({
data(): {
sendingStatus: "initial" | "sending" | "sent" | "canceled" | "failed";
sendingRootEventId: string | null;
sendingPromise: Promise<any> | null;
sendingAttachments: Attachment[];
} {
return {
// sendStatuses: Object.freeze({
// INITIAL: 0,
// SENDING: 1,
// SENT: 2,
// CANCELED: 3,
// FAILED: 4,
// }),
sendingStatus: "initial",
sendingPromise: null,
sendingRootEventId: null,
sendingAttachments: [] as Attachment[],
};
},
computed: {
attachmentsSentCount(): number {
return this.sendingAttachments
? this.sendingAttachments.reduce((a, elem) => (elem.sendInfo?.status == "sent" ? a + 1 : a), 0)
: 0;
},
attachmentsSending(): Attachment[] {
return this.sendingAttachments
? this.sendingAttachments.filter(
(elem) => elem.sendInfo?.status == "initial" || elem.sendInfo?.status == "sending"
)
: [];
},
attachmentsSent(): Attachment[] {
this.sortSendingAttachments();
return this.sendingAttachments ? this.sendingAttachments.filter((elem) => elem.sendInfo?.status == "sent") : [];
},
},
methods: {
sendAttachments(text: string, attachments: Attachment[]) {
this.sendingStatus = "sending";
this.sendingAttachments = attachments.map((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);
return attachment;
});
this.sendingPromise = util
.sendTextMessage(this.$matrix.matrixClient, this.room.roomId, text)
.then((eventId: string) => {
this.sendingRootEventId = eventId;
// Use the eventId as a thread root for all the media
let promiseChain = Promise.resolve();
const getItemPromise = (index: number) => {
if (index < this.sendingAttachments.length) {
const attachment = this.sendingAttachments[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 = util
.sendFile(
this.$matrix.matrixClient,
this.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 (this.attachmentsSent.length > 0) {
if (this.attachmentsSent[0].sendInfo!.randomRotation >= 0) {
signR = -1;
}
if (this.attachmentsSent[0].sendInfo!.randomTranslationX >= 0) {
signX = -1;
}
if (this.attachmentsSent[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(() => {
this.sendingStatus = "sent";
this.sendingRootEventId = null;
})
.catch((err: any) => {
console.error("ERROR", err);
});
return this.sendingPromise;
},
cancelSendAttachments() {
this.sendingAttachments.toReversed().forEach((item) => {
this.cancelSendAttachmentItem(item);
});
this.sendingStatus = "canceled";
if (this.sendingRootEventId && this.room) {
// Redact all media we already sent, plus the root event
let promises = this.sendingAttachments
.filter((item) => item.sendInfo?.mediaEventId !== undefined)
.map((item) =>
this.$matrix.matrixClient.redactEvent(this.room.roomId, item.sendInfo!.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: Attachment) {
if (item.sendInfo) {
if (item.sendInfo.promise && item.sendInfo.status != "initial") {
item.sendInfo.promise.abort();
}
item.sendInfo.status = "canceled";
}
},
sortSendingAttachments() {
this.sendingAttachments.sort((a, b) => (b.sendInfo?.statusDate ?? 0) - (a.sendInfo?.statusDate ?? 0));
},
},
});

51
src/models/attachment.ts Normal file
View file

@ -0,0 +1,51 @@
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;
};

View file

@ -0,0 +1,325 @@
import { MatrixClient, MatrixEvent } from "matrix-js-sdk";
import { KeanuEventExtension } from "./eventAttachment";
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { Counter, ModeOfOperation } from "aes-js";
import { Attachment } from "./attachment";
import proofmode from "../plugins/proofmode";
import imageSize from "image-size";
import imageResize from "image-resize";
import { reactive } from "vue";
type CacheEntry = {
attachment?: string;
thumbnail?: string;
attachmentPromise?: Promise<string>;
thumbnailPromise?: Promise<string>;
attachmentProgress?: ((progress: number) => void)[];
thumbnailProgress?: ((progress: number) => void)[];
};
export class AttachmentManager {
matrixClient: MatrixClient;
useAuthedMedia: boolean;
maxSizeUploads: number;
maxSizeAutoDownloads: number;
cache: Map<string | undefined, CacheEntry>;
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 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 async loadEventAttachment(
event: MatrixEvent & KeanuEventExtension,
progress?: (percent: number) => void,
outputObject?: { src: string; thumbnailSrc: string }
): Promise<string> {
console.error("GET ATTACHMENT FOR EVENT", event.getId());
const entry = this.cache.get(event.getId()) ?? {};
if (entry.attachment) {
if (outputObject) {
outputObject.src = entry.attachment;
}
return entry.attachment;
}
if (!entry.attachmentPromise) {
entry.attachmentPromise = this._loadEventAttachmentOrThumbnail(event, false, progress)
.then((attachment) => {
entry.attachment = attachment;
return attachment;
})
.catch((err) => {
entry.attachmentPromise = undefined;
throw err;
});
this.cache.set(event.getId(), entry);
}
entry.attachmentProgress = (entry.attachmentProgress ?? []).concat();
return entry.attachmentPromise.then((attachment) => {
console.error("GOT ATTACHMENT", attachment);
if (outputObject) {
outputObject.src = attachment;
}
return attachment;
});
}
public async loadEventThumbnail(
event: MatrixEvent & KeanuEventExtension,
progress?: (percent: number) => void,
outputObject?: { src: string; thumbnailSrc: string }
): Promise<string> {
console.error("GET THUMB FOR EVENT", event.getId());
const entry = this.cache.get(event.getId()) ?? {};
if (entry.thumbnail) {
if (outputObject) {
outputObject.thumbnailSrc = entry.thumbnail;
}
return entry.thumbnail;
}
if (!entry.thumbnailPromise) {
entry.thumbnailPromise = this._loadEventAttachmentOrThumbnail(event, true, progress)
.then((thummbnail) => {
entry.thumbnail = thummbnail;
return thummbnail;
})
.catch((err) => {
entry.thumbnailPromise = undefined;
throw err;
});
this.cache.set(event.getId(), entry);
}
return entry.thumbnailPromise.then((thumbnail) => {
console.error("GOT THUMB", thumbnail);
if (outputObject) {
outputObject.thumbnailSrc = thumbnail;
}
return thumbnail;
});
}
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 }));
}
releaseEvent(event: MatrixEvent & KeanuEventExtension): void {
console.error("Release event", event.getId());
const entry = this.cache.get(event.getId());
if (entry) {
// TODO - abortable promises
this.cache.delete(event.getId());
if (entry.attachment) {
URL.revokeObjectURL(entry.attachment);
}
if (entry.thumbnail) {
URL.revokeObjectURL(entry.thumbnail);
}
}
}
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");
});
});
}
}

View file

@ -0,0 +1,15 @@
import { MatrixEvent } from "matrix-js-sdk";
export type KeanuEventExtension = {
isMxThread?: boolean;
isChannelMessage?: boolean;
isPinned?: boolean;
}
export type EventAttachment = {
event: MatrixEvent & KeanuEventExtension;
src?: string;
thumbnail?: string;
srcPromise?: Promise<string>;
thumbnailPromise?: Promise<string>;
};

38
src/plugins/proofmode.ts Normal file
View 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();

View 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);

View file

@ -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,50 +138,28 @@ 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, } else if (content.url != null) {
undefined, url = matrixClient.mxcUrlToHttp(content.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
useAuthedMedia
);
decrypt = false; decrypt = false;
if (content.info) { if (content.info) {
mime = content.info.mimetype; mime = content.info.mimetype;
} }
} else if (content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) { } else if (content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) {
file = content.info.thumbnail_file; file = content.info.thumbnail_file;
// var width = w; url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
// 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; mime = file.mimetype;
} else if ( } else if (
content.file && content.file &&
@ -217,41 +169,23 @@ class Util {
) { ) {
// No thumb, use real url // No thumb, use real url
file = content.file; file = content.file;
url = matrixClient.mxcUrlToHttp( url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
file.url,
undefined,
undefined,
undefined,
undefined,
undefined,
useAuthedMedia
);
mime = file.mimetype; mime = file.mimetype;
} }
if (url == null) { if (url == null) {
reject("No url found!"); throw new Error("No url found!");
return;
} }
axios 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,8 +403,12 @@ class Util {
reject("Aborted"); reject("Aborted");
return; return;
} }
try {
const fileContents = e.target.result; const fileContents = e.target.result;
var data = new Uint8Array(fileContents); var data = new Uint8Array(fileContents);
let thumbnailData = undefined;
let thumbnailInfo = undefined;
const info = { const info = {
mimetype: file.type, mimetype: file.type,
@ -456,19 +424,38 @@ class Util {
var msgtype = "m.file"; var msgtype = "m.file";
if (file.type.startsWith("image/")) { if (file.type.startsWith("image/")) {
msgtype = "m.image"; msgtype = "m.image";
// Generate thumbnail?
if (dimensions) {
const w = dimensions.width;
const h = dimensions.height;
if (w > THUMBNAIL_MAX_WIDTH || h > THUMBNAIL_MAX_HEIGHT) {
var aspect = w / h;
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());
const scaled = await imageResize(file, {
format: "webp",
width: newWidth,
height: newHeight,
outputType: "blob",
}).catch(() => {return Promise.resolve(undefined)});
if (scaled && file.size > scaled.size) {
thumbnailData = new Uint8Array(await scaled.arrayBuffer());
thumbnailInfo = {
mimetype: scaled.type,
size: scaled.size,
h: newHeight,
w: newWidth,
};
}
}
}
} else if (file.type.startsWith("audio/")) { } else if (file.type.startsWith("audio/")) {
msgtype = "m.audio"; msgtype = "m.audio";
} else if (file.type.startsWith("video/")) { } else if (file.type.startsWith("video/")) {
msgtype = "m.video"; msgtype = "m.video";
} }
const opts = {
type: file.type,
name: description,
progressHandler: onUploadProgress,
onlyContentUri: false,
};
var messageContent = { var messageContent = {
body: description, body: description,
info: info, info: info,
@ -488,82 +475,68 @@ class Util {
messageContent.filename = file.name; messageContent.filename = file.name;
} }
if (!matrixClient.isRoomEncrypted(roomId)) { const useEncryption = matrixClient.isRoomEncrypted(roomId);
// Not encrypted.
const promise = matrixClient.uploadContent(data, opts);
uploadPromise.onAbort = () => {
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))); const dataUploadOpts = {
let iv = Buffer.concat([Buffer.from(crypto.getRandomValues(new Uint8Array(8))), Buffer.alloc(8)]); // Initialization vector. type: useEncryption ? "application/octet-stream" : file.type,
name: description,
// Encrypt progressHandler: onUploadProgress,
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv)); onlyContentUri: false,
var encryptedBytes = aesCtr.encrypt(data);
data = encryptedBytes;
// Calculate sha256
let hash = await crypto.subtle.digest("SHA-256", data);
const jwk = {
kty: "oct",
key_ops: ["encrypt", "decrypt"],
alg: "A256CTR",
k: key.toString("base64").replaceAll(/\//g, "_").replaceAll(/\+/g, "-"),
ext: true,
};
const encryptedFile = {
mimetype: file.type,
key: jwk,
iv: Buffer.from(iv).toString("base64").replace(/=/g, ""),
hashes: { sha256: Buffer.from(hash).toString("base64").replace(/=/g, "") },
v: "v2",
}; };
if (useEncryption) {
const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(data, file.type);
messageContent.file = encryptedFile; messageContent.file = encryptedFile;
data = encryptedBytes;
// 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);
} }
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") {
this.generateWaveform(fileContents, messageContent);
}
const result = await this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
resolve(result); resolve(result);
}) } catch (error) {
.catch((err) => { reject(error);
reject(err); }
});
}; };
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();

View file

@ -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;

View file

@ -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"]
} }

View file

@ -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: [
{ {
@ -68,54 +47,8 @@ export default defineConfig(({mode}) => ({
{ find: "~@", replacement: fileURLToPath(new URL("./src", import.meta.url)) }, { find: "~@", replacement: fileURLToPath(new URL("./src", import.meta.url)) },
{ 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: {
//global: "window",
//module: {},
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: { build: {
commonjsOptions: { transformMixedEsModules: true } // Change commonjsOptions: { transformMixedEsModules: true }
} }
// 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",
// },
// },
})); }));