More work on sending/reading proof hint flags
This commit is contained in:
parent
dd76692640
commit
66eef037e0
9 changed files with 267 additions and 219 deletions
|
|
@ -1,132 +1,99 @@
|
|||
<template>
|
||||
<div v-if="c2pa">
|
||||
<div v-if="props.flags">
|
||||
<div class="detail-title">
|
||||
{{ t("file_mode.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>
|
||||
|
||||
<!-- {{ JSON.stringify(props.c2pa, undefined, 4) }} -->
|
||||
<div class="detail-info" v-if="infoText !== undefined">
|
||||
{{ infoText }}
|
||||
</div>
|
||||
|
||||
<CCProperty
|
||||
v-if="props.flags?.source"
|
||||
icon="$vuetify.icons.ic_media_camera"
|
||||
:title="t('file_mode.cc_source')"
|
||||
:value="props.flags?.source"
|
||||
/>
|
||||
<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,
|
||||
ProofHintFlags,
|
||||
} from "../../models/proof";
|
||||
import { computed, ref, Ref, watch } from "vue";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { ref, Ref, watch } from "vue";
|
||||
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;
|
||||
flags?: ProofHintFlags;
|
||||
}>();
|
||||
|
||||
//console.error("C2PA", JSON.stringify(props.c2pa, undefined, 4));
|
||||
|
||||
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 infoText: Ref<string | undefined> = ref(undefined);
|
||||
const creationDate: Ref<string | undefined> = ref(undefined);
|
||||
const valid: Ref<boolean> = ref(false);
|
||||
|
||||
watch(
|
||||
props,
|
||||
() => {
|
||||
creator.value = undefined;
|
||||
dateCreated.value = undefined;
|
||||
screenCapture.value = undefined;
|
||||
cameraCapture.value = undefined;
|
||||
ai.value = false;
|
||||
infoText.value = undefined;
|
||||
creationDate.value = undefined;
|
||||
valid.value = false;
|
||||
|
||||
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.camera) {
|
||||
result += t("file_mode.captured_with_camera");
|
||||
}
|
||||
if (props.flags.screenshot) {
|
||||
if (date) {
|
||||
result += t("file_mode.captured_screenshot_ago", { ago: date.fromNow(true) });
|
||||
} else {
|
||||
result += t("file_mode.captured_screenshot");
|
||||
}
|
||||
}
|
||||
if (props.flags.aiGenerated) {
|
||||
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");
|
||||
}
|
||||
|
||||
infoText.value = result === "" ? undefined : result;
|
||||
}
|
||||
|
||||
let results = props.c2pa?.manifest_info.validation_results?.activeManifest;
|
||||
if (results) {
|
||||
valid.value = results.failure.length == 0 && results.success.length > 0;
|
||||
}
|
||||
valid.value = props.flags?.valid ?? false;
|
||||
} 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">
|
||||
|
|
|
|||
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>
|
||||
|
|
@ -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="hasC2PA" :flags="attachment.proofHintFlags" />
|
||||
<EXIFInfo class="attachment-info__detail-box" v-if="hasExif" :exif="attachment.proof?.integrity?.exif" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,33 @@
|
|||
<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 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]" />
|
||||
<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: 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>
|
||||
</div>
|
||||
|
||||
|
|
@ -27,81 +36,78 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MoreMenuPopup from "../MoreMenuPopup";
|
||||
import messageMixin from "../messages/messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
import ThumbnailView from './ThumbnailView.vue';
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import MoreMenuPopup from "../MoreMenuPopup.vue";
|
||||
import util, { CLIENT_EVENT_PROOF_HINT } from "../../plugins/utils";
|
||||
import ThumbnailView from "./ThumbnailView.vue";
|
||||
import { EventAttachment, KeanuEvent } from "../../models/eventAttachment";
|
||||
import { computed, inject, onBeforeUnmount, onMounted, Ref, ref, watch } from "vue";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
components: { MoreMenuPopup, ThumbnailView },
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
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));
|
||||
const { t } = useI18n();
|
||||
const $matrix: any = inject("globalMatrix");
|
||||
|
||||
const props = defineProps<{ originalEvent: KeanuEvent, items: EventAttachment[]; initialItem: EventAttachment | undefined }>();
|
||||
|
||||
const currentItemIndex: Ref<number> = ref(0);
|
||||
const showMoreMenu: Ref<boolean> = ref(false);
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
document.body.classList.add("dark");
|
||||
if (props.initialItem) {
|
||||
currentItemIndex.value = props.items.findIndex((v) => v === props.initialItem);
|
||||
if (currentItemIndex.value < 0) {
|
||||
currentItemIndex.value = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.body.classList.remove("dark");
|
||||
});
|
||||
|
||||
const displayDate = computed(() => {
|
||||
return util.formatRecordStartTime(props.originalEvent.getTs());
|
||||
});
|
||||
|
||||
const showInfoButton = computed(() => {
|
||||
const item = currentItemIndex.value >= 0 && currentItemIndex.value < props.items.length ? props.items[currentItemIndex.value] : undefined;
|
||||
if (item) {
|
||||
return item.event.getContent()[CLIENT_EVENT_PROOF_HINT] !== undefined;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const moreMenuItems = computed(() => {
|
||||
let items = [];
|
||||
items.push({
|
||||
icon: "$vuetify.icons.ic_download",
|
||||
text: t("message.download_all"),
|
||||
handler: () => {
|
||||
downloadAll();
|
||||
},
|
||||
});
|
||||
return items;
|
||||
});
|
||||
|
||||
watch(props.items, (newValue: EventAttachment[], oldValue: EventAttachment[]) => {
|
||||
// Added or removed?
|
||||
if (newValue && oldValue && newValue.length > oldValue.length) {
|
||||
currentItemIndex.value = oldValue.length;
|
||||
} else if (newValue) {
|
||||
currentItemIndex.value = newValue.length - 1;
|
||||
}
|
||||
});
|
||||
|
||||
const downloadOne = () => {
|
||||
if (currentItemIndex.value >= 0 && currentItemIndex.value < props.items.length) {
|
||||
util.download($matrix.matrixClient, $matrix.useAuthedMedia, props.items[currentItemIndex.value].event);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadAll = () => {
|
||||
props.items.forEach((item) => util.download($matrix.matrixClient, $matrix.useAuthedMedia, item.event));
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -125,7 +131,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 +139,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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
@ -93,10 +84,6 @@ const retryUpload = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const onItemClick = (event: any) => {
|
||||
showItem.value = event.item;
|
||||
};
|
||||
|
||||
const layout = () => {
|
||||
if (!items.value || items.value.length == 0) {
|
||||
layoutedItems.value = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue