Send progress in main view

This commit is contained in:
N-Pex 2025-08-20 15:12:04 +02:00
parent 41232fbd90
commit e9accdebf1
11 changed files with 465 additions and 86 deletions

View file

@ -1,5 +1,5 @@
import { ComputedRef, Ref } from "vue";
import { Proof } from "./proof";
import { Proof, ProofHintFlags } from "./proof";
export class UploadPromise<Type> {
wrappedPromise: Promise<Type>;
@ -39,6 +39,7 @@ export type AttachmentSendInfo = {
randomRotation: number; // For UI effects
randomTranslationX: number; // For UI effects
randomTranslationY: number; // For UI effects
proofHintFlags?: ProofHintFlags;
};
export type AttachmentThumbnail = {
@ -58,15 +59,15 @@ export type Attachment = {
useScaled: boolean;
src?: string;
proof?: Proof;
sendInfo?: AttachmentSendInfo;
thumbnail?: AttachmentThumbnail;
sendInfo: AttachmentSendInfo;
};
export type AttachmentBatch = {
sendingStatus: Ref<"initial" | "sending" | "sent" | "canceled" | "failed">;
sendingStatus: Ref<AttachmentSendStatus>;
sendingRootEventId: Ref<string | undefined>;
sendingPromise: Ref<Promise<any> | undefined>;
attachments: Ref<Attachment[]>;
progressPercent: Ref<number>;
attachments: Attachment[];
attachmentsSentCount: ComputedRef<number>;
attachmentsSending: ComputedRef<Attachment[]>;
attachmentsSent: ComputedRef<Attachment[]>;
@ -78,4 +79,3 @@ export type AttachmentBatch = {
cancel: () => void;
cancelSendAttachment: (attachment: Attachment) => void;
};

View file

@ -1,4 +1,4 @@
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk";
import { MatrixClient, MatrixEvent, Room, RoomEvent } from "matrix-js-sdk";
import {
EventAttachment,
EventAttachmentUrlData,
@ -8,11 +8,18 @@ import {
} from "./eventAttachment";
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { Counter, ModeOfOperation } from "aes-js";
import { Attachment, AttachmentBatch, AttachmentSendInfo, AttachmentThumbnail } from "./attachment";
import {
Attachment,
AttachmentBatch,
AttachmentSendInfo,
AttachmentSendStatus,
AttachmentThumbnail,
} from "./attachment";
import proofmode from "../plugins/proofmode";
import imageResize from "image-resize";
import { computed, Reactive, reactive, ref, Ref, shallowReactive } from "vue";
import { computed, ref, Ref, shallowReactive, unref } from "vue";
import utils, { THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT } from "@/plugins/utils";
import { extractProofHintFlags } from "./proof";
export class AttachmentManager {
matrixClient: MatrixClient;
@ -20,7 +27,8 @@ export class AttachmentManager {
maxSizeUploads: number;
maxSizeAutoDownloads: number;
cache: Map<string | undefined, Reactive<EventAttachment>>;
cache: Map<string, EventAttachment>;
cacheUploads: Map<string, AttachmentBatch>;
constructor(matrixClient: MatrixClient, useAuthedMedia: boolean, maxSizeAutoDownloads: number) {
this.matrixClient = matrixClient;
@ -29,6 +37,7 @@ export class AttachmentManager {
this.maxSizeAutoDownloads = maxSizeAutoDownloads;
this.cache = new Map();
this.cacheUploads = new Map();
// Get max upload size
this.matrixClient
@ -40,7 +49,7 @@ export class AttachmentManager {
}
public createUpload(room: Room) {
return createUploadBatch(this.matrixClient, room, this.maxSizeUploads);
return createUploadBatch(this, room);
}
public createAttachment(file: File): Attachment {
@ -48,6 +57,16 @@ export class AttachmentManager {
status: "loading",
file: file,
useScaled: false,
sendInfo: {
status: "initial",
statusDate: 0,
mediaEventId: undefined,
progress: 0,
promise: undefined,
randomRotation: 0,
randomTranslationX: 0,
randomTranslationY: 0,
},
};
const ra = shallowReactive(a);
this.prepareUpload(ra);
@ -182,15 +201,15 @@ export class AttachmentManager {
return 0;
}
public getEventAttachment(event: KeanuEvent): Reactive<EventAttachment> {
let entry = this.cache.get(event.getId());
public getEventAttachment(event: KeanuEvent): EventAttachment {
let entry = this.cache.get(event.getId() ?? "invalid");
if (entry !== undefined) {
return entry;
}
const fileSize = this.getSrcFileSize(event);
const attachment: Reactive<EventAttachment> = shallowReactive({
const attachment: EventAttachment = {
event: event,
name: this.getFileName(event),
srcSize: fileSize,
@ -201,7 +220,7 @@ export class AttachmentManager {
loadThumbnail: () => Promise.reject("Not implemented"),
loadBlob: () => Promise.reject("Not implemented"),
release: () => Promise.reject("Not implemented"),
});
};
attachment.loadSrc = () => {
if (attachment.src) {
return Promise.resolve({ data: attachment.src, type: "src" });
@ -246,7 +265,7 @@ export class AttachmentManager {
// TODO - figure out logic
if (entry) {
// TODO - abortable promises
this.cache.delete(event.getId());
this.cache.delete(event.getId() ?? "invalid");
if (attachment.src) {
URL.revokeObjectURL(attachment.src);
}
@ -255,7 +274,9 @@ export class AttachmentManager {
}
}
};
this.cache.set(event.getId(), attachment!);
if (event.getId()) {
this.cache.set(event.getId()!, attachment!);
}
return attachment;
}
@ -391,16 +412,35 @@ export class AttachmentManager {
}
}
export const createUploadBatch = (
matrixClient: MatrixClient | null,
room: Room | null,
maxSizeUploads: number
): AttachmentBatch => {
const sendingStatus: Ref<"initial" | "sending" | "sent" | "canceled" | "failed"> = ref("initial");
export const createUploadBatch = (manager: AttachmentManager | null, room: Room | null): AttachmentBatch => {
const matrixClient = manager?.matrixClient;
const maxSizeUploads = manager?.maxSizeUploads ?? 0;
const txnId = utils.randomPass();
console.error("Created txnId", txnId);
const sendingStatus: Ref<AttachmentSendStatus> = ref("initial");
const sendingRootEventId: Ref<string | undefined> = ref(undefined);
const sendingPromise: Ref<Promise<any> | undefined> = ref(undefined);
const attachments: Ref<Attachment[]> = ref([]);
const totalOriginalFileSize: Ref<number> = ref(0);
const progressPercent: Ref<number> = ref(0);
const updateProgress = () => {
// Use relative sizes of the original files to determine how many percent
// the individual files contribute to the total progress.
const progress = attachments.value.reduce((cb, item) => {
const info = item.sendInfo;
const thisFileCurrent = item.file.size;
const max = totalOriginalFileSize.value > 0 ? thisFileCurrent / totalOriginalFileSize.value : 0;
const q = (info.progress * max) / 100;
return cb + q;
}, 0);
const percent = parseInt(Math.floor(progress * 100).toFixed());
progressPercent.value = percent;
};
const attachmentsSentCount = computed(() => {
return attachments.value.reduce((a, elem) => (elem.sendInfo?.status == "sent" ? a + 1 : a), 0);
});
@ -486,9 +526,10 @@ export const createUploadBatch = (
}
};
const send = (message: string): Promise<any> => {
const send = async (message: string): Promise<any> => {
if (!matrixClient || !room) return Promise.reject("Not configured");
sendingStatus.value = "sending";
attachments.value.forEach((attachment) => {
let sendInfo: AttachmentSendInfo = {
status: "initial",
@ -499,25 +540,54 @@ export const createUploadBatch = (
randomTranslationX: 0,
randomTranslationY: 0,
promise: undefined,
proofHintFlags: extractProofHintFlags(attachment.proof),
};
attachment.sendInfo = shallowReactive(sendInfo);
attachment.proof = undefined;
});
const sendingPromise = utils
.sendTextMessage(matrixClient, room.roomId, message)
totalOriginalFileSize.value = attachments.value.reduce((cb, item) => {
const thisFileCurrent = item.scaledFile && item.useScaled ? item.scaledFile.size : item.file.size;
return cb + thisFileCurrent;
}, 0);
const onLocalEchoUpdated = (event: KeanuEvent) => {
console.error("Local echo updated", event.getTxnId(), event.uploadBatch);
if (!event.uploadBatch && event.getTxnId() === txnId) {
event.uploadBatch = {
sendingStatus,
sendingRootEventId,
progressPercent,
attachments: unref(attachments),
attachmentsSentCount,
attachmentsSending,
attachmentsSent,
addAttachments,
removeAttachment,
isTooLarge,
canSend,
send,
cancel,
cancelSendAttachment,
};
}
};
room.on(RoomEvent.LocalEchoUpdated, onLocalEchoUpdated);
const promise = utils
.sendTextMessage(matrixClient, room.roomId, message, undefined, undefined, txnId)
.then((eventId: string) => {
sendingRootEventId.value = eventId;
// Use the eventId as a thread root for all the media
let promiseChain = Promise.resolve();
const getItemPromise = (index: number) => {
if (index < attachments.value.length) {
const attachment = attachments.value[index];
const item = attachment.sendInfo!;
if (item.status !== "initial") {
const item = attachment;
if (item.sendInfo.status !== "initial") {
return getItemPromise(++index);
}
item.status = "sending";
item.sendInfo.status = "sending";
let file = (() => {
if (attachment.scaledFile && attachment.useScaled) {
@ -536,10 +606,11 @@ export const createUploadBatch = (
file,
({ loaded, total }: { loaded: number; total: number }) => {
if (loaded == total) {
item.progress = 100;
item.sendInfo.progress = 100;
} else if (total > 0) {
item.progress = (100 * loaded) / total;
item.sendInfo.progress = (100 * loaded) / total;
}
updateProgress();
},
eventId,
attachment.dimensions,
@ -561,23 +632,23 @@ export const createUploadBatch = (
signY = -1;
}
}
item.randomRotation = signR * (2 + Math.random() * 10);
item.randomTranslationX = signX * Math.random() * 20;
item.randomTranslationY = signY * Math.random() * 20;
item.mediaEventId = mediaEventId;
item.status = "sent";
item.statusDate = Date.now();
item.sendInfo.randomRotation = signR * (2 + Math.random() * 10);
item.sendInfo.randomTranslationX = signX * Math.random() * 20;
item.sendInfo.randomTranslationY = signY * Math.random() * 20;
item.sendInfo.mediaEventId = mediaEventId;
item.sendInfo.status = "sent";
item.sendInfo.statusDate = Date.now();
})
.catch((ignorederr: any) => {
if (item.promise?.aborted) {
item.status = "canceled";
if (item.sendInfo.promise?.aborted) {
item.sendInfo.status = "canceled";
} else {
console.error("ERROR", ignorederr);
item.status = "failed";
item.sendInfo.status = "failed";
}
return Promise.resolve();
});
item.promise = itemPromise;
item.sendInfo.promise = itemPromise;
return itemPromise.then(() => getItemPromise(++index));
} else return Promise.resolve();
};
@ -589,16 +660,20 @@ export const createUploadBatch = (
sendingRootEventId.value = undefined;
})
.catch((err: any) => {
console.error("ERROR", err);
console.error("Upload error", err);
})
.finally(() => {
room.off(RoomEvent.LocalEchoUpdated, onLocalEchoUpdated);
});
return sendingPromise;
sendingPromise.value = promise;
return promise;
};
return {
sendingStatus,
sendingRootEventId,
sendingPromise,
attachments,
progressPercent,
attachments: unref(attachments),
attachmentsSentCount,
attachmentsSending,
attachmentsSent,

View file

@ -1,4 +1,5 @@
import { MatrixEvent, Room } from "matrix-js-sdk";
import { AttachmentBatch } from "./attachment";
export type KeanuEventExtension = {
isMxThread?: boolean;
@ -6,6 +7,7 @@ export type KeanuEventExtension = {
isPinned?: boolean;
parentThread?: MatrixEvent & KeanuEventExtension;
replyEvent?: MatrixEvent & KeanuEventExtension;
uploadBatch?: AttachmentBatch;
}
export type KeanuEvent = MatrixEvent & KeanuEventExtension;

View file

@ -53,6 +53,72 @@ export type Proof = {
data?: any;
name?: string;
json?: string;
integrity?: { pgp?: any; c2pa?: any; exif?: {[key: string]: string | Object}; opentimestamps?: any };
integrity?: { pgp?: any; c2pa?: C2PAData; exif?: {[key: string]: string | Object}; opentimestamps?: any };
ai?: { inferenceResult?: AIInferenceResult};
}
export type ProofHintFlags = {
aiGenerated?: boolean;
aiEdited?: boolean;
screenshot?: boolean;
camera?: boolean;
}
export const extractProofHintFlags = (proof?: Proof): (ProofHintFlags | undefined) => {
if (!proof) return undefined;
let screenshot = false;
let camera = false;
let aiGenerated = false;
let aiEdited = false;
let valid = false;
try {
let results = proof.integrity?.c2pa?.manifest_info.validation_results?.activeManifest;
if (results) {
valid = results.failure.length == 0 && results.success.length > 0;
}
const manifests = Object.values(proof.integrity?.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) {
screenshot = true;
}
if (
a.digitalSourceType === C2PASourceTypeDigitalCapture ||
a.digitalSourceType === C2PASourceTypeComputationalCapture ||
a.digitalSourceType === C2PASourceTypeCompositeCapture
) {
camera = true;
}
if (
a.digitalSourceType === C2PASourceTypeTrainedAlgorithmicMedia ||
a.digitalSourceType === C2PASourceTypeCompositeWithTrainedAlgorithmicMedia
) {
aiGenerated = true;
}
return;
}
}
}
}
if (valid) {
const flags: ProofHintFlags = {
aiGenerated,
aiEdited,
screenshot,
camera
};
return flags;
}
} catch (error) {
}
return undefined;
};