Send progress in main view
This commit is contained in:
parent
41232fbd90
commit
e9accdebf1
11 changed files with 465 additions and 86 deletions
|
|
@ -52,6 +52,7 @@ import ReadMarker from "./messages/ReadMarker.vue";
|
|||
import RoomTombstone from "./messages/composition/RoomTombstone.vue";
|
||||
import roomDisplayOptionsMixin from "./roomDisplayOptionsMixin";
|
||||
import roomTypeMixin from "./roomTypeMixin";
|
||||
import MessageThreadSending from "./messages/composition/MessageThreadSending.vue";
|
||||
|
||||
export const ROOM_READ_MARKER_EVENT_PLACEHOLDER = { getId: () => "ROOM_READ_MARKER", getTs: () => Date.now() };
|
||||
|
||||
|
|
@ -225,6 +226,9 @@ export default {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
if (!isForExport && event.uploadBatch) {
|
||||
return MessageThreadSending;
|
||||
}
|
||||
if (event.isMxThread) {
|
||||
// Outgoing thread
|
||||
return isForExport ? MessageThreadExport : MessageThread;
|
||||
|
|
|
|||
|
|
@ -245,8 +245,8 @@ export default defineComponent({
|
|||
|
||||
batch: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return reactive(createUploadBatch(null, null, 0));
|
||||
default: () => {
|
||||
return createUploadBatch(null, null);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -294,7 +294,7 @@ export default defineComponent({
|
|||
this.currentItemIndex = newValue.length - 1;
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
deep: 1,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -323,6 +323,7 @@ export default defineComponent({
|
|||
},
|
||||
sendAll() {
|
||||
this.status = this.mainStatuses.SENDING;
|
||||
this.$emit("close");
|
||||
this.batch
|
||||
.send(this.messageInput && this.messageInput.length > 0 ? this.messageInput : this.defaultRootMessageText)
|
||||
.then(() => {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import utils from "../../plugins/utils";
|
||||
import { singleOrDoubleTapRecognizer } from "../../plugins/touch";
|
||||
import { computed, inject, onBeforeUnmount, onMounted, Ref, ref, useTemplateRef, watch } from "vue";
|
||||
import { EventAttachment } from "../../models/eventAttachment";
|
||||
import { useThumbnail } from "../messages/composition/useThumbnail";
|
||||
|
|
@ -119,7 +119,7 @@ const loadingProgress = computed(() => {
|
|||
|
||||
onMounted(() => {
|
||||
if (thumbnailRef.value && (item as EventAttachment)) {
|
||||
const hammerInstance = utils.singleOrDoubleTabRecognizer(thumbnailRef.value);
|
||||
const hammerInstance = singleOrDoubleTapRecognizer(thumbnailRef.value);
|
||||
hammerInstance.on("singletap doubletap", (ev: any) => {
|
||||
if (ev.type === "singletap") {
|
||||
emits("itemclick", { item: item as EventAttachment });
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { singleOrDoubleTapRecognizer } from "@/plugins/touch";
|
||||
import { computed, inject, onMounted, ref, useTemplateRef, watch } from "vue";
|
||||
import MessageIncoming from "./MessageIncoming.vue";
|
||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||
|
|
@ -30,7 +31,6 @@ import { useI18n } from "vue-i18n";
|
|||
import { MessageProps, useMessage } from "./useMessage";
|
||||
import { EventAttachment } from "../../../models/eventAttachment";
|
||||
import { useDisplay } from "vuetify";
|
||||
import utils from "@/plugins/utils";
|
||||
import Hammer from "hammerjs";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
|
@ -80,7 +80,7 @@ onMounted(() => {
|
|||
contain.value = true;
|
||||
}
|
||||
if (imageRef.value) {
|
||||
const hammerInstance = utils.singleOrDoubleTabRecognizer(imageRef.value);
|
||||
const hammerInstance = singleOrDoubleTapRecognizer(imageRef.value);
|
||||
hammerInstance.on("singletap doubletap", (ev: Hammer.HammerInput) => {
|
||||
if (ev.type === "singletap") {
|
||||
attachment.value?.loadSrc();
|
||||
|
|
|
|||
194
src/components/messages/composition/MessageThreadSending.vue
Normal file
194
src/components/messages/composition/MessageThreadSending.vue
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
<template>
|
||||
<MessageOutgoing :is="rootComponent" ref="root" v-bind="{ ...$props, ...$attrs }">
|
||||
<div class="bubble">
|
||||
<div class="original-message" v-if="inReplyToText">
|
||||
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||
<div class="original-message-text" v-html="linkify($$sanitize(inReplyToText))" />
|
||||
</div>
|
||||
|
||||
<div class="message">
|
||||
<div class="message-upload-progress" v-if="unref(uploadBatch?.sendingStatus) === 'sending'">
|
||||
<v-progress-circular
|
||||
class="message-upload-progress__progress clickable"
|
||||
:model-value="unref(uploadBatch?.progressPercent ?? 0)"
|
||||
color="white"
|
||||
bg-color="rgba(255,255,255,0.5)"
|
||||
width="2"
|
||||
size="16"
|
||||
@click.stop="cancelUpload"
|
||||
>
|
||||
<v-icon size="8">close</v-icon> </v-progress-circular
|
||||
>{{ uploadBatch?.progressPercent }}%
|
||||
</div>
|
||||
<SwipeableThumbnailsView :items="items" v-if="room.displayType == ROOM_TYPE_CHANNEL" v-bind="$attrs" />
|
||||
<v-container v-else fluid class="imageCollection">
|
||||
<v-row wrap>
|
||||
<v-col v-for="{ size, item } in layoutedItems" :key="item.file.name + item.file.size" :cols="size">
|
||||
<ThumbnailView :file="item" v-on:itemclick="onItemClick($event)" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<span v-html="linkify($$sanitize(messageText))" v-if="messageText" />
|
||||
<span class="edit-marker" v-if="event && event.replacingEventId() && !event.isRedacted()">
|
||||
{{ t("message.edited") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<GalleryItemsView
|
||||
:originalEvent="originalEvent"
|
||||
:items="items"
|
||||
:initialItem="showItem"
|
||||
v-if="!!showItem"
|
||||
v-on:close="showItem = undefined"
|
||||
/>
|
||||
</MessageOutgoing>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||
import { MessageProps, useMessage } from "./useMessage";
|
||||
import { ROOM_TYPE_CHANNEL } from "@/plugins/utils";
|
||||
import GalleryItemsView from "../../file_mode/GalleryItemsView.vue";
|
||||
import ThumbnailView from "../../file_mode/ThumbnailView.vue";
|
||||
import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.vue";
|
||||
import { inject, ref, Ref, unref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Attachment, AttachmentBatch } from "../../../models/attachment";
|
||||
|
||||
const { t } = useI18n();
|
||||
const $matrix: any = inject("globalMatrix");
|
||||
const $$sanitize: any = inject("globalSanitize");
|
||||
|
||||
let items: Ref<Attachment[]> = ref([]);
|
||||
const layoutedItems: Ref<{ size: number; item: Attachment }[]> = ref([]);
|
||||
const showItem: Ref<Attachment | undefined> = ref(undefined);
|
||||
|
||||
const uploadBatch: Ref<AttachmentBatch | undefined> = ref(undefined);
|
||||
|
||||
const props = defineProps<MessageProps>();
|
||||
|
||||
const { room } = props;
|
||||
|
||||
const {
|
||||
event,
|
||||
inReplyToSender,
|
||||
inReplyToText,
|
||||
messageText,
|
||||
linkify,
|
||||
} = useMessage($matrix, t, props, undefined, undefined);
|
||||
|
||||
const cancelUpload = () => {
|
||||
if (uploadBatch.value) {
|
||||
uploadBatch.value.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
const onItemClick = (event: any) => {
|
||||
showItem.value = event.item;
|
||||
};
|
||||
|
||||
const layout = () => {
|
||||
if (!items.value || items.value.length == 0) {
|
||||
layoutedItems.value = [];
|
||||
return;
|
||||
}
|
||||
let array = items.value.slice(0);
|
||||
let rows = [];
|
||||
while (array.length > 0) {
|
||||
if (array.length >= 7) {
|
||||
rows.push({ size: 6, item: array[0] });
|
||||
rows.push({ size: 6, item: array[1] });
|
||||
rows.push({ size: 12, item: array[2] });
|
||||
rows.push({ size: 3, item: array[3] });
|
||||
rows.push({ size: 3, item: array[4] });
|
||||
rows.push({ size: 3, item: array[5] });
|
||||
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: 6, item: array[1] });
|
||||
rows.push({ size: 12, item: array[2] });
|
||||
array = array.slice(3);
|
||||
} else if (array.length >= 2) {
|
||||
rows.push({ size: 6, item: array[0] });
|
||||
rows.push({ size: 6, item: array[1] });
|
||||
array = array.slice(2);
|
||||
} else {
|
||||
rows.push({ size: 12, item: array[0] });
|
||||
array = array.slice(1);
|
||||
}
|
||||
}
|
||||
layoutedItems.value = rows;
|
||||
};
|
||||
|
||||
watch(
|
||||
event,
|
||||
(updated) => {
|
||||
const batch = updated?.uploadBatch;
|
||||
if (batch) {
|
||||
uploadBatch.value = updated?.uploadBatch;
|
||||
items = ref(batch.attachments);
|
||||
layout();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bubble {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.imageCollection {
|
||||
border-radius: 15px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.row {
|
||||
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.col {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-upload-progress {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-family: "Inter";
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 10px;
|
||||
line-height: 125%;
|
||||
letter-spacing: 0.4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 2px;
|
||||
padding-right: 4px;
|
||||
.message-upload-progress__progress {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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;
|
||||
};
|
||||
13
src/plugins/touch.js
Normal file
13
src/plugins/touch.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import Hammer from "hammerjs";
|
||||
|
||||
export function singleOrDoubleTapRecognizer(element) {
|
||||
// reference: https://codepen.io/jtangelder/pen/xxYyJQ
|
||||
const hm = new Hammer.Manager(element);
|
||||
|
||||
hm.add(new Hammer.Tap({ event: "doubletap", taps: 2 }));
|
||||
hm.add(new Hammer.Tap({ event: "singletap" }));
|
||||
|
||||
hm.get("doubletap").recognizeWith("singletap");
|
||||
hm.get("singletap").requireFailure("doubletap");
|
||||
return hm;
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import imageResize from "image-resize";
|
|||
import { AutoDiscovery, Method } from "matrix-js-sdk";
|
||||
import User from "../models/user";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import Hammer from "hammerjs";
|
||||
import { Thread } from "matrix-js-sdk/lib/models/thread";
|
||||
import { imageSize } from "image-size";
|
||||
import dayjs from "dayjs";
|
||||
|
|
@ -144,13 +143,29 @@ class Util {
|
|||
var file = null;
|
||||
let decrypt = true;
|
||||
if (!!content.info && !!content.info.thumbnail_url) {
|
||||
url = matrixClient.mxcUrlToHttp(content.info.thumbnail_url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
|
||||
url = matrixClient.mxcUrlToHttp(
|
||||
content.info.thumbnail_url,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
useAuthedMedia
|
||||
);
|
||||
decrypt = false;
|
||||
if (content.info.thumbnail_info) {
|
||||
mime = content.info.thumbnail_info.mimetype;
|
||||
}
|
||||
} else if (content.url != null) {
|
||||
url = matrixClient.mxcUrlToHttp(content.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
|
||||
url = matrixClient.mxcUrlToHttp(
|
||||
content.url,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
useAuthedMedia
|
||||
);
|
||||
decrypt = false;
|
||||
if (content.info) {
|
||||
mime = content.info.mimetype;
|
||||
|
|
@ -175,13 +190,17 @@ class Util {
|
|||
throw new Error("No url found!");
|
||||
}
|
||||
|
||||
const response = await axios
|
||||
.get(url, useAuthedMedia ? {
|
||||
const response = await axios.get(
|
||||
url,
|
||||
useAuthedMedia
|
||||
? {
|
||||
responseType: "arraybuffer",
|
||||
headers: {
|
||||
Authorization: `Bearer ${matrixClient.getAccessToken()}`,
|
||||
},
|
||||
} : { responseType: "arraybuffer" });
|
||||
}
|
||||
: { responseType: "arraybuffer" }
|
||||
);
|
||||
const bytes = decrypt ? await this.decryptIfNeeded(file, response) : { buffer: response.data };
|
||||
return URL.createObjectURL(new Blob([bytes.buffer], { type: mime }));
|
||||
}
|
||||
|
|
@ -217,7 +236,7 @@ class Util {
|
|||
});
|
||||
}
|
||||
|
||||
sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent) {
|
||||
sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent, txnId) {
|
||||
var content = ContentHelpers.makeTextMessage(text);
|
||||
if (editedEvent) {
|
||||
content["m.relates_to"] = {
|
||||
|
|
@ -248,7 +267,7 @@ class Util {
|
|||
.join("\n");
|
||||
content.body = prefix + "\n\n" + content.body;
|
||||
}
|
||||
return this.sendMessage(matrixClient, roomId, "m.room.message", content);
|
||||
return this.sendMessage(matrixClient, roomId, "m.room.message", content, txnId);
|
||||
}
|
||||
|
||||
sendQuickReaction(matrixClient, roomId, emoji, event, extraData = {}) {
|
||||
|
|
@ -306,10 +325,10 @@ class Util {
|
|||
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.response", content);
|
||||
}
|
||||
|
||||
sendMessage(matrixClient, roomId, eventType, content) {
|
||||
sendMessage(matrixClient, roomId, eventType, content, txnId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
matrixClient
|
||||
.sendEvent(roomId, eventType, content, undefined, undefined)
|
||||
.sendEvent(roomId, eventType, content, txnId, undefined)
|
||||
.then((result) => {
|
||||
console.log("Message sent: ", result);
|
||||
resolve(result.event_id);
|
||||
|
|
@ -446,7 +465,9 @@ class Util {
|
|||
width: newWidth,
|
||||
height: newHeight,
|
||||
outputType: "blob",
|
||||
}).catch(() => {return Promise.resolve(undefined)});
|
||||
}).catch(() => {
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
if (scaled && file.size > scaled.size) {
|
||||
thumbnailData = new Uint8Array(await scaled.arrayBuffer());
|
||||
thumbnailInfo = {
|
||||
|
|
@ -483,12 +504,19 @@ class Util {
|
|||
messageContent.filename = file.name;
|
||||
}
|
||||
|
||||
let totalBytes = 0;
|
||||
let thumbBytes = 0;
|
||||
|
||||
const useEncryption = matrixClient.isRoomEncrypted(roomId);
|
||||
|
||||
const dataUploadOpts = {
|
||||
type: useEncryption ? "application/octet-stream" : file.type,
|
||||
name: description,
|
||||
progressHandler: onUploadProgress,
|
||||
progressHandler: ({ loaded, total }) => {
|
||||
if (onUploadProgress) {
|
||||
onUploadProgress({ loaded: loaded + thumbBytes, total: totalBytes });
|
||||
}
|
||||
},
|
||||
onlyContentUri: false,
|
||||
};
|
||||
|
||||
|
|
@ -498,6 +526,8 @@ class Util {
|
|||
data = encryptedBytes;
|
||||
}
|
||||
|
||||
totalBytes = data.length;
|
||||
|
||||
if (thumbnailData) {
|
||||
messageContent.thumbnail_info = thumbnailInfo;
|
||||
if (useEncryption) {
|
||||
|
|
@ -505,10 +535,16 @@ class Util {
|
|||
messageContent.info.thumbnail_file = encryptedFile;
|
||||
thumbnailData = encryptedBytes;
|
||||
}
|
||||
thumbBytes = thumbnailData.length;
|
||||
totalBytes += thumbnailData.length;
|
||||
const thumbnailUploadOpts = {
|
||||
type: useEncryption ? "application/octet-stream" : file.type,
|
||||
name: "thumb:" + description,
|
||||
progressHandler: onUploadProgress,
|
||||
progressHandler: ({ loaded, total }) => {
|
||||
if (onUploadProgress) {
|
||||
onUploadProgress({ loaded: loaded, total: totalBytes });
|
||||
}
|
||||
},
|
||||
onlyContentUri: false,
|
||||
};
|
||||
const thumbUploadPromise = matrixClient.uploadContent(thumbnailData, thumbnailUploadOpts);
|
||||
|
|
@ -1162,18 +1198,6 @@ class Util {
|
|||
return mobileTabletPattern.test(userAgent);
|
||||
}
|
||||
|
||||
singleOrDoubleTabRecognizer(element) {
|
||||
// reference: https://codepen.io/jtangelder/pen/xxYyJQ
|
||||
const hm = new Hammer.Manager(element);
|
||||
|
||||
hm.add(new Hammer.Tap({ event: "doubletap", taps: 2 }));
|
||||
hm.add(new Hammer.Tap({ event: "singletap" }));
|
||||
|
||||
hm.get("doubletap").recognizeWith("singletap");
|
||||
hm.get("singletap").requireFailure("doubletap");
|
||||
return hm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Possibly convert numerals to local representation (currently only for "bo" locale)
|
||||
* @param str String in which to convert numerals [0-9]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue