Send progress in main view
This commit is contained in:
parent
41232fbd90
commit
e9accdebf1
11 changed files with 465 additions and 86 deletions
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue