Start on intervention display
This commit is contained in:
parent
46479d4c37
commit
27b27876c0
12 changed files with 268 additions and 99 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue