Lots of fixes to "media threads"
This commit is contained in:
parent
fe081edc62
commit
8bcceafcff
23 changed files with 867 additions and 333 deletions
109
package-lock.json
generated
109
package-lock.json
generated
|
|
@ -29,7 +29,7 @@
|
||||||
"linkify-html": "^4.1.0",
|
"linkify-html": "^4.1.0",
|
||||||
"linkifyjs": "^4.1.0",
|
"linkifyjs": "^4.1.0",
|
||||||
"material-design-icons-iconfont": "^6.1",
|
"material-design-icons-iconfont": "^6.1",
|
||||||
"matrix-js-sdk": "^23.4.0",
|
"matrix-js-sdk": "^29.1.0",
|
||||||
"md-gum-polyfill": "^1.0.0",
|
"md-gum-polyfill": "^1.0.0",
|
||||||
"mic-recorder-to-mp3": "^2.2.2",
|
"mic-recorder-to-mp3": "^2.2.2",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
|
|
@ -1844,10 +1844,10 @@
|
||||||
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
|
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@matrix-org/matrix-sdk-crypto-js": {
|
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
||||||
"version": "0.1.0-alpha.4",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.4.tgz",
|
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-2.2.0.tgz",
|
||||||
"integrity": "sha512-mdaDKrw3P5ZVCpq0ioW0pV6ihviDEbS8ZH36kpt9stLKHwwDSopPogE6CkQhi0B1jn1yBUtOYi32mBV/zcOR7g==",
|
"integrity": "sha512-txmvaTiZpVV0/kWCRcE7tZvRESCEc1ynLJDVh9OUsFlaXfl13c7qdD3E6IJEJ8YiPMIn+PHogdfBZsO84reaMg==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
|
|
@ -2066,9 +2066,9 @@
|
||||||
"integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ=="
|
"integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/events": {
|
"node_modules/@types/events": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.2.tgz",
|
||||||
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
|
"integrity": "sha512-v4Mr60wJuF069iZZCdY5DKhfj0l6eXNJtbSM/oMDNdRLoBEUsktmKnswkz0X3OAic5W8Qy/YU6owKE4A66Y46A=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/express": {
|
"node_modules/@types/express": {
|
||||||
"version": "4.17.14",
|
"version": "4.17.14",
|
||||||
|
|
@ -5968,6 +5968,11 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||||
|
},
|
||||||
"node_modules/css-declaration-sorter": {
|
"node_modules/css-declaration-sorter": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz",
|
||||||
|
|
@ -9263,6 +9268,11 @@
|
||||||
"setimmediate": "^1.0.5"
|
"setimmediate": "^1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwt-decode": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||||
|
},
|
||||||
"node_modules/kind-of": {
|
"node_modules/kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
|
@ -9946,31 +9956,33 @@
|
||||||
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
||||||
},
|
},
|
||||||
"node_modules/matrix-js-sdk": {
|
"node_modules/matrix-js-sdk": {
|
||||||
"version": "23.4.0",
|
"version": "29.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-23.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-29.1.0.tgz",
|
||||||
"integrity": "sha512-3gHT6IrDYBkFYzaZM052uZXv1WFGoN+q83rTvmTpMtxZYrwosLHb6Y/w/Lfl26y1N1gTDdyQ0Vd3NpSycKmpJA==",
|
"integrity": "sha512-nF+ACFioDltGCf2KFfXK7QoJ70Ytnzm4Jse2UI+BDXeR9WCjtKefXJtboN2rmU4MFmLCTHcnBTmu6yig67YUqw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3",
|
"@matrix-org/matrix-sdk-crypto-wasm": "^2.0.0",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"bs58": "^5.0.0",
|
"bs58": "^5.0.0",
|
||||||
"content-type": "^1.0.4",
|
"content-type": "^1.0.4",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
"loglevel": "^1.7.1",
|
"loglevel": "^1.7.1",
|
||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
"matrix-widget-api": "^1.0.0",
|
"matrix-widget-api": "^1.6.0",
|
||||||
|
"oidc-client-ts": "^2.2.4",
|
||||||
"p-retry": "4",
|
"p-retry": "4",
|
||||||
"sdp-transform": "^2.14.1",
|
"sdp-transform": "^2.14.1",
|
||||||
"unhomoglyph": "^1.0.6",
|
"unhomoglyph": "^1.0.6",
|
||||||
"uuid": "9"
|
"uuid": "9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/matrix-widget-api": {
|
"node_modules/matrix-widget-api": {
|
||||||
"version": "1.2.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.6.0.tgz",
|
||||||
"integrity": "sha512-BkBTREtXjCUM3Kx4UBgDmKoz39w7AXfIjBIC/jISBdcJkg8upFUhpIy+zUrSCIrmfO2Ke8LOsSoFoQkOyhqGxQ==",
|
"integrity": "sha512-VXIJyAZ/WnBmT4C7ePqevgMYGneKMCP/0JuCOqntSsaNlCRHJvwvTxmqUU+ufOpzIF5gYNyIrAjbgrEbK3iqJQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/events": "^3.0.0",
|
"@types/events": "^3.0.0",
|
||||||
"events": "^3.2.0"
|
"events": "^3.2.0"
|
||||||
|
|
@ -10746,6 +10758,18 @@
|
||||||
"resolved": "https://registry.npmjs.org/octal/-/octal-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/octal/-/octal-1.0.0.tgz",
|
||||||
"integrity": "sha512-nnda7W8d+A3vEIY+UrDQzzboPf1vhs4JYVhff5CDkq9QNoZY7Xrxeo/htox37j9dZf7yNHevZzqtejWgy1vCqQ=="
|
"integrity": "sha512-nnda7W8d+A3vEIY+UrDQzzboPf1vhs4JYVhff5CDkq9QNoZY7Xrxeo/htox37j9dZf7yNHevZzqtejWgy1vCqQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/oidc-client-ts": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==",
|
||||||
|
"dependencies": {
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"jwt-decode": "^3.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
|
@ -17608,10 +17632,10 @@
|
||||||
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
|
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@matrix-org/matrix-sdk-crypto-js": {
|
"@matrix-org/matrix-sdk-crypto-wasm": {
|
||||||
"version": "0.1.0-alpha.4",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.4.tgz",
|
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-2.2.0.tgz",
|
||||||
"integrity": "sha512-mdaDKrw3P5ZVCpq0ioW0pV6ihviDEbS8ZH36kpt9stLKHwwDSopPogE6CkQhi0B1jn1yBUtOYi32mBV/zcOR7g=="
|
"integrity": "sha512-txmvaTiZpVV0/kWCRcE7tZvRESCEc1ynLJDVh9OUsFlaXfl13c7qdD3E6IJEJ8YiPMIn+PHogdfBZsO84reaMg=="
|
||||||
},
|
},
|
||||||
"@matrix-org/olm": {
|
"@matrix-org/olm": {
|
||||||
"version": "3.2.12",
|
"version": "3.2.12",
|
||||||
|
|
@ -17803,9 +17827,9 @@
|
||||||
"integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ=="
|
"integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ=="
|
||||||
},
|
},
|
||||||
"@types/events": {
|
"@types/events": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.2.tgz",
|
||||||
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
|
"integrity": "sha512-v4Mr60wJuF069iZZCdY5DKhfj0l6eXNJtbSM/oMDNdRLoBEUsktmKnswkz0X3OAic5W8Qy/YU6owKE4A66Y46A=="
|
||||||
},
|
},
|
||||||
"@types/express": {
|
"@types/express": {
|
||||||
"version": "4.17.14",
|
"version": "4.17.14",
|
||||||
|
|
@ -20893,6 +20917,11 @@
|
||||||
"randomfill": "^1.0.3"
|
"randomfill": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||||
|
},
|
||||||
"css-declaration-sorter": {
|
"css-declaration-sorter": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz",
|
||||||
|
|
@ -23451,6 +23480,11 @@
|
||||||
"setimmediate": "^1.0.5"
|
"setimmediate": "^1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jwt-decode": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||||
|
},
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
|
@ -24033,18 +24067,20 @@
|
||||||
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
||||||
},
|
},
|
||||||
"matrix-js-sdk": {
|
"matrix-js-sdk": {
|
||||||
"version": "23.4.0",
|
"version": "29.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-23.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-29.1.0.tgz",
|
||||||
"integrity": "sha512-3gHT6IrDYBkFYzaZM052uZXv1WFGoN+q83rTvmTpMtxZYrwosLHb6Y/w/Lfl26y1N1gTDdyQ0Vd3NpSycKmpJA==",
|
"integrity": "sha512-nF+ACFioDltGCf2KFfXK7QoJ70Ytnzm4Jse2UI+BDXeR9WCjtKefXJtboN2rmU4MFmLCTHcnBTmu6yig67YUqw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3",
|
"@matrix-org/matrix-sdk-crypto-wasm": "^2.0.0",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"bs58": "^5.0.0",
|
"bs58": "^5.0.0",
|
||||||
"content-type": "^1.0.4",
|
"content-type": "^1.0.4",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
"loglevel": "^1.7.1",
|
"loglevel": "^1.7.1",
|
||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
"matrix-widget-api": "^1.0.0",
|
"matrix-widget-api": "^1.6.0",
|
||||||
|
"oidc-client-ts": "^2.2.4",
|
||||||
"p-retry": "4",
|
"p-retry": "4",
|
||||||
"sdp-transform": "^2.14.1",
|
"sdp-transform": "^2.14.1",
|
||||||
"unhomoglyph": "^1.0.6",
|
"unhomoglyph": "^1.0.6",
|
||||||
|
|
@ -24052,9 +24088,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"matrix-widget-api": {
|
"matrix-widget-api": {
|
||||||
"version": "1.2.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.6.0.tgz",
|
||||||
"integrity": "sha512-BkBTREtXjCUM3Kx4UBgDmKoz39w7AXfIjBIC/jISBdcJkg8upFUhpIy+zUrSCIrmfO2Ke8LOsSoFoQkOyhqGxQ==",
|
"integrity": "sha512-VXIJyAZ/WnBmT4C7ePqevgMYGneKMCP/0JuCOqntSsaNlCRHJvwvTxmqUU+ufOpzIF5gYNyIrAjbgrEbK3iqJQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/events": "^3.0.0",
|
"@types/events": "^3.0.0",
|
||||||
"events": "^3.2.0"
|
"events": "^3.2.0"
|
||||||
|
|
@ -24693,6 +24729,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/octal/-/octal-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/octal/-/octal-1.0.0.tgz",
|
||||||
"integrity": "sha512-nnda7W8d+A3vEIY+UrDQzzboPf1vhs4JYVhff5CDkq9QNoZY7Xrxeo/htox37j9dZf7yNHevZzqtejWgy1vCqQ=="
|
"integrity": "sha512-nnda7W8d+A3vEIY+UrDQzzboPf1vhs4JYVhff5CDkq9QNoZY7Xrxeo/htox37j9dZf7yNHevZzqtejWgy1vCqQ=="
|
||||||
},
|
},
|
||||||
|
"oidc-client-ts": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==",
|
||||||
|
"requires": {
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"jwt-decode": "^3.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"on-finished": {
|
"on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"linkify-html": "^4.1.0",
|
"linkify-html": "^4.1.0",
|
||||||
"linkifyjs": "^4.1.0",
|
"linkifyjs": "^4.1.0",
|
||||||
"material-design-icons-iconfont": "^6.1",
|
"material-design-icons-iconfont": "^6.1",
|
||||||
"matrix-js-sdk": "^23.4.0",
|
"matrix-js-sdk": "^29.1.0",
|
||||||
"md-gum-polyfill": "^1.0.0",
|
"md-gum-polyfill": "^1.0.0",
|
||||||
"mic-recorder-to-mp3": "^2.2.2",
|
"mic-recorder-to-mp3": "^2.2.2",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,52 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<link rel="icon" id="favicon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" id="favicon" href="<%= BASE_URL %>favicon.ico" />
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-72x72.png" sizes="72x72">
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-72x72.png" sizes="72x72" />
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-96x96.png" sizes="96x96">
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-96x96.png" sizes="96x96" />
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-128x128.png" sizes="128x128">
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-128x128.png" sizes="128x128" />
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-144x144.png" sizes="144x144">
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-144x144.png" sizes="144x144" />
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-152x152.png" sizes="152x152">
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-152x152.png" sizes="152x152" />
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-192x192.png" sizes="192x192">
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-192x192.png" sizes="192x192" />
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-384x384.png" sizes="384x384">
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-384x384.png" sizes="384x384" />
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-512x512.png" sizes="512x512">
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-512x512.png" sizes="512x512" />
|
||||||
<link rel="manifest" href="<%= BASE_URL %>manifest.json">
|
<link rel="manifest" href="<%= BASE_URL %>manifest.json" />
|
||||||
|
<script src="./lottie-player.js"></script>
|
||||||
|
<style>
|
||||||
|
#loader {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
<strong
|
||||||
|
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
|
||||||
|
enable it to continue.</strong
|
||||||
|
>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app">
|
||||||
|
<!-- To get rid of blank white screen, show loading indicator before Vue app is initialized -->
|
||||||
|
<!-- This loader will be replaced by the Vue component on mounted -->
|
||||||
|
<div id="loader">
|
||||||
|
<lottie-player autoplay loop mode="normal" src="./loader.json" style="width: 128px"> </lottie-player>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
1
public/loader.json
Normal file
1
public/loader.json
Normal file
File diff suppressed because one or more lines are too long
77
public/lottie-player.js
Normal file
77
public/lottie-player.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -228,7 +228,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
||||||
margin-top: 72px;
|
//margin-top: 72px;
|
||||||
margin-bottom: 70px;
|
margin-bottom: 70px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1332,6 +1332,10 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.new-room {
|
.new-room {
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
font-size: 16px !important;
|
font-size: 16px !important;
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,9 @@
|
||||||
"user_was_banned_you": "You were kicked and banned from the chat.",
|
"user_was_banned_you": "You were kicked and banned from the chat.",
|
||||||
"user_joined": "{user} joined the chat",
|
"user_joined": "{user} joined the chat",
|
||||||
"user_left": "{user} left the chat",
|
"user_left": "{user} left the chat",
|
||||||
|
"someone": "Someone",
|
||||||
"user_said": "{user} said:",
|
"user_said": "{user} said:",
|
||||||
|
"sent_media": "Sent {count} media items.",
|
||||||
"file_prefix": "File: ",
|
"file_prefix": "File: ",
|
||||||
"edited": "(edited)",
|
"edited": "(edited)",
|
||||||
"download_progress": "{percentage}% downloaded",
|
"download_progress": "{percentage}% downloaded",
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
:attachments="currentFileInputs"
|
:attachments="currentFileInputs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="!useVoiceMode && !useFileModeNonAdmin" class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer"
|
<div v-if="!useVoiceMode && !useFileModeNonAdmin" :class="{'chat-content': true, 'flex-grow-1': true, 'flex-shrink-1': true, 'invisible': !initialLoadDone}" ref="chatContainer"
|
||||||
v-on:scroll="onScroll" @click="closeContextMenusIfOpen">
|
v-on:scroll="onScroll" @click="closeContextMenusIfOpen">
|
||||||
<div ref="messageOperationsStrut" class="message-operations-strut">
|
<div ref="messageOperationsStrut" class="message-operations-strut">
|
||||||
<message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
|
<message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
|
||||||
|
|
@ -42,8 +42,8 @@
|
||||||
v-on:addreply="addReply(selectedEvent)" v-on:edit="edit(selectedEvent)" v-on:redact="redact(selectedEvent)"
|
v-on:addreply="addReply(selectedEvent)" v-on:edit="edit(selectedEvent)" v-on:redact="redact(selectedEvent)"
|
||||||
v-on:download="download(selectedEvent)" v-on:more="
|
v-on:download="download(selectedEvent)" v-on:more="
|
||||||
isEmojiQuickReaction= true
|
isEmojiQuickReaction= true
|
||||||
showMoreMessageOperations($event)
|
showMoreMessageOperations({event: selectedEvent, anchor: $event.anchor})
|
||||||
" :originalEvent="selectedEvent" />
|
" :originalEvent="selectedEvent" :timelineSet="timelineSet" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="avatarOperationsStrut" class="avatar-operations-strut">
|
<div ref="avatarOperationsStrut" class="avatar-operations-strut">
|
||||||
|
|
@ -87,6 +87,7 @@
|
||||||
isEmojiQuickReaction = true
|
isEmojiQuickReaction = true
|
||||||
showMoreMessageOperations({event: event, anchor: $event.anchor})
|
showMoreMessageOperations({event: event, anchor: $event.anchor})
|
||||||
"
|
"
|
||||||
|
v-on:layout-change="onLayoutChange"
|
||||||
/>
|
/>
|
||||||
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
||||||
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
||||||
|
|
@ -114,6 +115,7 @@
|
||||||
<div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body">
|
<div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body">
|
||||||
{{ replyToEvent.getContent().body | latestReply }}
|
{{ replyToEvent.getContent().body | latestReply }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="replyToContentType === 'm.thread'">{{ replyToThreadMessage }}</div>
|
||||||
<div v-if="replyToContentType === 'm.image'">{{ $t("message.reply_image") }}</div>
|
<div v-if="replyToContentType === 'm.image'">{{ $t("message.reply_image") }}</div>
|
||||||
<div v-if="replyToContentType === 'm.audio'">{{ $t("message.reply_audio_message") }}</div>
|
<div v-if="replyToContentType === 'm.audio'">{{ $t("message.reply_audio_message") }}</div>
|
||||||
<div v-if="replyToContentType === 'm.video'">{{ $t("message.reply_video") }}</div>
|
<div v-if="replyToContentType === 'm.video'">{{ $t("message.reply_video") }}</div>
|
||||||
|
|
@ -533,7 +535,7 @@ export default {
|
||||||
if (contentArr[0] === "") {
|
if (contentArr[0] === "") {
|
||||||
contentArr.shift();
|
contentArr.shift();
|
||||||
}
|
}
|
||||||
return contentArr[0].replace(/^> (<.*> )?/g, "");
|
return (contentArr && contentArr.length > 0) ? contentArr[0].replace(/^> (<.*> )?/g, "") : "";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -792,6 +794,18 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we are replying to a (media) thread, this is the hint we show when replying.
|
||||||
|
*/
|
||||||
|
replyToThreadMessage() {
|
||||||
|
if (this.replyToEvent && this.timelineSet) {
|
||||||
|
return this.$t("message.sent_media", {count: this.timelineSet.relations
|
||||||
|
.getAllChildEventsForEvent(this.replyToEvent.getId())
|
||||||
|
.filter((e) => util.downloadableTypes().includes(e.getContent().msgtype)).length});
|
||||||
|
}
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -800,6 +814,8 @@ export default {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(value, oldValue) {
|
handler(value, oldValue) {
|
||||||
if (value && !oldValue) {
|
if (value && !oldValue) {
|
||||||
|
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
|
||||||
|
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
|
||||||
console.log("Loading finished!");
|
console.log("Loading finished!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -842,7 +858,7 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.initialLoadDone = true;
|
this.setInitialLoadDone();
|
||||||
return; // no room
|
return; // no room
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -889,6 +905,15 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Set initialLoadDone to 'true'. First process all events, setting threadParent and replyEvent if needed.
|
||||||
|
*/
|
||||||
|
setInitialLoadDone() {
|
||||||
|
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
|
||||||
|
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
|
||||||
|
this.initialLoadDone = true;
|
||||||
|
console.log("Loading finished!");
|
||||||
|
},
|
||||||
windowNotificationPermission,
|
windowNotificationPermission,
|
||||||
onNotificationDialog() {
|
onNotificationDialog() {
|
||||||
if(this.windowNotificationPermission() === 'denied') {
|
if(this.windowNotificationPermission() === 'denied') {
|
||||||
|
|
@ -943,9 +968,23 @@ export default {
|
||||||
console.log("ERROR " + err);
|
console.log("ERROR " + err);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
self.initialLoadDone = true;
|
// const [timelineEvents, threadedEvents, unknownRelations] =
|
||||||
if (initialEventId && !this.showCreatedRoomWelcomeHeader) {
|
// this.room.partitionThreadedEvents(self.events);
|
||||||
self.scrollToEvent(initialEventId);
|
// this.$matrix.matrixClient.processAggregatedTimelineEvents(this.room, timelineEvents);
|
||||||
|
// //room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||||
|
// this.$matrix.matrixClient.processThreadEvents(this.room, threadedEvents, true);
|
||||||
|
// unknownRelations.forEach((event) => this.room.relations.aggregateChildEvent(event));
|
||||||
|
|
||||||
|
this.setInitialLoadDone();
|
||||||
|
if (initialEventId && !this.showCreatedRoomWelcomeHeader) {
|
||||||
|
const event = this.room.findEventById(initialEventId);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (event && event.parentThread) {
|
||||||
|
self.scrollToEvent(event.parentThread.getId());
|
||||||
|
} else {
|
||||||
|
self.scrollToEvent(initialEventId);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (this.showCreatedRoomWelcomeHeader || this.showDirectChatWelcomeHeader) {
|
} else if (this.showCreatedRoomWelcomeHeader || this.showDirectChatWelcomeHeader) {
|
||||||
self.onScroll();
|
self.onScroll();
|
||||||
}
|
}
|
||||||
|
|
@ -960,7 +999,7 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
// Error. Done loading.
|
// Error. Done loading.
|
||||||
this.events = this.timelineWindow.getEvents();
|
this.events = this.timelineWindow.getEvents();
|
||||||
this.initialLoadDone = true;
|
this.setInitialLoadDone();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
@ -1094,12 +1133,83 @@ export default {
|
||||||
|
|
||||||
this.restartRRTimer();
|
this.restartRRTimer();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setParentThread(event) {
|
||||||
|
const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
|
||||||
|
if (parentEvent) {
|
||||||
|
Vue.set(parentEvent, "isMxThread", true);
|
||||||
|
Vue.set(event, "parentThread", parentEvent);
|
||||||
|
} else {
|
||||||
|
// Try to load from server.
|
||||||
|
this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.threadRootId).then((tl) => {
|
||||||
|
if (tl) {
|
||||||
|
const parentEvent = tl.getEvents().find((e) => e.getId() === event.threadRootId);
|
||||||
|
if (parentEvent) {
|
||||||
|
this.events = this.timelineWindow.getEvents();
|
||||||
|
const fn = () => {
|
||||||
|
Vue.set(parentEvent, "isMxThread", true);
|
||||||
|
Vue.set(event, "parentThread", parentEvent);
|
||||||
|
};
|
||||||
|
if (this.initialLoadDone) {
|
||||||
|
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
|
||||||
|
const element = document.querySelector(sel);
|
||||||
|
if (element) {
|
||||||
|
this.onLayoutChange(fn, element);
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setReplyToEvent(event) {
|
||||||
|
const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
|
||||||
|
if (parentEvent) {
|
||||||
|
Vue.set(event, "replyEvent", parentEvent);
|
||||||
|
} else {
|
||||||
|
// Try to load from server.
|
||||||
|
this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.replyEventId)
|
||||||
|
.then((tl) => {
|
||||||
|
if (tl) {
|
||||||
|
const parentEvent = tl.getEvents().find((e) => e.getId() === event.replyEventId);
|
||||||
|
if (parentEvent) {
|
||||||
|
this.events = this.timelineWindow.getEvents();
|
||||||
|
const fn = () => {Vue.set(event, "replyEvent", parentEvent);};
|
||||||
|
if (this.initialLoadDone) {
|
||||||
|
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
|
||||||
|
const element = document.querySelector(sel);
|
||||||
|
if (element) {
|
||||||
|
this.onLayoutChange(fn, element);
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(e => console.error(e));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onEvent(event) {
|
onEvent(event) {
|
||||||
//console.log("OnEvent", JSON.stringify(event));
|
//console.log("OnEvent", JSON.stringify(event));
|
||||||
if (event.getRoomId() !== this.roomId) {
|
if (event.getRoomId() !== this.roomId) {
|
||||||
return; // Not for this room
|
return; // Not for this room
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.initialLoadDone && event.threadRootId && !event.parentThread) {
|
||||||
|
this.setParentThread(event);
|
||||||
|
}
|
||||||
|
if (this.initialLoadDone && event.replyEventId && !event.replyEvent) {
|
||||||
|
this.setReplyToEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
const loadingDone = this.initialLoadDone;
|
const loadingDone = this.initialLoadDone;
|
||||||
this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
|
this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
|
||||||
|
|
||||||
|
|
@ -1107,7 +1217,7 @@ export default {
|
||||||
this.paginateBackIfNeeded();
|
this.paginateBackIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadingDone && event.forwardLooking && !event.isRelation()) {
|
if (loadingDone && event.forwardLooking && (!event.isRelation() || event.isMxThread || event.threadRootId || event.parentThread )) {
|
||||||
// If we are at bottom, scroll to see new events...
|
// If we are at bottom, scroll to see new events...
|
||||||
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
|
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
|
||||||
const container = this.chatContainer;
|
const container = this.chatContainer;
|
||||||
|
|
@ -1315,6 +1425,28 @@ export default {
|
||||||
this.cancelSendAttachment();
|
this.cancelSendAttachment();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by message components that need to change their layout. This will avoid "jumping" in the UI, because
|
||||||
|
* we remember scroll position, apply the layout change, then restore the scroll.
|
||||||
|
* NOTE: we use "parentElement" below, because it is expected to be called with "element" set to the message component
|
||||||
|
* and the message component in turn being wrapped by a "message-wrapper" element (see html above).
|
||||||
|
* @param {} action A function that performs desired layout changes.
|
||||||
|
* @param {*} element Root element for the chat message.
|
||||||
|
*/
|
||||||
|
onLayoutChange(action, element) {
|
||||||
|
if (!element || !element.parentElemen || this.useVoiceMode || this.useFileModeNonAdmin) {
|
||||||
|
action();
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const container = this.chatContainer;
|
||||||
|
this.scrollPosition.prepareFor(element.parentElement.offsetTop >= container.scrollTop ? "down" : "up");
|
||||||
|
action();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// restore scroll position!
|
||||||
|
this.scrollPosition.restore();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
handleScrolledToTop() {
|
handleScrolledToTop() {
|
||||||
if (
|
if (
|
||||||
this.timelineWindow &&
|
this.timelineWindow &&
|
||||||
|
|
@ -1379,9 +1511,18 @@ export default {
|
||||||
const container = this.chatContainer;
|
const container = this.chatContainer;
|
||||||
const ref = this.$refs[eventId];
|
const ref = this.$refs[eventId];
|
||||||
if (container && ref) {
|
if (container && ref) {
|
||||||
const targetY = container.clientHeight / 2;
|
const parent = container.getBoundingClientRect();
|
||||||
const sourceY = ref[0].offsetTop;
|
const item = ref[0].getBoundingClientRect();
|
||||||
container.scrollTo(0, sourceY - targetY);
|
let offsetY = (parent.bottom - parent.top) / 2;
|
||||||
|
if (ref[0].clientHeight > offsetY) {
|
||||||
|
offsetY = Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight);
|
||||||
|
}
|
||||||
|
const targetY = parent.top + offsetY;
|
||||||
|
const currentY = item.top;
|
||||||
|
const y = container.scrollTop + (currentY - targetY);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
container.scrollTo(0, y);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1433,7 +1574,11 @@ export default {
|
||||||
addReply(event) {
|
addReply(event) {
|
||||||
this.replyToEvent = event;
|
this.replyToEvent = event;
|
||||||
this.$refs.messageInput.focus();
|
this.$refs.messageInput.focus();
|
||||||
this.replyToContentType = event.getContent().msgtype || 'm.poll';
|
if (event.parentThread || event.isThreadRoot || event.isMxThread) {
|
||||||
|
this.replyToContentType = 'm.thread';
|
||||||
|
} else {
|
||||||
|
this.replyToContentType = event.getContent().msgtype || 'm.poll';
|
||||||
|
}
|
||||||
this.setReplyToImage(event);
|
this.setReplyToImage(event);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1455,7 +1600,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
download(event) {
|
download(event) {
|
||||||
util.download(this.$matrix.matrixClient, event);
|
if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) {
|
||||||
|
const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
|
||||||
|
children.forEach(child => util.download(this.$matrix.matrixClient, child));
|
||||||
|
} else {
|
||||||
|
util.download(this.$matrix.matrixClient, event);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelEditReply() {
|
cancelEditReply() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-root">
|
<div class="chat-root">
|
||||||
<div class="chat-root d-flex flex-column" ref="exportRoot">
|
<div class="chat-root export d-flex flex-column" ref="exportRoot">
|
||||||
<!-- Header-->
|
<!-- Header-->
|
||||||
<v-container fluid class="chat-header flex-grow-0 flex-shrink-0">
|
<v-container fluid class="chat-header flex-grow-0 flex-shrink-0">
|
||||||
<v-row class="chat-header-row flex-nowrap">
|
<v-row class="chat-header-row flex-nowrap">
|
||||||
|
|
@ -18,18 +18,17 @@
|
||||||
<div class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer">
|
<div class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer">
|
||||||
<div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
|
<div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
|
||||||
<!-- DAY Marker, shown for every new day in the timeline -->
|
<!-- DAY Marker, shown for every new day in the timeline -->
|
||||||
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
|
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker">
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="text">{{ dayForEvent(event) }}</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
|
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
|
||||||
<div class="message-wrapper">
|
<div class="message-wrapper">
|
||||||
<component
|
<component :is="componentForEvent(event, true)" :room="room" :originalEvent="event"
|
||||||
:is="componentForEvent(event, true)"
|
:nextEvent="events[index + 1]" :timelineSet="timelineSet" :componentFn="componentForEventForExport"
|
||||||
:room="room"
|
ref="exportedEvent" v-on:layout-change="onLayoutChange" />
|
||||||
:originalEvent="event"
|
|
||||||
:nextEvent="events[index + 1]"
|
|
||||||
:timelineSet="timelineSet"
|
|
||||||
ref="exportedEvent"
|
|
||||||
/>
|
|
||||||
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
||||||
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -54,6 +53,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Vue from "vue";
|
||||||
import MessageIncomingText from "./messages/MessageIncomingText.vue";
|
import MessageIncomingText from "./messages/MessageIncomingText.vue";
|
||||||
import MessageIncomingFile from "./messages/MessageIncomingFile.vue";
|
import MessageIncomingFile from "./messages/MessageIncomingFile.vue";
|
||||||
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
||||||
|
|
@ -98,6 +98,7 @@ import util from "../plugins/utils";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
import { EventTimelineSet } from "matrix-js-sdk";
|
import { EventTimelineSet } from "matrix-js-sdk";
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "RoomExport",
|
name: "RoomExport",
|
||||||
|
|
@ -146,7 +147,7 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
room: {
|
room: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: function() {
|
default: function () {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -181,6 +182,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
componentForEventForExport(event) {
|
||||||
|
return this.componentForEvent(event, true);
|
||||||
|
},
|
||||||
cancelExport() {
|
cancelExport() {
|
||||||
this.cancelled = true;
|
this.cancelled = true;
|
||||||
},
|
},
|
||||||
|
|
@ -254,6 +258,21 @@ export default {
|
||||||
this.timelineSet.addEventsToTimeline(events.reverse(), true, this.timelineSet.getLiveTimeline(), "");
|
this.timelineSet.addEventsToTimeline(events.reverse(), true, this.timelineSet.getLiveTimeline(), "");
|
||||||
this.events = events;
|
this.events = events;
|
||||||
|
|
||||||
|
// Need to set thread root events and replyEvents so stuff is rendered correctly.
|
||||||
|
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => {
|
||||||
|
const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
|
||||||
|
if (parentEvent) {
|
||||||
|
Vue.set(parentEvent, "isMxThread", true);
|
||||||
|
Vue.set(event, "parentThread", parentEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => {
|
||||||
|
const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
|
||||||
|
if (parentEvent) {
|
||||||
|
Vue.set(event, "replyEvent", parentEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Wait a tick so UI is updated.
|
// Wait a tick so UI is updated.
|
||||||
return new Promise((resolve, ignoredReject) => {
|
return new Promise((resolve, ignoredReject) => {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|
@ -264,129 +283,192 @@ export default {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// UI updated, start processing events
|
// UI updated, start processing events
|
||||||
zip = new JSZip();
|
zip = new JSZip();
|
||||||
|
var avatarFolder = zip.folder("avatars");
|
||||||
var imageFolder = zip.folder("images");
|
var imageFolder = zip.folder("images");
|
||||||
var audioFolder = zip.folder("audio");
|
var audioFolder = zip.folder("audio");
|
||||||
var videoFolder = zip.folder("video");
|
var videoFolder = zip.folder("video");
|
||||||
|
|
||||||
var downloadPromises = [];
|
var downloadPromises = [];
|
||||||
let components = this.$refs.exportedEvent;
|
let components = this.$refs.exportedEvent;
|
||||||
for (const comp of components) {
|
for (const parentComp of components) {
|
||||||
let componentClass = comp.$vnode.tag.split("-").reverse()[0];
|
let childComponents = [parentComp];
|
||||||
switch (componentClass) {
|
|
||||||
case "MessageIncomingImageExport":
|
|
||||||
case "MessageOutgoingImageExport":
|
|
||||||
// TODO - maybe consider what media to download based on the file size we already have?
|
|
||||||
// info = comp.event.getContent().info;
|
|
||||||
// if (info && info.size && currentMediaSize + info.size > maxMediaSize) {
|
|
||||||
// // No need to even download.
|
|
||||||
// console.log("Dont download!");
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
downloadPromises.push(
|
// Some components, i.e. the media threads, have subcomponents
|
||||||
util
|
// that we want to export. So pickup subcomponents here as well.
|
||||||
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
if (parentComp.$refs && parentComp.$refs.exportedEvent) {
|
||||||
.then((blob) => {
|
if (Array.isArray(parentComp.$refs.exportedEvent)) {
|
||||||
return new Promise((resolve, ignoredReject) => {
|
for (const child of parentComp.$refs.exportedEvent) {
|
||||||
let mime = blob.type;
|
childComponents.push(child);
|
||||||
var extension = ".png";
|
}
|
||||||
switch (mime) {
|
} else {
|
||||||
case "image/jpeg":
|
childComponents.push(parentComp.$refs.exportedEvent);
|
||||||
case "image/jpg":
|
}
|
||||||
extension = ".jpg";
|
}
|
||||||
break;
|
for (const comp of childComponents) {
|
||||||
case "image/gif":
|
|
||||||
extension = ".gif";
|
// Avatars need downloading?
|
||||||
|
if (comp.$el) {
|
||||||
|
const avatars = comp.$el.getElementsByClassName("v-avatar");
|
||||||
|
if (avatars && avatars.length > 0) {
|
||||||
|
const member = this.room.getMember(comp.event.getSender());
|
||||||
|
if (member) {
|
||||||
|
const fileName = comp.event.getSender() + ".png";
|
||||||
|
|
||||||
|
const setSource = (fileName) => {
|
||||||
|
for (let avatarIndex = 0; avatarIndex < avatars.length; avatarIndex++) {
|
||||||
|
const avatarElement = avatars[avatarIndex];
|
||||||
|
const images = avatarElement.getElementsByTagName("img");
|
||||||
|
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
|
||||||
|
const img = images[imageIndex];
|
||||||
|
img.onerror = undefined;
|
||||||
|
img.src = './avatars/' + fileName;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!avatarFolder.file(fileName)) {
|
||||||
|
const url = member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true);
|
||||||
|
if (url) {
|
||||||
|
avatarFolder.file(fileName, "empty");
|
||||||
|
downloadPromises.push(
|
||||||
|
axios.get(url, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
if (result.data) {
|
||||||
|
avatarFolder.file(fileName, result.data);
|
||||||
|
setSource(fileName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Download error: ", err);
|
||||||
|
avatarFolder.remove(fileName);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSource(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let componentClass = comp.$vnode.tag.split("-").reverse()[0];
|
||||||
|
switch (componentClass) {
|
||||||
|
case "MessageIncomingImageExport":
|
||||||
|
case "MessageOutgoingImageExport":
|
||||||
|
// TODO - maybe consider what media to download based on the file size we already have?
|
||||||
|
// info = comp.event.getContent().info;
|
||||||
|
// if (info && info.size && currentMediaSize + info.size > maxMediaSize) {
|
||||||
|
// // No need to even download.
|
||||||
|
// console.log("Dont download!");
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
downloadPromises.push(
|
||||||
|
util
|
||||||
|
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
||||||
|
.then((blob) => {
|
||||||
|
return new Promise((resolve, ignoredReject) => {
|
||||||
|
let mime = blob.type;
|
||||||
|
var extension = ".png";
|
||||||
|
switch (mime) {
|
||||||
|
case "image/jpeg":
|
||||||
|
case "image/jpg":
|
||||||
|
extension = ".jpg";
|
||||||
|
break;
|
||||||
|
case "image/gif":
|
||||||
|
extension = ".gif";
|
||||||
|
}
|
||||||
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
|
currentMediaSize += blob.size;
|
||||||
|
|
||||||
|
let fileName = comp.event.getId() + extension;
|
||||||
|
imageFolder.file(fileName, blob); // TODO calc bytes
|
||||||
|
|
||||||
|
let blobUrl = URL.createObjectURL(blob);
|
||||||
|
comp.src = blobUrl;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// Update source
|
||||||
|
let elements = comp.$el.getElementsByClassName("v-image__image");
|
||||||
|
let element = elements && elements[0];
|
||||||
|
if (element) {
|
||||||
|
element.style.backgroundImage = 'url("./images/' + fileName + '")';
|
||||||
|
element.classList.remove("v-image__image--preload");
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(blobUrl); // Give the blob back
|
||||||
|
this.processedEvents += 1;
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((ignoredErr) => {
|
||||||
|
this.processedEvents += 1;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "MessageIncomingAudioExport":
|
||||||
|
case "MessageOutgoingAudioExport":
|
||||||
|
downloadPromises.push(
|
||||||
|
util
|
||||||
|
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
||||||
|
.then((blob) => {
|
||||||
if (currentMediaSize + blob.size <= maxMediaSize) {
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
currentMediaSize += blob.size;
|
currentMediaSize += blob.size;
|
||||||
|
return new Promise((resolve, ignoredReject) => {
|
||||||
let fileName = comp.event.getId() + extension;
|
//let mime = blob.type;
|
||||||
imageFolder.file(fileName, blob); // TODO calc bytes
|
var extension = ".mp3";
|
||||||
|
let fileName = comp.event.getId() + extension;
|
||||||
let blobUrl = URL.createObjectURL(blob);
|
audioFolder.file(fileName, blob); // TODO calc bytes
|
||||||
comp.src = blobUrl;
|
let elements = comp.$el.getElementsByTagName("audio");
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
// Update source
|
|
||||||
let elements = comp.$el.getElementsByClassName("v-image__image");
|
|
||||||
let element = elements && elements[0];
|
let element = elements && elements[0];
|
||||||
if (element) {
|
if (element) {
|
||||||
element.style.backgroundImage = 'url("./images/' + fileName + '")';
|
element.src = "./audio/" + fileName;
|
||||||
element.classList.remove("v-image__image--preload");
|
|
||||||
}
|
}
|
||||||
URL.revokeObjectURL(blobUrl); // Give the blob back
|
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
})
|
.catch((ignoredErr) => {
|
||||||
.catch((ignoredErr) => {
|
this.processedEvents += 1;
|
||||||
this.processedEvents += 1;
|
})
|
||||||
})
|
);
|
||||||
);
|
break;
|
||||||
break;
|
case "MessageIncomingVideoExport":
|
||||||
case "MessageIncomingAudioExport":
|
case "MessageOutgoingVideoExport":
|
||||||
case "MessageOutgoingAudioExport":
|
downloadPromises.push(
|
||||||
downloadPromises.push(
|
util
|
||||||
util
|
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
||||||
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
.then((blob) => {
|
||||||
.then((blob) => {
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
if (currentMediaSize + blob.size <= maxMediaSize) {
|
currentMediaSize += blob.size;
|
||||||
currentMediaSize += blob.size;
|
return new Promise((resolve, ignoredReject) => {
|
||||||
return new Promise((resolve, ignoredReject) => {
|
//let mime = blob.type;
|
||||||
//let mime = blob.type;
|
var extension = ".mp4";
|
||||||
var extension = ".mp3";
|
let fileName = comp.event.getId() + extension;
|
||||||
let fileName = comp.event.getId() + extension;
|
videoFolder.file(fileName, blob); // TODO calc bytes
|
||||||
audioFolder.file(fileName, blob); // TODO calc bytes
|
let elements = comp.$el.getElementsByTagName("video");
|
||||||
let elements = comp.$el.getElementsByTagName("audio");
|
let element = elements && elements[0];
|
||||||
let element = elements && elements[0];
|
if (element) {
|
||||||
if (element) {
|
element.src = "./video/" + fileName;
|
||||||
element.src = "./audio/" + fileName;
|
}
|
||||||
}
|
this.processedEvents += 1;
|
||||||
this.processedEvents += 1;
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((ignoredErr) => {
|
|
||||||
this.processedEvents += 1;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "MessageIncomingVideoExport":
|
|
||||||
case "MessageOutgoingVideoExport":
|
|
||||||
downloadPromises.push(
|
|
||||||
util
|
|
||||||
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
|
||||||
.then((blob) => {
|
|
||||||
if (currentMediaSize + blob.size <= maxMediaSize) {
|
|
||||||
currentMediaSize += blob.size;
|
|
||||||
return new Promise((resolve, ignoredReject) => {
|
|
||||||
//let mime = blob.type;
|
|
||||||
var extension = ".mp4";
|
|
||||||
let fileName = comp.event.getId() + extension;
|
|
||||||
videoFolder.file(fileName, blob); // TODO calc bytes
|
|
||||||
let elements = comp.$el.getElementsByTagName("video");
|
|
||||||
let element = elements && elements[0];
|
|
||||||
if (element) {
|
|
||||||
element.src = "./video/" + fileName;
|
|
||||||
}
|
|
||||||
this.processedEvents += 1;
|
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((ignoredErr) => {
|
.catch((ignoredErr) => {
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.all(downloadPromises);
|
return Promise.all(downloadPromises);
|
||||||
|
|
@ -410,7 +492,7 @@ export default {
|
||||||
}
|
}
|
||||||
doc +=
|
doc +=
|
||||||
"</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
|
"</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
|
||||||
const getCssRules = function(el) {
|
const getCssRules = function (el) {
|
||||||
if (el.classList.contains("op-button")) {
|
if (el.classList.contains("op-button")) {
|
||||||
el.innerHTML = "";
|
el.innerHTML = "";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -441,6 +523,30 @@ export default {
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onLayoutChange(action, ignoredelement) {
|
||||||
|
action();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.chat-root.export {
|
||||||
|
.messageIn-thread, .messageOut-thread {
|
||||||
|
/** For media threads, hide all duplicated metadata, like
|
||||||
|
sender, sender avatar, time, quick reactions etc. They are
|
||||||
|
shown for the root thread event */
|
||||||
|
.messageIn {
|
||||||
|
margin-left: 50px !important;
|
||||||
|
}
|
||||||
|
.messageOut {
|
||||||
|
margin-right: 50px !important;
|
||||||
|
}
|
||||||
|
.messageIn, .messageOut {
|
||||||
|
.quick-reaction-container, .senderAndTime, .avatar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -18,9 +18,11 @@ import MessageOutgoingThread from "./messages/MessageOutgoingThread.vue";
|
||||||
import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport";
|
import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport";
|
||||||
import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport";
|
import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport";
|
||||||
import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport";
|
import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport";
|
||||||
|
import MessageIncomingThreadExport from "./messages/export/MessageIncomingThreadExport";
|
||||||
import MessageOutgoingImageExport from "./messages/export/MessageOutgoingImageExport";
|
import MessageOutgoingImageExport from "./messages/export/MessageOutgoingImageExport";
|
||||||
import MessageOutgoingAudioExport from "./messages/export/MessageOutgoingAudioExport";
|
import MessageOutgoingAudioExport from "./messages/export/MessageOutgoingAudioExport";
|
||||||
import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoExport";
|
import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoExport";
|
||||||
|
import MessageOutgoingThreadExport from "./messages/export/MessageOutgoingThreadExport";
|
||||||
import ContactJoin from "./messages/ContactJoin.vue";
|
import ContactJoin from "./messages/ContactJoin.vue";
|
||||||
import ContactLeave from "./messages/ContactLeave.vue";
|
import ContactLeave from "./messages/ContactLeave.vue";
|
||||||
import ContactInvited from "./messages/ContactInvited.vue";
|
import ContactInvited from "./messages/ContactInvited.vue";
|
||||||
|
|
@ -159,9 +161,9 @@ export default {
|
||||||
|
|
||||||
case "m.room.message":
|
case "m.room.message":
|
||||||
if (event.getSender() != this.$matrix.currentUserId) {
|
if (event.getSender() != this.$matrix.currentUserId) {
|
||||||
if (event.isThreadRoot || event.isThread) {
|
if (event.isMxThread) {
|
||||||
// Incoming thread, e.g. a file drop!
|
// Incoming thread, e.g. a file drop!
|
||||||
return MessageIncomingThread;
|
return isForExport ? MessageIncomingThreadExport : MessageIncomingThread;
|
||||||
}
|
}
|
||||||
if (event.getContent().msgtype == "m.image") {
|
if (event.getContent().msgtype == "m.image") {
|
||||||
// For SVG, make downloadable
|
// For SVG, make downloadable
|
||||||
|
|
@ -193,9 +195,9 @@ export default {
|
||||||
}
|
}
|
||||||
return MessageIncomingText;
|
return MessageIncomingText;
|
||||||
} else {
|
} else {
|
||||||
if (event.isThreadRoot || event.isThread) {
|
if (event.isMxThread) {
|
||||||
// Outgoing thread
|
// Outgoing thread
|
||||||
return MessageOutgoingThread;
|
return isForExport ? MessageOutgoingThreadExport : MessageOutgoingThread;
|
||||||
}
|
}
|
||||||
if (event.getContent().msgtype == "m.image") {
|
if (event.getContent().msgtype == "m.image") {
|
||||||
// For SVG, make downloadable
|
// For SVG, make downloadable
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<v-avatar class="avatar" ref="avatar" size="32" color="#ededed" @click.stop="otherAvatarClicked($refs.avatar.$el)">
|
<v-avatar class="avatar" ref="avatar" size="32" color="#ededed" @click.stop="otherAvatarClicked($refs.avatar.$el)">
|
||||||
<img v-if="messageEventAvatar(event)" :src="messageEventAvatar(event)" />
|
<img v-if="messageEventAvatar(event)" :src="messageEventAvatar(event)" onerror="this.style.display='none'" />
|
||||||
<span v-else class="white--text headline">{{
|
<span v-else class="white--text headline">{{
|
||||||
eventSenderDisplayName(event).substring(0, 1).toUpperCase()
|
eventSenderDisplayName(event).substring(0, 1).toUpperCase()
|
||||||
}}</span>
|
}}</span>
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<v-icon>more_vert</v-icon>
|
<v-icon>more_vert</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
|
<QuickReactions :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||||
<SeenBy :room="room" :event="event"/>
|
<SeenBy :room="room" :event="event"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,13 @@
|
||||||
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
||||||
<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">
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="original-message-text"
|
class="original-message-text"
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<span>{{ $t('message.file_prefix') }}</span>
|
<span>{{ $t('message.file_prefix') }}</span>
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,13 @@
|
||||||
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
||||||
<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">
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="original-message-text"
|
class="original-message-text"
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,13 @@
|
||||||
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1">
|
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1">
|
||||||
<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">
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="original-message-text"
|
class="original-message-text"
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<v-container fluid class="imageCollection">
|
<v-container fluid class="imageCollection">
|
||||||
<v-row wrap>
|
<v-row wrap>
|
||||||
|
|
@ -51,54 +50,46 @@ export default {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
showItem: null,
|
showItem: null,
|
||||||
thread: null,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
this.processThread();
|
if (!this.thread) {
|
||||||
},
|
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
||||||
beforeDestroy() {
|
|
||||||
this.thread = null;
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
thread: {
|
|
||||||
handler(newValue, oldValue) {
|
|
||||||
if (oldValue) {
|
|
||||||
oldValue.off('Relations.add', this.onAddRelation);
|
|
||||||
}
|
|
||||||
if (newValue) {
|
|
||||||
newValue.on('Relations.add', this.onAddRelation);
|
|
||||||
}
|
|
||||||
this.processThread();
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onAddRelation() {
|
onRelationsCreated() {
|
||||||
this.processThread();
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
},
|
},
|
||||||
onItemClick(event) {
|
onItemClick(event) {
|
||||||
this.showItem = event.item;
|
this.showItem = event.item;
|
||||||
},
|
},
|
||||||
processThread() {
|
processThread() {
|
||||||
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()).map(e => {
|
this.$emit('layout-change', () => {
|
||||||
let ret = {
|
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
|
||||||
event: e,
|
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype))
|
||||||
src: null,
|
.map(e => {
|
||||||
};
|
let ret = {
|
||||||
ret.promise =
|
event: e,
|
||||||
util
|
src: null,
|
||||||
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
|
};
|
||||||
.then((url) => {
|
ret.promise =
|
||||||
ret.src = url;
|
util
|
||||||
})
|
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
|
||||||
.catch((err) => {
|
.then((url) => {
|
||||||
console.log("Failed to fetch thumbnail: ", err);
|
ret.src = url;
|
||||||
});
|
})
|
||||||
return ret;
|
.catch((err) => {
|
||||||
});
|
console.log("Failed to fetch thumbnail: ", err);
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
}, this.$el);
|
||||||
},
|
},
|
||||||
layoutedItems() {
|
layoutedItems() {
|
||||||
if (!this.items || this.items.length == 0) { return [] }
|
if (!this.items || this.items.length == 0) { return [] }
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<img v-if="userAvatar" :src="userAvatar" />
|
<img v-if="userAvatar" :src="userAvatar" />
|
||||||
<span v-else class="white--text headline">{{ userAvatarLetter }}</span>
|
<span v-else class="white--text headline">{{ userAvatarLetter }}</span>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
|
<QuickReactions :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||||
<SeenBy :room="room" :event="event"/>
|
<SeenBy :room="room" :event="event"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,14 @@
|
||||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
<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">
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="original-message-text"
|
class="original-message-text"
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<span>{{ $t('message.file_prefix') }}</span>
|
<span>{{ $t('message.file_prefix') }}</span>
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
<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">
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="original-message-text"
|
class="original-message-text"
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@
|
||||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1">
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1">
|
||||||
<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">
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
{{ $t('message.user_said', { user: inReplyToSender || "Someone" }) }}
|
<div
|
||||||
</div>
|
class="original-message-text"
|
||||||
<div class="original-message-text" v-html="linkify($sanitize(inReplyToText))" />
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<v-container fluid class="imageCollection">
|
<v-container fluid class="imageCollection">
|
||||||
<v-row wrap>
|
<v-row wrap>
|
||||||
|
|
@ -49,54 +51,46 @@ export default {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
showItem: null,
|
showItem: null,
|
||||||
thread: null,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
this.processThread();
|
if (!this.thread) {
|
||||||
},
|
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
||||||
beforeDestroy() {
|
|
||||||
this.thread = null;
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
thread: {
|
|
||||||
handler(newValue, oldValue) {
|
|
||||||
if (oldValue) {
|
|
||||||
oldValue.off('Relations.add', this.onAddRelation);
|
|
||||||
}
|
|
||||||
if (newValue) {
|
|
||||||
newValue.on('Relations.add', this.onAddRelation);
|
|
||||||
}
|
|
||||||
this.processThread();
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onAddRelation() {
|
onRelationsCreated() {
|
||||||
this.processThread();
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
},
|
},
|
||||||
onItemClick(event) {
|
onItemClick(event) {
|
||||||
this.showItem = event.item;
|
this.showItem = event.item;
|
||||||
},
|
},
|
||||||
processThread() {
|
processThread() {
|
||||||
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()).map(e => {
|
this.$emit('layout-change', () => {
|
||||||
let ret = {
|
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
|
||||||
event: e,
|
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype))
|
||||||
src: null,
|
.map(e => {
|
||||||
};
|
let ret = {
|
||||||
ret.promise =
|
event: e,
|
||||||
util
|
src: null,
|
||||||
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
|
};
|
||||||
.then((url) => {
|
ret.promise =
|
||||||
ret.src = url;
|
util
|
||||||
})
|
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
|
||||||
.catch((err) => {
|
.then((url) => {
|
||||||
console.log("Failed to fetch thumbnail: ", err);
|
ret.src = url;
|
||||||
});
|
})
|
||||||
return ret;
|
.catch((err) => {
|
||||||
});
|
console.log("Failed to fetch thumbnail: ", err);
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
}, this.$el);
|
||||||
},
|
},
|
||||||
layoutedItems() {
|
layoutedItems() {
|
||||||
if (!this.items || this.items.length == 0) { return [] }
|
if (!this.items || this.items.length == 0) { return [] }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<message-incoming class="messageIn-thread" v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<component v-for="item in items" :is="componentFn(item.event)" :originalEvent="item.event" :key="item.event.getId()"
|
||||||
|
v-bind="{ ...$props }" v-on="$listeners" ref="exportedEvent" />
|
||||||
|
</message-incoming>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageIncoming from "../MessageIncoming.vue";
|
||||||
|
import messageMixin from "./../messageMixin";
|
||||||
|
import util from "../../../plugins/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: MessageIncoming,
|
||||||
|
components: { MessageIncoming },
|
||||||
|
mixins: [messageMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
if (!this.thread) {
|
||||||
|
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRelationsCreated() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
processThread() {
|
||||||
|
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
|
||||||
|
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype))
|
||||||
|
.map(e => {
|
||||||
|
let ret = {
|
||||||
|
event: e,
|
||||||
|
src: null,
|
||||||
|
};
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bubble {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<message-outgoing class="messageOut-thread" v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<component v-for="item in items" :is="componentFn(item.event)" :originalEvent="item.event" :key="item.event.getId()"
|
||||||
|
v-bind="{ ...$props }" v-on="$listeners" ref="exportedEvent" />
|
||||||
|
</message-outgoing>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageOutgoing from "../MessageOutgoing.vue";
|
||||||
|
import messageMixin from "./../messageMixin";
|
||||||
|
import util from "../../../plugins/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: MessageOutgoing,
|
||||||
|
components: { MessageOutgoing },
|
||||||
|
mixins: [messageMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
if (!this.thread) {
|
||||||
|
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRelationsCreated() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
processThread() {
|
||||||
|
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
|
||||||
|
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype))
|
||||||
|
.map(e => {
|
||||||
|
let ret = {
|
||||||
|
event: e,
|
||||||
|
src: null,
|
||||||
|
};
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bubble {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -2,7 +2,7 @@ import QuickReactions from "./QuickReactions.vue";
|
||||||
import * as linkify from 'linkifyjs';
|
import * as linkify from 'linkifyjs';
|
||||||
import linkifyHtml from 'linkify-html';
|
import linkifyHtml from 'linkify-html';
|
||||||
import utils from "../../plugins/utils"
|
import utils from "../../plugins/utils"
|
||||||
import Vue from "vue";
|
import util from "../../plugins/utils";
|
||||||
|
|
||||||
linkify.options.defaults.className = "link";
|
linkify.options.defaults.className = "link";
|
||||||
linkify.options.defaults.target = { url: "_blank" };
|
linkify.options.defaults.target = { url: "_blank" };
|
||||||
|
|
@ -40,51 +40,22 @@ export default {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: function () {
|
default: function () {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
event: {},
|
event: {},
|
||||||
inReplyToEvent: null,
|
thread: null,
|
||||||
inReplyToSender: null,
|
utils,
|
||||||
utils
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const relatesTo = this.validEvent && this.event.getWireContent()["m.relates_to"];
|
|
||||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
|
||||||
// Can we find the original message?
|
|
||||||
const originalEventId = relatesTo["m.in_reply_to"].event_id;
|
|
||||||
if (originalEventId && this.timelineSet) {
|
|
||||||
const originalEvent = this.timelineSet.findEventById(originalEventId);
|
|
||||||
if (originalEvent) {
|
|
||||||
this.inReplyToEvent = originalEvent;
|
|
||||||
this.inReplyToSender = this.eventSenderDisplayName(originalEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeDestroy() {
|
||||||
if (this.validEvent) {
|
this.thread = null;
|
||||||
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
event: {
|
|
||||||
immediate: true,
|
|
||||||
handler(newValue, oldValue) {
|
|
||||||
if (oldValue && oldValue.getId) {
|
|
||||||
oldValue.off("Event.relationsCreated", this.onRelationsCreated);
|
|
||||||
}
|
|
||||||
if (newValue && newValue.getId) {
|
|
||||||
newValue.on("Event.relationsCreated", this.onRelationsCreated);
|
|
||||||
if (newValue.isThreadRoot) {
|
|
||||||
Vue.set(newValue, "isThread", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
originalEvent: {
|
originalEvent: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(originalEvent, ignoredOldValue) {
|
handler(originalEvent, ignoredOldValue) {
|
||||||
|
|
@ -96,7 +67,19 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
thread: {
|
||||||
|
handler(newValue, oldValue) {
|
||||||
|
if (oldValue) {
|
||||||
|
oldValue.off("Relations.add", this.onAddRelation);
|
||||||
|
}
|
||||||
|
if (newValue) {
|
||||||
|
newValue.on("Relations.add", this.onAddRelation);
|
||||||
|
}
|
||||||
|
this.processThread();
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
/**
|
/**
|
||||||
|
|
@ -107,6 +90,16 @@ export default {
|
||||||
return this.event && Object.keys(this.event).length !== 0;
|
return this.event && Object.keys(this.event).length !== 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is a thread event, we return the root here, so all reactions will land on the root event.
|
||||||
|
*/
|
||||||
|
eventForReactions() {
|
||||||
|
if (this.event.parentThread) {
|
||||||
|
return this.event.parentThread;
|
||||||
|
}
|
||||||
|
return this.event;
|
||||||
|
},
|
||||||
|
|
||||||
incoming() {
|
incoming() {
|
||||||
return this.event && this.event.getSender() != this.$matrix.currentUserId;
|
return this.event && this.event.getSender() != this.$matrix.currentUserId;
|
||||||
},
|
},
|
||||||
|
|
@ -123,11 +116,34 @@ export default {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
inReplyToSender() {
|
||||||
|
const originalEvent = this.validEvent && this.event.replyEvent;
|
||||||
|
if (originalEvent) {
|
||||||
|
const sender = this.eventSenderDisplayName(originalEvent);
|
||||||
|
if (originalEvent.isThreadRoot || originalEvent.isMxThread) {
|
||||||
|
return sender || this.$t("message.someone");
|
||||||
|
} else {
|
||||||
|
return this.$t("message.user_said", { user: sender || this.$t("message.someone") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
inReplyToEvent() {
|
||||||
|
return this.validEvent && this.event.replyEvent;
|
||||||
|
},
|
||||||
|
|
||||||
inReplyToText() {
|
inReplyToText() {
|
||||||
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
||||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
||||||
|
if (this.inReplyToEvent && (this.inReplyToEvent.isThreadRoot || this.inReplyToEvent.isMxThread)) {
|
||||||
|
const children = this.timelineSet.relations
|
||||||
|
.getAllChildEventsForEvent(this.inReplyToEvent.getId())
|
||||||
|
.filter((e) => util.downloadableTypes().includes(e.getContent().msgtype));
|
||||||
|
return this.$t("message.sent_media", { count: children.length });
|
||||||
|
}
|
||||||
const content = this.event.getContent();
|
const content = this.event.getContent();
|
||||||
if ('body' in content) {
|
if ("body" in content) {
|
||||||
const lines = content.body.split("\n").reverse() || [];
|
const lines = content.body.split("\n").reverse() || [];
|
||||||
while (lines.length && !lines[0].startsWith("> ")) lines.shift();
|
while (lines.length && !lines[0].startsWith("> ")) lines.shift();
|
||||||
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||||
|
|
@ -137,12 +153,10 @@ export default {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.inReplyToEvent) {
|
if (this.inReplyToEvent) {
|
||||||
var c = this.inReplyToEvent.getContent();
|
var c = this.inReplyToEvent.getContent();
|
||||||
return c.body;
|
return c.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't have the original text (at the moment at least)
|
// We don't have the original text (at the moment at least)
|
||||||
return this.$t("fallbacks.original_text");
|
return this.$t("fallbacks.original_text");
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +167,7 @@ export default {
|
||||||
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
||||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
||||||
const content = this.event.getContent();
|
const content = this.event.getContent();
|
||||||
if ('body' in content) {
|
if ("body" in content) {
|
||||||
// Remove the new text and strip "> " from the old original text
|
// Remove the new text and strip "> " from the old original text
|
||||||
const lines = content.body.split("\n");
|
const lines = content.body.split("\n");
|
||||||
while (lines.length && lines[0].startsWith("> ")) lines.shift();
|
while (lines.length && lines[0].startsWith("> ")) lines.shift();
|
||||||
|
|
@ -190,10 +204,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onRelationsCreated(relationType, ignoredEventType) {
|
onAddRelation() {
|
||||||
if (relationType === "m.thread") {
|
console.error("onAddRelation");
|
||||||
Vue.set(this.event, "isThread", true);
|
this.processThread();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
ownAvatarClicked() {
|
ownAvatarClicked() {
|
||||||
this.$emit("own-avatar-clicked", { event: this.event });
|
this.$emit("own-avatar-clicked", { event: this.event });
|
||||||
|
|
@ -308,5 +321,10 @@ export default {
|
||||||
linkify(text) {
|
linkify(text) {
|
||||||
return linkifyHtml(text);
|
return linkifyHtml(text);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this to handle updates to (the) message thread.
|
||||||
|
*/
|
||||||
|
processThread() {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import util from "../../plugins/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -5,8 +6,12 @@ export default {
|
||||||
return !this.incoming && this.event.getContent().msgtype == "m.text";
|
return !this.incoming && this.event.getContent().msgtype == "m.text";
|
||||||
},
|
},
|
||||||
isDownloadable() {
|
isDownloadable() {
|
||||||
|
if ((this.event.isThreadRoot || this.event.isMxThread) && this.timelineSet) {
|
||||||
|
const children = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
|
||||||
|
return children.length > 0;
|
||||||
|
}
|
||||||
const msgtype = this.event.getContent().msgtype;
|
const msgtype = this.event.getContent().msgtype;
|
||||||
return ['m.video','m.audio','m.image','m.file'].includes(msgtype);
|
return util.downloadableTypes().includes(msgtype);
|
||||||
},
|
},
|
||||||
isRedactable() {
|
isRedactable() {
|
||||||
const room = this.$matrix.matrixClient.getRoom(this.event.getRoomId());
|
const room = this.$matrix.matrixClient.getRoom(this.event.getRoomId());
|
||||||
|
|
|
||||||
|
|
@ -299,10 +299,8 @@ class Util {
|
||||||
if (err && err.name == "UnknownDeviceError") {
|
if (err && err.name == "UnknownDeviceError") {
|
||||||
console.log("Unknown devices. Mark as known before retrying.");
|
console.log("Unknown devices. Mark as known before retrying.");
|
||||||
var setAsKnownPromises = [];
|
var setAsKnownPromises = [];
|
||||||
for (var user of Object.keys(err.devices)) {
|
err.devices.forEach((userDevices, user) => {
|
||||||
const userDevices = err.devices[user];
|
userDevices.forEach((deviceInfo, deviceId) => {
|
||||||
for (var deviceId of Object.keys(userDevices)) {
|
|
||||||
const deviceInfo = userDevices[deviceId];
|
|
||||||
if (!deviceInfo.known) {
|
if (!deviceInfo.known) {
|
||||||
setAsKnownPromises.push(
|
setAsKnownPromises.push(
|
||||||
matrixClient.setDeviceKnown(
|
matrixClient.setDeviceKnown(
|
||||||
|
|
@ -312,9 +310,9 @@ class Util {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
});
|
||||||
Promise.all(setAsKnownPromises)
|
return Promise.all(setAsKnownPromises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// All devices now marked as "known", try to resend
|
// All devices now marked as "known", try to resend
|
||||||
let event = err.event;
|
let event = err.event;
|
||||||
|
|
@ -499,7 +497,6 @@ class Util {
|
||||||
// Have we changed our local view mode of this room?
|
// Have we changed our local view mode of this room?
|
||||||
const tags = room.tags;
|
const tags = room.tags;
|
||||||
if (tags && tags["ui_options"]) {
|
if (tags && tags["ui_options"]) {
|
||||||
console.error("We have a tag!");
|
|
||||||
if (tags["ui_options"]["voice_mode"] === 1) {
|
if (tags["ui_options"]["voice_mode"] === 1) {
|
||||||
return ROOM_TYPE_VOICE_MODE;
|
return ROOM_TYPE_VOICE_MODE;
|
||||||
} else if (tags["ui_options"]["file_mode"] === 1) {
|
} else if (tags["ui_options"]["file_mode"] === 1) {
|
||||||
|
|
@ -897,6 +894,10 @@ class Util {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadableTypes() {
|
||||||
|
return ['m.video','m.audio','m.image','m.file'];
|
||||||
|
}
|
||||||
|
|
||||||
download(matrixClient, event) {
|
download(matrixClient, event) {
|
||||||
this
|
this
|
||||||
.getAttachment(matrixClient, event)
|
.getAttachment(matrixClient, event)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue