Merge branch 'dev'
This commit is contained in:
commit
f3999308cc
52 changed files with 2328 additions and 683 deletions
92
package-lock.json
generated
92
package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "keanuapp-weblite",
|
"name": "keanuapp-weblite",
|
||||||
"version": "0.1.21",
|
"version": "0.1.25",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1441,9 +1441,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/retry": {
|
"@types/retry": {
|
||||||
"version": "0.12.1",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||||
"integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g=="
|
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
|
||||||
},
|
},
|
||||||
"@types/serve-static": {
|
"@types/serve-static": {
|
||||||
"version": "1.13.10",
|
"version": "1.13.10",
|
||||||
|
|
@ -2334,7 +2334,7 @@
|
||||||
"another-json": {
|
"another-json": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz",
|
||||||
"integrity": "sha1-tfQBnJc7bdXGUGotk0acttMq7tw="
|
"integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg=="
|
||||||
},
|
},
|
||||||
"ansi-colors": {
|
"ansi-colors": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
|
|
@ -2963,7 +2963,7 @@
|
||||||
"browser-request": {
|
"browser-request": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz",
|
||||||
"integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc="
|
"integrity": "sha512-YyNI4qJJ+piQG6MMEuo7J3Bzaqssufx04zpEKYfSrl/1Op59HWali9zMtBpXnkmqMcOuWJPZvudrm9wISmnCbg=="
|
||||||
},
|
},
|
||||||
"browserify-aes": {
|
"browserify-aes": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|
@ -3081,7 +3081,7 @@
|
||||||
"bs58": {
|
"bs58": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
"integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=",
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"base-x": "^3.0.2"
|
"base-x": "^3.0.2"
|
||||||
}
|
}
|
||||||
|
|
@ -6119,6 +6119,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
|
},
|
||||||
"file-uri-to-path": {
|
"file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
|
@ -6982,6 +6987,11 @@
|
||||||
"queue": "6.0.2"
|
"queue": "6.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
|
||||||
|
},
|
||||||
"import-cwd": {
|
"import-cwd": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
|
||||||
|
|
@ -7758,6 +7768,17 @@
|
||||||
"verror": "1.10.0"
|
"verror": "1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jszip": {
|
||||||
|
"version": "3.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.0.tgz",
|
||||||
|
"integrity": "sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q==",
|
||||||
|
"requires": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"setimmediate": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"killable": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
|
|
@ -7812,6 +7833,14 @@
|
||||||
"type-check": "~0.4.0"
|
"type-check": "~0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"requires": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"lines-and-columns": {
|
"lines-and-columns": {
|
||||||
"version": "1.1.6",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
||||||
|
|
@ -8023,10 +8052,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.1.0.tgz",
|
||||||
"integrity": "sha512-wRJtOo1v1ch+gN8PRsj0IGJznk+kQ8mz13ds/nuhLI+Qyf/931ZlRpd92oq0IRPpZIb+bhX8pRjzIVdcPDKmiQ=="
|
"integrity": "sha512-wRJtOo1v1ch+gN8PRsj0IGJznk+kQ8mz13ds/nuhLI+Qyf/931ZlRpd92oq0IRPpZIb+bhX8pRjzIVdcPDKmiQ=="
|
||||||
},
|
},
|
||||||
|
"matrix-events-sdk": {
|
||||||
|
"version": "0.0.1-beta.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz",
|
||||||
|
"integrity": "sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA=="
|
||||||
|
},
|
||||||
"matrix-js-sdk": {
|
"matrix-js-sdk": {
|
||||||
"version": "15.2.0",
|
"version": "17.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-15.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-17.2.0.tgz",
|
||||||
"integrity": "sha512-jZOM8Fn86oNvU3zVQcc+JTKKrtYq4ADN6rPZs4Mwxj/X/GDP+2YIP5176GtviF0GM6VO1dcnPZY73ykl8DayjA==",
|
"integrity": "sha512-/IrgHCSVUZNVcKoPO20OF9Xog9X79a1ckmR7FwF5lSTNdmC7eQvU0XcFYCi5IXo57du+im69lEw8dLbPngZhoQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
|
|
@ -8034,6 +8068,7 @@
|
||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
"content-type": "^1.0.4",
|
"content-type": "^1.0.4",
|
||||||
"loglevel": "^1.7.1",
|
"loglevel": "^1.7.1",
|
||||||
|
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||||
"p-retry": "^4.5.0",
|
"p-retry": "^4.5.0",
|
||||||
"qs": "^6.9.6",
|
"qs": "^6.9.6",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
|
|
@ -8902,11 +8937,11 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"p-retry": {
|
"p-retry": {
|
||||||
"version": "4.6.1",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||||
"integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==",
|
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/retry": "^0.12.0",
|
"@types/retry": "0.12.0",
|
||||||
"retry": "^0.13.1"
|
"retry": "^0.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -8918,8 +8953,7 @@
|
||||||
"pako": {
|
"pako": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"parallel-transform": {
|
"parallel-transform": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|
@ -10702,8 +10736,7 @@
|
||||||
"process-nextick-args": {
|
"process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
|
|
@ -10817,9 +10850,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.10.2",
|
"version": "6.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
|
||||||
"integrity": "sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw==",
|
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.4"
|
||||||
}
|
}
|
||||||
|
|
@ -10922,7 +10955,6 @@
|
||||||
"version": "2.3.7",
|
"version": "2.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
|
|
@ -10936,14 +10968,12 @@
|
||||||
"isarray": {
|
"isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
"dev": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -11599,8 +11629,7 @@
|
||||||
"setimmediate": {
|
"setimmediate": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
|
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"setprototypeof": {
|
"setprototypeof": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
|
|
@ -12196,7 +12225,6 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -12204,8 +12232,7 @@
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
"dev": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -13051,8 +13078,7 @@
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"util.promisify": {
|
"util.promisify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,17 @@
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"data-uri-to-buffer": "^3.0.1",
|
"data-uri-to-buffer": "^3.0.1",
|
||||||
"dayjs": "^1.10.3",
|
"dayjs": "^1.10.3",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"fix-webm-duration": "^1.0.0",
|
"fix-webm-duration": "^1.0.0",
|
||||||
"image-resize": "^1.1.5",
|
"image-resize": "^1.1.5",
|
||||||
"image-size": "^1.0.0",
|
"image-size": "^1.0.0",
|
||||||
"intersection-observer": "^0.12",
|
"intersection-observer": "^0.12",
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
"json-web-key": "^0.4.0",
|
"json-web-key": "^0.4.0",
|
||||||
|
"jszip": "^3.9.1",
|
||||||
"linkifyjs": "3.0.0-beta.3",
|
"linkifyjs": "3.0.0-beta.3",
|
||||||
"material-design-icons-iconfont": "^6.1",
|
"material-design-icons-iconfont": "^6.1",
|
||||||
"matrix-js-sdk": "^15.2.0",
|
"matrix-js-sdk": "^17.2.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",
|
||||||
"pretty-bytes": "^5.6.0",
|
"pretty-bytes": "^5.6.0",
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,6 @@ $chat-button-height: 50px;
|
||||||
|
|
||||||
$voice-recorder-color: #6f6f6f;
|
$voice-recorder-color: #6f6f6f;
|
||||||
$voice-recording-color: red;
|
$voice-recording-color: red;
|
||||||
$voice-recorded-color: #3ae17d;
|
$voice-recorded-color: #3ae17d;
|
||||||
|
$poll-hilite-color: #6360f0;
|
||||||
|
$poll-hilite-color-bg: #d6d5fc;
|
||||||
|
|
@ -119,7 +119,7 @@ $admin-fg: white;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-content {
|
.chat-content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: $chat-standard-padding-s;
|
padding-top: $chat-standard-padding-s;
|
||||||
|
|
@ -596,16 +596,9 @@ $admin-fg: white;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
height: 44px;
|
height: 34px;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
box-shadow: 4px 4px 8px rgba(0,0,0,0.15);
|
box-shadow: 4px 4px 8px rgba(0,0,0,0.15);
|
||||||
// &.incoming {
|
|
||||||
// right: 30%;
|
|
||||||
// }
|
|
||||||
// &.outgoing {
|
|
||||||
// left: unset !important;
|
|
||||||
// right: 10px !important;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-operations {
|
.avatar-operations {
|
||||||
|
|
@ -618,6 +611,10 @@ $admin-fg: white;
|
||||||
box-shadow: 4px 4px 8px rgba(0,0,0,0.15);
|
box-shadow: 4px 4px 8px rgba(0,0,0,0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.send-options {
|
||||||
|
z-index: 11; // Above mic button
|
||||||
|
}
|
||||||
|
|
||||||
.message-operations-picker {
|
.message-operations-picker {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -1140,4 +1137,9 @@ $admin-fg: white;
|
||||||
.loading-indicator {
|
.loading-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exporting-indicator {
|
||||||
|
position: absolute;
|
||||||
|
background-color: white;
|
||||||
}
|
}
|
||||||
116
src/assets/css/components/_poll.scss
Normal file
116
src/assets/css/components/_poll.scss
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
.poll-bubble {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-bubble {
|
||||||
|
color: black;
|
||||||
|
padding: $chat-standard-padding-s !important;
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
font-size: 16 * $chat-text-size;
|
||||||
|
line-height: 16 * $chat-text-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-admin .poll-bubble {
|
||||||
|
color: rgba(white, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-icon {
|
||||||
|
path {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-check-icon {
|
||||||
|
width: 14.18px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-question {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: $chat-standard-padding-xs;
|
||||||
|
margin-bottom: $chat-standard-padding-s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-answer {
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px 14px;
|
||||||
|
margin: 0px;
|
||||||
|
&.winner {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
&.selected {
|
||||||
|
border: 1px solid $poll-hilite-color;
|
||||||
|
background-color: $poll-hilite-color-bg;
|
||||||
|
color: #1d1d1d;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
&.result {
|
||||||
|
border: none;
|
||||||
|
padding: 15px 0px;
|
||||||
|
}
|
||||||
|
.poll-answer-title {
|
||||||
|
}
|
||||||
|
.poll-answer-num-votes {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-percent-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
height: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
.bar {
|
||||||
|
background-color: #7e7cf8;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-status {
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 117%;
|
||||||
|
margin: 0px;
|
||||||
|
.poll-status-title {
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-status-close {
|
||||||
|
color: $poll-hilite-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-submit {
|
||||||
|
.v-btn {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11 * $chat-text-size;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background-color: $poll-hilite-color !important;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 21px !important;
|
||||||
|
height: 42px !important;
|
||||||
|
margin-top: $chat-standard-padding-xs;
|
||||||
|
margin-bottom: $chat-standard-padding-xs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creation dialog
|
||||||
|
//
|
||||||
|
.poll-create-dialog-content {
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.poll-create-status {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
@ -37,12 +37,6 @@
|
||||||
background: white;
|
background: white;
|
||||||
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.08);
|
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
.join-user-info {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
src/assets/css/vendors/_v-emoji-picker.scss
vendored
1
src/assets/css/vendors/_v-emoji-picker.scss
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
#EmojiPicker {
|
#EmojiPicker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
.container-emoji {
|
.container-emoji {
|
||||||
height: 60vh;
|
height: 60vh;
|
||||||
|
|
|
||||||
6
src/assets/icons/addReaction.vue
Normal file
6
src/assets/icons/addReaction.vue
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#757575">
|
||||||
|
<rect fill="none" height="24" width="24"/>
|
||||||
|
<path d="M18,9V7h-2V2.84C14.77,2.3,13.42,2,11.99,2C6.47,2,2,6.48,2,12s4.47,10,9.99,10C17.52,22,22,17.52,22,12 c0-1.05-0.17-2.05-0.47-3H18z M15.5,8C16.33,8,17,8.67,17,9.5S16.33,11,15.5,11S14,10.33,14,9.5S14.67,8,15.5,8z M8.5,8 C9.33,8,10,8.67,10,9.5S9.33,11,8.5,11S7,10.33,7,9.5S7.67,8,8.5,8z M12,17.5c-2.33,0-4.31-1.46-5.11-3.5h10.22 C16.31,16.04,14.33,17.5,12,17.5z M22,3h2v2h-2v2h-2V5h-2V3h2V1h2V3z"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
3
src/assets/icons/ic_check.svg
Normal file
3
src/assets/icons/ic_check.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="15" height="12" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.9265 0.278549C13.6041 -0.0706278 13.0413 -0.0953257 12.6944 0.225985L4.74201 7.59645C4.66894 7.6675 4.55947 7.67068 4.48341 7.6057L2.44807 5.93997C2.20767 5.74531 1.92476 5.64018 1.62665 5.64018C1.22503 5.64018 0.844759 5.83181 0.586146 6.15932L0.321439 6.49622C-0.153089 7.10185 -0.095373 7.992 0.449243 8.52339L3.64365 11.6229C3.89315 11.8671 4.2187 12 4.55335 12C4.93661 12 5.30168 11.827 5.5603 11.521L13.9508 1.5886C14.2672 1.21487 14.2581 0.64014 13.9265 0.278521L13.9265 0.278549Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 620 B |
6
src/assets/icons/ic_poll.svg
Normal file
6
src/assets/icons/ic_poll.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.31462 16.4718C3.31462 16.9496 3.70026 17.3368 4.17609 17.3368L16.1385 17.3368C16.6144 17.3368 17 16.9496 17 16.4718L16.9998 13.6229C16.9998 13.1452 16.6142 12.7579 16.1383 12.7579L4.1764 12.7579C3.70056 12.7579 3.31492 13.1452 3.31492 13.6229L3.31512 16.4718L3.31462 16.4718Z" fill="white"/>
|
||||||
|
<path d="M3.31462 10.4557C3.31462 10.9335 3.70026 11.3208 4.17609 11.3208L11.3428 11.3208C11.8186 11.3208 12.2043 10.9335 12.2043 10.4557L12.2043 7.60711C12.2043 7.12931 11.8186 6.74208 11.3428 6.74208L4.17609 6.74208C3.70026 6.74208 3.31462 7.12932 3.31462 7.60711L3.31462 10.4557Z" fill="white"/>
|
||||||
|
<path d="M3.31451 1.59127L3.31451 4.44011C3.31451 4.91791 3.70016 5.30514 4.17598 5.30514L6.99509 5.30514C7.47093 5.30514 7.85657 4.91791 7.85657 4.44011L7.85637 1.59127C7.85637 1.11347 7.47073 0.726242 6.9949 0.726242L4.17599 0.726242C3.70035 0.726242 3.31452 1.11348 3.31452 1.59127L3.31451 1.59127Z" fill="white"/>
|
||||||
|
<path d="M-2.00529e-05 0.587841L-2.0791e-05 17.4747C-2.08052e-05 17.7995 0.262306 18.0625 0.585404 18.0625L1.38198 18.0625C1.70528 18.0625 1.96741 17.7995 1.96741 17.4747L1.96741 0.587841C1.96741 0.263208 1.70508 -1.14667e-08 1.38198 -2.55897e-08L0.585405 -6.04092e-08C0.261911 -7.45496e-08 -2.00387e-05 0.263213 -2.00529e-05 0.587841Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -69,7 +69,8 @@
|
||||||
"new_room": "ཁ་བརྡ་ཁང་གསར་པ།",
|
"new_room": "ཁ་བརྡ་ཁང་གསར་པ།",
|
||||||
"name_room": "ཁ་བརྡ་ཁང་ལ་མིང་ཐོགས།",
|
"name_room": "ཁ་བརྡ་ཁང་ལ་མིང་ཐོགས།",
|
||||||
"room_topic": "གལ་ཏེ་འདོད་པ་ཡོད་ན། ཚོགས་པའི་སྐོར་གྱི་འགྲེལ་བཤད་ཐུང་ངུ་ཞིག་འབྲི་ཆོག",
|
"room_topic": "གལ་ཏེ་འདོད་པ་ཡོད་ན། ཚོགས་པའི་སྐོར་གྱི་འགྲེལ་བཤད་ཐུང་ངུ་ཞིག་འབྲི་ཆོག",
|
||||||
"create": "བཟོས།"
|
"create": "བཟོས།",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"logout": "ཕྱིར་ཐོན།",
|
"logout": "ཕྱིར་ཐོན།",
|
||||||
|
|
@ -222,7 +223,8 @@
|
||||||
"remember_me": "ང་དྲན་པར་བྱོས།",
|
"remember_me": "ང་དྲན་པར་བྱོས།",
|
||||||
"user_name_label": "སྤྱོད་མིང་།",
|
"user_name_label": "སྤྱོད་མིང་།",
|
||||||
"title": "ཁྱེད་རང་ནང་དུ་ཞུགས་པར་དགའ་བསུ་ཞུ།",
|
"title": "ཁྱེད་རང་ནང་དུ་ཞུགས་པར་དགའ་བསུ་ཞུ།",
|
||||||
"join_failed": "ཁ་བརྡ་ཁང་དུ་འཛུལ་ཐུབ་མ་སོང་།"
|
"join_failed": "ཁ་བརྡ་ཁང་དུ་འཛུལ་ཐུབ་མ་སོང་།",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"profile_info_popup": {
|
"profile_info_popup": {
|
||||||
"powered_by": "ཁ་བརྡ་ཁང་འདི་{product} ནུས་ཤུགས་བསྩལ་ཡོད། {productLink} ནས་དེ་ལས་མང་བ་སྦྱོང་ཆོག་ལ། མདུན་དུ་བསྐྱོད་དེ་ཁ་བརྡ་ཁང་གཞན་ཞིག་བསྐྲུན་ཆོག",
|
"powered_by": "ཁ་བརྡ་ཁང་འདི་{product} ནུས་ཤུགས་བསྩལ་ཡོད། {productLink} ནས་དེ་ལས་མང་བ་སྦྱོང་ཆོག་ལ། མདུན་དུ་བསྐྱོད་དེ་ཁ་བརྡ་ཁང་གཞན་ཞིག་བསྐྲུན་ཆོག",
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,8 @@
|
||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
"next": "Nächste",
|
"next": "Nächste",
|
||||||
"name_room": "Raum benennen",
|
"name_room": "Raum benennen",
|
||||||
"room_topic": "Füge eine Beschreibung hinzu, wenn du möchtest"
|
"room_topic": "Füge eine Beschreibung hinzu, wenn du möchtest",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"device_list": {
|
"device_list": {
|
||||||
"title": "GERÄTE",
|
"title": "GERÄTE",
|
||||||
|
|
@ -144,7 +145,8 @@
|
||||||
"status_logging_in": "Wird angemeldet …",
|
"status_logging_in": "Wird angemeldet …",
|
||||||
"status_joining": "Raum beitreten …",
|
"status_joining": "Raum beitreten …",
|
||||||
"join_failed": "Beitritt zum Raum fehlgeschlagen.",
|
"join_failed": "Beitritt zum Raum fehlgeschlagen.",
|
||||||
"title": "Willkommen in {roomName}"
|
"title": "Willkommen in {roomName}",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"title": "Freunde hinzufügen",
|
"title": "Freunde hinzufügen",
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
"new_room": "New Room",
|
"new_room": "New Room",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"name_room": "Name room",
|
"name_room": "Room name",
|
||||||
"room_topic": "Add a description if you like",
|
"room_topic": "Add a description if you like",
|
||||||
"join_permissions": "Join permissions",
|
"join_permissions": "Join permissions",
|
||||||
"set_join_permissions": "Set Join Permissions",
|
"set_join_permissions": "Set Join Permissions",
|
||||||
|
|
@ -104,7 +104,8 @@
|
||||||
"invite_description": "Choose from a list or search by account ID",
|
"invite_description": "Choose from a list or search by account ID",
|
||||||
"status_creating": "Creating room",
|
"status_creating": "Creating room",
|
||||||
"status_avatar_total": "Uploading avatar: {count} of {total}",
|
"status_avatar_total": "Uploading avatar: {count} of {total}",
|
||||||
"status_avatar": "Uploading avatar: {count}"
|
"status_avatar": "Uploading avatar: {count}",
|
||||||
|
"room_name_limit_error_msg": "Maximum 50 characters allowed"
|
||||||
},
|
},
|
||||||
"device_list": {
|
"device_list": {
|
||||||
"title": "DEVICES",
|
"title": "DEVICES",
|
||||||
|
|
@ -158,7 +159,8 @@
|
||||||
"enter_room": "Enter room",
|
"enter_room": "Enter room",
|
||||||
"status_logging_in": "Logging in...",
|
"status_logging_in": "Logging in...",
|
||||||
"status_joining": "Joining room...",
|
"status_joining": "Joining room...",
|
||||||
"join_failed": "Failed to join room."
|
"join_failed": "Failed to join room.",
|
||||||
|
"choose_name": "Choose a name to use"
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"title": "Add Friends",
|
"title": "Add Friends",
|
||||||
|
|
@ -208,7 +210,8 @@
|
||||||
"show_all": "Show all >",
|
"show_all": "Show all >",
|
||||||
"leave_room": "Leave",
|
"leave_room": "Leave",
|
||||||
"version_info": "Powered by Guardian Project. Version: {version}",
|
"version_info": "Powered by Guardian Project. Version: {version}",
|
||||||
"scan_code": "Scan to join the room"
|
"scan_code": "Scan to join the room",
|
||||||
|
"export_room": "Export chat"
|
||||||
},
|
},
|
||||||
"room_info_sheet": {
|
"room_info_sheet": {
|
||||||
"this_room": "This room",
|
"this_room": "This room",
|
||||||
|
|
@ -233,5 +236,34 @@
|
||||||
"video_file": "Video file",
|
"video_file": "Video file",
|
||||||
"original_text": "<original text>",
|
"original_text": "<original text>",
|
||||||
"download_name": "Download"
|
"download_name": "Download"
|
||||||
|
},
|
||||||
|
"poll_create": {
|
||||||
|
"title": "Create poll",
|
||||||
|
"intro": "Please fill in details below.",
|
||||||
|
"create": "Create",
|
||||||
|
"creating": "Creating poll",
|
||||||
|
"poll_disclosed": "Open - current results are shown at all times.",
|
||||||
|
"poll_undisclosed": "Closed - users will see the results when poll is closed.",
|
||||||
|
"add_answer": "Add answer",
|
||||||
|
"failed": "Failed to create poll, please try again later.",
|
||||||
|
"question_label": "Type your question here",
|
||||||
|
"question_required": "You need to enter a question!",
|
||||||
|
"answer_label": "Answer no {index}",
|
||||||
|
"answer_required": "Answer can't be empty. Please enter some text or remove this option.",
|
||||||
|
"create_poll_menu_option": "Create poll",
|
||||||
|
"poll_status_closed": "Poll is closed",
|
||||||
|
"poll_status_disclosed": "Results will be shown when poll is closed.",
|
||||||
|
"poll_status_open": "Poll is open",
|
||||||
|
"poll_status_open_not_voted": "Poll is open - vote to see the results",
|
||||||
|
"close_poll": "Close poll",
|
||||||
|
"poll_submit": "Submit",
|
||||||
|
"num_answered": "{count} have answered"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"exported_date": "Exported on {date}",
|
||||||
|
"fetched_n_events": "Fetched {count} events",
|
||||||
|
"fetched_n_of_total_events": "Fetched {count} of {total} events",
|
||||||
|
"processed_n_of_total_events": "Processed media for {count} of {total} events",
|
||||||
|
"export_filename": "Exported chat {date}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@
|
||||||
"remember_me": "Recordarme",
|
"remember_me": "Recordarme",
|
||||||
"user_name_label": "Nombre de usuario",
|
"user_name_label": "Nombre de usuario",
|
||||||
"title": "Bienvenido has sido invitado a unirte",
|
"title": "Bienvenido has sido invitado a unirte",
|
||||||
"join_failed": "No se pudo unir a la sala."
|
"join_failed": "No se pudo unir a la sala.",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"display_name": "Nombre para mostrar",
|
"display_name": "Nombre para mostrar",
|
||||||
|
|
@ -117,7 +118,8 @@
|
||||||
"done": "Listo",
|
"done": "Listo",
|
||||||
"new_room": "Nueva Sala",
|
"new_room": "Nueva Sala",
|
||||||
"create": "Crear",
|
"create": "Crear",
|
||||||
"room_topic": "Añade una descripción si quieres"
|
"room_topic": "Añade una descripción si quieres",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"room_welcome": {
|
"room_welcome": {
|
||||||
"join_public": "Cualquiera puede unirse abriendo este vínculo: {link}",
|
"join_public": "Cualquiera puede unirse abriendo este vínculo: {link}",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@
|
||||||
"room_topic": "Lisää kuvaus, jos haluat",
|
"room_topic": "Lisää kuvaus, jos haluat",
|
||||||
"add_people": "Lisää ihmisiä",
|
"add_people": "Lisää ihmisiä",
|
||||||
"link_copied": "Linkki kopioitu!",
|
"link_copied": "Linkki kopioitu!",
|
||||||
"public_info": "Kuka tahansa, jolla on linkki"
|
"public_info": "Kuka tahansa, jolla on linkki",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"purge_room": {
|
"purge_room": {
|
||||||
"n_seconds": "{seconds} sekuntia",
|
"n_seconds": "{seconds} sekuntia",
|
||||||
|
|
@ -72,7 +73,8 @@
|
||||||
"user_name_label": "Käyttäjätunnus",
|
"user_name_label": "Käyttäjätunnus",
|
||||||
"status_joining": "Liittyminen huoneeseen…",
|
"status_joining": "Liittyminen huoneeseen…",
|
||||||
"status_logging_in": "Kirjautuminen sisään…",
|
"status_logging_in": "Kirjautuminen sisään…",
|
||||||
"join_failed": "Huoneeseen liittyminen epäonnistui."
|
"join_failed": "Huoneeseen liittyminen epäonnistui.",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"leave": {
|
"leave": {
|
||||||
"title_public": "Näkemiin, {user}",
|
"title_public": "Näkemiin, {user}",
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,8 @@
|
||||||
"get_link": "Obtenir le lien",
|
"get_link": "Obtenir le lien",
|
||||||
"public_info": "Quiconque avec un lien",
|
"public_info": "Quiconque avec un lien",
|
||||||
"join_permissions_info": "Ces autorisations déterminent comment les personnes peuvent rejoindre le salon et avec quelle facilité d’autres personnes peuvent être invitées. Elles peuvent être modifiées à tout moment.",
|
"join_permissions_info": "Ces autorisations déterminent comment les personnes peuvent rejoindre le salon et avec quelle facilité d’autres personnes peuvent être invitées. Elles peuvent être modifiées à tout moment.",
|
||||||
"status_creating": "Création du salon"
|
"status_creating": "Création du salon",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"device_list": {
|
"device_list": {
|
||||||
"title": "APPAREILS",
|
"title": "APPAREILS",
|
||||||
|
|
@ -144,7 +145,8 @@
|
||||||
"status_joining": "Adhésion au salon…",
|
"status_joining": "Adhésion au salon…",
|
||||||
"join_failed": "Impossible de rejoindre le salon.",
|
"join_failed": "Impossible de rejoindre le salon.",
|
||||||
"joining_as": "Vous rejoignez en tant que :",
|
"joining_as": "Vous rejoignez en tant que :",
|
||||||
"join_guest": "Rejoindre comme invité·e"
|
"join_guest": "Rejoindre comme invité·e",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"title": "Ajouter des amis",
|
"title": "Ajouter des amis",
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,8 @@
|
||||||
"new_room": "Nuova stanza",
|
"new_room": "Nuova stanza",
|
||||||
"invite_info": "Solo le persone aggiunte",
|
"invite_info": "Solo le persone aggiunte",
|
||||||
"join_permissions_info": "Questi permessi determinano come le persone possono entrare nella stanza e quanto facilmente gli altri possono essere invitati. Possono essere cambiati in qualsiasi momento.",
|
"join_permissions_info": "Questi permessi determinano come le persone possono entrare nella stanza e quanto facilmente gli altri possono essere invitati. Possono essere cambiati in qualsiasi momento.",
|
||||||
"public_info": "Chiunque abbia un collegamento"
|
"public_info": "Chiunque abbia un collegamento",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"device_list": {
|
"device_list": {
|
||||||
"title": "DISPOSITIVI",
|
"title": "DISPOSITIVI",
|
||||||
|
|
@ -143,7 +144,8 @@
|
||||||
"join_guest": "Unisciti come ospite",
|
"join_guest": "Unisciti come ospite",
|
||||||
"status_joining": "Unendosi alla stanza…",
|
"status_joining": "Unendosi alla stanza…",
|
||||||
"join_failed": "Impossibile unirsi alla stanza.",
|
"join_failed": "Impossibile unirsi alla stanza.",
|
||||||
"status_logging_in": "Accesso in corso…"
|
"status_logging_in": "Accesso in corso…",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"title": "Aggiungi amici",
|
"title": "Aggiungi amici",
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@
|
||||||
"status_joining": "Tar del i rom…",
|
"status_joining": "Tar del i rom…",
|
||||||
"status_logging_in": "Logger inn …",
|
"status_logging_in": "Logger inn …",
|
||||||
"enter_room": "",
|
"enter_room": "",
|
||||||
"title": "Velkommen til {roomName}"
|
"title": "Velkommen til {roomName}",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"profile_info_popup": {
|
"profile_info_popup": {
|
||||||
"identity_temporary": "{displayName}",
|
"identity_temporary": "{displayName}",
|
||||||
|
|
@ -113,7 +114,8 @@
|
||||||
"link_copied": "Lenke kopiert.",
|
"link_copied": "Lenke kopiert.",
|
||||||
"next": "Neste",
|
"next": "Neste",
|
||||||
"create": "Opprett",
|
"create": "Opprett",
|
||||||
"new_room": "Nytt rom"
|
"new_room": "Nytt rom",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"room_welcome": {
|
"room_welcome": {
|
||||||
"room_history_is": "Romhistorikken er {type}.",
|
"room_history_is": "Romhistorikken er {type}.",
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,8 @@
|
||||||
"invite_description": "Escolha numa lista ou busque pelo ID da conta",
|
"invite_description": "Escolha numa lista ou busque pelo ID da conta",
|
||||||
"status_creating": "Criando a sala",
|
"status_creating": "Criando a sala",
|
||||||
"status_avatar_total": "Enviando o avatar: {count} de {total}",
|
"status_avatar_total": "Enviando o avatar: {count} de {total}",
|
||||||
"status_avatar": "Enviando avatar: {count}"
|
"status_avatar": "Enviando avatar: {count}",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"device_list": {
|
"device_list": {
|
||||||
"title": "DISPOSITIVOS",
|
"title": "DISPOSITIVOS",
|
||||||
|
|
@ -171,7 +172,8 @@
|
||||||
"enter_room": "Entre na sala",
|
"enter_room": "Entre na sala",
|
||||||
"status_logging_in": "Fazendo login...",
|
"status_logging_in": "Fazendo login...",
|
||||||
"status_joining": "Entrando na sala...",
|
"status_joining": "Entrando na sala...",
|
||||||
"join_failed": "Houve uma falha ao entrar na sala."
|
"join_failed": "Houve uma falha ao entrar na sala.",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"leave": {
|
"leave": {
|
||||||
"title_public": "Adeus, {user}",
|
"title_public": "Adeus, {user}",
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,8 @@
|
||||||
"joining_as": "Vă înscrieți ca:",
|
"joining_as": "Vă înscrieți ca:",
|
||||||
"remember_me": "Amintește-ți de mine",
|
"remember_me": "Amintește-ți de mine",
|
||||||
"user_name_label": "Numele utilizatorului",
|
"user_name_label": "Numele utilizatorului",
|
||||||
"title": "Bine ați venit, ați fost invitat să vă alăturați"
|
"title": "Bine ați venit, ați fost invitat să vă alăturați",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"profile_info_popup": {
|
"profile_info_popup": {
|
||||||
"new_room": "+ Cameră nouă",
|
"new_room": "+ Cameră nouă",
|
||||||
|
|
@ -162,7 +163,8 @@
|
||||||
"name_room": "Nume cameră",
|
"name_room": "Nume cameră",
|
||||||
"next": "Următorul",
|
"next": "Următorul",
|
||||||
"create": "Creați",
|
"create": "Creați",
|
||||||
"new_room": "Cameră nouă"
|
"new_room": "Cameră nouă",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"room_welcome": {
|
"room_welcome": {
|
||||||
"got_it": "L-am prins",
|
"got_it": "L-am prins",
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,8 @@
|
||||||
"name_room": "مۇنازىرەخانىغا ئىسىم قويۇڭ",
|
"name_room": "مۇنازىرەخانىغا ئىسىم قويۇڭ",
|
||||||
"next": "كېيىنكى",
|
"next": "كېيىنكى",
|
||||||
"create": "قۇرۇش",
|
"create": "قۇرۇش",
|
||||||
"new_room": "يېڭى مۇنازىرەخانا"
|
"new_room": "يېڭى مۇنازىرەخانا",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"room": {
|
"room": {
|
||||||
"purge_failed": "مۇنازىرەخانىنى يۇيۇش مەغلۇب بولدى!",
|
"purge_failed": "مۇنازىرەخانىنى يۇيۇش مەغلۇب بولدى!",
|
||||||
|
|
@ -114,7 +115,8 @@
|
||||||
"joining_as": "سىز تۆۋەندىكىدەك قاتنىشىۋاتىسىز:",
|
"joining_as": "سىز تۆۋەندىكىدەك قاتنىشىۋاتىسىز:",
|
||||||
"remember_me": "",
|
"remember_me": "",
|
||||||
"user_name_label": "قوللانغۇچى ئىسمى",
|
"user_name_label": "قوللانغۇچى ئىسمى",
|
||||||
"title": "{ياتاق ئىسمى} غا خۇش كەپسىز"
|
"title": "{ياتاق ئىسمى} غا خۇش كەپسىز",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"room_welcome": {
|
"room_welcome": {
|
||||||
"info_permissions": "ياتاق تەڭشىكىدە خالىغان ۋاقىتتا «قوشۇلۇش ئىجازەتنامىسى» نى ئۆزگەرتەلەيسىز.",
|
"info_permissions": "ياتاق تەڭشىكىدە خالىغان ۋاقىتتا «قوشۇلۇش ئىجازەتنامىسى» نى ئۆزگەرتەلەيسىز.",
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,8 @@
|
||||||
"remember_me": "记得我",
|
"remember_me": "记得我",
|
||||||
"user_name_label": "用户名",
|
"user_name_label": "用户名",
|
||||||
"title": "欢迎您被邀请加入",
|
"title": "欢迎您被邀请加入",
|
||||||
"join_failed": "加入聊天室失败。"
|
"join_failed": "加入聊天室失败。",
|
||||||
|
"choose_name": ""
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"display_name": "显示名称",
|
"display_name": "显示名称",
|
||||||
|
|
@ -211,7 +212,8 @@
|
||||||
"done": "完毕",
|
"done": "完毕",
|
||||||
"new_room": "新的聊天室",
|
"new_room": "新的聊天室",
|
||||||
"room_topic": "如果您愿意,请添加说明",
|
"room_topic": "如果您愿意,请添加说明",
|
||||||
"create": "创建"
|
"create": "创建",
|
||||||
|
"room_name_limit_error_msg": ""
|
||||||
},
|
},
|
||||||
"room_welcome": {
|
"room_welcome": {
|
||||||
"got_it": "知道了",
|
"got_it": "知道了",
|
||||||
|
|
|
||||||
|
|
@ -34,4 +34,4 @@ export default {
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "@/assets/css/chat.scss";
|
@import "@/assets/css/chat.scss";
|
||||||
</style>
|
</style>
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,7 @@
|
||||||
@click.stop="onHeaderClicked"
|
@click.stop="onHeaderClicked"
|
||||||
>
|
>
|
||||||
<v-avatar size="40" class="me-2">
|
<v-avatar size="40" class="me-2">
|
||||||
<v-img :src="room.avatar || memberAvatar" />
|
<v-img v-if="room.avatar || memberAvatar" :src="room.avatar || memberAvatar" />
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
|
|
|
||||||
195
src/components/CreatePollDialog.vue
Normal file
195
src/components/CreatePollDialog.vue
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="showDialog" v-show="room" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '688px' : '95%'">
|
||||||
|
<div class="dialog-content text-center">
|
||||||
|
<template>
|
||||||
|
<v-icon color="black" size="30">poll</v-icon>
|
||||||
|
<h2 class="dialog-title">
|
||||||
|
{{ $t("poll_create.title") }}
|
||||||
|
</h2>
|
||||||
|
<div class="dialog-text">{{ $t("poll_create.intro") }}</div>
|
||||||
|
</template>
|
||||||
|
<v-form v-model="isValid" ref="pollcreateform">
|
||||||
|
<v-container fluid class="poll-create-dialog-content">
|
||||||
|
<v-row cols="12">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-switch
|
||||||
|
v-model="pollIsDisclosed"
|
||||||
|
inset
|
||||||
|
:label="pollIsDisclosed ? $t('poll_create.poll_disclosed') : $t('poll_create.poll_undisclosed')"
|
||||||
|
></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row cols="12">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
outlined
|
||||||
|
:disabled="isCreating"
|
||||||
|
v-model="pollQuestion"
|
||||||
|
:label="$t('poll_create.question_label')"
|
||||||
|
:rules="[(v) => !!v || $t('poll_create.question_required')]"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-for="(answer, index) in pollAnswers" cols="12" :key="answer.id">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
outlined
|
||||||
|
:disabled="isCreating"
|
||||||
|
v-model="answer.text"
|
||||||
|
:label="$t('poll_create.answer_label', { index: index + 1 })"
|
||||||
|
:append-icon="pollAnswers.length > 1 ? 'delete' : null"
|
||||||
|
@click:append="removeAnswer(index)"
|
||||||
|
:rules="[(v) => !!v || $t('poll_create.answer_required')]"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row
|
||||||
|
><v-col
|
||||||
|
><v-btn @click.stop="addAnswer">{{ $t("poll_create.add_answer") }}</v-btn></v-col
|
||||||
|
></v-row
|
||||||
|
>
|
||||||
|
</v-container>
|
||||||
|
</v-form>
|
||||||
|
<div class="poll-create-status">
|
||||||
|
{{ status }}
|
||||||
|
<v-progress-circular v-if="isCreating" indeterminate color="primary" size="14"></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row cols="12">
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-btn
|
||||||
|
id="btn-create-poll-cancel"
|
||||||
|
depressed
|
||||||
|
text
|
||||||
|
block
|
||||||
|
class="text-button"
|
||||||
|
@click="showDialog = false"
|
||||||
|
:disabled="isCreating"
|
||||||
|
>{{ $t("menu.cancel") }}</v-btn
|
||||||
|
>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" align="center">
|
||||||
|
<v-btn
|
||||||
|
id="btn-create-poll"
|
||||||
|
color="red"
|
||||||
|
depressed
|
||||||
|
block
|
||||||
|
class="filled-button"
|
||||||
|
@click.stop="onCreatePoll()"
|
||||||
|
:disabled="isCreating"
|
||||||
|
>{{ $t("poll_create.create") }}</v-btn
|
||||||
|
>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import roomInfoMixin from "./roomInfoMixin";
|
||||||
|
import util from "../plugins/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "CreatePollDialog",
|
||||||
|
mixins: [roomInfoMixin],
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: function() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return this.defaultData();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal, ignoredOldVal) {
|
||||||
|
this.showDialog = newVal;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showDialog() {
|
||||||
|
if (!this.showDialog) {
|
||||||
|
this.$emit("close");
|
||||||
|
} else {
|
||||||
|
// Reset values
|
||||||
|
let defaults = this.defaultData();
|
||||||
|
this.isValid = defaults.isValid;
|
||||||
|
this.isCreating = defaults.isCreating;
|
||||||
|
this.status = defaults.status;
|
||||||
|
this.pollIsDisclosed = defaults.pollIsDisclosed;
|
||||||
|
this.pollQuestion = defaults.pollQuestion;
|
||||||
|
this.pollAnswers = defaults.pollAnswers;
|
||||||
|
this.autoIncrementId = defaults.autoIncrementId;
|
||||||
|
if (this.$refs.pollcreateform) {
|
||||||
|
this.$refs.pollcreateform.resetValidation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
defaultData() {
|
||||||
|
return {
|
||||||
|
showDialog: false,
|
||||||
|
isValid: true,
|
||||||
|
isCreating: false,
|
||||||
|
status: String.fromCharCode(160),
|
||||||
|
pollIsDisclosed: true,
|
||||||
|
pollQuestion: "",
|
||||||
|
pollAnswers: [
|
||||||
|
{ id: "1", text: "" },
|
||||||
|
{ id: "2", text: "" },
|
||||||
|
],
|
||||||
|
autoIncrementId: 2,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addAnswer() {
|
||||||
|
if (this.pollAnswers.length < 20) {
|
||||||
|
// MAX length according to spec
|
||||||
|
this.autoIncrementId += 1;
|
||||||
|
this.pollAnswers.push({ id: "" + this.autoIncrementId, text: "" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAnswer(index) {
|
||||||
|
this.pollAnswers.splice(index, 1);
|
||||||
|
},
|
||||||
|
onCreatePoll() {
|
||||||
|
if (this.$refs.pollcreateform.validate()) {
|
||||||
|
this.isCreating = true;
|
||||||
|
this.status = this.$t("poll_create.creating");
|
||||||
|
util
|
||||||
|
.createPoll(
|
||||||
|
this.$matrix.matrixClient,
|
||||||
|
this.room.roomId,
|
||||||
|
this.pollQuestion,
|
||||||
|
this.pollAnswers,
|
||||||
|
this.pollIsDisclosed
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
console.log("Sent message");
|
||||||
|
this.isCreating = false;
|
||||||
|
this.showDialog = false;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("Failed to send:", err);
|
||||||
|
this.isCreating = false;
|
||||||
|
this.status = this.$t("poll_create.failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {}, 3000);
|
||||||
|
} else {
|
||||||
|
this.isValid = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
@import "@/assets/css/components/poll.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
!roomName || (step != steps.INITIAL && step != steps.CREATED)
|
!roomName || (step != steps.INITIAL && step != steps.CREATED)
|
||||||
"
|
"
|
||||||
class="header-button-right"
|
class="header-button-right"
|
||||||
@click.stop="next"
|
@click.stop="onCreate"
|
||||||
>
|
>
|
||||||
<span>{{
|
<span>{{
|
||||||
step == steps.CREATED ? $t("new_room.done") : $t("new_room.next")
|
step == steps.CREATED ? $t("new_room.done") : $t("new_room.next")
|
||||||
|
|
@ -28,54 +28,6 @@
|
||||||
</v-btn> -->
|
</v-btn> -->
|
||||||
</v-container>
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
<v-container class="join-user-info">
|
|
||||||
<v-row v-if="canEditProfile">
|
|
||||||
<v-col class="flex-grow-0 flex-shrink-0">
|
|
||||||
<v-avatar @click="showAvatarPickerList">
|
|
||||||
<v-img v-if="selectedProfile" :src="selectedProfile.image" />
|
|
||||||
</v-avatar>
|
|
||||||
</v-col>
|
|
||||||
<v-col class="flex-shrink-1 flex-grow-1">
|
|
||||||
<v-select
|
|
||||||
ref="avatar"
|
|
||||||
:items="availableAvatars"
|
|
||||||
cache-items
|
|
||||||
:label="$t('join.user_name_label')"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
@change="selectAvatar"
|
|
||||||
:value="availableAvatars[0]"
|
|
||||||
single-line
|
|
||||||
>
|
|
||||||
<template v-slot:selection>
|
|
||||||
<v-text-field
|
|
||||||
background-color="transparent"
|
|
||||||
solo
|
|
||||||
flat
|
|
||||||
hide-details
|
|
||||||
@click.native.stop="
|
|
||||||
{
|
|
||||||
}
|
|
||||||
"
|
|
||||||
v-model="selectedProfile.name"
|
|
||||||
></v-text-field>
|
|
||||||
</template>
|
|
||||||
<template v-slot:item="data">
|
|
||||||
<v-avatar size="32">
|
|
||||||
<v-img :src="data.item.image" />
|
|
||||||
</v-avatar>
|
|
||||||
<div class="ms-2">{{ data.item.name }}</div>
|
|
||||||
</template>
|
|
||||||
</v-select>
|
|
||||||
<v-checkbox
|
|
||||||
id="chk-remember-me"
|
|
||||||
class="mt-0"
|
|
||||||
v-model="rememberMe"
|
|
||||||
:label="$t('join.remember_me')"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<v-container fluid class="mt-40">
|
<v-container fluid class="mt-40">
|
||||||
<v-row align="center">
|
<v-row align="center">
|
||||||
|
|
@ -86,34 +38,49 @@
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row cols="12" align="center">
|
<v-row cols="12" align="center" justify="center">
|
||||||
<v-col cols="8" offset="2" align="center">
|
<v-col sm="8" align="center">
|
||||||
|
<div class="text-left font-weight-light">{{ $t("new_room.name_room") }}</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="roomName"
|
v-model="roomName"
|
||||||
:label="$t('new_room.name_room')"
|
|
||||||
color="black"
|
color="black"
|
||||||
|
:rules="roomNamerules"
|
||||||
|
counter="50"
|
||||||
background-color="white"
|
background-color="white"
|
||||||
v-on:keyup.enter="$refs.topic.focus()"
|
v-on:keyup.enter="$refs.topic.focus()"
|
||||||
:disabled="step > steps.INITIAL"
|
:disabled="step > steps.INITIAL"
|
||||||
|
autofocus
|
||||||
|
solo
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
<div class="text-left font-weight-light" v-show="roomName.length> 0">{{ $t("new_room.room_topic") }}</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="roomTopic"
|
v-model="roomTopic"
|
||||||
v-show="roomName.length > 0"
|
v-show="roomName.length > 0"
|
||||||
:label="$t('new_room.room_topic')"
|
|
||||||
color="black"
|
color="black"
|
||||||
background-color="white"
|
background-color="white"
|
||||||
v-on:keyup.enter="$refs.create.focus()"
|
v-on:keyup.enter="$refs.create.focus()"
|
||||||
:disabled="step > steps.INITIAL"
|
:disabled="step > steps.INITIAL"
|
||||||
|
solo
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-btn
|
<v-btn
|
||||||
id="btn-room-create"
|
id="btn-room-create"
|
||||||
color="black"
|
color="black"
|
||||||
depressed
|
depressed
|
||||||
class="filled-button"
|
class="filled-button"
|
||||||
@click.stop="next"
|
@click.stop="onCreate"
|
||||||
:disabled="roomName.length == 0"
|
:disabled="status ? true : roomName.length === 0 || roomName.length > 50"
|
||||||
>{{ $t("new_room.create") }}</v-btn
|
>
|
||||||
>
|
<div v-if="status && !enterRoomDialog" class="text-center">
|
||||||
|
{{ status }}
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="step == steps.CREATING"
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
size="20"
|
||||||
|
></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
<span v-else>{{ $t("new_room.create") }}</span>
|
||||||
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
@ -192,7 +159,7 @@
|
||||||
{{ $t("new_room.link_copied") }}
|
{{ $t("new_room.link_copied") }}
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
<div v-if="status" class="text-center">
|
<!-- <div v-if="status && !enterRoomDialog" class="text-center">
|
||||||
<v-progress-circular
|
<v-progress-circular
|
||||||
v-if="step == steps.CREATING"
|
v-if="step == steps.CREATING"
|
||||||
indeterminate
|
indeterminate
|
||||||
|
|
@ -200,7 +167,7 @@
|
||||||
size="20"
|
size="20"
|
||||||
></v-progress-circular>
|
></v-progress-circular>
|
||||||
{{ status }}
|
{{ status }}
|
||||||
</div>
|
</div> -->
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
</v-fade-transition>
|
</v-fade-transition>
|
||||||
<input
|
<input
|
||||||
|
|
@ -212,11 +179,79 @@
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="d-none"
|
class="d-none"
|
||||||
/>
|
/>
|
||||||
|
<v-dialog
|
||||||
|
v-model="enterRoomDialog"
|
||||||
|
:width="$vuetify.breakpoint.smAndUp ? '50%' : '90%'"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-container v-if="canEditProfile" class="pa-10">
|
||||||
|
<v-row class="align-center">
|
||||||
|
<v-col class="py-0">
|
||||||
|
<div class="text-left font-weight-bold">{{ $t("join.choose_name") }}</div>
|
||||||
|
<v-select
|
||||||
|
ref="avatar"
|
||||||
|
:items="availableAvatars"
|
||||||
|
cache-items
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
@change="selectAvatar"
|
||||||
|
:value="availableAvatars[0]"
|
||||||
|
single-line
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
<template v-slot:selection>
|
||||||
|
<v-text-field
|
||||||
|
background-color="transparent"
|
||||||
|
solo
|
||||||
|
flat
|
||||||
|
hide-details
|
||||||
|
@click.native.stop="{}"
|
||||||
|
v-model="selectedProfile.name"
|
||||||
|
></v-text-field>
|
||||||
|
</template>
|
||||||
|
<template v-slot:item="data">
|
||||||
|
<v-avatar size="32">
|
||||||
|
<v-img :src="data.item.image" />
|
||||||
|
</v-avatar>
|
||||||
|
<div class="ms-2">{{ data.item.name }}</div>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="2" class="py-0">
|
||||||
|
<v-avatar @click="showAvatarPickerList">
|
||||||
|
<v-img v-if="selectedProfile" :src="selectedProfile.image" />
|
||||||
|
</v-avatar>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row class="mt-0">
|
||||||
|
<v-col class="py-0">
|
||||||
|
<v-checkbox
|
||||||
|
id="chk-remember-me"
|
||||||
|
class="mt-0"
|
||||||
|
v-model="rememberMe"
|
||||||
|
@change="onRememberMe"
|
||||||
|
:label="$t('join.remember_me')"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-btn
|
||||||
|
color="black"
|
||||||
|
depressed
|
||||||
|
class="filled-button"
|
||||||
|
@click.stop="onEnterRoom"
|
||||||
|
:disabled="!selectedProfile.name"
|
||||||
|
>
|
||||||
|
{{ $t("join.enter_room") }}
|
||||||
|
</v-btn>
|
||||||
|
</v-container>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import util from "../plugins/utils";
|
import util from "../plugins/utils";
|
||||||
|
import rememberMeMixin from "./rememberMeMixin";
|
||||||
|
|
||||||
const steps = Object.freeze({
|
const steps = Object.freeze({
|
||||||
INITIAL: 0,
|
INITIAL: 0,
|
||||||
|
|
@ -227,6 +262,7 @@ const steps = Object.freeze({
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CreateRoom",
|
name: "CreateRoom",
|
||||||
|
mixins:[rememberMeMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
steps,
|
steps,
|
||||||
|
|
@ -255,7 +291,13 @@ export default {
|
||||||
publicRoomLink: null,
|
publicRoomLink: null,
|
||||||
publicRoomLinkCopied: false,
|
publicRoomLinkCopied: false,
|
||||||
availableAvatars: [],
|
availableAvatars: [],
|
||||||
selectedProfile: null,
|
selectedProfile: {
|
||||||
|
id: "",
|
||||||
|
image: "",
|
||||||
|
name: ""
|
||||||
|
},
|
||||||
|
enterRoomDialog: false,
|
||||||
|
roomNamerules: [v => v.length <= 50 || this.$t("new_room.room_name_limit_error_msg")],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -290,15 +332,7 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
}
|
||||||
rememberMe: {
|
|
||||||
get: function () {
|
|
||||||
return this.$store.state.useLocalStorage;
|
|
||||||
},
|
|
||||||
set: function (rememberMe) {
|
|
||||||
this.$store.commit("setUseLocalStorage", rememberMe);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -309,7 +343,17 @@ export default {
|
||||||
this.$navigation.pop();
|
this.$navigation.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
next() {
|
|
||||||
|
onCreate() {
|
||||||
|
if(this.currentUser) {
|
||||||
|
this.onEnterRoom();
|
||||||
|
} else {
|
||||||
|
this.enterRoomDialog = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onEnterRoom() {
|
||||||
|
this.enterRoomDialog = false;
|
||||||
if (this.step == steps.CREATED) {
|
if (this.step == steps.CREATED) {
|
||||||
this.openRoom();
|
this.openRoom();
|
||||||
} else if (this.step == steps.INITIAL) {
|
} else if (this.step == steps.INITIAL) {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,13 @@
|
||||||
@click.native.stop="{}"
|
@click.native.stop="{}"
|
||||||
v-model="selectedProfile.name"
|
v-model="selectedProfile.name"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-checkbox id="chk-remember-me" class="mt-0" v-model="rememberMe" :label="$t('join.remember_me')" />
|
<v-checkbox
|
||||||
|
id="chk-remember-me"
|
||||||
|
class="mt-0"
|
||||||
|
v-model="rememberMe"
|
||||||
|
:label="$t('join.remember_me')"
|
||||||
|
@change="onRememberMe"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="2" sm="5">
|
<v-col cols="2" sm="5">
|
||||||
<v-avatar @click="showAvatarPickerList">
|
<v-avatar @click="showAvatarPickerList">
|
||||||
|
|
@ -119,12 +125,13 @@
|
||||||
<script>
|
<script>
|
||||||
import util from "../plugins/utils";
|
import util from "../plugins/utils";
|
||||||
import LanguageMixin from "./languageMixin";
|
import LanguageMixin from "./languageMixin";
|
||||||
|
import rememberMeMixin from "./rememberMeMixin";
|
||||||
|
|
||||||
import SelectLanguageDialog from "./SelectLanguageDialog.vue";
|
import SelectLanguageDialog from "./SelectLanguageDialog.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Join",
|
name: "Join",
|
||||||
mixins: [LanguageMixin],
|
mixins: [LanguageMixin,rememberMeMixin],
|
||||||
components: {
|
components: {
|
||||||
SelectLanguageDialog,
|
SelectLanguageDialog,
|
||||||
},
|
},
|
||||||
|
|
@ -197,14 +204,6 @@ export default {
|
||||||
.substring(0, 1)
|
.substring(0, 1)
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
},
|
},
|
||||||
rememberMe: {
|
|
||||||
get: function () {
|
|
||||||
return this.$store.state.useLocalStorage;
|
|
||||||
},
|
|
||||||
set: function (rememberMe) {
|
|
||||||
this.$store.commit("setUseLocalStorage", rememberMe);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getDisplayLanguage() {
|
getDisplayLanguage() {
|
||||||
let displayLanguages = [...this.getLanguages()];
|
let displayLanguages = [...this.getLanguages()];
|
||||||
return displayLanguages.filter(lang => lang.display && lang.value !== this.$i18n.locale);
|
return displayLanguages.filter(lang => lang.display && lang.value !== this.$i18n.locale);
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@
|
||||||
id="chk-remember-me"
|
id="chk-remember-me"
|
||||||
class="mt-0"
|
class="mt-0"
|
||||||
v-model="rememberMe"
|
v-model="rememberMe"
|
||||||
|
@change="onRememberMe"
|
||||||
:label="$t('join.remember_me')"
|
:label="$t('join.remember_me')"
|
||||||
/>
|
/>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
|
@ -97,9 +98,11 @@
|
||||||
<script>
|
<script>
|
||||||
import User from "../models/user";
|
import User from "../models/user";
|
||||||
import util from "../plugins/utils";
|
import util from "../plugins/utils";
|
||||||
|
import rememberMeMixin from "./rememberMeMixin";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Login",
|
name: "Login",
|
||||||
|
mixins:[rememberMeMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: new User(this.$config.defaultServer, "", ""),
|
user: new User(this.$config.defaultServer, "", ""),
|
||||||
|
|
@ -120,15 +123,7 @@ export default {
|
||||||
},
|
},
|
||||||
showCloseButton() {
|
showCloseButton() {
|
||||||
return this.$navigation && this.$navigation.canPop();
|
return this.$navigation && this.$navigation.canPop();
|
||||||
},
|
}
|
||||||
rememberMe: {
|
|
||||||
get: function () {
|
|
||||||
return this.$store.state.useLocalStorage;
|
|
||||||
},
|
|
||||||
set: function (rememberMe) {
|
|
||||||
this.$store.commit("setUseLocalStorage", rememberMe);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.loggedIn) {
|
if (this.loggedIn) {
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,13 @@
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<v-btn
|
<v-btn
|
||||||
fab
|
fab
|
||||||
small
|
x-small
|
||||||
elevation="0"
|
elevation="0"
|
||||||
color="black"
|
color="black"
|
||||||
@click.stop="backgroundClick"
|
@click.stop="backgroundClick"
|
||||||
class="bottom-sheet-close"
|
class="bottom-sheet-close"
|
||||||
>
|
>
|
||||||
<v-icon color="white">cancel</v-icon>
|
<v-icon color="white" >cancel</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</SwipeableBottomSheet>
|
</SwipeableBottomSheet>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -91,8 +91,8 @@ export default {
|
||||||
.message-operations-bottom-sheet {
|
.message-operations-bottom-sheet {
|
||||||
.bottom-sheet-close {
|
.bottom-sheet-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 0;
|
||||||
top: 10px;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transition-bg {
|
.transition-bg {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
size="48"
|
size="48"
|
||||||
color="#e0e0e0"
|
color="#e0e0e0"
|
||||||
@click="showAvatarPicker"
|
@click="showAvatarPicker"
|
||||||
|
v-if="isAvatarLoaded"
|
||||||
>
|
>
|
||||||
<img v-if="userAvatar" :src="userAvatar" />
|
<img v-if="userAvatar" :src="userAvatar" />
|
||||||
<span v-else class="white--text">{{ userAvatarLetter }}</span>
|
<span v-else class="white--text">{{ userAvatarLetter }}</span>
|
||||||
|
|
@ -36,6 +37,15 @@
|
||||||
class="d-none"
|
class="d-none"
|
||||||
/>
|
/>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
|
<v-progress-circular
|
||||||
|
:rotate="360"
|
||||||
|
v-else
|
||||||
|
:width="3"
|
||||||
|
:value="loadValue"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{{ loadValue }}
|
||||||
|
</v-progress-circular>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col class="flex-shrink-1 flex-grow-1">
|
<v-col class="flex-shrink-1 flex-grow-1">
|
||||||
<div class="h1">{{ displayName }}</div>
|
<div class="h1">{{ displayName }}</div>
|
||||||
|
|
@ -191,6 +201,8 @@ export default {
|
||||||
newPassword2: null,
|
newPassword2: null,
|
||||||
settingPassword: false,
|
settingPassword: false,
|
||||||
passwordErrorMessage: null,
|
passwordErrorMessage: null,
|
||||||
|
isAvatarLoaded: true,
|
||||||
|
loadValue: 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -326,9 +338,14 @@ export default {
|
||||||
reader.readAsDataURL(event.target.files[0]);
|
reader.readAsDataURL(event.target.files[0]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setAvatar(image) {
|
setAvatar(image) {
|
||||||
|
const self = this;
|
||||||
|
this.isAvatarLoaded = false;
|
||||||
return util.setAvatar(this.$matrix, image, function (progress) {
|
return util.setAvatar(this.$matrix, image, function (progress) {
|
||||||
|
self.loadValue = Math.round(progress.loaded/progress.total * 100);
|
||||||
|
if(progress.loaded === progress.total) {
|
||||||
|
self.isAvatarLoaded = true;
|
||||||
|
}
|
||||||
console.log("Progress: " + JSON.stringify(progress));
|
console.log("Progress: " + JSON.stringify(progress));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
453
src/components/RoomExport.vue
Normal file
453
src/components/RoomExport.vue
Normal file
|
|
@ -0,0 +1,453 @@
|
||||||
|
<template>
|
||||||
|
<div class="chat-root">
|
||||||
|
<div class="chat-root d-flex flex-column" ref="exportRoot">
|
||||||
|
<!-- Header-->
|
||||||
|
<v-container fluid class="chat-header flex-grow-0 flex-shrink-0">
|
||||||
|
<v-row class="chat-header-row flex-nowrap">
|
||||||
|
<v-col cols="auto" class="chat-header-members text-start ma-0 pa-0" @click.stop="onHeaderClicked">
|
||||||
|
<v-avatar size="40" class="me-2">
|
||||||
|
<v-img v-if="room.avatar" :src="room.avatar" />
|
||||||
|
</v-avatar>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col class="chat-header-name ma-0 pa-0 flex-shrink-1 flex-nowrap">
|
||||||
|
<div class="room-name-inline text-truncate" :title="room.name">
|
||||||
|
{{ room.name }}
|
||||||
|
</div>
|
||||||
|
<div class="num-members">{{ $tc("room.members", room.getJoinedMemberCount()) }}</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="auto" class="text-end ma-0 pa-0">{{ exportDate }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<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()">
|
||||||
|
<!-- DAY Marker, shown for every new day in the timeline -->
|
||||||
|
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker" :title="dateForEvent(event)" />
|
||||||
|
|
||||||
|
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
|
||||||
|
<div class="message-wrapper">
|
||||||
|
<component
|
||||||
|
:is="componentForEvent(event, true)"
|
||||||
|
:room="room"
|
||||||
|
:event="event"
|
||||||
|
:nextEvent="events[index + 1]"
|
||||||
|
:reactions="timelineSet.getRelationsForEvent(event.getId(), 'm.annotation', 'm.reaction')"
|
||||||
|
:timelineSet="timelineSet"
|
||||||
|
ref="exportedEvent"
|
||||||
|
/>
|
||||||
|
<!-- <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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<v-container fluid fill-height class="exporting-indicator">
|
||||||
|
<v-row align="center" justify="center">
|
||||||
|
<v-col class="text-center">
|
||||||
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||||
|
<div>{{ statusText }}</div>
|
||||||
|
<v-btn color="black" depressed class="filled-button mt-5" @click.stop="cancelExport">{{
|
||||||
|
$t("menu.cancel")
|
||||||
|
}}</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageIncomingText from "./messages/MessageIncomingText.vue";
|
||||||
|
import MessageIncomingFile from "./messages/MessageIncomingFile.vue";
|
||||||
|
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
||||||
|
import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue";
|
||||||
|
import MessageIncomingVideo from "./messages/MessageIncomingVideo.vue";
|
||||||
|
import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue";
|
||||||
|
import MessageOutgoingText from "./messages/MessageOutgoingText.vue";
|
||||||
|
import MessageOutgoingFile from "./messages/MessageOutgoingFile.vue";
|
||||||
|
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
|
||||||
|
import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue";
|
||||||
|
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
|
||||||
|
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
|
||||||
|
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
|
||||||
|
import ContactJoin from "./messages/ContactJoin.vue";
|
||||||
|
import ContactLeave from "./messages/ContactLeave.vue";
|
||||||
|
import ContactInvited from "./messages/ContactInvited.vue";
|
||||||
|
import ContactChanged from "./messages/ContactChanged.vue";
|
||||||
|
import RoomCreated from "./messages/RoomCreated.vue";
|
||||||
|
import RoomAliased from "./messages/RoomAliased.vue";
|
||||||
|
import RoomNameChanged from "./messages/RoomNameChanged.vue";
|
||||||
|
import RoomTopicChanged from "./messages/RoomTopicChanged.vue";
|
||||||
|
import RoomAvatarChanged from "./messages/RoomAvatarChanged.vue";
|
||||||
|
import RoomHistoryVisibility from "./messages/RoomHistoryVisibility.vue";
|
||||||
|
import RoomJoinRules from "./messages/RoomJoinRules.vue";
|
||||||
|
import RoomPowerLevelsChanged from "./messages/RoomPowerLevelsChanged.vue";
|
||||||
|
import RoomGuestAccessChanged from "./messages/RoomGuestAccessChanged.vue";
|
||||||
|
import RoomEncrypted from "./messages/RoomEncrypted.vue";
|
||||||
|
import RoomDeletionNotice from "./messages/RoomDeletionNotice.vue";
|
||||||
|
import DebugEvent from "./messages/DebugEvent.vue";
|
||||||
|
import MessageOperations from "./messages/MessageOperations.vue";
|
||||||
|
import AvatarOperations from "./messages/AvatarOperations.vue";
|
||||||
|
import ChatHeader from "./ChatHeader.vue";
|
||||||
|
import VoiceRecorder from "./VoiceRecorder.vue";
|
||||||
|
import RoomInfoBottomSheet from "./RoomInfoBottomSheet.vue";
|
||||||
|
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader.vue";
|
||||||
|
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet.vue";
|
||||||
|
import StickerPickerBottomSheet from "./StickerPickerBottomSheet.vue";
|
||||||
|
import BottomSheet from "./BottomSheet.vue";
|
||||||
|
import CreatePollDialog from "./CreatePollDialog.vue";
|
||||||
|
import chatMixin from "./chatMixin";
|
||||||
|
import util from "../plugins/utils";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
import { EventTimelineSet } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "RoomExport",
|
||||||
|
mixins: [chatMixin],
|
||||||
|
components: {
|
||||||
|
ChatHeader,
|
||||||
|
MessageIncomingText,
|
||||||
|
MessageIncomingFile,
|
||||||
|
MessageIncomingImage,
|
||||||
|
MessageIncomingAudio,
|
||||||
|
MessageIncomingVideo,
|
||||||
|
MessageIncomingSticker,
|
||||||
|
MessageOutgoingText,
|
||||||
|
MessageOutgoingFile,
|
||||||
|
MessageOutgoingImage,
|
||||||
|
MessageOutgoingAudio,
|
||||||
|
MessageOutgoingVideo,
|
||||||
|
MessageOutgoingSticker,
|
||||||
|
MessageOutgoingPoll,
|
||||||
|
ContactJoin,
|
||||||
|
ContactLeave,
|
||||||
|
ContactInvited,
|
||||||
|
ContactChanged,
|
||||||
|
RoomCreated,
|
||||||
|
RoomAliased,
|
||||||
|
RoomNameChanged,
|
||||||
|
RoomTopicChanged,
|
||||||
|
RoomAvatarChanged,
|
||||||
|
RoomHistoryVisibility,
|
||||||
|
RoomJoinRules,
|
||||||
|
RoomPowerLevelsChanged,
|
||||||
|
RoomGuestAccessChanged,
|
||||||
|
RoomEncrypted,
|
||||||
|
RoomDeletionNotice,
|
||||||
|
DebugEvent,
|
||||||
|
MessageOperations,
|
||||||
|
VoiceRecorder,
|
||||||
|
RoomInfoBottomSheet,
|
||||||
|
CreatedRoomWelcomeHeader,
|
||||||
|
MessageOperationsBottomSheet,
|
||||||
|
StickerPickerBottomSheet,
|
||||||
|
BottomSheet,
|
||||||
|
AvatarOperations,
|
||||||
|
CreatePollDialog,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
room: {
|
||||||
|
type: Object,
|
||||||
|
default: function() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timelineSet: null,
|
||||||
|
events: [],
|
||||||
|
fetchedEvents: 0,
|
||||||
|
totalEvents: 0,
|
||||||
|
processedEvents: 0,
|
||||||
|
statusText: "",
|
||||||
|
cancelled: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.doExport();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
processedEvents() {
|
||||||
|
this.statusText = this.$t("export.processed_n_of_total_events", {
|
||||||
|
count: this.processedEvents,
|
||||||
|
total: this.totalEvents,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
exportDate() {
|
||||||
|
return this.$t("export.exported_date", { date: util.formatDay(Date.now().valueOf()) });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancelExport() {
|
||||||
|
this.cancelled = true;
|
||||||
|
},
|
||||||
|
async getEvents() {
|
||||||
|
const eventsPerBatch = 100;
|
||||||
|
let batchToken = null;
|
||||||
|
var nToFetch = null;
|
||||||
|
this.totalEvents = nToFetch == null ? 0 : nToFetch;
|
||||||
|
var fetchedEvents = [];
|
||||||
|
const eventMapper = this.$matrix.matrixClient.getEventMapper();
|
||||||
|
|
||||||
|
while (nToFetch == null || nToFetch > 0) {
|
||||||
|
const result = await this.$matrix.matrixClient.createMessagesRequest(
|
||||||
|
this.room.roomId,
|
||||||
|
batchToken,
|
||||||
|
nToFetch == null ? eventsPerBatch : Math.min(nToFetch, eventsPerBatch),
|
||||||
|
"b"
|
||||||
|
);
|
||||||
|
// For testing, uncomment to give a chance to cancel...
|
||||||
|
// await new Promise((resolve, ignoredReject) => {
|
||||||
|
// setTimeout(() => {
|
||||||
|
// resolve(true);
|
||||||
|
// }, 1000);
|
||||||
|
// });
|
||||||
|
if (this.cancelled) {
|
||||||
|
return Promise.reject("cancelled");
|
||||||
|
}
|
||||||
|
if (result.chunk.length === 0) break;
|
||||||
|
if (nToFetch != null) {
|
||||||
|
nToFetch -= result.chunk.length;
|
||||||
|
this.statusText = this.$t("export.fetched_n_of_total_events", {
|
||||||
|
count: this.totalEvents - nToFetch,
|
||||||
|
total: this.totalEvents,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.totalEvents += result.chunk.length;
|
||||||
|
this.statusText = this.$t("export.fetched_n_events", { count: this.totalEvents });
|
||||||
|
}
|
||||||
|
fetchedEvents.push(...result.chunk.map(eventMapper));
|
||||||
|
|
||||||
|
if (!result.end) break;
|
||||||
|
batchToken = result.end;
|
||||||
|
}
|
||||||
|
return fetchedEvents;
|
||||||
|
},
|
||||||
|
doExport() {
|
||||||
|
var zip = null;
|
||||||
|
var currentMediaSize = 0;
|
||||||
|
var maxMediaSize = 1024 * 1024 * 1024; // 1GB
|
||||||
|
|
||||||
|
this.getEvents()
|
||||||
|
.then((events) => {
|
||||||
|
var decryptionPromises = [];
|
||||||
|
for (const event of this.events) {
|
||||||
|
if (event.isEncrypted()) {
|
||||||
|
decryptionPromises.push(
|
||||||
|
this.$matrix.matrixClient.decryptEventIfNeeded(event, {
|
||||||
|
isRetry: true,
|
||||||
|
emit: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.all(decryptionPromises).then(() => {
|
||||||
|
return events;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((events) => {
|
||||||
|
// Create a timeline and add the events to that, so that relations etc are aggregated correctly!
|
||||||
|
this.timelineSet = new EventTimelineSet(null, { unstableClientRelationAggregation: true });
|
||||||
|
this.timelineSet.addEventsToTimeline(events.reverse(), true, this.timelineSet.getLiveTimeline(), "");
|
||||||
|
this.events = events;
|
||||||
|
|
||||||
|
// Wait a tick so UI is updated.
|
||||||
|
return new Promise((resolve, ignoredReject) => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// UI updated, start processing events
|
||||||
|
zip = new JSZip();
|
||||||
|
var imageFolder = zip.folder("images");
|
||||||
|
var audioFolder = zip.folder("audio");
|
||||||
|
var videoFolder = zip.folder("video");
|
||||||
|
|
||||||
|
var downloadPromises = [];
|
||||||
|
let components = this.$refs.exportedEvent;
|
||||||
|
for (const comp of components) {
|
||||||
|
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) {
|
||||||
|
currentMediaSize += blob.size;
|
||||||
|
return new Promise((resolve, ignoredReject) => {
|
||||||
|
//let mime = blob.type;
|
||||||
|
var extension = ".mp3";
|
||||||
|
let fileName = comp.event.getId() + extension;
|
||||||
|
audioFolder.file(fileName, blob); // TODO calc bytes
|
||||||
|
let elements = comp.$el.getElementsByTagName("audio");
|
||||||
|
let element = elements && elements[0];
|
||||||
|
if (element) {
|
||||||
|
element.src = "./audio/" + fileName;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((ignoredErr) => {
|
||||||
|
this.processedEvents += 1;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.processedEvents += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.all(downloadPromises);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log("All media added, total size: " + currentMediaSize);
|
||||||
|
|
||||||
|
let root = this.$refs.exportRoot;
|
||||||
|
|
||||||
|
var doc = "<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\"/>\n";
|
||||||
|
|
||||||
|
for (const sheet of document.styleSheets) {
|
||||||
|
doc += "<style type='text/css'>\n";
|
||||||
|
for (const rule of sheet.cssRules) {
|
||||||
|
if (rule.constructor.name != "CSSFontFaceRule") {
|
||||||
|
// Strip font face rules for now.
|
||||||
|
doc += rule.cssText + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc += "</style>\n";
|
||||||
|
}
|
||||||
|
doc +=
|
||||||
|
"</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
|
||||||
|
const getCssRules = function(el) {
|
||||||
|
if (el.classList.contains("op-button")) {
|
||||||
|
el.innerHTML = "";
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < el.children.length; i++) {
|
||||||
|
getCssRules(el.children[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getCssRules(root);
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
doc += this.$refs.exportRoot.outerHTML;
|
||||||
|
doc += "</div></body></html>";
|
||||||
|
|
||||||
|
zip.file("chat.html", doc);
|
||||||
|
zip.generateAsync({ type: "blob" }).then((content) => {
|
||||||
|
saveAs(
|
||||||
|
content,
|
||||||
|
this.$t("export.export_filename", { date: util.formatDay(Date.now().valueOf()) }) + ".zip"
|
||||||
|
);
|
||||||
|
this.status = "";
|
||||||
|
this.$emit("close");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to export:", err);
|
||||||
|
this.$emit("close");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<div v-if="member.userId != $matrix.currentUserId && !$matrix.isDirectRoomWith(room, member.userId) && expandedMembers.includes(member)" class="start-private-chat clickable" @click="startPrivateChat(member.userId)">Start private chat</div>
|
<div v-if="member.userId != $matrix.currentUserId && !$matrix.isDirectRoomWith(room, member.userId) && expandedMembers.includes(member)" class="start-private-chat clickable" @click="startPrivateChat(member.userId)">{{ $t("menu.start_private_chat") }}</div>
|
||||||
<DeviceList
|
<DeviceList
|
||||||
v-if="expandedMembers.includes(member)"
|
v-if="expandedMembers.includes(member)"
|
||||||
:member="member"
|
:member="member"
|
||||||
|
|
@ -167,6 +167,30 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<!-- EXPORT CHAT -->
|
||||||
|
<div style="text-align: center">
|
||||||
|
<v-btn
|
||||||
|
v-if="userCanExportChat"
|
||||||
|
color="black"
|
||||||
|
depressed
|
||||||
|
class="filled-button"
|
||||||
|
@click.stop="exportRoom"
|
||||||
|
>{{ $t("room_info.export_room") }}</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PURGE ROOM -->
|
||||||
|
<div class="members ma-3 pa-3 text-center">
|
||||||
|
<v-btn
|
||||||
|
id="btn-purge-room"
|
||||||
|
v-if="userCanPurgeRoom"
|
||||||
|
color="red"
|
||||||
|
depressed
|
||||||
|
class="filled-button"
|
||||||
|
@click.stop="showPurgeConfirmation = true"
|
||||||
|
>{{ $t("room_info.purge") }}</v-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="build-version">
|
<div class="build-version">
|
||||||
{{ $t("room_info.version_info", { version: buildVersion }) }}
|
{{ $t("room_info.version_info", { version: buildVersion }) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -177,14 +201,23 @@
|
||||||
@close="showLeaveConfirmation = false"
|
@close="showLeaveConfirmation = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PurgeRoomDialog
|
||||||
|
:show="showPurgeConfirmation"
|
||||||
|
:room="room"
|
||||||
|
@close="showPurgeConfirmation = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<QRCodePopup :show="showFullScreenQR" :message="publicRoomLink" @close="showFullScreenQR = false" />
|
<QRCodePopup :show="showFullScreenQR" :message="publicRoomLink" @close="showFullScreenQR = false" />
|
||||||
|
|
||||||
|
<RoomExport :room="room" v-if="exporting" v-on:close="exporting = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import LeaveRoomDialog from "../components/LeaveRoomDialog";
|
import LeaveRoomDialog from "../components/LeaveRoomDialog";
|
||||||
|
import PurgeRoomDialog from "../components/PurgeRoomDialog";
|
||||||
import DeviceList from "../components/DeviceList";
|
import DeviceList from "../components/DeviceList";
|
||||||
|
import RoomExport from "../components/RoomExport";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
import roomInfoMixin from "./roomInfoMixin";
|
import roomInfoMixin from "./roomInfoMixin";
|
||||||
import QRCodePopup from './QRCodePopup.vue';
|
import QRCodePopup from './QRCodePopup.vue';
|
||||||
|
|
@ -195,7 +228,9 @@ export default {
|
||||||
mixins: [roomInfoMixin],
|
mixins: [roomInfoMixin],
|
||||||
components: {
|
components: {
|
||||||
LeaveRoomDialog,
|
LeaveRoomDialog,
|
||||||
|
PurgeRoomDialog,
|
||||||
DeviceList,
|
DeviceList,
|
||||||
|
RoomExport,
|
||||||
QRCodePopup,
|
QRCodePopup,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -205,6 +240,7 @@ export default {
|
||||||
displayName: "",
|
displayName: "",
|
||||||
showAllMembers: false,
|
showAllMembers: false,
|
||||||
showLeaveConfirmation: false,
|
showLeaveConfirmation: false,
|
||||||
|
showPurgeConfirmation: false,
|
||||||
showFullScreenQR: false,
|
showFullScreenQR: false,
|
||||||
expandedMembers: [],
|
expandedMembers: [],
|
||||||
buildVersion: "",
|
buildVersion: "",
|
||||||
|
|
@ -222,7 +258,8 @@ export default {
|
||||||
icon: "person_add",
|
icon: "person_add",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
SHOW_MEMBER_LIMIT: 5
|
SHOW_MEMBER_LIMIT: 5,
|
||||||
|
exporting: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
@ -443,6 +480,11 @@ export default {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
exportRoom() {
|
||||||
|
if (this.room) {
|
||||||
|
this.exporting = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,30 @@
|
||||||
>
|
>
|
||||||
<div class="room-info-sheet" ref="roomInfoSheetContent">
|
<div class="room-info-sheet" ref="roomInfoSheetContent">
|
||||||
<div class="text-center current-room">
|
<div class="text-center current-room">
|
||||||
<v-avatar class="room-avatar">
|
<v-avatar class="room-avatar cursor-pointer" @click="showRoomAvatarPicker" v-if="isRoomAvatarLoaded">
|
||||||
<v-img v-if="roomAvatar" :src="roomAvatar" />
|
<v-img v-if="roomAvatar" :src="roomAvatar"/>
|
||||||
<span v-else class="white--text headline">{{
|
<span v-else class="white--text headline">{{
|
||||||
roomName.substring(0, 1).toUpperCase()
|
roomName.substring(0, 1).toUpperCase()
|
||||||
}}</span>
|
}}</span>
|
||||||
|
<input
|
||||||
|
id="room-avatar-picker"
|
||||||
|
ref="roomAvatar"
|
||||||
|
type="file"
|
||||||
|
name="roomAvatar"
|
||||||
|
@change="handleRoomPickedAvatar($event)"
|
||||||
|
accept="image/*"
|
||||||
|
class="d-none"
|
||||||
|
/>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
|
<v-progress-circular
|
||||||
|
:rotate="360"
|
||||||
|
v-else
|
||||||
|
:width="3"
|
||||||
|
:value="loadValue"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{{ loadValue }}
|
||||||
|
</v-progress-circular>
|
||||||
<div class="h4">{{$t('room_info_sheet.this_room')}}</div>
|
<div class="h4">{{$t('room_info_sheet.this_room')}}</div>
|
||||||
<div class="h2">{{ roomName }}</div>
|
<div class="h2">{{ roomName }}</div>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
|
@ -28,6 +46,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import util from "../plugins/utils";
|
||||||
import BottomSheet from "./BottomSheet";
|
import BottomSheet from "./BottomSheet";
|
||||||
import RoomList from "./RoomList.vue";
|
import RoomList from "./RoomList.vue";
|
||||||
import roomInfoMixin from "./roomInfoMixin";
|
import roomInfoMixin from "./roomInfoMixin";
|
||||||
|
|
@ -35,6 +54,12 @@ import roomInfoMixin from "./roomInfoMixin";
|
||||||
export default {
|
export default {
|
||||||
name: "RoomInfoBottomSheet",
|
name: "RoomInfoBottomSheet",
|
||||||
mixins: [roomInfoMixin],
|
mixins: [roomInfoMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isRoomAvatarLoaded: true,
|
||||||
|
loadValue: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
BottomSheet,
|
BottomSheet,
|
||||||
RoomList,
|
RoomList,
|
||||||
|
|
@ -56,9 +81,36 @@ export default {
|
||||||
createRoom() {
|
createRoom() {
|
||||||
this.close();
|
this.close();
|
||||||
this.$navigation.push({ name: "CreateRoom" });
|
this.$navigation.push({ name: "CreateRoom" });
|
||||||
}
|
},
|
||||||
|
|
||||||
},
|
showRoomAvatarPicker() {
|
||||||
|
this.$refs.roomAvatar.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
setRoomAvatar(file) {
|
||||||
|
const self = this;
|
||||||
|
this.isRoomAvatarLoaded = false;
|
||||||
|
return util.setRoomAvatar(
|
||||||
|
this.$matrix.matrixClient,
|
||||||
|
this.room.roomId,
|
||||||
|
file,
|
||||||
|
function (progress) {
|
||||||
|
self.loadValue = Math.round(progress.loaded/progress.total * 100);
|
||||||
|
if(progress.loaded === progress.total) {
|
||||||
|
self.isRoomAvatarLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Handle room picked avatar
|
||||||
|
*/
|
||||||
|
handleRoomPickedAvatar(event) {
|
||||||
|
if (event.target.files && event.target.files[0]) {
|
||||||
|
this.setRoomAvatar(event.target.files[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@
|
||||||
:value="room.roomId"
|
:value="room.roomId"
|
||||||
>
|
>
|
||||||
<v-list-item-avatar size="40" color="#e0e0e0">
|
<v-list-item-avatar size="40" color="#e0e0e0">
|
||||||
<v-img :src="room.avatar" />
|
<v-img v-if="room.avatar" :src="room.avatar" />
|
||||||
|
<span v-else class="white--text headline">{{
|
||||||
|
room.name.substring(0, 1).toUpperCase()
|
||||||
|
}}</span>
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title>{{ room.name }}</v-list-item-title>
|
<v-list-item-title>{{ room.name }}</v-list-item-title>
|
||||||
|
|
@ -50,7 +53,10 @@
|
||||||
:value="room.roomId"
|
:value="room.roomId"
|
||||||
>
|
>
|
||||||
<v-list-item-avatar size="40" color="#e0e0e0">
|
<v-list-item-avatar size="40" color="#e0e0e0">
|
||||||
<v-img :src="room.avatar" />
|
<v-img v-if="room.avatar" :src="room.avatar" />
|
||||||
|
<span v-else class="white--text headline">{{
|
||||||
|
room.name.substring(0, 1).toUpperCase()
|
||||||
|
}}</span>
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
<div class="room-list-notification-count" v-if="notificationCount(room) > 0">
|
<div class="room-list-notification-count" v-if="notificationCount(room) > 0">
|
||||||
{{ notificationCount(room) }}
|
{{ notificationCount(room) }}
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,7 @@ export default {
|
||||||
this.state = State.INITIAL;
|
this.state = State.INITIAL;
|
||||||
this.errorMessage = null;
|
this.errorMessage = null;
|
||||||
this.recordedFile = null;
|
this.recordedFile = null;
|
||||||
|
this.recordingTime = String.fromCharCode(160);
|
||||||
if (this.ptt) {
|
if (this.ptt) {
|
||||||
document.addEventListener("mouseup", this.mouseUp, false);
|
document.addEventListener("mouseup", this.mouseUp, false);
|
||||||
document.addEventListener("mousemove", this.mouseMove, false);
|
document.addEventListener("mousemove", this.mouseMove, false);
|
||||||
|
|
|
||||||
247
src/components/chatMixin.js
Normal file
247
src/components/chatMixin.js
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
import util from "../plugins/utils";
|
||||||
|
import MessageIncomingText from "./messages/MessageIncomingText";
|
||||||
|
import MessageIncomingFile from "./messages/MessageIncomingFile";
|
||||||
|
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
||||||
|
import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue";
|
||||||
|
import MessageIncomingVideo from "./messages/MessageIncomingVideo.vue";
|
||||||
|
import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue";
|
||||||
|
import MessageIncomingPoll from "./messages/MessageIncomingPoll.vue";
|
||||||
|
import MessageOutgoingText from "./messages/MessageOutgoingText";
|
||||||
|
import MessageOutgoingFile from "./messages/MessageOutgoingFile";
|
||||||
|
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
|
||||||
|
import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue";
|
||||||
|
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
|
||||||
|
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
|
||||||
|
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
|
||||||
|
import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport";
|
||||||
|
import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport";
|
||||||
|
import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport";
|
||||||
|
import MessageOutgoingImageExport from "./messages/export/MessageOutgoingImageExport";
|
||||||
|
import MessageOutgoingAudioExport from "./messages/export/MessageOutgoingAudioExport";
|
||||||
|
import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoExport";
|
||||||
|
import ContactJoin from "./messages/ContactJoin.vue";
|
||||||
|
import ContactLeave from "./messages/ContactLeave.vue";
|
||||||
|
import ContactInvited from "./messages/ContactInvited.vue";
|
||||||
|
import ContactChanged from "./messages/ContactChanged.vue";
|
||||||
|
import RoomCreated from "./messages/RoomCreated.vue";
|
||||||
|
import RoomAliased from "./messages/RoomAliased.vue";
|
||||||
|
import RoomNameChanged from "./messages/RoomNameChanged.vue";
|
||||||
|
import RoomTopicChanged from "./messages/RoomTopicChanged.vue";
|
||||||
|
import RoomAvatarChanged from "./messages/RoomAvatarChanged.vue";
|
||||||
|
import RoomHistoryVisibility from "./messages/RoomHistoryVisibility.vue";
|
||||||
|
import MessageOperations from "./messages/MessageOperations.vue";
|
||||||
|
import AvatarOperations from "./messages/AvatarOperations.vue";
|
||||||
|
import ChatHeader from "./ChatHeader";
|
||||||
|
import VoiceRecorder from "./VoiceRecorder";
|
||||||
|
import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
|
||||||
|
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
|
||||||
|
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
|
||||||
|
import stickers from "../plugins/stickers";
|
||||||
|
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
|
||||||
|
import BottomSheet from "./BottomSheet.vue";
|
||||||
|
import CreatePollDialog from "./CreatePollDialog.vue";
|
||||||
|
import RoomJoinRules from "./messages/RoomJoinRules.vue";
|
||||||
|
import RoomPowerLevelsChanged from "./messages/RoomPowerLevelsChanged.vue";
|
||||||
|
import RoomGuestAccessChanged from "./messages/RoomGuestAccessChanged.vue";
|
||||||
|
import RoomEncrypted from "./messages/RoomEncrypted.vue";
|
||||||
|
import RoomDeletionNotice from "./messages/RoomDeletionNotice.vue";
|
||||||
|
import DebugEvent from "./messages/DebugEvent.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ChatHeader,
|
||||||
|
MessageIncomingText,
|
||||||
|
MessageIncomingFile,
|
||||||
|
MessageIncomingImage,
|
||||||
|
MessageIncomingAudio,
|
||||||
|
MessageIncomingVideo,
|
||||||
|
MessageIncomingSticker,
|
||||||
|
MessageOutgoingText,
|
||||||
|
MessageOutgoingFile,
|
||||||
|
MessageOutgoingImage,
|
||||||
|
MessageOutgoingAudio,
|
||||||
|
MessageOutgoingVideo,
|
||||||
|
MessageOutgoingSticker,
|
||||||
|
MessageOutgoingPoll,
|
||||||
|
ContactJoin,
|
||||||
|
ContactLeave,
|
||||||
|
ContactInvited,
|
||||||
|
ContactChanged,
|
||||||
|
RoomCreated,
|
||||||
|
RoomAliased,
|
||||||
|
RoomNameChanged,
|
||||||
|
RoomTopicChanged,
|
||||||
|
RoomAvatarChanged,
|
||||||
|
RoomHistoryVisibility,
|
||||||
|
RoomJoinRules,
|
||||||
|
RoomPowerLevelsChanged,
|
||||||
|
RoomGuestAccessChanged,
|
||||||
|
RoomEncrypted,
|
||||||
|
RoomDeletionNotice,
|
||||||
|
DebugEvent,
|
||||||
|
MessageOperations,
|
||||||
|
VoiceRecorder,
|
||||||
|
RoomInfoBottomSheet,
|
||||||
|
CreatedRoomWelcomeHeader,
|
||||||
|
MessageOperationsBottomSheet,
|
||||||
|
StickerPickerBottomSheet,
|
||||||
|
BottomSheet,
|
||||||
|
AvatarOperations,
|
||||||
|
CreatePollDialog,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showDayMarkerBeforeEvent(event) {
|
||||||
|
const idx = this.events.indexOf(event);
|
||||||
|
if (idx <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const previousEvent = this.events[idx - 1];
|
||||||
|
return util.dayDiff(previousEvent.getTs(), event.getTs()) > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
dayForEvent(event) {
|
||||||
|
let dayDiff = util.dayDiffToday(event.getTs());
|
||||||
|
if (dayDiff < 7) {
|
||||||
|
return this.$tc("message.time_ago", dayDiff);
|
||||||
|
} else {
|
||||||
|
return util.formatDay(event.getTs());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
dateForEvent(event) {
|
||||||
|
return util.formatDay(event.getTs());
|
||||||
|
},
|
||||||
|
|
||||||
|
componentForEvent(event, isForExport = false) {
|
||||||
|
switch (event.getType()) {
|
||||||
|
case "m.room.member":
|
||||||
|
if (event.getContent().membership == "join") {
|
||||||
|
if (event.getPrevContent() && event.getPrevContent().membership == "join") {
|
||||||
|
// We we already joined, so this must be a display name and/or avatar update!
|
||||||
|
return ContactChanged;
|
||||||
|
} else {
|
||||||
|
return ContactJoin;
|
||||||
|
}
|
||||||
|
} else if (event.getContent().membership == "leave") {
|
||||||
|
return ContactLeave;
|
||||||
|
} else if (event.getContent().membership == "invite") {
|
||||||
|
return ContactInvited;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "m.room.message":
|
||||||
|
if (event.getSender() != this.$matrix.currentUserId) {
|
||||||
|
if (event.getContent().msgtype == "m.image") {
|
||||||
|
// For SVG, make downloadable
|
||||||
|
if (
|
||||||
|
event.getContent().info &&
|
||||||
|
event.getContent().info.mimetype &&
|
||||||
|
event.getContent().info.mimetype.startsWith("image/svg")
|
||||||
|
) {
|
||||||
|
return MessageIncomingFile;
|
||||||
|
}
|
||||||
|
if (isForExport) {
|
||||||
|
return MessageIncomingImageExport;
|
||||||
|
}
|
||||||
|
return MessageIncomingImage;
|
||||||
|
} else if (event.getContent().msgtype == "m.audio") {
|
||||||
|
if (isForExport) {
|
||||||
|
return MessageIncomingAudioExport;
|
||||||
|
}
|
||||||
|
return MessageIncomingAudio;
|
||||||
|
} else if (event.getContent().msgtype == "m.video") {
|
||||||
|
if (isForExport) {
|
||||||
|
return MessageIncomingVideoExport;
|
||||||
|
}
|
||||||
|
return MessageIncomingVideo;
|
||||||
|
} else if (event.getContent().msgtype == "m.file") {
|
||||||
|
return MessageIncomingFile;
|
||||||
|
} else if (stickers.isStickerShortcode(event.getContent().body)) {
|
||||||
|
return MessageIncomingSticker;
|
||||||
|
}
|
||||||
|
return MessageIncomingText;
|
||||||
|
} else {
|
||||||
|
if (event.getContent().msgtype == "m.image") {
|
||||||
|
// For SVG, make downloadable
|
||||||
|
if (
|
||||||
|
event.getContent().info &&
|
||||||
|
event.getContent().info.mimetype &&
|
||||||
|
event.getContent().info.mimetype.startsWith("image/svg")
|
||||||
|
) {
|
||||||
|
return MessageOutgoingImage;
|
||||||
|
}
|
||||||
|
if (isForExport) {
|
||||||
|
return MessageOutgoingImageExport;
|
||||||
|
}
|
||||||
|
return MessageOutgoingImage;
|
||||||
|
} else if (event.getContent().msgtype == "m.audio") {
|
||||||
|
if (isForExport) {
|
||||||
|
return MessageOutgoingAudioExport;
|
||||||
|
}
|
||||||
|
return MessageOutgoingAudio;
|
||||||
|
} else if (event.getContent().msgtype == "m.video") {
|
||||||
|
if (isForExport) {
|
||||||
|
return MessageOutgoingVideoExport;
|
||||||
|
}
|
||||||
|
return MessageOutgoingVideo;
|
||||||
|
} else if (event.getContent().msgtype == "m.file") {
|
||||||
|
return MessageOutgoingFile;
|
||||||
|
} else if (stickers.isStickerShortcode(event.getContent().body)) {
|
||||||
|
return MessageOutgoingSticker;
|
||||||
|
}
|
||||||
|
return MessageOutgoingText;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "m.room.create":
|
||||||
|
return RoomCreated;
|
||||||
|
|
||||||
|
case "m.room.canonical_alias":
|
||||||
|
return RoomAliased;
|
||||||
|
|
||||||
|
case "m.room.name":
|
||||||
|
return RoomNameChanged;
|
||||||
|
|
||||||
|
case "m.room.topic":
|
||||||
|
return RoomTopicChanged;
|
||||||
|
|
||||||
|
case "m.room.avatar":
|
||||||
|
return RoomAvatarChanged;
|
||||||
|
|
||||||
|
case "m.room.history_visibility":
|
||||||
|
return RoomHistoryVisibility;
|
||||||
|
|
||||||
|
case "m.room.join_rules":
|
||||||
|
return RoomJoinRules;
|
||||||
|
|
||||||
|
case "m.room.power_levels":
|
||||||
|
return RoomPowerLevelsChanged;
|
||||||
|
|
||||||
|
case "m.room.guest_access":
|
||||||
|
return RoomGuestAccessChanged;
|
||||||
|
|
||||||
|
case "m.room.encryption":
|
||||||
|
return RoomEncrypted;
|
||||||
|
|
||||||
|
case "m.poll.start":
|
||||||
|
case "org.matrix.msc3381.poll.start":
|
||||||
|
if (event.getSender() != this.$matrix.currentUserId) {
|
||||||
|
return MessageIncomingPoll;
|
||||||
|
} else {
|
||||||
|
return MessageOutgoingPoll;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "im.keanu.room_deletion_notice": {
|
||||||
|
// Custom event for notice 30 seconds before a room is deleted/purged.
|
||||||
|
const deletionNotices = this.room.currentState.getStateEvents("im.keanu.room_deletion_notice");
|
||||||
|
if (deletionNotices && deletionNotices.length > 0 && deletionNotices[deletionNotices.length - 1] == event) {
|
||||||
|
// This is the latest/last one. Look at the status flag. Show nothing if it is "cancel".
|
||||||
|
if (event.getContent().status != "cancel") {
|
||||||
|
return RoomDeletionNotice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.debugging ? DebugEvent : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
100
src/components/messages/MessageIncomingPoll.vue
Normal file
100
src/components/messages/MessageIncomingPoll.vue
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<div class="bubble poll-bubble">
|
||||||
|
<div class="poll-icon">
|
||||||
|
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M3.31462 16.4718C3.31462 16.9496 3.70026 17.3368 4.17609 17.3368L16.1385 17.3368C16.6144 17.3368 17 16.9496 17 16.4718L16.9998 13.6229C16.9998 13.1452 16.6142 12.7579 16.1383 12.7579L4.1764 12.7579C3.70056 12.7579 3.31492 13.1452 3.31492 13.6229L3.31512 16.4718L3.31462 16.4718Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.31462 10.4557C3.31462 10.9335 3.70026 11.3208 4.17609 11.3208L11.3428 11.3208C11.8186 11.3208 12.2043 10.9335 12.2043 10.4557L12.2043 7.60711C12.2043 7.12931 11.8186 6.74208 11.3428 6.74208L4.17609 6.74208C3.70026 6.74208 3.31462 7.12932 3.31462 7.60711L3.31462 10.4557Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.31451 1.59127L3.31451 4.44011C3.31451 4.91791 3.70016 5.30514 4.17598 5.30514L6.99509 5.30514C7.47093 5.30514 7.85657 4.91791 7.85657 4.44011L7.85637 1.59127C7.85637 1.11347 7.47073 0.726242 6.9949 0.726242L4.17599 0.726242C3.70035 0.726242 3.31452 1.11348 3.31452 1.59127L3.31451 1.59127Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M-2.00529e-05 0.587841L-2.0791e-05 17.4747C-2.08052e-05 17.7995 0.262306 18.0625 0.585404 18.0625L1.38198 18.0625C1.70528 18.0625 1.96741 17.7995 1.96741 17.4747L1.96741 0.587841C1.96741 0.263208 1.70508 -1.14667e-08 1.38198 -2.55897e-08L0.585405 -6.04092e-08C0.261911 -7.45496e-08 -2.00387e-05 0.263213 -2.00529e-05 0.587841Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="poll-question">{{ pollQuestion }}</div>
|
||||||
|
<v-container fluid ma-0 pa-0>
|
||||||
|
<v-row
|
||||||
|
v-for="answer in pollAnswers"
|
||||||
|
:key="answer.id"
|
||||||
|
@click="pollAnswer(answer.id)"
|
||||||
|
:class="{
|
||||||
|
'poll-answer': true,
|
||||||
|
selected: !userHasVoted && answer.id == pollTentativeAnswer,
|
||||||
|
result: userHasVoted || pollIsClosed,
|
||||||
|
winner: answer.winner,
|
||||||
|
}"
|
||||||
|
ma-0
|
||||||
|
pa-0
|
||||||
|
>
|
||||||
|
<v-col cols="auto" class="ma-0 pa-0 poll-answer-title">{{ answer.text }} {{ answer.max }}</v-col>
|
||||||
|
<v-col v-if="answer.id == pollTentativeAnswer" cols="auto" class="ma-0 pa-0 poll-answer-title"
|
||||||
|
><v-img class="poll-check-icon" src="@/assets/icons/ic_check.svg"
|
||||||
|
/></v-col>
|
||||||
|
<v-col
|
||||||
|
v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin"
|
||||||
|
cols="auto"
|
||||||
|
class="ma-0 pa-0 poll-answer-num-votes"
|
||||||
|
>{{ answer.percentage }}%</v-col
|
||||||
|
>
|
||||||
|
<div v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin" class="poll-percent-indicator">
|
||||||
|
<div class="bar" :style="{ width: `${answer.percentage}%` }"></div>
|
||||||
|
</div>
|
||||||
|
</v-row>
|
||||||
|
<v-row class="poll-status">
|
||||||
|
<v-col cols="auto" class="ma-0 pa-0 poll-status-title">
|
||||||
|
{{ $t("poll_create.num_answered", { count: pollNumAnswers }) }}
|
||||||
|
</v-col>
|
||||||
|
<!-- <v-col cols="auto" class="ma-0 pa-0 poll-status-title">{{
|
||||||
|
pollIsClosed
|
||||||
|
? $t("poll_create.poll_status_closed")
|
||||||
|
: pollIsDisclosed
|
||||||
|
? userHasVoted
|
||||||
|
? $t("poll_create.poll_status_open")
|
||||||
|
: $t("poll_create.poll_status_open_not_voted")
|
||||||
|
: $t("poll_create.poll_status_disclosed")
|
||||||
|
}}</v-col> -->
|
||||||
|
<v-col
|
||||||
|
cols="auto"
|
||||||
|
class="ma-0 pa-0 poll-status-close clickable"
|
||||||
|
v-if="!pollIsClosed && userCanClosePoll"
|
||||||
|
@click.stop="pollClose"
|
||||||
|
>{{ $t("poll_create.close_poll") }}</v-col
|
||||||
|
>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="pollTentativeAnswer" justify="center">
|
||||||
|
<v-col cols="auto" class="ma-0 pa-0 poll-submit">
|
||||||
|
<v-btn @click.stop="pollAnswerSubmit">
|
||||||
|
{{ $t("poll_create.poll_submit") }}
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</message-incoming>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import pollMixin from "./pollMixin";
|
||||||
|
import MessageIncoming from "./MessageIncoming.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: MessageIncoming,
|
||||||
|
mixins: [pollMixin],
|
||||||
|
components: { MessageIncoming },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
@import "@/assets/css/components/poll.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -1,16 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{'message-operations':true,'incoming':incoming,'outgoing':!incoming}">
|
<div :class="{'message-operations':true,'incoming':incoming,'outgoing':!incoming}">
|
||||||
<template v-for="(item,index) in emojis">
|
<template v-for="(item,index) in getEmojis">
|
||||||
<v-btn v-if="index < maxRecents" :key="item.data" id="btn-quick-reaction" icon @click.stop="addQuickReaction(item.data)" class="ma-0 pa-0" dense>
|
<v-btn v-if="index < maxRecents" :key="item.data" id="btn-quick-reaction" icon @click.stop="addQuickReaction(item.data)" class="ma-0 pa-0" dense>
|
||||||
<span class="recent-emoji">{{ item.data }}</span>
|
<span class="recent-emoji">{{ item.data }}</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-btn v-if="incoming" id="btn-reply" icon @click.stop="addReply" class="ma-0 pa-0" large>
|
<v-btn id="btn-more" icon @click.stop="more" class="ma-0 pa-0">
|
||||||
|
<v-icon small> $vuetify.icons.addReaction </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn v-if="incoming" id="btn-reply" icon @click.stop="addReply" class="ma-0 pa-0">
|
||||||
<v-icon>reply</v-icon>
|
<v-icon>reply</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn id="btn-more" icon @click.stop="more" class="ma-0 pa-0" large>
|
<v-btn id="btn-edit" icon @click.stop="edit" class="ma-0 pa-0" v-if="isEditable">
|
||||||
<v-icon>more_horiz</v-icon>
|
<v-icon small>edit</v-icon>
|
||||||
</v-btn> </div>
|
</v-btn>
|
||||||
|
<v-btn id="btn-redact" icon @click.stop="redact" class="ma-0 pa-0" v-if="isRedactable">
|
||||||
|
<v-icon small>delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn id="btn-download" icon @click.stop="download" class="ma-0 pa-0" v-if="isDownloadable">
|
||||||
|
<v-icon small>get_app</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -32,6 +42,11 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
getEmojis() {
|
||||||
|
return this.emojis.slice(0,2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
emojis: {
|
emojis: {
|
||||||
|
|
|
||||||
101
src/components/messages/MessageOutgoingPoll.vue
Normal file
101
src/components/messages/MessageOutgoingPoll.vue
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<div class="bubble poll-bubble">
|
||||||
|
<div class="poll-icon">
|
||||||
|
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M3.31462 16.4718C3.31462 16.9496 3.70026 17.3368 4.17609 17.3368L16.1385 17.3368C16.6144 17.3368 17 16.9496 17 16.4718L16.9998 13.6229C16.9998 13.1452 16.6142 12.7579 16.1383 12.7579L4.1764 12.7579C3.70056 12.7579 3.31492 13.1452 3.31492 13.6229L3.31512 16.4718L3.31462 16.4718Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.31462 10.4557C3.31462 10.9335 3.70026 11.3208 4.17609 11.3208L11.3428 11.3208C11.8186 11.3208 12.2043 10.9335 12.2043 10.4557L12.2043 7.60711C12.2043 7.12931 11.8186 6.74208 11.3428 6.74208L4.17609 6.74208C3.70026 6.74208 3.31462 7.12932 3.31462 7.60711L3.31462 10.4557Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.31451 1.59127L3.31451 4.44011C3.31451 4.91791 3.70016 5.30514 4.17598 5.30514L6.99509 5.30514C7.47093 5.30514 7.85657 4.91791 7.85657 4.44011L7.85637 1.59127C7.85637 1.11347 7.47073 0.726242 6.9949 0.726242L4.17599 0.726242C3.70035 0.726242 3.31452 1.11348 3.31452 1.59127L3.31451 1.59127Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M-2.00529e-05 0.587841L-2.0791e-05 17.4747C-2.08052e-05 17.7995 0.262306 18.0625 0.585404 18.0625L1.38198 18.0625C1.70528 18.0625 1.96741 17.7995 1.96741 17.4747L1.96741 0.587841C1.96741 0.263208 1.70508 -1.14667e-08 1.38198 -2.55897e-08L0.585405 -6.04092e-08C0.261911 -7.45496e-08 -2.00387e-05 0.263213 -2.00529e-05 0.587841Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="poll-question">{{ pollQuestion }}</div>
|
||||||
|
<v-container fluid ma-0 pa-0>
|
||||||
|
<v-row
|
||||||
|
v-for="answer in pollAnswers"
|
||||||
|
:key="answer.id"
|
||||||
|
@click="pollAnswer(answer.id)"
|
||||||
|
:class="{
|
||||||
|
'poll-answer': true,
|
||||||
|
selected: !userHasVoted && answer.id == pollTentativeAnswer,
|
||||||
|
result: userHasVoted || pollIsClosed,
|
||||||
|
winner: answer.winner,
|
||||||
|
}"
|
||||||
|
ma-0
|
||||||
|
pa-0
|
||||||
|
>
|
||||||
|
<v-col cols="auto" class="ma-0 pa-0 poll-answer-title">{{ answer.text }} {{ answer.max }}</v-col>
|
||||||
|
<v-col v-if="answer.id == pollTentativeAnswer" cols="auto" class="ma-0 pa-0 poll-answer-title"
|
||||||
|
><v-img class="poll-check-icon" src="@/assets/icons/ic_check.svg"
|
||||||
|
/></v-col>
|
||||||
|
<v-col
|
||||||
|
v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin"
|
||||||
|
cols="auto"
|
||||||
|
class="ma-0 pa-0 poll-answer-num-votes"
|
||||||
|
>{{ answer.percentage }}%</v-col
|
||||||
|
>
|
||||||
|
<div v-if="pollIsClosed || (pollIsDisclosed && userHasVoted) || pollIsAdmin" class="poll-percent-indicator">
|
||||||
|
<div class="bar" :style="{ width: `${answer.percentage}%` }"></div>
|
||||||
|
</div>
|
||||||
|
</v-row>
|
||||||
|
<v-row class="poll-status">
|
||||||
|
<v-col cols="auto" class="ma-0 pa-0 poll-status-title">
|
||||||
|
{{ $t("poll_create.num_answered", { count: pollNumAnswers }) }}
|
||||||
|
</v-col>
|
||||||
|
<!-- <v-col cols="auto" class="ma-0 pa-0 poll-status-title">{{
|
||||||
|
pollIsClosed
|
||||||
|
? $t("poll_create.poll_status_closed")
|
||||||
|
: pollIsDisclosed
|
||||||
|
? userHasVoted
|
||||||
|
? $t("poll_create.poll_status_open")
|
||||||
|
: $t("poll_create.poll_status_open_not_voted")
|
||||||
|
: $t("poll_create.poll_status_disclosed")
|
||||||
|
}}</v-col> -->
|
||||||
|
<v-col
|
||||||
|
cols="auto"
|
||||||
|
class="ma-0 pa-0 poll-status-close clickable"
|
||||||
|
v-if="!pollIsClosed && userCanClosePoll"
|
||||||
|
@click.stop="pollClose"
|
||||||
|
>{{ $t("poll_create.close_poll") }}</v-col
|
||||||
|
>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="pollTentativeAnswer" justify="center">
|
||||||
|
<v-col cols="auto" class="ma-0 pa-0 poll-submit">
|
||||||
|
<v-btn @click.stop="pollAnswerSubmit">
|
||||||
|
{{ $t("poll_create.poll_submit") }}
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</message-outgoing>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import pollMixin from "./pollMixin";
|
||||||
|
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: MessageOutgoing,
|
||||||
|
mixins: [pollMixin],
|
||||||
|
components: { MessageOutgoing },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
@import "@/assets/css/components/poll.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<audio controls :src="src">{{ $t("fallbacks.audio_file") }}</audio>
|
||||||
|
</message-incoming>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import attachmentMixin from "../attachmentMixin";
|
||||||
|
import MessageIncoming from "../MessageIncoming.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageIncomingAudioExport",
|
||||||
|
extends: MessageIncoming,
|
||||||
|
mixins: [attachmentMixin],
|
||||||
|
components: { MessageIncoming },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<div class="bubble image-bubble">
|
||||||
|
<v-img :aspect-ratio="16 / 9" ref="image" :src="src" :cover="cover" :contain="contain" />
|
||||||
|
</div>
|
||||||
|
</message-incoming>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageIncoming from "../MessageIncoming.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageIncomingImageExport",
|
||||||
|
extends: MessageIncoming,
|
||||||
|
components: { MessageIncoming },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
src: null,
|
||||||
|
cover: true,
|
||||||
|
contain: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const info = this.event.getContent().info;
|
||||||
|
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
|
||||||
|
// be stickers and small emoji type things.
|
||||||
|
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
|
||||||
|
this.cover = true;
|
||||||
|
this.contain = false;
|
||||||
|
} else {
|
||||||
|
this.cover = false;
|
||||||
|
this.contain = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.src) {
|
||||||
|
const objectUrl = this.src;
|
||||||
|
this.src = null;
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<div class="bubble image-bubble">
|
||||||
|
<v-responsive :aspect-ratio="16 / 9" :src="src">
|
||||||
|
<video :src="src" controls class="w-100 h-100">
|
||||||
|
{{ $t("fallbacks.video_file") }}
|
||||||
|
</video>
|
||||||
|
</v-responsive>
|
||||||
|
</div>
|
||||||
|
</message-incoming>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import attachmentMixin from "../attachmentMixin";
|
||||||
|
import MessageIncoming from "../MessageIncoming.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageIncomingVideoExport",
|
||||||
|
extends: MessageIncoming,
|
||||||
|
components: { MessageIncoming },
|
||||||
|
mixins: [attachmentMixin],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<audio controls :src="src">{{ $t("fallbacks.audio_file") }}</audio>
|
||||||
|
</message-outgoing>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import attachmentMixin from "../attachmentMixin";
|
||||||
|
import MessageOutgoing from "../MessageOutgoing.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageOutgoingAudioExport",
|
||||||
|
extends: MessageOutgoing,
|
||||||
|
components: { MessageOutgoing },
|
||||||
|
mixins: [attachmentMixin],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<div class="bubble image-bubble">
|
||||||
|
<v-img :aspect-ratio="16 / 9" ref="image" :src="src" :cover="cover" :contain="contain" />
|
||||||
|
</div>
|
||||||
|
</message-outgoing>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageOutgoing from "../MessageOutgoing.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageOutgoingImageExport",
|
||||||
|
extends: MessageOutgoing,
|
||||||
|
components: { MessageOutgoing },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
src: null,
|
||||||
|
cover: true,
|
||||||
|
contain: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const info = this.event.getContent().info;
|
||||||
|
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
|
||||||
|
// be stickers and small emoji type things.
|
||||||
|
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
|
||||||
|
this.cover = true;
|
||||||
|
this.contain = false;
|
||||||
|
} else {
|
||||||
|
this.cover = false;
|
||||||
|
this.contain = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.src) {
|
||||||
|
const objectUrl = this.src;
|
||||||
|
this.src = null;
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<div class="bubble image-bubble">
|
||||||
|
<v-responsive :aspect-ratio="16 / 9" class="ma-0 pa-0">
|
||||||
|
<video :src="src" controls class="w-100 h-100">
|
||||||
|
{{ $t("fallbacks.video_file") }}
|
||||||
|
</video>
|
||||||
|
</v-responsive>
|
||||||
|
</div>
|
||||||
|
</message-outgoing>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import attachmentMixin from "../attachmentMixin";
|
||||||
|
import MessageOutgoing from "../MessageOutgoing.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageOutgoingVideoExport",
|
||||||
|
extends: MessageOutgoing,
|
||||||
|
components: { MessageOutgoing },
|
||||||
|
mixins: [attachmentMixin],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
235
src/components/messages/pollMixin.js
Normal file
235
src/components/messages/pollMixin.js
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
import util from "../../plugins/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pollQuestion: "",
|
||||||
|
pollAnswers: [],
|
||||||
|
pollTotalVotes: 0,
|
||||||
|
pollResponseRelations: null,
|
||||||
|
pollEndRelations: null,
|
||||||
|
pollEndTs: null,
|
||||||
|
pollIsDisclosed: true,
|
||||||
|
pollTentativeAnswer: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$matrix.on("Room.timeline", this.pollMixinOnEvent);
|
||||||
|
this.pollQuestion =
|
||||||
|
(this.event &&
|
||||||
|
this.event.getContent()["m.poll.start"] &&
|
||||||
|
this.event.getContent()["m.poll.start"]["question"]["body"]) ||
|
||||||
|
(this.event &&
|
||||||
|
this.event.getContent()["org.matrix.msc3381.poll.start"] &&
|
||||||
|
this.event.getContent()["org.matrix.msc3381.poll.start"]["question"]["body"]) ||
|
||||||
|
"";
|
||||||
|
this.updateAnswers();
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
this.$matrix.off("Room.timeline", this.pollMixinOnEvent);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.pollResponseRelations) {
|
||||||
|
this.pollResponseRelations.off("Relations.add", this.onAddRelation);
|
||||||
|
this.pollResponseRelations = null;
|
||||||
|
}
|
||||||
|
if (this.pollEndRelations) {
|
||||||
|
this.pollEndRelations.off("Relations.add", this.onAddRelation);
|
||||||
|
this.pollEndRelations = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateAnswers() {
|
||||||
|
let answers =
|
||||||
|
(this.event && this.event.getContent()["m.poll.start"] && this.event.getContent()["m.poll.start"]["answers"]) ||
|
||||||
|
(this.event &&
|
||||||
|
this.event.getContent()["org.matrix.msc3381.poll.start"] &&
|
||||||
|
this.event.getContent()["org.matrix.msc3381.poll.start"]["answers"]) ||
|
||||||
|
[];
|
||||||
|
var answerMap = {};
|
||||||
|
var answerArray = [];
|
||||||
|
answers.forEach((a) => {
|
||||||
|
let text = a["org.matrix.msc1767.text"];
|
||||||
|
let answer = { id: a.id, text: text, numVotes: 0, percentage: 0 };
|
||||||
|
answerMap[a.id] = answer;
|
||||||
|
answerArray.push(answer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kind of poll
|
||||||
|
this.pollIsDisclosed = false;
|
||||||
|
if (this.event) {
|
||||||
|
if (this.event.getContent()["m.poll.start"]) {
|
||||||
|
this.pollIssDisclosed = this.event.getContent()["m.poll.start"]["kind"] != "m.poll.undisclosed" || false;
|
||||||
|
} else if (this.event.getContent()["org.matrix.msc3381.poll.start"]) {
|
||||||
|
this.pollIssDisclosed =
|
||||||
|
this.event.getContent()["org.matrix.msc3381.poll.start"]["kind"] != "org.matrix.msc3381.poll.undisclosed" ||
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for poll end
|
||||||
|
this.pollEndRelations =
|
||||||
|
this.timelineSet.getRelationsForEvent(this.event.getId(), "m.reference", "m.poll.end") ||
|
||||||
|
this.timelineSet.getRelationsForEvent(this.event.getId(), "m.reference", "org.matrix.msc3381.poll.end");
|
||||||
|
if (this.pollEndRelations) {
|
||||||
|
const endMessages = this.pollEndRelations.getRelations() || [];
|
||||||
|
if (endMessages.length > 0) {
|
||||||
|
this.pollEndTs = endMessages[endMessages.length - 1].getTs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process votes
|
||||||
|
this.pollResponseRelations =
|
||||||
|
this.timelineSet.getRelationsForEvent(this.event.getId(), "m.reference", "m.poll.response") ||
|
||||||
|
this.timelineSet.getRelationsForEvent(this.event.getId(), "m.reference", "org.matrix.msc3381.poll.response");
|
||||||
|
var userVotes = {};
|
||||||
|
if (this.pollResponseRelations) {
|
||||||
|
const votes = this.pollResponseRelations.getRelations() || [];
|
||||||
|
for (const vote of votes) {
|
||||||
|
//const emoji = r.getRelation().key;
|
||||||
|
if (this.pollEndTs && vote.getTs() > this.pollEndTs) {
|
||||||
|
continue; // Invalid vote, after poll was closed.
|
||||||
|
}
|
||||||
|
const sender = vote.getSender();
|
||||||
|
const answersFromThisUser =
|
||||||
|
(vote.getContent()["m.poll.response"] && vote.getContent()["m.poll.response"]["answers"]) ||
|
||||||
|
(vote.getContent()["org.matrix.msc3381.poll.response"] &&
|
||||||
|
vote.getContent()["org.matrix.msc3381.poll.response"]["answers"]) ||
|
||||||
|
[];
|
||||||
|
if (answersFromThisUser.length == 0) {
|
||||||
|
delete userVotes[sender];
|
||||||
|
} else {
|
||||||
|
userVotes[sender] = answersFromThisUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var totalVotes = 0;
|
||||||
|
for (const [user, answersFromThisUser] of Object.entries(userVotes)) {
|
||||||
|
for (const a of answersFromThisUser) {
|
||||||
|
if (answerMap[a]) {
|
||||||
|
answerMap[a].numVotes += 1;
|
||||||
|
totalVotes += 1;
|
||||||
|
if (user == this.$matrix.currentUserId) {
|
||||||
|
answerMap[a].hasMyVote = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update percentages. For algorithm, see here: https://revs.runtime-revolution.com/getting-100-with-rounded-percentages-273ffa70252b
|
||||||
|
// (need to add up to 100%)
|
||||||
|
if (totalVotes > 0) {
|
||||||
|
let a = answerArray.map((a) => {
|
||||||
|
let votes = (100 * a.numVotes) / totalVotes;
|
||||||
|
return {
|
||||||
|
answer: a,
|
||||||
|
unrounded: votes,
|
||||||
|
floor: Math.floor(votes),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
a.sort((item1, item2) => {
|
||||||
|
Math.abs(item2.floor) - Math.abs(item1.floor);
|
||||||
|
});
|
||||||
|
var diff =
|
||||||
|
100 -
|
||||||
|
a.reduce((previousValue, currentValue) => {
|
||||||
|
return previousValue + currentValue.floor;
|
||||||
|
}, 0);
|
||||||
|
const maxVotes = Math.max(...a.map((item) => item.unrounded));
|
||||||
|
a.map((answer, index) => {
|
||||||
|
answer.answer.percentage = index < diff ? answer.floor + 1 : answer.floor;
|
||||||
|
answer.answer.winner = answer.unrounded == maxVotes;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.pollAnswers = answerArray;
|
||||||
|
this.pollTotalVotes = totalVotes;
|
||||||
|
},
|
||||||
|
pollAnswer(id) {
|
||||||
|
if (!this.userHasVoted) {
|
||||||
|
this.pollTentativeAnswer = id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pollAnswerSubmit() {
|
||||||
|
if (!this.pollTentativeAnswer || this.pollIsClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
util
|
||||||
|
.sendPollAnswer(this.$matrix.matrixClient, this.room.roomId, [this.pollTentativeAnswer], this.event)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("Failed to send:", err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.pollTentativeAnswer = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pollClose() {
|
||||||
|
util.closePoll(this.$matrix.matrixClient, this.room.roomId, this.event).catch((err) => {
|
||||||
|
console.log("Failed to send:", err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onAddRelation(ignoredevent) {
|
||||||
|
this.updateAnswers();
|
||||||
|
},
|
||||||
|
pollMixinOnEvent(event) {
|
||||||
|
if (event.getRoomId() !== this.room.roomId) {
|
||||||
|
return; // Not for this room
|
||||||
|
}
|
||||||
|
this.$matrix.matrixClient.decryptEventIfNeeded(event).then(() => {
|
||||||
|
if (event.getType().startsWith("m.poll.") || event.getType().startsWith("org.matrix.msc3381.poll.")) {
|
||||||
|
this.updateAnswers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pollIsClosed() {
|
||||||
|
return this.pollEndTs != null && this.pollEndTs !== undefined;
|
||||||
|
},
|
||||||
|
userCanClosePoll() {
|
||||||
|
return (
|
||||||
|
this.room &&
|
||||||
|
this.room.currentState &&
|
||||||
|
this.room.currentState.maySendRedactionForEvent(this.event, this.$matrix.currentUserId)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
userHasVoted() {
|
||||||
|
return this.pollAnswers.some((a) => a.hasMyVote);
|
||||||
|
},
|
||||||
|
pollNumAnswers() {
|
||||||
|
return this.pollTotalVotes;
|
||||||
|
},
|
||||||
|
pollIsAdmin() {
|
||||||
|
// Admins can view results of not-yet-closed undisclosed polls.
|
||||||
|
const me = this.room && this.room.getMember(this.$matrix.currentUserId);
|
||||||
|
let isAdmin =
|
||||||
|
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
|
||||||
|
return isAdmin;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
pollResponseRelations: {
|
||||||
|
handler(newValue, oldValue) {
|
||||||
|
if (oldValue) {
|
||||||
|
oldValue.off("Relations.add", this.onAddRelation);
|
||||||
|
}
|
||||||
|
if (newValue) {
|
||||||
|
newValue.on("Relations.add", this.onAddRelation);
|
||||||
|
}
|
||||||
|
this.updateAnswers();
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
pollEndRelations: {
|
||||||
|
handler(newValue, oldValue) {
|
||||||
|
if (oldValue) {
|
||||||
|
oldValue.off("Relations.add", this.onAddRelation);
|
||||||
|
}
|
||||||
|
if (newValue) {
|
||||||
|
newValue.on("Relations.add", this.onAddRelation);
|
||||||
|
}
|
||||||
|
this.updateAnswers();
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
12
src/components/rememberMeMixin.js
Normal file
12
src/components/rememberMeMixin.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rememberMe: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRememberMe(val) {
|
||||||
|
this.$store.commit("setUseLocalStorage", val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,7 +62,16 @@ export default {
|
||||||
return this.room.shouldEncryptForInvitedMembers()
|
return this.room.shouldEncryptForInvitedMembers()
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
userCanExportChat() {
|
||||||
|
// We say that if you can redact events, you are allowed to export chats.
|
||||||
|
const me = this.room && this.room.getMember(this.$matrix.currentUserId);
|
||||||
|
let isAdmin =
|
||||||
|
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
|
||||||
|
return isAdmin;
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
room: {
|
room: {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ var _browserCanRecordAudioF = function () {
|
||||||
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
||||||
|
|
||||||
class Util {
|
class Util {
|
||||||
getAttachment(matrixClient, event, progressCallback) {
|
getAttachment(matrixClient, event, progressCallback, asBlob = false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
if (content.url != null) {
|
if (content.url != null) {
|
||||||
|
|
@ -53,7 +53,11 @@ class Util {
|
||||||
return this.decryptIfNeeded(file, response);
|
return this.decryptIfNeeded(file, response);
|
||||||
})
|
})
|
||||||
.then(bytes => {
|
.then(bytes => {
|
||||||
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype })));
|
if (asBlob) {
|
||||||
|
resolve(new Blob([bytes.buffer], { type: file.mimetype }));
|
||||||
|
} else {
|
||||||
|
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype })));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log("Download error: ", err);
|
console.log("Download error: ", err);
|
||||||
|
|
@ -180,6 +184,51 @@ class Util {
|
||||||
return this.sendMessage(matrixClient, roomId, "m.reaction", content);
|
return this.sendMessage(matrixClient, roomId, "m.reaction", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createPoll(matrixClient, roomId, question, answers, isDisclosed) {
|
||||||
|
var idx = 0;
|
||||||
|
let answerData = answers.map(a => {
|
||||||
|
idx++;
|
||||||
|
return {id: "" + idx, 'org.matrix.msc1767.text': a.text }
|
||||||
|
});
|
||||||
|
const content = {
|
||||||
|
'org.matrix.msc3381.poll.start': {
|
||||||
|
question: {
|
||||||
|
'org.matrix.msc1767.text': question,
|
||||||
|
body: question
|
||||||
|
},
|
||||||
|
kind: isDisclosed ? "org.matrix.msc3381.poll.disclosed" : "org.matrix.msc3381.poll.undisclosed",
|
||||||
|
max_selections: 1,
|
||||||
|
answers: answerData
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.start", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
closePoll(matrixClient, roomId, event) {
|
||||||
|
const content = {
|
||||||
|
'm.relates_to': {
|
||||||
|
rel_type: 'm.reference',
|
||||||
|
event_id: event.getId()
|
||||||
|
},
|
||||||
|
'org.matrix.msc3381.poll.end': {
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.end", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPollAnswer(matrixClient, roomId, answers, event) {
|
||||||
|
const content = {
|
||||||
|
'm.relates_to': {
|
||||||
|
rel_type: 'm.reference',
|
||||||
|
event_id: event.getId()
|
||||||
|
},
|
||||||
|
'org.matrix.msc3381.poll.response': {
|
||||||
|
answers: answers
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.response", content);
|
||||||
|
}
|
||||||
|
|
||||||
sendMessage(matrixClient, roomId, eventType, content) {
|
sendMessage(matrixClient, roomId, eventType, content) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
matrixClient.sendEvent(roomId, eventType, content, undefined, undefined)
|
matrixClient.sendEvent(roomId, eventType, content, undefined, undefined)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import icUser from '@/assets/icons/user.vue';
|
||||||
import icPassword from '@/assets/icons/password.vue';
|
import icPassword from '@/assets/icons/password.vue';
|
||||||
import icEdit from '@/assets/icons/edit.vue';
|
import icEdit from '@/assets/icons/edit.vue';
|
||||||
import icGlobe from '@/assets/icons/globe.vue';
|
import icGlobe from '@/assets/icons/globe.vue';
|
||||||
|
import icAddReaction from '@/assets/icons/addReaction.vue';
|
||||||
|
|
||||||
Vue.use(Vuetify);
|
Vue.use(Vuetify);
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ export default new Vuetify({
|
||||||
component: icUser
|
component: icUser
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
component: icPassword
|
component: icPassword
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
component: icEdit
|
component: icEdit
|
||||||
|
|
@ -23,6 +24,9 @@ export default new Vuetify({
|
||||||
globe: {
|
globe: {
|
||||||
component: icGlobe
|
component: icGlobe
|
||||||
},
|
},
|
||||||
|
addReaction: {
|
||||||
|
component: icAddReaction
|
||||||
|
},
|
||||||
},
|
},
|
||||||
user: icUser
|
user: icUser
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,7 @@ export default {
|
||||||
|
|
||||||
addMatrixClientListeners(client) {
|
addMatrixClientListeners(client) {
|
||||||
if (client) {
|
if (client) {
|
||||||
|
client.setMaxListeners(100); // Increate max number of listeners.
|
||||||
client.on("event", this.onEvent);
|
client.on("event", this.onEvent);
|
||||||
client.on("Room", this.onRoom);
|
client.on("Room", this.onRoom);
|
||||||
client.on("Session.logged_out", this.onSessionLoggedOut);
|
client.on("Session.logged_out", this.onSessionLoggedOut);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue