Lots of fixes to "media threads"

This commit is contained in:
N Pex 2023-11-06 15:28:26 +00:00
parent fe081edc62
commit 8bcceafcff
23 changed files with 867 additions and 333 deletions

109
package-lock.json generated
View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

77
public/lottie-player.js Normal file

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

@ -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() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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))"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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