Start on intervention display
This commit is contained in:
parent
46479d4c37
commit
27b27876c0
12 changed files with 268 additions and 99 deletions
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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
margin-top: 12px;
|
||||
font-family: "Inter", sans-serif;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
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>
|
||||
|
|
@ -515,5 +515,8 @@
|
|||
"cc_source": "Source",
|
||||
"cc_capture_timestamp": "Capture Timestamp",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@
|
|||
{{ t("file_mode.content_credentials_info") }}
|
||||
</div>
|
||||
|
||||
<div class="detail-info" v-if="infoText !== undefined">
|
||||
<div class="cc-detail-info" v-if="infoText !== undefined">
|
||||
{{ infoText }}
|
||||
</div>
|
||||
|
||||
<CCProperty
|
||||
v-if="props.flags?.source"
|
||||
v-if="props.flags?.device"
|
||||
icon="$vuetify.icons.ic_media_camera"
|
||||
:title="t('file_mode.cc_source')"
|
||||
:value="props.flags?.source"
|
||||
:value="props.flags?.device"
|
||||
/>
|
||||
<CCProperty
|
||||
v-if="creationDate"
|
||||
|
|
@ -29,9 +29,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
ProofHintFlags,
|
||||
} from "../../models/proof";
|
||||
import { ProofHintFlags } from "../../models/proof";
|
||||
import { ref, Ref, watch } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import CCProperty from "./CCProperty.vue";
|
||||
|
|
@ -64,17 +62,15 @@ watch(
|
|||
}
|
||||
|
||||
let result = "";
|
||||
if (props.flags.camera) {
|
||||
if (props.flags.generator === "camera") {
|
||||
result += t("file_mode.captured_with_camera");
|
||||
}
|
||||
if (props.flags.screenshot) {
|
||||
} 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");
|
||||
}
|
||||
}
|
||||
if (props.flags.aiGenerated) {
|
||||
} else if (props.flags.generator === "ai") {
|
||||
if (date) {
|
||||
result += t("file_mode.generated_with_ai_ago", { ago: date.fromNow(true) });
|
||||
} else {
|
||||
|
|
@ -93,9 +89,9 @@ watch(
|
|||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
@use "@/assets/css/contentcredentials.scss" as *;
|
||||
</style>
|
||||
|
|
|
|||
42
src/components/content-credentials/CCSummary.vue
Normal file
42
src/components/content-credentials/CCSummary.vue
Normal 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>
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
<v-progress-circular indeterminate class="mb-0"></v-progress-circular>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="metaStripped" class="cc-detail-info white-space-pre">{{ t("cc.metadata-stripped") }}</div>
|
||||
<div v-else>
|
||||
<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" />
|
||||
|
|
@ -19,23 +20,29 @@ 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;
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
@ -43,6 +50,8 @@ onMounted(() => {
|
|||
.finally(() => {
|
||||
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">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
@use "@/assets/css/sendattachments.scss" as *;
|
||||
@use "@/assets/css/contentcredentials.scss" as *;
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ 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) {
|
||||
} else if (newValue && oldValue && newValue.length < oldValue.length) {
|
||||
currentAttachment.value = newValue[newValue.length - 1];
|
||||
}
|
||||
});
|
||||
|
|
@ -119,6 +119,7 @@ const downloadOne = () => {
|
|||
const downloadAll = () => {
|
||||
props.items.forEach((item) => util.download($matrix.matrixClient, $matrix.useAuthedMedia, item.event));
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
<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 :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 +14,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">
|
||||
<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 +60,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 +89,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 +108,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 +122,52 @@ 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 (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 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)) ||
|
||||
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 && 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) => {
|
||||
|
|
@ -190,9 +211,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 +226,6 @@ const layoutedItems = computed(() => {
|
|||
}
|
||||
return rows;
|
||||
});
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
|
|
@ -214,20 +234,29 @@ const layoutedItems = computed(() => {
|
|||
<style lang="scss" scoped>
|
||||
.bubble {
|
||||
width: 100%;
|
||||
margin: 0 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bubble-inset {
|
||||
padding: 8px 8px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
|
|
|||
|
|
@ -59,14 +59,22 @@ export type Proof = {
|
|||
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 = {
|
||||
valid: boolean;
|
||||
source?: string;
|
||||
device?: string;
|
||||
creationDate?: Date;
|
||||
aiGenerated?: boolean;
|
||||
aiEdited?: boolean;
|
||||
screenshot?: boolean;
|
||||
camera?: boolean;
|
||||
generator?: ProofHintFlagsGenerator;
|
||||
generatorSource?: ProofHintFlagsGeneratorSource;
|
||||
edits?: ProofHintFlagsEdit[];
|
||||
};
|
||||
|
||||
type FlagMatchRule = {
|
||||
|
|
@ -86,7 +94,7 @@ type FlagMatchInfo = {
|
|||
re: string;
|
||||
};
|
||||
|
||||
const ruleScreenshot = (): FlagMatchRule[] => {
|
||||
const ruleScreenshotC2PA = (): FlagMatchRule[] => {
|
||||
return [
|
||||
{
|
||||
field:
|
||||
|
|
@ -97,6 +105,17 @@ const ruleScreenshot = (): FlagMatchRule[] => {
|
|||
];
|
||||
};
|
||||
|
||||
const ruleScreenshotMeta = (): FlagMatchRule[] => {
|
||||
return [
|
||||
{
|
||||
field:
|
||||
"name",
|
||||
match: ["screenshot"],
|
||||
description: "Screen capture",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const ruleCamera = (): FlagMatchRule[] => {
|
||||
return [
|
||||
{
|
||||
|
|
@ -238,7 +257,7 @@ const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] =>
|
|||
export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => {
|
||||
if (!proof) return undefined;
|
||||
|
||||
let aiEdited = false;
|
||||
let edits: ProofHintFlagsEdit[] | undefined = undefined;
|
||||
let valid = false;
|
||||
|
||||
try {
|
||||
|
|
@ -264,14 +283,25 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined
|
|||
}
|
||||
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 = {
|
||||
valid: valid,
|
||||
source: source && source.length == 1 ? source[0].value : undefined,
|
||||
device: source && source.length == 1 ? source[0].value : undefined,
|
||||
creationDate: date,
|
||||
aiGenerated: matchFlag(ruleAiGenerated(), proof).result,
|
||||
aiEdited,
|
||||
screenshot: matchFlag(ruleScreenshot(), proof).result,
|
||||
camera: matchFlag(ruleCamera(), proof).result,
|
||||
generator: generator,
|
||||
generatorSource: generatorSource,
|
||||
edits: edits,
|
||||
};
|
||||
return flags;
|
||||
} catch (error) {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue