Merge branch 'dev' into 'main'
Build 72 Merge from dev See merge request keanuapp/keanuapp-weblite!357
This commit is contained in:
commit
a5b12c5bff
31 changed files with 993 additions and 472 deletions
81
package-lock.json
generated
81
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "keanuapp-weblite",
|
||||
"version": "0.1.59",
|
||||
"version": "0.1.70",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "keanuapp-weblite",
|
||||
"version": "0.1.59",
|
||||
"version": "0.1.70",
|
||||
"dependencies": {
|
||||
"@guardianproject/proofmode": "^0.4.0",
|
||||
"@matrix-org/olm": "^3.2.12",
|
||||
|
|
@ -3073,12 +3073,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cipher-base": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
|
||||
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz",
|
||||
"integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
"inherits": "^2.0.4",
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/clean-insights-sdk": {
|
||||
|
|
@ -6535,15 +6539,23 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
|
||||
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
|
||||
"license": "(MIT AND BSD-3-Clause)",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
"inherits": "^2.0.4",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"to-buffer": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"sha.js": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
|
|
@ -7367,9 +7379,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
|
@ -7441,9 +7453,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite-plugin-static-copy": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.0.tgz",
|
||||
"integrity": "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.2.tgz",
|
||||
"integrity": "sha512-iwrrf+JupY4b9stBttRWzGHzZbeMjAHBhkrn67MNACXJVjEMRpCI10Q3AkxdBkl45IHaTfw/CNVevzQhP7yTwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -9914,12 +9926,12 @@
|
|||
"peer": true
|
||||
},
|
||||
"cipher-base": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
|
||||
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz",
|
||||
"integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
"inherits": "^2.0.4",
|
||||
"safe-buffer": "^5.2.1"
|
||||
}
|
||||
},
|
||||
"clean-insights-sdk": {
|
||||
|
|
@ -12358,12 +12370,13 @@
|
|||
"dev": true
|
||||
},
|
||||
"sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
|
||||
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
"inherits": "^2.0.4",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"to-buffer": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
|
|
@ -12921,9 +12934,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"requires": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
|
|
@ -12948,9 +12961,9 @@
|
|||
}
|
||||
},
|
||||
"vite-plugin-static-copy": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.0.tgz",
|
||||
"integrity": "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.2.tgz",
|
||||
"integrity": "sha512-iwrrf+JupY4b9stBttRWzGHzZbeMjAHBhkrn67MNACXJVjEMRpCI10Q3AkxdBkl45IHaTfw/CNVevzQhP7yTwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": "^3.5.3",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "keanuapp-weblite",
|
||||
"version": "0.1.68",
|
||||
"version": "0.1.72",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "keanuapp-weblite",
|
||||
"version": "0.1.67",
|
||||
"version": "0.1.71",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ body {
|
|||
|
||||
.icon-dropdown {
|
||||
margin: 0px 8px;
|
||||
color: var(--v-foreground-color);
|
||||
}
|
||||
|
||||
.notification-alert {
|
||||
|
|
@ -467,6 +468,10 @@ body {
|
|||
position: relative;
|
||||
max-width: 70%;
|
||||
|
||||
.bubble-inset {
|
||||
padding: 8px 8px;
|
||||
}
|
||||
|
||||
@media #{map.get($display-breakpoints, 'sm-and-down')} {
|
||||
min-height: $min-touch-target;
|
||||
}
|
||||
|
|
@ -561,6 +566,10 @@ body {
|
|||
position: relative;
|
||||
max-width: 70%;
|
||||
|
||||
.bubble-inset {
|
||||
padding: 8px 8px;
|
||||
}
|
||||
|
||||
@media #{map.get($display-breakpoints, 'sm-and-down')} {
|
||||
min-height: $min-touch-target;
|
||||
}
|
||||
|
|
|
|||
13
src/assets/css/contentcredentials.scss
Normal file
13
src/assets/css/contentcredentials.scss
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.cc-detail-info {
|
||||
color: #333333;
|
||||
background-color: #DAD9FC;
|
||||
padding: 16px;
|
||||
margin: 16px 0 16px 0;
|
||||
border-radius: 8px 8px 0 8px;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 125%;
|
||||
letter-spacing: 0.4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
@ -482,7 +482,8 @@ $hiliteColor: #4642f1;
|
|||
}
|
||||
|
||||
.send-attachments-info-popup {
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 24px 24px 0 0;
|
||||
|
||||
.done-button {
|
||||
padding: 14px 24px;
|
||||
|
|
@ -629,9 +630,29 @@ $hiliteColor: #4642f1;
|
|||
line-height: 125%;
|
||||
letter-spacing: 0.4px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
|
||||
.v-icon {
|
||||
padding: 9.33px;
|
||||
margin-right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.detail-row__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-row__title {
|
||||
color: #dad9fc;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 125%;
|
||||
letter-spacing: 0.4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,3 +74,16 @@
|
|||
text-decoration-skip-ink: none;
|
||||
color: rgba(0,0,0,0.60);
|
||||
}
|
||||
|
||||
.common-caption-small {
|
||||
font-family: "Inter";
|
||||
font-size: 12 * $chat-text-size;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
line-height: 125%;
|
||||
letter-spacing: 0.40 * $chat-text-size;
|
||||
text-align: left;
|
||||
text-underline-position: from-font;
|
||||
text-decoration-skip-ink: none;
|
||||
color: #545F71;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
<svg width="11" height="6" viewBox="0 0 11 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.5 3.21932L8.9822 0.289048C9.49482 -0.142463 10.2664 -0.0840681 10.7056 0.418955C11.1451 0.922246 11.0856 1.67975 10.5733 2.11099L6.29552 5.71092C5.83774 6.09636 5.16221 6.09636 4.70448 5.71092L0.42672 2.11099C-0.0856265 1.67975 -0.145103 0.92226 0.294406 0.418955C0.733642 -0.0840681 1.50517 -0.142463 2.0178 0.289048L5.5 3.21932Z"
|
||||
fill="black" />
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
</template>
|
||||
<style>
|
||||
path {
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
23
src/assets/icons/ic_intervention.vue
Normal file
23
src/assets/icons/ic_intervention.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.8" clip-path="url(#clip0_770_11697)">
|
||||
<g clip-path="url(#clip1_770_11697)">
|
||||
<path
|
||||
d="M1.5 3.9C1.5 3.05992 1.5 2.63988 1.66349 2.31901C1.8073 2.03677 2.03677 1.8073 2.31901 1.66349C2.63988 1.5 3.05992 1.5 3.9 1.5H8.1C8.94008 1.5 9.36012 1.5 9.68099 1.66349C9.96323 1.8073 10.1927 2.03677 10.3365 2.31901C10.5 2.63988 10.5 3.05992 10.5 3.9V6.75C10.5 7.44891 10.5 7.79837 10.3858 8.07403C10.2336 8.44157 9.94157 8.73358 9.57403 8.88582C9.29837 9 8.94891 9 8.25 9C8.00571 9 7.88357 9 7.77025 9.02675C7.61915 9.06242 7.47844 9.13278 7.35925 9.23225C7.26986 9.30685 7.19657 9.40457 7.05 9.6L6.32 10.5733C6.21144 10.7181 6.15716 10.7905 6.09062 10.8163C6.03233 10.839 5.96767 10.839 5.90938 10.8163C5.84284 10.7905 5.78856 10.7181 5.68 10.5733L4.95 9.6C4.80343 9.40457 4.73014 9.30685 4.64075 9.23225C4.52156 9.13278 4.38085 9.06242 4.22975 9.02675C4.11643 9 3.99429 9 3.75 9C3.05109 9 2.70163 9 2.42597 8.88582C2.05843 8.73358 1.76642 8.44157 1.61418 8.07403C1.5 7.79837 1.5 7.44891 1.5 6.75V3.9Z"
|
||||
stroke="#4642F1"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_770_11697">
|
||||
<rect width="12" height="12" fill="white" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_770_11697">
|
||||
<rect width="12" height="12" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
23
src/assets/icons/ic_intervention_check.vue
Normal file
23
src/assets/icons/ic_intervention_check.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_770_11705)">
|
||||
<g clip-path="url(#clip1_770_11705)">
|
||||
<path
|
||||
d="M4.5 5.5L5.5 6.5L7.75 4.25M4.95 9.6L5.68 10.5733C5.78856 10.7181 5.84284 10.7905 5.90938 10.8163C5.96767 10.839 6.03233 10.839 6.09062 10.8163C6.15716 10.7905 6.21144 10.7181 6.32 10.5733L7.05 9.6C7.19657 9.40457 7.26986 9.30685 7.35925 9.23225C7.47844 9.13278 7.61915 9.06242 7.77025 9.02675C7.88357 9 8.00571 9 8.25 9C8.94891 9 9.29837 9 9.57403 8.88582C9.94157 8.73358 10.2336 8.44157 10.3858 8.07403C10.5 7.79837 10.5 7.44891 10.5 6.75V3.9C10.5 3.05992 10.5 2.63988 10.3365 2.31901C10.1927 2.03677 9.96323 1.8073 9.68099 1.66349C9.36012 1.5 8.94008 1.5 8.1 1.5H3.9C3.05992 1.5 2.63988 1.5 2.31901 1.66349C2.03677 1.8073 1.8073 2.03677 1.66349 2.31901C1.5 2.63988 1.5 3.05992 1.5 3.9V6.75C1.5 7.44891 1.5 7.79837 1.61418 8.07403C1.76642 8.44157 2.05843 8.73358 2.42597 8.88582C2.70163 9 3.05109 9 3.75 9C3.99429 9 4.11643 9 4.22975 9.02675C4.38085 9.06242 4.52156 9.13278 4.64075 9.23225C4.73014 9.30685 4.80343 9.40457 4.95 9.6Z"
|
||||
stroke="#4642F1"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_770_11705">
|
||||
<rect width="12" height="12" fill="white" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_770_11705">
|
||||
<rect width="12" height="12" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -500,12 +500,23 @@
|
|||
"metadata_info_compressed": "Compressing the image automatically excludes its metadata.",
|
||||
"metadata_info_original": "Sharing the original automatically includes its metadata.",
|
||||
"exif_data": "Exif Data",
|
||||
"content_credentials": "Content Credentials",
|
||||
"content_credentials_info": "Source or history information is available for this media to be verified.",
|
||||
"learn_more": "Learn more",
|
||||
"ai_used": "Photo modified with AI",
|
||||
"screenshot": "Screenshot. ",
|
||||
"screenshot_taken_on": "Screenshot taken on {date}",
|
||||
"captured_with_camera": "Captured with a camera",
|
||||
"old_photo": "Photo older than 3 months"
|
||||
"captured_with_camera": "Captured with a real camera. ",
|
||||
"captured_screenshot": "Screenshot. ",
|
||||
"captured_screenshot_ago": "Screenshot captured {ago} ago. ",
|
||||
"generated_with_ai": "Generated with AI. ",
|
||||
"generated_with_ai_ago": "Generated with AI {ago} ago. ",
|
||||
"old_photo": "Photo older than 3 months. ",
|
||||
"cc_source": "Source",
|
||||
"cc_capture_timestamp": "Capture Timestamp",
|
||||
"cc_location": "Location"
|
||||
},
|
||||
"cc": {
|
||||
"content_credentials": "Content Credentials",
|
||||
"content_credentials_info": "Source or history information is available for this media to be verified.",
|
||||
"metadata-stripped": "Image has been compressed and stripped of metadata.\n\nWe think this image has additional file information. If you want to learn more, ask the sender to share the original."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -393,6 +393,7 @@ export default {
|
|||
break;
|
||||
case "image/gif":
|
||||
extension = ".gif";
|
||||
break;
|
||||
}
|
||||
|
||||
let fileName = event.getId() + extension;
|
||||
|
|
@ -407,6 +408,14 @@ export default {
|
|||
}
|
||||
} else if (mime.startsWith("audio/")) {
|
||||
var extension = ".webm";
|
||||
switch (mime) {
|
||||
case "audio/mpeg":
|
||||
extension = ".mp3";
|
||||
break;
|
||||
case "audio/x-m4a":
|
||||
extension = ".m4a";
|
||||
break;
|
||||
}
|
||||
let fileName = event.getId() + extension;
|
||||
audioFolder.file(fileName, blob); // TODO calc bytes
|
||||
let elements = comp.el.getElementsByTagName("audio");
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ export default {
|
|||
if (isForExport) {
|
||||
return MessageIncomingImageExport;
|
||||
}
|
||||
return MessageImage;
|
||||
return MessageThread;
|
||||
} else if (event.getContent().msgtype == "m.audio") {
|
||||
if (isForExport) {
|
||||
return MessageIncomingAudioExport;
|
||||
|
|
@ -208,7 +208,7 @@ export default {
|
|||
if (isForExport) {
|
||||
return MessageIncomingVideoExport;
|
||||
}
|
||||
return MessageVideo;
|
||||
return MessageThread;
|
||||
} else if (event.getContent().msgtype == "m.file") {
|
||||
if (isForExport) {
|
||||
return MessageIncomingFileExport;
|
||||
|
|
@ -245,7 +245,7 @@ export default {
|
|||
if (isForExport) {
|
||||
return MessageOutgoingImageExport;
|
||||
}
|
||||
return MessageImage;
|
||||
return MessageThread;
|
||||
} else if (event.getContent().msgtype == "m.audio") {
|
||||
if (isForExport) {
|
||||
return MessageOutgoingAudioExport;
|
||||
|
|
@ -255,7 +255,7 @@ export default {
|
|||
if (isForExport) {
|
||||
return MessageOutgoingVideoExport;
|
||||
}
|
||||
return MessageVideo;
|
||||
return MessageThread;
|
||||
} else if (event.getContent().msgtype == "m.file") {
|
||||
if (isForExport) {
|
||||
return MessageOutgoingFileExport;
|
||||
|
|
|
|||
|
|
@ -1,134 +1,98 @@
|
|||
<template>
|
||||
<div v-if="c2pa">
|
||||
<div v-if="props.flags">
|
||||
<div class="detail-title">
|
||||
{{ t("file_mode.content_credentials") }}
|
||||
{{ t("cc.content_credentials") }}
|
||||
<v-icon>$vuetify.icons.ic_cr</v-icon>
|
||||
</div>
|
||||
<div class="detail-subtitle">
|
||||
{{ t("file_mode.content_credentials_info") }}
|
||||
<!-- <a href="" target="_blank">{{ t("file_mode.learn_more") }}</a> -->
|
||||
</div>
|
||||
<div class="detail-row" v-if="screenCapture">
|
||||
<v-icon>$vuetify.icons.ic_media_screenshot</v-icon>{{ screenCapture }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="detail-row" v-if="dateCreated">
|
||||
<v-icon>$vuetify.icons.ic_exif_time</v-icon>{{ dateCreatedDisplay }}
|
||||
</div>
|
||||
<div class="detail-row" v-if="creator"><v-icon>$vuetify.icons.ic_media_device</v-icon>{{ creator }}</div>
|
||||
<div class="detail-row" v-if="valid && cameraCapture">
|
||||
<v-icon>$vuetify.icons.ic_media_camera</v-icon>{{ cameraCapture }}
|
||||
</div>
|
||||
</template>
|
||||
<div class="detail-row" v-if="ai || aiInferenceResult?.aiGenerated">
|
||||
<v-icon>$vuetify.icons.ic_cc_ai</v-icon>{{ t("file_mode.ai_used") }}
|
||||
</div>
|
||||
<div class="detail-row" v-if="showAgeWarning">
|
||||
<v-icon>$vuetify.icons.ic_media_flag</v-icon>{{ t("file_mode.old_photo") }}
|
||||
<div class="detail-subtitle" v-if="hasC2PA">
|
||||
{{ t("cc.content_credentials_info") }}
|
||||
</div>
|
||||
|
||||
<!-- {{ JSON.stringify(props.c2pa, undefined, 4) }} -->
|
||||
<div class="cc-detail-info" v-if="infoText !== undefined">
|
||||
{{ infoText }}
|
||||
</div>
|
||||
|
||||
<CCProperty
|
||||
v-if="props.flags?.device"
|
||||
icon="$vuetify.icons.ic_media_camera"
|
||||
:title="t('file_mode.cc_source')"
|
||||
:value="props.flags?.device"
|
||||
/>
|
||||
<CCProperty
|
||||
v-if="creationDate"
|
||||
icon="$vuetify.icons.ic_exif_time"
|
||||
:title="t('file_mode.cc_capture_timestamp')"
|
||||
:value="creationDate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
AIInferenceResult,
|
||||
C2PAActionsAssertion,
|
||||
C2PAData,
|
||||
C2PASourceTypeCompositeCapture,
|
||||
C2PASourceTypeCompositeWithTrainedAlgorithmicMedia,
|
||||
C2PASourceTypeComputationalCapture,
|
||||
C2PASourceTypeDigitalCapture,
|
||||
C2PASourceTypeScreenCapture,
|
||||
C2PASourceTypeTrainedAlgorithmicMedia,
|
||||
} from "../../models/proof";
|
||||
import { Proof, ProofHintFlags } from "../../models/proof";
|
||||
import { computed, ref, Ref, watch } from "vue";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import dayjs from "dayjs";
|
||||
import CCProperty from "./CCProperty.vue";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
c2pa?: C2PAData;
|
||||
aiInferenceResult?: AIInferenceResult;
|
||||
proof?: Proof;
|
||||
flags?: ProofHintFlags;
|
||||
}>();
|
||||
|
||||
//console.error("C2PA", JSON.stringify(props.c2pa, undefined, 4));
|
||||
const infoText: Ref<string | undefined> = ref(undefined);
|
||||
const creationDate: Ref<string | undefined> = ref(undefined);
|
||||
|
||||
const creator: Ref<string | undefined> = ref(undefined);
|
||||
const dateCreated: Ref<Dayjs | undefined> = ref(undefined);
|
||||
const screenCapture: Ref<string | undefined> = ref(undefined);
|
||||
const cameraCapture: Ref<string | undefined> = ref(undefined);
|
||||
const ai: Ref<boolean> = ref(false);
|
||||
const valid: Ref<boolean> = ref(false);
|
||||
const hasC2PA = computed(() => {
|
||||
return props.proof?.integrity?.c2pa !== undefined;
|
||||
});
|
||||
|
||||
watch(
|
||||
props,
|
||||
() => {
|
||||
creator.value = undefined;
|
||||
dateCreated.value = undefined;
|
||||
screenCapture.value = undefined;
|
||||
cameraCapture.value = undefined;
|
||||
ai.value = false;
|
||||
valid.value = false;
|
||||
infoText.value = undefined;
|
||||
creationDate.value = undefined;
|
||||
|
||||
try {
|
||||
const manifests = Object.values(props.c2pa?.manifest_info.manifests ?? {});
|
||||
for (const manifest of manifests) {
|
||||
for (const assertion of manifest.assertions) {
|
||||
if (assertion.label === "c2pa.actions") {
|
||||
const actions = (assertion.data as C2PAActionsAssertion)?.actions ?? [];
|
||||
const a = actions.find((a) => a.action === "c2pa.created");
|
||||
if (a) {
|
||||
creator.value = a.softwareAgent;
|
||||
dateCreated.value = dayjs(Date.parse(manifest.signature_info.time));
|
||||
if (a.digitalSourceType === C2PASourceTypeScreenCapture) {
|
||||
screenCapture.value = t("file_mode.screenshot_taken_on", { date: dateCreated.value });
|
||||
}
|
||||
if (
|
||||
a.digitalSourceType === C2PASourceTypeDigitalCapture ||
|
||||
a.digitalSourceType === C2PASourceTypeComputationalCapture ||
|
||||
a.digitalSourceType === C2PASourceTypeCompositeCapture
|
||||
) {
|
||||
cameraCapture.value = t("file_mode.captured_with_camera");
|
||||
}
|
||||
if (
|
||||
a.digitalSourceType === C2PASourceTypeTrainedAlgorithmicMedia ||
|
||||
a.digitalSourceType === C2PASourceTypeCompositeWithTrainedAlgorithmicMedia
|
||||
) {
|
||||
ai.value = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (props.flags) {
|
||||
let date = props.flags.creationDate ? dayjs(props.flags.creationDate) : undefined;
|
||||
if (date) {
|
||||
creationDate.value = date.format("lll");
|
||||
}
|
||||
|
||||
let result = "";
|
||||
if (props.flags.generator === "camera") {
|
||||
result += t("file_mode.captured_with_camera");
|
||||
} else if (props.flags.generator === "screenshot") {
|
||||
if (date) {
|
||||
result += t("file_mode.captured_screenshot_ago", { ago: date.fromNow(true) });
|
||||
} else {
|
||||
result += t("file_mode.captured_screenshot");
|
||||
}
|
||||
} else if (props.flags.generator === "ai") {
|
||||
if (date) {
|
||||
result += t("file_mode.generated_with_ai_ago", { ago: date.fromNow(true) });
|
||||
} else {
|
||||
result += t("file_mode.generated_with_ai");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (date && dayjs().diff(date, "month") >= 3) {
|
||||
result += t("file_mode.old_photo");
|
||||
}
|
||||
|
||||
let results = props.c2pa?.manifest_info.validation_results?.activeManifest;
|
||||
if (results) {
|
||||
valid.value = results.failure.length == 0 && results.success.length > 0;
|
||||
infoText.value = result === "" ? undefined : result;
|
||||
}
|
||||
} catch (error) {}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const dateCreatedDisplay = computed(() => {
|
||||
if (dateCreated.value) {
|
||||
return dateCreated.value.format("lll");
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const showAgeWarning = computed(() => {
|
||||
if (dateCreated.value) {
|
||||
return dayjs().diff(dateCreated.value, "month") >= 3;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
@use "@/assets/css/contentcredentials.scss" as *;
|
||||
</style>
|
||||
|
|
|
|||
23
src/components/content-credentials/CCProperty.vue
Normal file
23
src/components/content-credentials/CCProperty.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div class="detail-row">
|
||||
<v-icon>{{ props.icon }}</v-icon>
|
||||
<div class="detail-row__text">
|
||||
<div class="detail-row__title">{{ props.title }}</div>
|
||||
<div>
|
||||
<slot name="default">{{ props.value }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
icon: string;
|
||||
title: string;
|
||||
value?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
</style>
|
||||
55
src/components/content-credentials/CCSummary.vue
Normal file
55
src/components/content-credentials/CCSummary.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<div class="cc-summary" v-if="flags.length > 0 && infoText.length > 0">
|
||||
<v-icon class="intervention-icon">{{
|
||||
showCheck ? "$vuetify.icons.ic_intervention_check" : "$vuetify.icons.ic_intervention"
|
||||
}}</v-icon
|
||||
><span class="common-caption-small" v-html="infoText" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ProofHintFlags } from "../../models/proof";
|
||||
|
||||
const { multiple, flags } = defineProps<{
|
||||
multiple: boolean;
|
||||
flags: ProofHintFlags[];
|
||||
}>();
|
||||
|
||||
const showCheck = computed(() => {
|
||||
if (!multiple && flags.length == 1) {
|
||||
return flags[0].generatorSource === "c2pa";
|
||||
} else if (multiple) {
|
||||
return flags.some((f) => f.generatorSource === "c2pa")
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const infoText = computed(() => {
|
||||
if (!multiple && flags.length == 1) {
|
||||
if (flags[0].generator === "ai") {
|
||||
return "<b>This image is generated by AI.</b> Take a closer look at the file details.";
|
||||
} else if (flags[0].generator === "screenshot") {
|
||||
return "<b>This is a screenshot.</b> Take a closer look at the file details.";
|
||||
}
|
||||
} else if (flags.some((f) => f.generator === "ai")) {
|
||||
return "<b>Contains AI generated media.</b> Take a closer look at the file details for each.";
|
||||
} else if (flags.some((f) => f.generator === "screenshot")) {
|
||||
return "<b>Contains screenshots.</b> Take a closer look at the file details for each.";
|
||||
}
|
||||
return "TODO - Content Credentials Info";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
|
||||
.cc-summary {
|
||||
text-align: start;
|
||||
.intervention-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,16 +3,13 @@
|
|||
<div class="detail-title">
|
||||
{{ t("file_mode.exif_data") }}
|
||||
</div>
|
||||
<div class="detail-row" v-if="dateTime">
|
||||
<v-icon>$vuetify.icons.ic_exif_time</v-icon>{{ dateTime }}
|
||||
</div>
|
||||
<div class="detail-row" v-if="location">
|
||||
<v-icon>$vuetify.icons.ic_exif_location</v-icon><a :href="locationLink">{{ location }}</a>
|
||||
</div>
|
||||
<div class="detail-row" v-if="makeAndModel">
|
||||
<v-icon>$vuetify.icons.ic_exif_device_camera</v-icon>{{ makeAndModel }}
|
||||
</div>
|
||||
<!-- Exif {{ JSON.stringify(props.exif, undefined, 4) }} -->
|
||||
<CCProperty v-if="makeAndModel" icon="$vuetify.icons.ic_exif_device_camera" :title="t('file_mode.cc_source')" :value="makeAndModel" />
|
||||
<CCProperty v-if="dateTime" icon="$vuetify.icons.ic_exif_time" :title="t('file_mode.cc_capture_timestamp')" :value="dateTime" />
|
||||
<CCProperty v-if="location" icon="$vuetify.icons.ic_exif_location" :title="t('file_mode.cc_location')">
|
||||
<template v-slot:default>
|
||||
<a :href="locationLink">{{ location }}</a>
|
||||
</template>
|
||||
</CCProperty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -20,6 +17,7 @@
|
|||
import { computed } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import CCProperty from "./CCProperty.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<C2PAInfo class="attachment-info__detail-box" v-if="hasC2PA" :c2pa="attachment.proof?.integrity?.c2pa" :ai-inference-result="attachment.proof?.ai?.inferenceResult" />
|
||||
<C2PAInfo class="attachment-info__detail-box" v-if="showC2PAInfo" :proof="attachment.proof" :flags="attachment.proofHintFlags" />
|
||||
<EXIFInfo class="attachment-info__detail-box" v-if="hasExif" :exif="attachment.proof?.integrity?.exif" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -49,8 +49,8 @@ const { attachment } = defineProps<{
|
|||
|
||||
//console.error("ATTACHMENT", attachment.proof);
|
||||
|
||||
const hasC2PA = computed(() => {
|
||||
return attachment.proof?.integrity?.c2pa !== undefined;
|
||||
const showC2PAInfo = computed(() => {
|
||||
return attachment.proof?.integrity?.c2pa !== undefined || attachment.proofHintFlags !== undefined;
|
||||
});
|
||||
|
||||
const hasExif = computed(() => {
|
||||
|
|
|
|||
71
src/components/file_mode/EventAttachmentInfo.vue
Normal file
71
src/components/file_mode/EventAttachmentInfo.vue
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div class="attachment-info">
|
||||
<div v-if="loadingProof">
|
||||
<div style="font-size: 0.7em; opacity: 0.7">
|
||||
<v-progress-circular indeterminate class="mb-0"></v-progress-circular>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="metaStripped" class="cc-detail-info white-space-pre">{{ t("cc.metadata-stripped") }}</div>
|
||||
<C2PAInfo class="attachment-info__detail-box" :proof="attachment?.proof" :flags="attachment?.proofHintFlags" />
|
||||
<EXIFInfo class="attachment-info__detail-box" v-if="hasExif" :exif="attachment?.proof?.integrity?.exif" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import C2PAInfo from "../content-credentials/C2PAInfo.vue";
|
||||
import EXIFInfo from "../content-credentials/EXIFInfo.vue";
|
||||
import { computed, onMounted, Ref, ref } from "vue";
|
||||
import { EventAttachment } from "../../models/eventAttachment";
|
||||
import proofmode from "../../plugins/proofmode";
|
||||
import { extractProofHintFlags } from "../../models/proof";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { attachment } = defineProps<{
|
||||
attachment?: EventAttachment;
|
||||
}>();
|
||||
|
||||
const loadingProof: Ref<boolean> = ref(false);
|
||||
const metaStripped: Ref<boolean> = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
if (attachment?.proofHintFlags && attachment.proof === undefined) {
|
||||
const a = attachment;
|
||||
loadingProof.value = true;
|
||||
metaStripped.value = true;
|
||||
a.loadSrc()
|
||||
.then((data) => {
|
||||
if (data && data.data) {
|
||||
return proofmode.proofCheckSource(data.data).then((res) => {
|
||||
a.proof = res;
|
||||
a.proofHintFlags = extractProofHintFlags(a.proof);
|
||||
metaStripped.value = a?.proof?.integrity?.c2pa === undefined && a?.proof?.integrity?.exif === undefined;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
loadingProof.value = false;
|
||||
});
|
||||
} else {
|
||||
metaStripped.value = attachment?.proof?.integrity?.c2pa === undefined && attachment?.proof?.integrity?.exif === undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const hasC2PA = computed(() => {
|
||||
return attachment?.proof?.integrity?.c2pa !== undefined;
|
||||
});
|
||||
|
||||
const hasExif = computed(() => {
|
||||
return attachment?.proof?.integrity?.exif !== undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
@use "@/assets/css/sendattachments.scss" as *;
|
||||
@use "@/assets/css/contentcredentials.scss" as *;
|
||||
</style>
|
||||
|
|
@ -1,108 +1,125 @@
|
|||
<template>
|
||||
<div class="fill-screen send-attachments">
|
||||
|
||||
<div class="chat-header">
|
||||
<v-container fluid class="d-flex justify-space-between align-center">
|
||||
<v-icon @click.stop="$emit('close')" color="white" class="clickable">arrow_back</v-icon>
|
||||
<div class="room-name no-upper">{{ displayDate }}</div>
|
||||
<v-icon @click.stop="showMoreMenu = true" color="white" class="clickable">more_vert</v-icon>
|
||||
<div>
|
||||
<v-icon @click.stop="showInfo = true" v-if="showInfoButton" color="white" class="clickable"
|
||||
>info_outline</v-icon
|
||||
>
|
||||
<v-icon @click.stop="showMoreMenu = true" color="white" class="clickable">more_vert</v-icon>
|
||||
</div>
|
||||
</v-container>
|
||||
</div>
|
||||
|
||||
<div class="gallery-current-item">
|
||||
<ThumbnailView :item="items[currentItemIndex]" />
|
||||
<ThumbnailView :item="currentAttachment" />
|
||||
<div class="download-button clickable" @click.stop="downloadOne">
|
||||
<v-icon color="black">arrow_downward</v-icon>
|
||||
<v-icon color="black">arrow_downward</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-thumbnail-container">
|
||||
<div :class="{ 'file-drop-thumbnail': true, 'clickable': true, 'current': id == currentItemIndex }"
|
||||
@click="currentItemIndex = id" v-for="(currentImageInput, id) in items" :key="id">
|
||||
<v-img v-if="currentImageInput" :src="currentImageInput.thumbnail ? currentImageInput.thumbnail : currentImageInput.src" />
|
||||
<div
|
||||
:class="{ 'file-drop-thumbnail': true, clickable: true, current: currentAttachment == attachment }"
|
||||
@click="currentAttachment = attachment"
|
||||
v-for="(attachment, index) in items"
|
||||
:key="index"
|
||||
>
|
||||
<v-img
|
||||
v-if="attachment"
|
||||
:src="attachment.thumbnail ? attachment.thumbnail : attachment.src"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MORE MENU POPUP -->
|
||||
<MoreMenuPopup :show="showMoreMenu" :menuItems="moreMenuItems" :showProfile="false" @close="showMoreMenu = false" />
|
||||
<v-bottom-sheet v-model="showInfo" theme="dark" height="80%">
|
||||
<v-card class="text-center send-attachments-info-popup">
|
||||
<v-card-title class="d-flex flex-column pa-0">
|
||||
<div class="align-self-end done-button clickable" @click="showInfo = false">{{ $t("menu.done") }}</div>
|
||||
<v-divider />
|
||||
</v-card-title>
|
||||
<v-card-title class="d-flex"> </v-card-title>
|
||||
<v-card-text>
|
||||
<EventAttachmentInfo :attachment="currentAttachment" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-bottom-sheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MoreMenuPopup from "../MoreMenuPopup";
|
||||
import messageMixin from "../messages/messageMixin";
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import MoreMenuPopup from "../MoreMenuPopup.vue";
|
||||
import util from "../../plugins/utils";
|
||||
import ThumbnailView from './ThumbnailView.vue';
|
||||
import ThumbnailView from "./ThumbnailView.vue";
|
||||
import { EventAttachment, KeanuEvent } from "../../models/eventAttachment";
|
||||
import { computed, inject, onBeforeUnmount, onMounted, Ref, ref, watch } from "vue";
|
||||
import EventAttachmentInfo from "./EventAttachmentInfo.vue";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
components: { MoreMenuPopup, ThumbnailView },
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
const { t } = useI18n();
|
||||
const $matrix: any = inject("globalMatrix");
|
||||
|
||||
const props = defineProps<{
|
||||
originalEvent: KeanuEvent;
|
||||
items: EventAttachment[];
|
||||
initialItem: EventAttachment | undefined;
|
||||
}>();
|
||||
|
||||
const currentAttachment: Ref<EventAttachment | undefined> = ref(undefined);
|
||||
const showMoreMenu: Ref<boolean> = ref(false);
|
||||
const showInfo: Ref<boolean> = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
document.body.classList.add("dark");
|
||||
currentAttachment.value = props.initialItem ? props.initialItem : props.items[0];
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.body.classList.remove("dark");
|
||||
});
|
||||
|
||||
const displayDate = computed(() => {
|
||||
return util.formatRecordStartTime(props.originalEvent.getTs());
|
||||
});
|
||||
|
||||
const showInfoButton = computed(() => {
|
||||
return currentAttachment.value?.proofHintFlags !== undefined;
|
||||
});
|
||||
|
||||
const moreMenuItems = computed(() => {
|
||||
let items = [];
|
||||
items.push({
|
||||
icon: "$vuetify.icons.ic_download",
|
||||
text: t("message.download_all"),
|
||||
handler: () => {
|
||||
downloadAll();
|
||||
},
|
||||
initialItem: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentItemIndex: 0,
|
||||
showMoreMenu: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.body.classList.add("dark");
|
||||
if (this.initialItem) {
|
||||
this.currentItemIndex = this.items.findIndex((v) => v === this.initialItem);
|
||||
if (this.currentItemIndex < 0) {
|
||||
this.currentItemIndex = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.body.classList.remove("dark");
|
||||
},
|
||||
computed: {
|
||||
displayDate() {
|
||||
return util.formatRecordStartTime(this.originalEvent.getTs())
|
||||
},
|
||||
moreMenuItems() {
|
||||
let items = [];
|
||||
items.push({
|
||||
icon: '$vuetify.icons.ic_download', text: this.$t("message.download_all"), handler: () => {
|
||||
this.downloadAll();
|
||||
}
|
||||
});
|
||||
return items;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
items(newValue, oldValue) {
|
||||
// Added or removed?
|
||||
if (newValue && oldValue && newValue.length > oldValue.length) {
|
||||
this.currentItemIndex = oldValue.length;
|
||||
} else if (newValue) {
|
||||
this.currentItemIndex = newValue.length - 1;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
downloadOne() {
|
||||
if (this.currentItemIndex >= 0 && this.currentItemIndex < this.items.length) {
|
||||
util.download(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, this.items[this.currentItemIndex].event);
|
||||
}
|
||||
},
|
||||
downloadAll() {
|
||||
this.items.forEach(item => util.download(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, item.event));
|
||||
}
|
||||
});
|
||||
return items;
|
||||
});
|
||||
|
||||
watch(props.items, (newValue: EventAttachment[], oldValue: EventAttachment[]) => {
|
||||
// Added or removed?
|
||||
if (newValue && oldValue && newValue.length > oldValue.length) {
|
||||
currentAttachment.value = newValue[oldValue.length];
|
||||
} else if (newValue && oldValue && newValue.length < oldValue.length) {
|
||||
currentAttachment.value = newValue[newValue.length - 1];
|
||||
}
|
||||
});
|
||||
|
||||
const downloadOne = () => {
|
||||
if (currentAttachment.value) {
|
||||
util.download($matrix.matrixClient, $matrix.useAuthedMedia, currentAttachment.value.event);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadAll = () => {
|
||||
props.items.forEach((item) => util.download($matrix.matrixClient, $matrix.useAuthedMedia, item.event));
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
@ -125,7 +142,7 @@ export default {
|
|||
bottom: 21px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: rgba(255,255,255,0.8);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 17px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -133,13 +150,13 @@ export default {
|
|||
}
|
||||
|
||||
.fill-screen {
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: black;
|
||||
z-index: 20 !important;
|
||||
justify-content: space-between !important;
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: black;
|
||||
z-index: 20 !important;
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const poster: Ref<string | undefined> = ref(undefined);
|
|||
const updateSource = () => {
|
||||
if (isEventAttachment(props.item)) {
|
||||
const eventAttachment = props.item;
|
||||
if (isVideo.value || eventAttachment.src) {
|
||||
if (eventAttachment.src) {
|
||||
source.value = eventAttachment.src;
|
||||
} else if (previewOnly) {
|
||||
eventAttachment.loadThumbnail().then((url) => {
|
||||
|
|
@ -74,6 +74,10 @@ const updateSource = () => {
|
|||
eventAttachment.loadSrc().then((url) => {
|
||||
source.value = url.data;
|
||||
})
|
||||
} else if (isVideo.value) {
|
||||
eventAttachment.loadSrc().then((url) => {
|
||||
source.value = url.data;
|
||||
})
|
||||
}
|
||||
} else if (isAttachment(props.item)) {
|
||||
const attachment = props.item;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
v-bind="{ ...$props, ...$attrs }"
|
||||
>
|
||||
<div class="bubble image-bubble" ref="imageRef">
|
||||
<div class="bubble-inset" v-if="attachment?.proofHintFlags">
|
||||
<CCSummary :multiple="false" :flags="attachment.proofHintFlags ? [attachment.proofHintFlags] : []" />
|
||||
</div>
|
||||
|
||||
<ImageWithProgress v-if="attachment"
|
||||
:aspect-ratio="16 / 9"
|
||||
ref="image"
|
||||
|
|
@ -32,6 +36,7 @@ import { MessageProps, useMessage } from "./useMessage";
|
|||
import { EventAttachment } from "../../../models/eventAttachment";
|
||||
import { useDisplay } from "vuetify";
|
||||
import Hammer from "hammerjs";
|
||||
import CCSummary from "../../content-credentials/CCSummary.vue";
|
||||
|
||||
const { t } = useI18n()
|
||||
const $matrix: any = inject('globalMatrix');
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
<template>
|
||||
<component
|
||||
:is="rootComponent"
|
||||
ref="root"
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
v-if="showMultiview"
|
||||
>
|
||||
<component :is="rootComponent" ref="root" v-bind="{ ...$props, ...$attrs }" v-if="showMultiview">
|
||||
<div class="bubble">
|
||||
<div class="original-message" v-if="inReplyToText">
|
||||
<div class="bubble-inset" v-if="showCCSummary">
|
||||
<CCSummary :multiple="items.length > 1" :flags="proofHintFlags" />
|
||||
</div>
|
||||
<div class="original-message bubble-inset" v-if="inReplyToText">
|
||||
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||
<div class="original-message-text" v-html="linkify($$sanitize(inReplyToText))" />
|
||||
</div>
|
||||
|
|
@ -18,24 +16,26 @@
|
|||
v-bind="$attrs"
|
||||
/>
|
||||
<v-container v-else-if="event && !event.isRedacted()" fluid class="imageCollection">
|
||||
<v-row wrap>
|
||||
<v-col v-for="{ size, item } in layoutedItems" :key="item.event.getId()" :cols="size">
|
||||
<v-row class="pa-0 ma-0" wrap>
|
||||
<v-col class="pa-0 ma-0" v-for="{ size, item } in layoutedItems" :key="item.event.getId()" :cols="size">
|
||||
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<i v-if="event && event.isRedacted()" class="deleted-text">
|
||||
<v-icon :color="isIncoming && senderIsAdminOrModerator(event) ? 'white' : ''" size="small">block</v-icon>
|
||||
{{
|
||||
redactedBySomeoneElse(event)
|
||||
? $t("message.incoming_message_deleted_text")
|
||||
: $t("message.outgoing_message_deleted_text")
|
||||
}}
|
||||
</i>
|
||||
<span v-html="linkify($$sanitize(messageText))" v-else-if="messageText" />
|
||||
<span class="edit-marker" v-if="event && event.replacingEventId() && !event.isRedacted()">
|
||||
{{ t("message.edited") }}
|
||||
</span>
|
||||
<div class="bubble-inset" v-if="showMessageText || event?.isRedacted() || event?.replacingEventId()">
|
||||
<i v-if="event && event.isRedacted()" class="deleted-text">
|
||||
<v-icon :color="isIncoming && senderIsAdminOrModerator(event) ? 'white' : ''" size="small">block</v-icon>
|
||||
{{
|
||||
redactedBySomeoneElse(event)
|
||||
? $t("message.incoming_message_deleted_text")
|
||||
: $t("message.outgoing_message_deleted_text")
|
||||
}}
|
||||
</i>
|
||||
<span v-html="linkify($$sanitize(messageText))" v-else-if="messageText" />
|
||||
<span class="edit-marker" v-if="event && event.replacingEventId() && !event.isRedacted()">
|
||||
{{ t("message.edited") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GalleryItemsView
|
||||
|
|
@ -62,20 +62,24 @@ import util, { ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE } from "@/plugins/utils";
|
|||
import GalleryItemsView from "../../file_mode/GalleryItemsView.vue";
|
||||
import ThumbnailView from "../../file_mode/ThumbnailView.vue";
|
||||
import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.vue";
|
||||
import CCSummary from "@/components/content-credentials/CCSummary.vue";
|
||||
import { computed, inject, onBeforeUnmount, ref, Ref, useTemplateRef, watch } from "vue";
|
||||
import { EventAttachment } from "../../../models/eventAttachment";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk";
|
||||
import { useLazyLoad } from "./useLazyLoad";
|
||||
import { ProofHintFlags } from "../../../models/proof";
|
||||
|
||||
const { t } = useI18n()
|
||||
const $matrix: any = inject('globalMatrix');
|
||||
const $$sanitize: any = inject('globalSanitize');
|
||||
const { t } = useI18n();
|
||||
const $matrix: any = inject("globalMatrix");
|
||||
const $$sanitize: any = inject("globalSanitize");
|
||||
|
||||
type RootType = InstanceType<typeof MessageOutgoing | typeof MessageIncoming>
|
||||
type RootType = InstanceType<typeof MessageOutgoing | typeof MessageIncoming>;
|
||||
const rootRef = useTemplateRef<RootType>("root");
|
||||
|
||||
const emits = defineEmits<{(event: "layout-change", value: {element: Element | undefined, action: () => void}): void}>();
|
||||
const emits = defineEmits<{
|
||||
(event: "layout-change", value: { element: Element | undefined; action: () => void }): void;
|
||||
}>();
|
||||
|
||||
const items: Ref<EventAttachment[]> = ref([]);
|
||||
const showItem: Ref<EventAttachment | undefined> = ref(undefined);
|
||||
|
|
@ -87,13 +91,11 @@ const { room } = props;
|
|||
const processThread = () => {
|
||||
if (!event.value?.isRedacted()) {
|
||||
const el = rootRef.value?.$el;
|
||||
emits("layout-change", {element: el, action: _processThread});
|
||||
emits("layout-change", { element: el, action: _processThread });
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
isVisible
|
||||
} = useLazyLoad({ root: rootRef });
|
||||
const { isVisible } = useLazyLoad({ root: rootRef });
|
||||
|
||||
const {
|
||||
event,
|
||||
|
|
@ -108,8 +110,8 @@ const {
|
|||
} = useMessage($matrix, t, props, undefined, processThread);
|
||||
|
||||
const rootComponent = computed(() => {
|
||||
return isIncoming.value ? MessageIncoming : MessageOutgoing;
|
||||
})
|
||||
return isIncoming.value ? MessageIncoming : MessageOutgoing;
|
||||
});
|
||||
|
||||
const onRelationsCreated = () => {
|
||||
if (event.value) {
|
||||
|
|
@ -122,31 +124,71 @@ const onRelationsCreated = () => {
|
|||
}
|
||||
};
|
||||
|
||||
watch(event, () => {
|
||||
if (event.value) {
|
||||
if (thread.value === undefined) {
|
||||
thread.value = props.timelineSet.relations.getChildEventsForEvent(
|
||||
event.value.getId() ?? "",
|
||||
util.threadMessageType(),
|
||||
"m.room.message"
|
||||
);
|
||||
watch(
|
||||
event,
|
||||
() => {
|
||||
if (event.value) {
|
||||
if (["m.image", "m.video"].includes(event.value.getContent().msgtype ?? "")) {
|
||||
// Single image mode
|
||||
items.value = [event.value].map((e: MatrixEvent) => {
|
||||
let ea = $matrix.attachmentManager.getEventAttachment(e);
|
||||
if (isVisible.value) {
|
||||
ea.loadThumbnail();
|
||||
}
|
||||
return ea;
|
||||
});
|
||||
} else if (thread.value === undefined) {
|
||||
thread.value = props.timelineSet.relations.getChildEventsForEvent(
|
||||
event.value.getId() ?? "",
|
||||
util.threadMessageType(),
|
||||
"m.room.message"
|
||||
);
|
||||
}
|
||||
if (!thread.value) {
|
||||
event.value.on(MatrixEventEvent.RelationsCreated, onRelationsCreated);
|
||||
}
|
||||
}
|
||||
if (!thread.value) {
|
||||
event.value.on(MatrixEventEvent.RelationsCreated, onRelationsCreated);
|
||||
}
|
||||
}
|
||||
}, { immediate: true});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
event.value?.off(MatrixEventEvent.RelationsCreated, onRelationsCreated);
|
||||
});
|
||||
|
||||
const showMessageText = computed((): boolean => {
|
||||
if (["m.image", "m.video"].includes(event.value?.getContent().msgtype ?? "")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const showMultiview = computed((): boolean => {
|
||||
return (isIncoming.value && props.room.displayType == ROOM_TYPE_FILE_MODE) ||
|
||||
items.value?.length > 1 ||
|
||||
(event.value && event.value.isRedacted()) ||
|
||||
(props.room.displayType == ROOM_TYPE_CHANNEL && items.value.length == 1 && util.isFileTypePDF(items.value[0].event)) ||
|
||||
if (["m.image", "m.video"].includes(event.value?.getContent().msgtype ?? "")) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
(isIncoming.value && props.room.displayType == ROOM_TYPE_FILE_MODE) ||
|
||||
items.value?.length > 1 ||
|
||||
(event.value && event.value.isRedacted()) ||
|
||||
(props.room.displayType == ROOM_TYPE_CHANNEL &&
|
||||
items.value.length == 1 &&
|
||||
util.isFileTypePDF(items.value[0].event)) ||
|
||||
messageText.value?.length > 0
|
||||
);
|
||||
});
|
||||
|
||||
const showCCSummary = computed(() => {
|
||||
return items.value?.some((i) => i.proofHintFlags !== undefined);
|
||||
});
|
||||
|
||||
const proofHintFlags = computed(() => {
|
||||
return items.value.reduce((res: ProofHintFlags[], item) => {
|
||||
if (item.proofHintFlags) {
|
||||
res.push(item.proofHintFlags);
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
});
|
||||
|
||||
watch(isVisible, (visible) => {
|
||||
|
|
@ -190,9 +232,9 @@ const layoutedItems = computed(() => {
|
|||
rows.push({ size: 3, item: array[6] });
|
||||
array = array.slice(7);
|
||||
} else if (array.length >= 3) {
|
||||
rows.push({ size: 6, item: array[0] });
|
||||
rows.push({ size: 12, item: array[0] });
|
||||
rows.push({ size: 6, item: array[1] });
|
||||
rows.push({ size: 12, item: array[2] });
|
||||
rows.push({ size: 6, item: array[2] });
|
||||
array = array.slice(3);
|
||||
} else if (array.length >= 2) {
|
||||
rows.push({ size: 6, item: array[0] });
|
||||
|
|
@ -205,7 +247,6 @@ const layoutedItems = computed(() => {
|
|||
}
|
||||
return rows;
|
||||
});
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
|
|
@ -214,20 +255,25 @@ const layoutedItems = computed(() => {
|
|||
<style lang="scss" scoped>
|
||||
.bubble {
|
||||
width: 100%;
|
||||
margin: 0 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.imageCollection {
|
||||
border-radius: 15px;
|
||||
padding: 0;
|
||||
width: unset;
|
||||
max-width: unset;
|
||||
margin: 0 0px !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
|
||||
.row {
|
||||
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
|
||||
padding: 0;
|
||||
.v-row {
|
||||
margin: -2px -2px 0 -2px !important;
|
||||
}
|
||||
|
||||
.col {
|
||||
padding: 2px;
|
||||
.v-col {
|
||||
padding: 2px !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
|
|
|
|||
|
|
@ -38,13 +38,6 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<GalleryItemsView
|
||||
:originalEvent="originalEvent"
|
||||
:items="items"
|
||||
:initialItem="showItem"
|
||||
v-if="!!showItem"
|
||||
v-on:close="showItem = undefined"
|
||||
/>
|
||||
</MessageOutgoing>
|
||||
</template>
|
||||
|
||||
|
|
@ -52,7 +45,6 @@
|
|||
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||
import { MessageProps, useMessage } from "./useMessage";
|
||||
import { ROOM_TYPE_CHANNEL } from "@/plugins/utils";
|
||||
import GalleryItemsView from "../../file_mode/GalleryItemsView.vue";
|
||||
import ThumbnailView from "../../file_mode/ThumbnailView.vue";
|
||||
import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.vue";
|
||||
import { inject, ref, Ref, unref, watch } from "vue";
|
||||
|
|
@ -65,7 +57,6 @@ const $$sanitize: any = inject("globalSanitize");
|
|||
|
||||
let items: Ref<Attachment[]> = ref([]);
|
||||
const layoutedItems: Ref<{ size: number; item: Attachment }[]> = ref([]);
|
||||
const showItem: Ref<Attachment | undefined> = ref(undefined);
|
||||
|
||||
const uploadBatch: Ref<AttachmentBatch | undefined> = ref(undefined);
|
||||
|
||||
|
|
@ -89,14 +80,10 @@ const cancelUpload = () => {
|
|||
|
||||
const retryUpload = () => {
|
||||
if (uploadBatch.value) {
|
||||
uploadBatch.value.send(uploadBatch.value.sendingRootMessage.value ?? "");
|
||||
uploadBatch.value.send(uploadBatch.value.sendingRootMessage?.value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
const onItemClick = (event: any) => {
|
||||
showItem.value = event.item;
|
||||
};
|
||||
|
||||
const layout = () => {
|
||||
if (!items.value || items.value.length == 0) {
|
||||
layoutedItems.value = [];
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
import proofmode from "../plugins/proofmode";
|
||||
import imageResize from "image-resize";
|
||||
import { computed, ref, Ref, shallowReactive, unref } from "vue";
|
||||
import utils, { THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT } from "@/plugins/utils";
|
||||
import utils, { THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT, CLIENT_EVENT_PROOF_HINT } from "@/plugins/utils";
|
||||
import { extractProofHintFlags } from "./proof";
|
||||
|
||||
export class AttachmentManager {
|
||||
|
|
@ -220,6 +220,8 @@ export class AttachmentManager {
|
|||
|
||||
const fileSize = this.getSrcFileSize(event);
|
||||
|
||||
let proofHintFlags = event.getContent()[CLIENT_EVENT_PROOF_HINT];
|
||||
|
||||
const attachment: EventAttachment = {
|
||||
event: event,
|
||||
name: this.getFileName(event),
|
||||
|
|
@ -227,6 +229,8 @@ export class AttachmentManager {
|
|||
srcProgress: -1,
|
||||
thumbnailProgress: -1,
|
||||
autoDownloadable: fileSize <= this.maxSizeAutoDownloads,
|
||||
proofHintFlags: proofHintFlags ? JSON.parse(proofHintFlags) : proofHintFlags,
|
||||
proof: undefined,
|
||||
loadSrc: () => Promise.reject("Not implemented"),
|
||||
loadThumbnail: () => Promise.reject("Not implemented"),
|
||||
loadBlob: () => Promise.reject("Not implemented"),
|
||||
|
|
@ -627,7 +631,8 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room
|
|||
},
|
||||
eventId,
|
||||
attachment.dimensions,
|
||||
attachment.thumbnail
|
||||
attachment.thumbnail,
|
||||
attachment.proofHintFlags
|
||||
)
|
||||
.then((mediaEventId: string) => {
|
||||
// Look at last item rotation, flipping the sign on this, so looks more like a true stack
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { MatrixEvent, Room } from "matrix-js-sdk";
|
||||
import { AttachmentBatch } from "./attachment";
|
||||
import { Proof, ProofHintFlags } from "./proof";
|
||||
|
||||
export type KeanuEventExtension = {
|
||||
isMxThread?: boolean;
|
||||
|
|
@ -26,6 +27,8 @@ export type EventAttachment = {
|
|||
thumbnailProgress: number;
|
||||
thumbnailPromise?: Promise<EventAttachmentUrlData>;
|
||||
autoDownloadable: boolean;
|
||||
proof?: Proof;
|
||||
proofHintFlags?: ProofHintFlags;
|
||||
loadSrc: () => Promise<EventAttachmentUrlData>;
|
||||
loadThumbnail: () => Promise<EventAttachmentUrlData>;
|
||||
loadBlob: () => Promise<{data: Blob}>;
|
||||
|
|
|
|||
|
|
@ -2,123 +2,314 @@ export type AIInferenceResult = {
|
|||
aiGenerated: boolean;
|
||||
aiProbability: number;
|
||||
humanProbability: number;
|
||||
}
|
||||
};
|
||||
|
||||
export const C2PASourceTypeScreenCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/screenCapture";
|
||||
export const C2PASourceTypeDigitalCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture";
|
||||
export const C2PASourceTypeComputationalCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture";
|
||||
export const C2PASourceTypeCompositeCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture";
|
||||
export const C2PASourceTypeTrainedAlgorithmicMedia = "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia";
|
||||
export const C2PASourceTypeCompositeWithTrainedAlgorithmicMedia = "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia";
|
||||
export const C2PASourceTypeTrainedAlgorithmicMedia =
|
||||
"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia";
|
||||
export const C2PASourceTypeCompositeWithTrainedAlgorithmicMedia =
|
||||
"http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia";
|
||||
|
||||
export type C2PAActionsAssertion = {
|
||||
actions: {
|
||||
action: string;
|
||||
softwareAgent?: string;
|
||||
digitalSourceType?: string;
|
||||
}[];
|
||||
}
|
||||
actions: {
|
||||
action: string;
|
||||
softwareAgent?: string;
|
||||
digitalSourceType?: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type C2PAAssertion = {
|
||||
label: string;
|
||||
data: C2PAActionsAssertion | undefined;
|
||||
}
|
||||
label: string;
|
||||
data: C2PAActionsAssertion | undefined;
|
||||
};
|
||||
|
||||
export type C2PAManifest = {
|
||||
assertions: C2PAAssertion[];
|
||||
signature_info: {
|
||||
time: string;
|
||||
}
|
||||
}
|
||||
assertions: C2PAAssertion[];
|
||||
signature_info: {
|
||||
time: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type C2PAValidationResults = {
|
||||
activeManifest?: {
|
||||
failure: any[];
|
||||
success: any[];
|
||||
informational: any[];
|
||||
}
|
||||
}
|
||||
activeManifest?: {
|
||||
failure: any[];
|
||||
success: any[];
|
||||
informational: any[];
|
||||
};
|
||||
};
|
||||
|
||||
export type C2PAManifestInfo = {
|
||||
active_manifest: string;
|
||||
manifests: {[key: string]: C2PAManifest};
|
||||
validation_results?: C2PAValidationResults;
|
||||
}
|
||||
active_manifest: string;
|
||||
manifests: { [key: string]: C2PAManifest };
|
||||
validation_results?: C2PAValidationResults;
|
||||
};
|
||||
|
||||
export type C2PAData = {
|
||||
manifest_info: C2PAManifestInfo;
|
||||
}
|
||||
manifest_info: C2PAManifestInfo;
|
||||
};
|
||||
|
||||
export type Proof = {
|
||||
data?: any;
|
||||
name?: string;
|
||||
json?: string;
|
||||
integrity?: { pgp?: any; c2pa?: C2PAData; exif?: {[key: string]: string | Object}; opentimestamps?: any };
|
||||
ai?: { inferenceResult?: AIInferenceResult};
|
||||
data?: any;
|
||||
name?: string;
|
||||
json?: string;
|
||||
integrity?: { pgp?: any; c2pa?: C2PAData; exif?: { [key: string]: string | Object }; opentimestamps?: any };
|
||||
ai?: { inferenceResult?: AIInferenceResult };
|
||||
};
|
||||
|
||||
export type ProofHintFlagsGenerator = "unknown" | "camera" | "screenshot" | "ai";
|
||||
export type ProofHintFlagsGeneratorSource = "c2pa" | "exif" | "metadata";
|
||||
export type ProofHintFlagsEditor = "unknown" | "manual" | "ai";
|
||||
|
||||
export type ProofHintFlagsEdit = {
|
||||
editor: ProofHintFlagsEditor;
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
export type ProofHintFlags = {
|
||||
aiGenerated?: boolean;
|
||||
aiEdited?: boolean;
|
||||
screenshot?: boolean;
|
||||
camera?: boolean;
|
||||
}
|
||||
device?: string;
|
||||
creationDate?: Date;
|
||||
generator?: ProofHintFlagsGenerator;
|
||||
generatorSource?: ProofHintFlagsGeneratorSource;
|
||||
edits?: ProofHintFlagsEdit[];
|
||||
};
|
||||
|
||||
export const extractProofHintFlags = (proof?: Proof): (ProofHintFlags | undefined) => {
|
||||
if (!proof) return undefined;
|
||||
type FlagMatchRule = {
|
||||
field: string;
|
||||
match: string[];
|
||||
description: string;
|
||||
};
|
||||
|
||||
let screenshot = false;
|
||||
let camera = false;
|
||||
let aiGenerated = false;
|
||||
let aiEdited = false;
|
||||
let valid = false;
|
||||
type FlagMatchRuleValue = {
|
||||
path: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type FlagMatchInfo = {
|
||||
field: string;
|
||||
value: string;
|
||||
re: string;
|
||||
};
|
||||
|
||||
const ruleScreenshotC2PA = (): FlagMatchRule[] => {
|
||||
return [
|
||||
{
|
||||
field:
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/digitalSourceType",
|
||||
match: [C2PASourceTypeScreenCapture],
|
||||
description: "Screen capture",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const ruleScreenshotMeta = (): FlagMatchRule[] => {
|
||||
return [
|
||||
{
|
||||
field:
|
||||
"name",
|
||||
match: ["screenshot"],
|
||||
description: "Screen capture",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const ruleCamera = (): FlagMatchRule[] => {
|
||||
return [
|
||||
{
|
||||
field:
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/digitalSourceType",
|
||||
match: [C2PASourceTypeDigitalCapture, C2PASourceTypeComputationalCapture, C2PASourceTypeCompositeCapture],
|
||||
description: "Captured by camera",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const ruleAiGenerated = (): FlagMatchRule[] => {
|
||||
return [
|
||||
{
|
||||
field:
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/digitalSourceType",
|
||||
match: [C2PASourceTypeTrainedAlgorithmicMedia, C2PASourceTypeCompositeWithTrainedAlgorithmicMedia],
|
||||
description: "Generated by AI",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const ruleAiMeta = (): FlagMatchRule[] => {
|
||||
const knownAIServices = [
|
||||
"ChatGPT",
|
||||
"OpenAI-API",
|
||||
"Adobe Firefly",
|
||||
"RunwayML",
|
||||
"Runway AI",
|
||||
"Google AI",
|
||||
"Stable Diffusion",
|
||||
];
|
||||
return [
|
||||
{ field: "name", match: ["^DALL_E_", "^Gen-3"], description: "File name" },
|
||||
{
|
||||
field: "integrity/c2pa/manifest_info/manifests[]/claim_generator",
|
||||
match: knownAIServices,
|
||||
description: "C2PA claim generator",
|
||||
},
|
||||
{ field: "iptc/Credit", match: knownAIServices, description: "IPTC Credit" },
|
||||
{ field: "iptc/Provider", match: knownAIServices, description: "IPTC Provider" },
|
||||
{ field: "iptc/ImageSupplier[]", match: knownAIServices, description: "IPTC ImageSupplier" },
|
||||
{ field: "iptc/ImageCreator[]", match: knownAIServices, description: "IPTC ImageCreator" },
|
||||
];
|
||||
};
|
||||
|
||||
const matchFlag = (rules: FlagMatchRule[], file: any) => {
|
||||
let result = false;
|
||||
let resultInfo: FlagMatchInfo[] = [];
|
||||
for (let rule of rules) {
|
||||
try {
|
||||
let results = proof.integrity?.c2pa?.manifest_info.validation_results?.activeManifest;
|
||||
if (results) {
|
||||
valid = results.failure.length == 0 && results.success.length > 0;
|
||||
const re: RegExp[] = (!Array.isArray(rule.match) ? [rule.match] : rule.match).map((m) => new RegExp(m, "gi"));
|
||||
const values = extractFlagValues(rule.field, file);
|
||||
values.forEach((v) => {
|
||||
re.forEach((r) => {
|
||||
if (r.test(v.value)) {
|
||||
result = true;
|
||||
resultInfo.push({ field: v.path, value: v.value, re: r.source });
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Invalid RE", e);
|
||||
}
|
||||
}
|
||||
return { result: result, matches: resultInfo };
|
||||
};
|
||||
|
||||
const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] => {
|
||||
const getValues = (
|
||||
path: string[],
|
||||
objectPath: any[],
|
||||
actualPath: string,
|
||||
o: any
|
||||
): FlagMatchRuleValue[] | undefined => {
|
||||
if (path.length == 0 || o == undefined) return undefined;
|
||||
let part = path[0];
|
||||
const lastBracket = part.lastIndexOf("[");
|
||||
if (part === "..") {
|
||||
return getValues(path.slice(1), objectPath.slice(1), actualPath + "/..", objectPath[0]);
|
||||
}
|
||||
if (part.endsWith("]") && lastBracket > 0) {
|
||||
const optionalConstraint = part.substring(lastBracket + 1, part.length - 1);
|
||||
part = part.substring(0, lastBracket);
|
||||
if (o[part] != undefined) {
|
||||
let opart: any[] = o[part];
|
||||
if (!Array.isArray(opart)) {
|
||||
opart = Object.values(opart) ?? [];
|
||||
}
|
||||
|
||||
const manifests = Object.values(proof.integrity?.c2pa?.manifest_info.manifests ?? {});
|
||||
for (const manifest of manifests) {
|
||||
for (const assertion of manifest.assertions) {
|
||||
if (assertion.label === "c2pa.actions") {
|
||||
const actions = (assertion.data as C2PAActionsAssertion)?.actions ?? [];
|
||||
const a = actions.find((a) => a.action === "c2pa.created");
|
||||
if (a) {
|
||||
// creator.value = a.softwareAgent;
|
||||
// dateCreated.value = dayjs(Date.parse(manifest.signature_info.time));
|
||||
if (a.digitalSourceType === C2PASourceTypeScreenCapture) {
|
||||
screenshot = true;
|
||||
}
|
||||
if (
|
||||
a.digitalSourceType === C2PASourceTypeDigitalCapture ||
|
||||
a.digitalSourceType === C2PASourceTypeComputationalCapture ||
|
||||
a.digitalSourceType === C2PASourceTypeCompositeCapture
|
||||
) {
|
||||
camera = true;
|
||||
}
|
||||
if (
|
||||
a.digitalSourceType === C2PASourceTypeTrainedAlgorithmicMedia ||
|
||||
a.digitalSourceType === C2PASourceTypeCompositeWithTrainedAlgorithmicMedia
|
||||
) {
|
||||
aiGenerated = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Any constraints controlling what array object(s) to consider?
|
||||
if (optionalConstraint) {
|
||||
const [prop, val] = optionalConstraint.split("=");
|
||||
opart = opart.filter((item) => item[prop] === val);
|
||||
}
|
||||
|
||||
if (path.length == 1) {
|
||||
let strings = opart as string[];
|
||||
return strings.map((s, i) => ({ path: actualPath + "/" + part + "[" + i + "]", value: s }));
|
||||
} else {
|
||||
return opart.reduce((res: FlagMatchRuleValue[] | undefined, o: any, i: number) => {
|
||||
const newObjectPaths = [o, ...objectPath];
|
||||
let matches = getValues(path.slice(1), newObjectPaths, actualPath + "/" + part + "[" + i + "]", o);
|
||||
if (matches) {
|
||||
const r2 = res || [];
|
||||
r2.push(...matches);
|
||||
return r2;
|
||||
}
|
||||
return res;
|
||||
}, undefined);
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
if (valid) {
|
||||
const flags: ProofHintFlags = {
|
||||
aiGenerated,
|
||||
aiEdited,
|
||||
screenshot,
|
||||
camera
|
||||
};
|
||||
return flags;
|
||||
} else {
|
||||
if (o[part] != undefined) {
|
||||
if (path.length == 1) {
|
||||
return [{ path: actualPath + "/" + part, value: o[part] }];
|
||||
} else {
|
||||
const newObjectPaths = [o, ...objectPath];
|
||||
return getValues(path.slice(1), newObjectPaths, actualPath + "/" + part, o[part]);
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
};
|
||||
|
||||
let result: FlagMatchRuleValue[] = [];
|
||||
try {
|
||||
let parts = flagPath.split("/");
|
||||
result = getValues(parts, [], "", file) ?? [];
|
||||
} catch (e) {
|
||||
console.error("Invalid RE", e);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => {
|
||||
if (!proof) return undefined;
|
||||
|
||||
let edits: ProofHintFlagsEdit[] | undefined = undefined;
|
||||
let valid = false;
|
||||
|
||||
try {
|
||||
let results = proof.integrity?.c2pa?.manifest_info.validation_results?.activeManifest;
|
||||
if (results) {
|
||||
valid = results.failure.length == 0 && results.success.length > 0;
|
||||
}
|
||||
|
||||
const source = extractFlagValues(
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/softwareAgent",
|
||||
proof
|
||||
);
|
||||
|
||||
const dateCreated = extractFlagValues(
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/../../../../signature_info/time",
|
||||
proof
|
||||
);
|
||||
let date: Date | undefined = undefined;
|
||||
if (dateCreated && dateCreated.length == 1) {
|
||||
try {
|
||||
date = new Date(Date.parse(dateCreated[0].value));
|
||||
} catch (error) {}
|
||||
}
|
||||
console.log("DATE CREATED", date);
|
||||
|
||||
let generator: ProofHintFlagsGenerator = matchFlag(ruleAiGenerated(), proof).result ? "ai" : matchFlag(ruleScreenshotC2PA(), proof).result ? "screenshot" : matchFlag(ruleCamera(), proof).result ? "camera" : "unknown";
|
||||
let generatorSource: ProofHintFlagsGeneratorSource | undefined = undefined;
|
||||
|
||||
if (generator !== "unknown" && valid) {
|
||||
generatorSource = "c2pa";
|
||||
} else {
|
||||
if (matchFlag(ruleScreenshotMeta(), proof).result) {
|
||||
generator = "screenshot";
|
||||
generatorSource = "metadata";
|
||||
} else if (matchFlag(ruleAiMeta(), proof).result) {
|
||||
generator = "ai";
|
||||
generatorSource = "metadata";
|
||||
}
|
||||
}
|
||||
|
||||
// Do we have any data? Else, return "undefined", we don't just want to send an object with all defaults.
|
||||
if (source.length === 0 && dateCreated.length === 0 && generator === "unknown") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const flags: ProofHintFlags = {
|
||||
device: source && source.length == 1 ? source[0].value : undefined,
|
||||
creationDate: date,
|
||||
generator: generator,
|
||||
generatorSource: generatorSource,
|
||||
edits: edits,
|
||||
};
|
||||
return flags;
|
||||
} catch (error) {}
|
||||
return undefined;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,6 +28,20 @@ class ProofMode {
|
|||
const worker = await this.getProofcheckWorker();
|
||||
const res = await worker.checkFiles([file]);
|
||||
if (res && res.files && res.files.length == 1) {
|
||||
delete res.files[0].data; // Don't need to hang on to the data!
|
||||
return res.files[0];
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async proofCheckSource(src: string): Promise<ProofCheckResult | undefined> {
|
||||
try {
|
||||
const worker = await this.getProofcheckWorker();
|
||||
const res = await worker.checkURLs([src]);
|
||||
if (res && res.files && res.files.length == 1) {
|
||||
delete res.files[0].data; // Don't need to hang on to the data!
|
||||
return res.files[0];
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -35,4 +49,6 @@ class ProofMode {
|
|||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new ProofMode();
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Observable, Subject } from "threads/observable";
|
||||
import { expose } from "threads/worker";
|
||||
import { checkFiles } from "@guardianproject/proofmode";
|
||||
import { checkFiles, checkURLs } from "@guardianproject/proofmode";
|
||||
|
||||
let subject = new Subject();
|
||||
|
||||
|
|
@ -12,6 +12,9 @@ const check = {
|
|||
checkFiles: (files) => {
|
||||
return checkFiles(files, sendMessage);
|
||||
},
|
||||
checkURLs: (urls) => {
|
||||
return checkURLs(urls, sendMessage);
|
||||
},
|
||||
values: () => {
|
||||
return Observable.from(subject);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export const ROOM_TYPE_FILE_MODE = "im.keanu.room_type_file";
|
|||
export const ROOM_TYPE_CHANNEL = "im.keanu.room_type_channel";
|
||||
|
||||
export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type";
|
||||
export const CLIENT_EVENT_PROOF_HINT = "im.keanu.proof_hint";
|
||||
|
||||
export const THUMBNAIL_MAX_WIDTH = 640;
|
||||
export const THUMBNAIL_MAX_HEIGHT = 640;
|
||||
|
|
@ -46,21 +47,6 @@ class Util {
|
|||
return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread";
|
||||
}
|
||||
|
||||
getAttachmentUrlAndDuration(event) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const content = event.getContent();
|
||||
if (content.url != null) {
|
||||
resolve([content.url, content.info.duration]);
|
||||
return;
|
||||
}
|
||||
if (content.file && content.file.url) {
|
||||
resolve([content.file.url, content.info.duration]);
|
||||
} else {
|
||||
reject("No url found!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAttachment(matrixClient, useAuthedMedia, event, progressCallback, asBlob = false, abortController = undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const content = event.getContent();
|
||||
|
|
@ -237,7 +223,7 @@ class Util {
|
|||
}
|
||||
|
||||
sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent, txnId) {
|
||||
var content = ContentHelpers.makeTextMessage(text);
|
||||
var content = ContentHelpers.makeTextMessage(text ?? "");
|
||||
if (editedEvent) {
|
||||
content["m.relates_to"] = {
|
||||
rel_type: "m.replace",
|
||||
|
|
@ -410,7 +396,7 @@ class Util {
|
|||
return [encryptedBytes, encryptedFile];
|
||||
}
|
||||
|
||||
sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot, dimensions, thumbnail) {
|
||||
sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot, dimensions, thumbnail, proofHintFlags) {
|
||||
const uploadPromise = new UploadPromise(undefined);
|
||||
uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
|
||||
var reader = new FileReader();
|
||||
|
|
@ -485,12 +471,16 @@ class Util {
|
|||
msgtype = "m.video";
|
||||
}
|
||||
|
||||
var messageContent = {
|
||||
let messageContent = {
|
||||
body: description,
|
||||
info: info,
|
||||
msgtype: msgtype,
|
||||
};
|
||||
|
||||
// if (proofHintFlags) {
|
||||
// messageContent[CLIENT_EVENT_PROOF_HINT] = JSON.stringify(proofHintFlags);
|
||||
// }
|
||||
|
||||
// If thread root (an eventId) is set, add that here
|
||||
if (threadRoot) {
|
||||
messageContent["m.relates_to"] = {
|
||||
|
|
@ -572,7 +562,7 @@ class Util {
|
|||
|
||||
// Generate audio waveforms
|
||||
if (msgtype == "m.audio") {
|
||||
this.generateWaveform(fileContents, messageContent);
|
||||
await this.generateWaveform(fileContents, messageContent);
|
||||
}
|
||||
|
||||
const result = await this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
|
||||
|
|
@ -589,46 +579,56 @@ class Util {
|
|||
return uploadPromise;
|
||||
}
|
||||
|
||||
generateWaveform(data, messageContent) {
|
||||
async generateWaveform(data, messageContent) {
|
||||
if (!(window.AudioContext || window.webkitAudioContext)) {
|
||||
return; // No support
|
||||
}
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
if (audioCtx) {
|
||||
return audioCtx.decodeAudioData(data).then((audioBuffer) => {
|
||||
const rawData = audioBuffer.getChannelData(0); // TODO - currently using only 1 channel
|
||||
const samples = 1000; // Number of samples
|
||||
const blockSize = Math.floor(rawData.length / samples);
|
||||
let filteredData = [];
|
||||
for (let i = 0; i < samples; i++) {
|
||||
let blockStart = blockSize * i; // the location of the first sample in the block
|
||||
let sum = 0;
|
||||
for (let j = 0; j < blockSize; j++) {
|
||||
sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block
|
||||
try {
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
if (audioCtx) {
|
||||
const audioBuffer = await audioCtx.decodeAudioData(data);
|
||||
if (audioBuffer) {
|
||||
const rawData = audioBuffer.getChannelData(0); // TODO - currently using only 1 channel
|
||||
const samples = 1000; // Number of samples
|
||||
const blockSize = Math.floor(rawData.length / samples);
|
||||
let filteredData = [];
|
||||
for (let i = 0; i < samples; i++) {
|
||||
let blockStart = blockSize * i; // the location of the first sample in the block
|
||||
let sum = 0;
|
||||
for (let j = 0; j < blockSize; j++) {
|
||||
sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block
|
||||
}
|
||||
filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
|
||||
}
|
||||
|
||||
// Normalize
|
||||
const multiplier = Math.pow(Math.max(...filteredData), -1);
|
||||
filteredData = filteredData.map((n) => n * multiplier);
|
||||
|
||||
// Integerize
|
||||
filteredData = filteredData.map((n) => parseInt((n * 255).toFixed()));
|
||||
|
||||
// Generate SVG of waveform
|
||||
let svg = `<svg viewBox="0 0 ${samples} 255" fill="none" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">`;
|
||||
svg += `<path d="`;
|
||||
filteredData.forEach((d, i) => {
|
||||
const delta = d / 2;
|
||||
svg += `M${i} ${128 - delta}V${128 + delta}`;
|
||||
});
|
||||
svg += `" style="fill:none;stroke:green;stroke-width:1" />`;
|
||||
svg += "</svg>";
|
||||
|
||||
messageContent.format = "org.matrix.custom.html";
|
||||
messageContent.formatted_body = svg;
|
||||
|
||||
// if duration is not set, do that here, since we have it
|
||||
if (!messageContent.info.duration) {
|
||||
messageContent.info.duration = parseInt((1000 * audioBuffer.duration).toFixed());
|
||||
}
|
||||
filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
|
||||
}
|
||||
|
||||
// Normalize
|
||||
const multiplier = Math.pow(Math.max(...filteredData), -1);
|
||||
filteredData = filteredData.map((n) => n * multiplier);
|
||||
|
||||
// Integerize
|
||||
filteredData = filteredData.map((n) => parseInt((n * 255).toFixed()));
|
||||
|
||||
// Generate SVG of waveform
|
||||
let svg = `<svg viewBox="0 0 ${samples} 255" fill="none" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">`;
|
||||
svg += `<path d="`;
|
||||
filteredData.forEach((d, i) => {
|
||||
const delta = d / 2;
|
||||
svg += `M${i} ${128 - delta}V${128 + delta}`;
|
||||
});
|
||||
svg += `" style="fill:none;stroke:green;stroke-width:1" />`;
|
||||
svg += "</svg>";
|
||||
|
||||
messageContent.format = "org.matrix.custom.html";
|
||||
messageContent.formatted_body = svg;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,14 +46,7 @@ export default {
|
|||
this.infoMap.set(eventId, entry);
|
||||
|
||||
// Get duration information
|
||||
utils
|
||||
.getAttachmentUrlAndDuration(event)
|
||||
.then(([ignoredurl, duration]) => {
|
||||
entry.duration = duration;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to fetch attachment duration: ", err);
|
||||
});
|
||||
entry.duration = event.getContent()?.info?.duration ?? 0;
|
||||
}
|
||||
entry.listeners.add(uid);
|
||||
return entry;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue