Start on intervention display

This commit is contained in:
N-Pex 2025-09-09 10:56:15 +02:00
parent 46479d4c37
commit 27b27876c0
12 changed files with 268 additions and 99 deletions

View 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;
}

View file

@ -622,20 +622,6 @@ $hiliteColor: #4642f1;
} }
} }
.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;
}
.detail-row { .detail-row {
margin-top: 12px; margin-top: 12px;
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;

View file

@ -74,3 +74,16 @@
text-decoration-skip-ink: none; text-decoration-skip-ink: none;
color: rgba(0,0,0,0.60); 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;
}

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

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

View file

@ -515,5 +515,8 @@
"cc_source": "Source", "cc_source": "Source",
"cc_capture_timestamp": "Capture Timestamp", "cc_capture_timestamp": "Capture Timestamp",
"cc_location": "Location" "cc_location": "Location"
},
"cc": {
"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."
} }
} }

View file

@ -8,15 +8,15 @@
{{ t("file_mode.content_credentials_info") }} {{ t("file_mode.content_credentials_info") }}
</div> </div>
<div class="detail-info" v-if="infoText !== undefined"> <div class="cc-detail-info" v-if="infoText !== undefined">
{{ infoText }} {{ infoText }}
</div> </div>
<CCProperty <CCProperty
v-if="props.flags?.source" v-if="props.flags?.device"
icon="$vuetify.icons.ic_media_camera" icon="$vuetify.icons.ic_media_camera"
:title="t('file_mode.cc_source')" :title="t('file_mode.cc_source')"
:value="props.flags?.source" :value="props.flags?.device"
/> />
<CCProperty <CCProperty
v-if="creationDate" v-if="creationDate"
@ -29,9 +29,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { import { ProofHintFlags } from "../../models/proof";
ProofHintFlags,
} from "../../models/proof";
import { ref, Ref, watch } from "vue"; import { ref, Ref, watch } from "vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import CCProperty from "./CCProperty.vue"; import CCProperty from "./CCProperty.vue";
@ -64,17 +62,15 @@ watch(
} }
let result = ""; let result = "";
if (props.flags.camera) { if (props.flags.generator === "camera") {
result += t("file_mode.captured_with_camera"); result += t("file_mode.captured_with_camera");
} } else if (props.flags.generator === "screenshot") {
if (props.flags.screenshot) {
if (date) { if (date) {
result += t("file_mode.captured_screenshot_ago", { ago: date.fromNow(true) }); result += t("file_mode.captured_screenshot_ago", { ago: date.fromNow(true) });
} else { } else {
result += t("file_mode.captured_screenshot"); result += t("file_mode.captured_screenshot");
} }
} } else if (props.flags.generator === "ai") {
if (props.flags.aiGenerated) {
if (date) { if (date) {
result += t("file_mode.generated_with_ai_ago", { ago: date.fromNow(true) }); result += t("file_mode.generated_with_ai_ago", { ago: date.fromNow(true) });
} else { } else {
@ -93,9 +89,9 @@ watch(
}, },
{ immediate: true } { immediate: true }
); );
</script> </script>
<style lang="scss"> <style lang="scss">
@use "@/assets/css/chat.scss" as *; @use "@/assets/css/chat.scss" as *;
@use "@/assets/css/contentcredentials.scss" as *;
</style> </style>

View file

@ -0,0 +1,42 @@
<template>
<div class="cc-summary" v-if="props.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 props = defineProps<{
flags: ProofHintFlags[];
}>();
const showCheck = computed(() => {
return props.flags.some((f) => f?.valid);
});
const infoText = computed(() => {
if (props.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 (props.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 {
.intervention-icon {
width: 12px;
height: 12px;
display: inline-flex;
}
}
</style>

View file

@ -5,6 +5,7 @@
<v-progress-circular indeterminate class="mb-0"></v-progress-circular> <v-progress-circular indeterminate class="mb-0"></v-progress-circular>
</div> </div>
</div> </div>
<div v-else-if="metaStripped" class="cc-detail-info white-space-pre">{{ t("cc.metadata-stripped") }}</div>
<div v-else> <div v-else>
<C2PAInfo class="attachment-info__detail-box" v-if="hasC2PA" :flags="attachment?.proofHintFlags" /> <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" /> <EXIFInfo class="attachment-info__detail-box" v-if="hasExif" :exif="attachment?.proof?.integrity?.exif" />
@ -19,23 +20,29 @@ import { computed, onMounted, Ref, ref } from "vue";
import { EventAttachment } from "../../models/eventAttachment"; import { EventAttachment } from "../../models/eventAttachment";
import proofmode from "../../plugins/proofmode"; import proofmode from "../../plugins/proofmode";
import { extractProofHintFlags } from "../../models/proof"; import { extractProofHintFlags } from "../../models/proof";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { attachment } = defineProps<{ const { attachment } = defineProps<{
attachment?: EventAttachment; attachment?: EventAttachment;
}>(); }>();
const loadingProof: Ref<boolean> = ref(false); const loadingProof: Ref<boolean> = ref(false);
const metaStripped: Ref<boolean> = ref(false);
onMounted(() => { onMounted(() => {
if (attachment?.proofHintFlags && attachment.proof === undefined) { if (attachment?.proofHintFlags && attachment.proof === undefined) {
const a = attachment; const a = attachment;
loadingProof.value = true; loadingProof.value = true;
metaStripped.value = true;
a.loadSrc() a.loadSrc()
.then((data) => { .then((data) => {
if (data && data.data) { if (data && data.data) {
return proofmode.proofCheckSource(data.data).then((res) => { return proofmode.proofCheckSource(data.data).then((res) => {
a.proof = res; a.proof = res;
a.proofHintFlags = extractProofHintFlags(a.proof); a.proofHintFlags = extractProofHintFlags(a.proof);
metaStripped.value = a?.proof?.integrity?.c2pa === undefined && a?.proof?.integrity?.exif === undefined;
}); });
} }
}) })
@ -43,6 +50,8 @@ onMounted(() => {
.finally(() => { .finally(() => {
loadingProof.value = false; loadingProof.value = false;
}); });
} else {
metaStripped.value = attachment?.proof?.integrity?.c2pa === undefined && attachment?.proof?.integrity?.exif === undefined;
} }
}); });
@ -58,4 +67,5 @@ const hasExif = computed(() => {
<style lang="scss"> <style lang="scss">
@use "@/assets/css/chat.scss" as *; @use "@/assets/css/chat.scss" as *;
@use "@/assets/css/sendattachments.scss" as *; @use "@/assets/css/sendattachments.scss" as *;
@use "@/assets/css/contentcredentials.scss" as *;
</style> </style>

View file

@ -105,7 +105,7 @@ watch(props.items, (newValue: EventAttachment[], oldValue: EventAttachment[]) =>
// Added or removed? // Added or removed?
if (newValue && oldValue && newValue.length > oldValue.length) { if (newValue && oldValue && newValue.length > oldValue.length) {
currentAttachment.value = newValue[oldValue.length]; currentAttachment.value = newValue[oldValue.length];
} else if (newValue) { } else if (newValue && oldValue && newValue.length < oldValue.length) {
currentAttachment.value = newValue[newValue.length - 1]; currentAttachment.value = newValue[newValue.length - 1];
} }
}); });
@ -119,6 +119,7 @@ const downloadOne = () => {
const downloadAll = () => { const downloadAll = () => {
props.items.forEach((item) => util.download($matrix.matrixClient, $matrix.useAuthedMedia, item.event)); props.items.forEach((item) => util.download($matrix.matrixClient, $matrix.useAuthedMedia, item.event));
}; };
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -1,12 +1,8 @@
<template> <template>
<component <component :is="rootComponent" ref="root" v-bind="{ ...$props, ...$attrs }" v-if="showMultiview">
:is="rootComponent"
ref="root"
v-bind="{ ...$props, ...$attrs }"
v-if="showMultiview"
>
<div class="bubble"> <div class="bubble">
<div class="original-message" v-if="inReplyToText"> <div class="bubble-inset" v-if="showCCSummary"><CCSummary :flags="proofHintFlags" /></div>
<div class="original-message bubble-inset" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div> <div class="original-message-sender">{{ inReplyToSender }}</div>
<div class="original-message-text" v-html="linkify($$sanitize(inReplyToText))" /> <div class="original-message-text" v-html="linkify($$sanitize(inReplyToText))" />
</div> </div>
@ -18,24 +14,26 @@
v-bind="$attrs" v-bind="$attrs"
/> />
<v-container v-else-if="event && !event.isRedacted()" fluid class="imageCollection"> <v-container v-else-if="event && !event.isRedacted()" fluid class="imageCollection">
<v-row wrap> <v-row class="pa-0 ma-0" wrap>
<v-col v-for="{ size, item } in layoutedItems" :key="item.event.getId()" :cols="size"> <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)" /> <ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
<i v-if="event && event.isRedacted()" class="deleted-text"> <div class="bubble-inset">
<v-icon :color="isIncoming && senderIsAdminOrModerator(event) ? 'white' : ''" size="small">block</v-icon> <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") redactedBySomeoneElse(event)
: $t("message.outgoing_message_deleted_text") ? $t("message.incoming_message_deleted_text")
}} : $t("message.outgoing_message_deleted_text")
</i> }}
<span v-html="linkify($$sanitize(messageText))" v-else-if="messageText" /> </i>
<span class="edit-marker" v-if="event && event.replacingEventId() && !event.isRedacted()"> <span v-html="linkify($$sanitize(messageText))" v-else-if="messageText" />
{{ t("message.edited") }} <span class="edit-marker" v-if="event && event.replacingEventId() && !event.isRedacted()">
</span> {{ t("message.edited") }}
</span>
</div>
</div> </div>
</div> </div>
<GalleryItemsView <GalleryItemsView
@ -62,20 +60,24 @@ import util, { ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE } from "@/plugins/utils";
import GalleryItemsView from "../../file_mode/GalleryItemsView.vue"; import GalleryItemsView from "../../file_mode/GalleryItemsView.vue";
import ThumbnailView from "../../file_mode/ThumbnailView.vue"; import ThumbnailView from "../../file_mode/ThumbnailView.vue";
import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.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 { computed, inject, onBeforeUnmount, ref, Ref, useTemplateRef, watch } from "vue";
import { EventAttachment } from "../../../models/eventAttachment"; import { EventAttachment } from "../../../models/eventAttachment";
import { useI18n } from 'vue-i18n' import { useI18n } from "vue-i18n";
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk"; import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk";
import { useLazyLoad } from "./useLazyLoad"; import { useLazyLoad } from "./useLazyLoad";
import { ProofHintFlags } from "../../../models/proof";
const { t } = useI18n() const { t } = useI18n();
const $matrix: any = inject('globalMatrix'); const $matrix: any = inject("globalMatrix");
const $$sanitize: any = inject('globalSanitize'); 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 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 items: Ref<EventAttachment[]> = ref([]);
const showItem: Ref<EventAttachment | undefined> = ref(undefined); const showItem: Ref<EventAttachment | undefined> = ref(undefined);
@ -87,13 +89,11 @@ const { room } = props;
const processThread = () => { const processThread = () => {
if (!event.value?.isRedacted()) { if (!event.value?.isRedacted()) {
const el = rootRef.value?.$el; const el = rootRef.value?.$el;
emits("layout-change", {element: el, action: _processThread}); emits("layout-change", { element: el, action: _processThread });
} }
}; };
const { const { isVisible } = useLazyLoad({ root: rootRef });
isVisible
} = useLazyLoad({ root: rootRef });
const { const {
event, event,
@ -108,8 +108,8 @@ const {
} = useMessage($matrix, t, props, undefined, processThread); } = useMessage($matrix, t, props, undefined, processThread);
const rootComponent = computed(() => { const rootComponent = computed(() => {
return isIncoming.value ? MessageIncoming : MessageOutgoing; return isIncoming.value ? MessageIncoming : MessageOutgoing;
}) });
const onRelationsCreated = () => { const onRelationsCreated = () => {
if (event.value) { if (event.value) {
@ -122,31 +122,52 @@ const onRelationsCreated = () => {
} }
}; };
watch(event, () => { watch(
if (event.value) { event,
if (thread.value === undefined) { () => {
thread.value = props.timelineSet.relations.getChildEventsForEvent( if (event.value) {
event.value.getId() ?? "", if (thread.value === undefined) {
util.threadMessageType(), thread.value = props.timelineSet.relations.getChildEventsForEvent(
"m.room.message" 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(() => { onBeforeUnmount(() => {
event.value?.off(MatrixEventEvent.RelationsCreated, onRelationsCreated); event.value?.off(MatrixEventEvent.RelationsCreated, onRelationsCreated);
}); });
const showMultiview = computed((): boolean => { const showMultiview = computed((): boolean => {
return (isIncoming.value && props.room.displayType == ROOM_TYPE_FILE_MODE) || return (
items.value?.length > 1 || (isIncoming.value && props.room.displayType == ROOM_TYPE_FILE_MODE) ||
(event.value && event.value.isRedacted()) || items.value?.length > 1 ||
(props.room.displayType == ROOM_TYPE_CHANNEL && items.value.length == 1 && util.isFileTypePDF(items.value[0].event)) || (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 messageText.value?.length > 0
);
});
const showCCSummary = computed(() => {
return items.value?.some((i) => i.proofHintFlags !== undefined && i.proofHintFlags.valid);
});
const proofHintFlags = computed(() => {
return items.value.reduce((res: ProofHintFlags[], item) => {
if (item.proofHintFlags) {
res.push(item.proofHintFlags);
}
return res;
}, []);
}); });
watch(isVisible, (visible) => { watch(isVisible, (visible) => {
@ -190,9 +211,9 @@ const layoutedItems = computed(() => {
rows.push({ size: 3, item: array[6] }); rows.push({ size: 3, item: array[6] });
array = array.slice(7); array = array.slice(7);
} else if (array.length >= 3) { } 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: 6, item: array[1] });
rows.push({ size: 12, item: array[2] }); rows.push({ size: 6, item: array[2] });
array = array.slice(3); array = array.slice(3);
} else if (array.length >= 2) { } else if (array.length >= 2) {
rows.push({ size: 6, item: array[0] }); rows.push({ size: 6, item: array[0] });
@ -205,7 +226,6 @@ const layoutedItems = computed(() => {
} }
return rows; return rows;
}); });
</script> </script>
<style lang="scss"> <style lang="scss">
@use "@/assets/css/chat.scss" as *; @use "@/assets/css/chat.scss" as *;
@ -214,20 +234,29 @@ const layoutedItems = computed(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.bubble { .bubble {
width: 100%; width: 100%;
margin: 0 0 !important;
padding: 0 !important;
overflow: hidden;
}
.bubble-inset {
padding: 8px 8px;
} }
.imageCollection { .imageCollection {
border-radius: 15px; width: unset;
padding: 0; max-width: unset;
margin: 0 0px !important;
padding: 0 !important;
overflow: hidden; overflow: hidden;
.row { .v-row {
margin: -4px; // Compensate for column padding, so the border-radius above looks round! margin: -2px -2px 0 -2px !important;
padding: 0;
} }
.col { .v-col {
padding: 2px; padding: 2px !important;
background-color: transparent !important;
} }
.file-item { .file-item {

View file

@ -59,14 +59,22 @@ export type Proof = {
ai?: { inferenceResult?: AIInferenceResult }; 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 = { export type ProofHintFlags = {
valid: boolean; valid: boolean;
source?: string; device?: string;
creationDate?: Date; creationDate?: Date;
aiGenerated?: boolean; generator?: ProofHintFlagsGenerator;
aiEdited?: boolean; generatorSource?: ProofHintFlagsGeneratorSource;
screenshot?: boolean; edits?: ProofHintFlagsEdit[];
camera?: boolean;
}; };
type FlagMatchRule = { type FlagMatchRule = {
@ -86,7 +94,7 @@ type FlagMatchInfo = {
re: string; re: string;
}; };
const ruleScreenshot = (): FlagMatchRule[] => { const ruleScreenshotC2PA = (): FlagMatchRule[] => {
return [ return [
{ {
field: field:
@ -97,6 +105,17 @@ const ruleScreenshot = (): FlagMatchRule[] => {
]; ];
}; };
const ruleScreenshotMeta = (): FlagMatchRule[] => {
return [
{
field:
"name",
match: ["screenshot"],
description: "Screen capture",
},
];
};
const ruleCamera = (): FlagMatchRule[] => { const ruleCamera = (): FlagMatchRule[] => {
return [ return [
{ {
@ -238,7 +257,7 @@ const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] =>
export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => { export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => {
if (!proof) return undefined; if (!proof) return undefined;
let aiEdited = false; let edits: ProofHintFlagsEdit[] | undefined = undefined;
let valid = false; let valid = false;
try { try {
@ -264,14 +283,25 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined
} }
console.log("DATE CREATED", date); 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";
}
}
const flags: ProofHintFlags = { const flags: ProofHintFlags = {
valid: valid, valid: valid,
source: source && source.length == 1 ? source[0].value : undefined, device: source && source.length == 1 ? source[0].value : undefined,
creationDate: date, creationDate: date,
aiGenerated: matchFlag(ruleAiGenerated(), proof).result, generator: generator,
aiEdited, generatorSource: generatorSource,
screenshot: matchFlag(ruleScreenshot(), proof).result, edits: edits,
camera: matchFlag(ruleCamera(), proof).result,
}; };
return flags; return flags;
} catch (error) {} } catch (error) {}