Support for running proof check on client side

This commit is contained in:
N-Pex 2025-09-05 13:48:52 +02:00
parent 66eef037e0
commit 46479d4c37
6 changed files with 126 additions and 29 deletions

View file

@ -0,0 +1,61 @@
<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>
<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>
</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";
const { attachment } = defineProps<{
attachment?: EventAttachment;
}>();
const loadingProof: Ref<boolean> = ref(false);
onMounted(() => {
if (attachment?.proofHintFlags && attachment.proof === undefined) {
const a = attachment;
loadingProof.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);
});
}
})
.catch(() => {})
.finally(() => {
loadingProof.value = false;
});
}
});
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 *;
</style>

View file

@ -5,62 +5,76 @@
<v-icon @click.stop="$emit('close')" color="white" class="clickable">arrow_back</v-icon>
<div class="room-name no-upper">{{ displayDate }}</div>
<div>
<v-icon v-if="showInfoButton" color="white" class="clickable">info_outline</v-icon>
<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>
</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"
: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="currentImageInput"
:src="currentImageInput.thumbnail ? currentImageInput.thumbnail : currentImageInput.src"
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 setup lang="ts">
import { useI18n } from "vue-i18n";
import MoreMenuPopup from "../MoreMenuPopup.vue";
import util, { CLIENT_EVENT_PROOF_HINT } from "../../plugins/utils";
import util from "../../plugins/utils";
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";
const { t } = useI18n();
const $matrix: any = inject("globalMatrix");
const props = defineProps<{ originalEvent: KeanuEvent, items: EventAttachment[]; initialItem: EventAttachment | undefined }>();
const props = defineProps<{
originalEvent: KeanuEvent;
items: EventAttachment[];
initialItem: EventAttachment | undefined;
}>();
const currentItemIndex: Ref<number> = ref(0);
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");
if (props.initialItem) {
currentItemIndex.value = props.items.findIndex((v) => v === props.initialItem);
if (currentItemIndex.value < 0) {
currentItemIndex.value = 0;
}
}
currentAttachment.value = props.initialItem ? props.initialItem : props.items[0];
});
onBeforeUnmount(() => {
@ -72,11 +86,7 @@ const displayDate = computed(() => {
});
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;
return currentAttachment.value?.proofHintFlags !== undefined;
});
const moreMenuItems = computed(() => {
@ -94,15 +104,15 @@ const moreMenuItems = computed(() => {
watch(props.items, (newValue: EventAttachment[], oldValue: EventAttachment[]) => {
// Added or removed?
if (newValue && oldValue && newValue.length > oldValue.length) {
currentItemIndex.value = oldValue.length;
currentAttachment.value = newValue[oldValue.length];
} else if (newValue) {
currentItemIndex.value = newValue.length - 1;
currentAttachment.value = newValue[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);
if (currentAttachment.value) {
util.download($matrix.matrixClient, $matrix.useAuthedMedia, currentAttachment.value.event);
}
};

View file

@ -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"),

View file

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

View file

@ -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();

View file

@ -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);
},