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

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

View file

@ -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(() => {

View file

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

View file

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

View 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>

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

13
src/plugins/touch.js Normal file
View 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;
}

View file

@ -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]