Support for retry when sending media
This commit is contained in:
parent
881270f149
commit
ce6398685f
7 changed files with 69 additions and 34 deletions
5
src/assets/icons/ic_arrow_narrow.vue
Normal file
5
src/assets/icons/ic_arrow_narrow.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 10V2M6 2L3 5M6 2L9 5" stroke="white" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
@ -21,7 +21,8 @@
|
||||||
"close": "close",
|
"close": "close",
|
||||||
"notify": "Notify",
|
"notify": "Notify",
|
||||||
"different_browser_title": "Try different browser",
|
"different_browser_title": "Try different browser",
|
||||||
"different_browser_content": "Some features may break. Copy and open link in a different browser."
|
"different_browser_content": "Some features may break. Copy and open link in a different browser.",
|
||||||
|
"retry": "Retry"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"start_private_chat": "Direct Message with this user",
|
"start_private_chat": "Direct Message with this user",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
@dragleave.prevent="dropTarget = false"
|
@dragleave.prevent="dropTarget = false"
|
||||||
@dragenter.prevent="dropTarget = true"
|
@dragenter.prevent="dropTarget = true"
|
||||||
>
|
>
|
||||||
<ThumbnailView :file="currentAttachment" />
|
<ThumbnailView :item="currentAttachment" />
|
||||||
<div v-if="currentAttachment && currentAttachment.status === 'loading'" class="send-attachments__selecting__current-item__preparing">
|
<div v-if="currentAttachment && currentAttachment.status === 'loading'" class="send-attachments__selecting__current-item__preparing">
|
||||||
<div style="font-size: 0.7em; opacity: 0.7">
|
<div style="font-size: 0.7em; opacity: 0.7">
|
||||||
<v-progress-circular indeterminate class="mb-0"></v-progress-circular>
|
<v-progress-circular indeterminate class="mb-0"></v-progress-circular>
|
||||||
|
|
|
||||||
|
|
@ -29,48 +29,58 @@ import { EventAttachment } from "../../models/eventAttachment";
|
||||||
import { useThumbnail } from "../messages/composition/useThumbnail";
|
import { useThumbnail } from "../messages/composition/useThumbnail";
|
||||||
import { Attachment } from "../../models/attachment";
|
import { Attachment } from "../../models/attachment";
|
||||||
import ImageWithProgress from "../ImageWithProgress.vue";
|
import ImageWithProgress from "../ImageWithProgress.vue";
|
||||||
|
|
||||||
|
function isEventAttachment(source: EventAttachment | Attachment | undefined): source is EventAttachment {
|
||||||
|
return (source as EventAttachment).event !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAttachment(source: EventAttachment | Attachment | undefined): source is Attachment {
|
||||||
|
return (source as Attachment).file !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const $$sanitize: any = inject("globalSanitize");
|
const $$sanitize: any = inject("globalSanitize");
|
||||||
|
|
||||||
const thumbnailRef = useTemplateRef("thumbnailRef");
|
const thumbnailRef = useTemplateRef("thumbnailRef");
|
||||||
|
|
||||||
interface ThumbnailProps {
|
interface ThumbnailProps {
|
||||||
item?: EventAttachment;
|
item?: EventAttachment | Attachment;
|
||||||
file?: Attachment;
|
|
||||||
previewOnly?: boolean;
|
previewOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThumbnailEmits = {
|
type ThumbnailEmits = {
|
||||||
(event: "itemclick", value: { item: EventAttachment }): void;
|
(event: "itemclick", value: { item: EventAttachment | Attachment }): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps<ThumbnailProps>();
|
const props = defineProps<ThumbnailProps>();
|
||||||
const { item, file, previewOnly = false } = props;
|
const { item, previewOnly = false } = props;
|
||||||
const emits = defineEmits<ThumbnailEmits>();
|
const emits = defineEmits<ThumbnailEmits>();
|
||||||
|
|
||||||
let { isVideo, isImage, fileTypeIcon, fileTypeIconClass, fileName, fileSize } = useThumbnail(file?.file ?? item?.event);
|
let { isVideo, isImage, fileTypeIcon, fileTypeIconClass, fileName, fileSize } = useThumbnail(isEventAttachment(props.item) ? props.item.event : isAttachment(props.item) ? props.item.file : undefined);
|
||||||
|
|
||||||
const fileURL: Ref<string | undefined> = ref(undefined);
|
const fileURL: Ref<string | undefined> = ref(undefined);
|
||||||
const source: Ref<string | undefined> = ref(undefined);
|
const source: Ref<string | undefined> = ref(undefined);
|
||||||
const poster: Ref<string | undefined> = ref(undefined);
|
const poster: Ref<string | undefined> = ref(undefined);
|
||||||
|
|
||||||
const updateSource = () => {
|
const updateSource = () => {
|
||||||
if (props.item) {
|
if (isEventAttachment(props.item)) {
|
||||||
if (isVideo.value || props.item.src) {
|
const eventAttachment = props.item;
|
||||||
source.value = props.item.src;
|
if (isVideo.value || eventAttachment.src) {
|
||||||
|
source.value = eventAttachment.src;
|
||||||
} else if (previewOnly) {
|
} else if (previewOnly) {
|
||||||
props.item.loadThumbnail().then((url) => {
|
eventAttachment.loadThumbnail().then((url) => {
|
||||||
source.value = url.data;
|
source.value = url.data;
|
||||||
})
|
})
|
||||||
} else if (isImage.value) {
|
} else if (isImage.value) {
|
||||||
props.item.loadSrc().then((url) => {
|
eventAttachment.loadSrc().then((url) => {
|
||||||
source.value = url.data;
|
source.value = url.data;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (props.file) {
|
} else if (isAttachment(props.item)) {
|
||||||
|
const attachment = props.item;
|
||||||
if (fileURL.value) {
|
if (fileURL.value) {
|
||||||
URL.revokeObjectURL(fileURL.value);
|
URL.revokeObjectURL(fileURL.value);
|
||||||
}
|
}
|
||||||
fileURL.value = URL.createObjectURL(props.file.file);
|
fileURL.value = URL.createObjectURL(attachment.file);
|
||||||
source.value = fileURL.value;
|
source.value = fileURL.value;
|
||||||
} else {
|
} else {
|
||||||
source.value = undefined;
|
source.value = undefined;
|
||||||
|
|
@ -78,11 +88,12 @@ const updateSource = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePoster = () => {
|
const updatePoster = () => {
|
||||||
if (props.item && isVideo.value) {
|
if (isEventAttachment(props.item) && isVideo.value) {
|
||||||
if (props.item.thumbnail) {
|
const eventAttachment = props.item;
|
||||||
poster.value = props.item.thumbnail;
|
if (eventAttachment.thumbnail) {
|
||||||
|
poster.value = eventAttachment.thumbnail;
|
||||||
} else {
|
} else {
|
||||||
props.item
|
eventAttachment
|
||||||
.loadThumbnail()
|
.loadThumbnail()
|
||||||
.then((url) => {
|
.then((url) => {
|
||||||
poster.value = url.data;
|
poster.value = url.data;
|
||||||
|
|
@ -99,7 +110,7 @@ updatePoster();
|
||||||
|
|
||||||
|
|
||||||
watch(props, (props: ThumbnailProps) => {
|
watch(props, (props: ThumbnailProps) => {
|
||||||
const updates = useThumbnail(props.file?.file ?? props.item?.event);
|
const updates = useThumbnail(isEventAttachment(props.item) ? props.item.event : isAttachment(props.item) ? props.item.file : undefined);
|
||||||
isVideo.value = updates.isVideo.value;
|
isVideo.value = updates.isVideo.value;
|
||||||
isImage.value = updates.isImage.value;
|
isImage.value = updates.isImage.value;
|
||||||
fileTypeIcon = updates.fileTypeIcon;
|
fileTypeIcon = updates.fileTypeIcon;
|
||||||
|
|
@ -111,23 +122,24 @@ watch(props, (props: ThumbnailProps) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadingProgress = computed(() => {
|
const loadingProgress = computed(() => {
|
||||||
if (item) {
|
if (isEventAttachment(item)) {
|
||||||
return previewOnly ? item.thumbnailProgress : item.srcProgress;
|
const eventAttachment = item;
|
||||||
|
return previewOnly ? eventAttachment.thumbnailProgress : eventAttachment.srcProgress;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (thumbnailRef.value && (item as EventAttachment)) {
|
if (thumbnailRef.value && item) {
|
||||||
const hammerInstance = singleOrDoubleTapRecognizer(thumbnailRef.value);
|
const hammerInstance = singleOrDoubleTapRecognizer(thumbnailRef.value);
|
||||||
hammerInstance.on("singletap doubletap", (ev: any) => {
|
hammerInstance.on("singletap doubletap", (ev: any) => {
|
||||||
if (ev.type === "singletap") {
|
if (ev.type === "singletap") {
|
||||||
emits("itemclick", { item: item as EventAttachment });
|
emits("itemclick", { item: item });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!previewOnly && item && (item as EventAttachment)) {
|
if (!previewOnly && item && isEventAttachment(item)) {
|
||||||
(item as EventAttachment).loadSrc();
|
item.loadSrc();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<MessageOutgoing :is="rootComponent" ref="root" v-bind="{ ...$props, ...$attrs }">
|
<MessageOutgoing v-bind="{ ...$props, ...$attrs }">
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<div class="original-message" v-if="inReplyToText">
|
<div class="original-message" v-if="inReplyToText">
|
||||||
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
|
|
@ -20,11 +20,15 @@
|
||||||
<v-icon size="8">close</v-icon> </v-progress-circular
|
<v-icon size="8">close</v-icon> </v-progress-circular
|
||||||
>{{ uploadBatch?.progressPercent }}%
|
>{{ uploadBatch?.progressPercent }}%
|
||||||
</div>
|
</div>
|
||||||
|
<div class="message-upload-progress clickable" @click.stop="retryUpload" v-else-if="unref(uploadBatch?.sendingStatus) === 'failed'">
|
||||||
|
<v-icon size="12">$vuetify.icons.ic_arrow_narrow</v-icon>
|
||||||
|
<div>{{ t("global.retry") }}</div>
|
||||||
|
</div>
|
||||||
<SwipeableThumbnailsView :items="items" v-if="room.displayType == ROOM_TYPE_CHANNEL" v-bind="$attrs" />
|
<SwipeableThumbnailsView :items="items" v-if="room.displayType == ROOM_TYPE_CHANNEL" v-bind="$attrs" />
|
||||||
<v-container v-else fluid class="imageCollection">
|
<v-container v-else fluid class="imageCollection">
|
||||||
<v-row wrap>
|
<v-row wrap>
|
||||||
<v-col v-for="{ size, item } in layoutedItems" :key="item.file.name + item.file.size" :cols="size">
|
<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)" />
|
<ThumbnailView :item="item" v-on:itemclick="onItemClick($event)" />
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
@ -83,6 +87,12 @@ const cancelUpload = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const retryUpload = () => {
|
||||||
|
if (uploadBatch.value) {
|
||||||
|
uploadBatch.value.send(uploadBatch.value.sendingRootMessage.value ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onItemClick = (event: any) => {
|
const onItemClick = (event: any) => {
|
||||||
showItem.value = event.item;
|
showItem.value = event.item;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ export type AttachmentSendInfo = {
|
||||||
randomRotation: number; // For UI effects
|
randomRotation: number; // For UI effects
|
||||||
randomTranslationX: number; // For UI effects
|
randomTranslationX: number; // For UI effects
|
||||||
randomTranslationY: number; // For UI effects
|
randomTranslationY: number; // For UI effects
|
||||||
proofHintFlags?: ProofHintFlags;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AttachmentThumbnail = {
|
export type AttachmentThumbnail = {
|
||||||
|
|
@ -59,6 +58,7 @@ export type Attachment = {
|
||||||
useScaled: boolean;
|
useScaled: boolean;
|
||||||
src?: string;
|
src?: string;
|
||||||
proof?: Proof;
|
proof?: Proof;
|
||||||
|
proofHintFlags?: ProofHintFlags;
|
||||||
thumbnail?: AttachmentThumbnail;
|
thumbnail?: AttachmentThumbnail;
|
||||||
sendInfo: AttachmentSendInfo;
|
sendInfo: AttachmentSendInfo;
|
||||||
};
|
};
|
||||||
|
|
@ -66,6 +66,7 @@ export type Attachment = {
|
||||||
export type AttachmentBatch = {
|
export type AttachmentBatch = {
|
||||||
sendingStatus: Ref<AttachmentSendStatus>;
|
sendingStatus: Ref<AttachmentSendStatus>;
|
||||||
sendingRootEventId: Ref<string | undefined>;
|
sendingRootEventId: Ref<string | undefined>;
|
||||||
|
sendingRootMessage: Ref<string | undefined>;
|
||||||
progressPercent: Ref<number>;
|
progressPercent: Ref<number>;
|
||||||
attachments: Attachment[];
|
attachments: Attachment[];
|
||||||
attachmentsSentCount: ComputedRef<number>;
|
attachmentsSentCount: ComputedRef<number>;
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ export class AttachmentManager {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
attachment.proof = await proofmode.proofCheckFile(file);
|
attachment.proof = await proofmode.proofCheckFile(file);
|
||||||
|
attachment.proofHintFlags = extractProofHintFlags(attachment.proof);
|
||||||
|
|
||||||
// Default to scaled version if the image does not contain Content Credentials
|
// Default to scaled version if the image does not contain Content Credentials
|
||||||
//
|
//
|
||||||
|
|
@ -420,6 +421,7 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room
|
||||||
|
|
||||||
const sendingStatus: Ref<AttachmentSendStatus> = ref("initial");
|
const sendingStatus: Ref<AttachmentSendStatus> = ref("initial");
|
||||||
const sendingRootEventId: Ref<string | undefined> = ref(undefined);
|
const sendingRootEventId: Ref<string | undefined> = ref(undefined);
|
||||||
|
const sendingRootMessage: Ref<string | undefined> = ref(undefined);
|
||||||
const sendingPromise: Ref<Promise<any> | undefined> = ref(undefined);
|
const sendingPromise: Ref<Promise<any> | undefined> = ref(undefined);
|
||||||
const attachments: Ref<Attachment[]> = ref([]);
|
const attachments: Ref<Attachment[]> = ref([]);
|
||||||
|
|
||||||
|
|
@ -528,6 +530,8 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room
|
||||||
const send = async (message: string): Promise<any> => {
|
const send = async (message: string): Promise<any> => {
|
||||||
if (!matrixClient || !room) return Promise.reject("Not configured");
|
if (!matrixClient || !room) return Promise.reject("Not configured");
|
||||||
sendingStatus.value = "sending";
|
sendingStatus.value = "sending";
|
||||||
|
progressPercent.value = 0;
|
||||||
|
sendingRootMessage.value = message;
|
||||||
|
|
||||||
attachments.value.forEach((attachment) => {
|
attachments.value.forEach((attachment) => {
|
||||||
let sendInfo: AttachmentSendInfo = {
|
let sendInfo: AttachmentSendInfo = {
|
||||||
|
|
@ -539,7 +543,6 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room
|
||||||
randomTranslationX: 0,
|
randomTranslationX: 0,
|
||||||
randomTranslationY: 0,
|
randomTranslationY: 0,
|
||||||
promise: undefined,
|
promise: undefined,
|
||||||
proofHintFlags: extractProofHintFlags(attachment.proof),
|
|
||||||
};
|
};
|
||||||
attachment.sendInfo = shallowReactive(sendInfo);
|
attachment.sendInfo = shallowReactive(sendInfo);
|
||||||
attachment.proof = undefined;
|
attachment.proof = undefined;
|
||||||
|
|
@ -555,6 +558,7 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room
|
||||||
event.uploadBatch = {
|
event.uploadBatch = {
|
||||||
sendingStatus,
|
sendingStatus,
|
||||||
sendingRootEventId,
|
sendingRootEventId,
|
||||||
|
sendingRootMessage,
|
||||||
progressPercent,
|
progressPercent,
|
||||||
attachments: unref(attachments),
|
attachments: unref(attachments),
|
||||||
attachmentsSentCount,
|
attachmentsSentCount,
|
||||||
|
|
@ -572,8 +576,7 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room
|
||||||
};
|
};
|
||||||
room.on(RoomEvent.LocalEchoUpdated, onLocalEchoUpdated);
|
room.on(RoomEvent.LocalEchoUpdated, onLocalEchoUpdated);
|
||||||
|
|
||||||
const promise = utils
|
const promise = (sendingRootEventId.value ? Promise.resolve(sendingRootEventId.value) : utils.sendTextMessage(matrixClient, room.roomId, message, undefined, undefined, txnId))
|
||||||
.sendTextMessage(matrixClient, room.roomId, message, undefined, undefined, txnId)
|
|
||||||
.then((eventId: string) => {
|
.then((eventId: string) => {
|
||||||
sendingRootEventId.value = eventId;
|
sendingRootEventId.value = eventId;
|
||||||
|
|
||||||
|
|
@ -582,7 +585,7 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room
|
||||||
if (index < attachments.value.length) {
|
if (index < attachments.value.length) {
|
||||||
const attachment = attachments.value[index];
|
const attachment = attachments.value[index];
|
||||||
const item = attachment;
|
const item = attachment;
|
||||||
if (item.sendInfo.status !== "initial") {
|
if (item.sendInfo.status !== "initial" && item.sendInfo.status !== "failed") {
|
||||||
return getItemPromise(++index);
|
return getItemPromise(++index);
|
||||||
}
|
}
|
||||||
item.sendInfo.status = "sending";
|
item.sendInfo.status = "sending";
|
||||||
|
|
@ -637,12 +640,13 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room
|
||||||
item.sendInfo.status = "sent";
|
item.sendInfo.status = "sent";
|
||||||
item.sendInfo.statusDate = Date.now();
|
item.sendInfo.statusDate = Date.now();
|
||||||
})
|
})
|
||||||
.catch((ignorederr: any) => {
|
.catch((error: any) => {
|
||||||
if (item.sendInfo.promise?.aborted) {
|
if (item.sendInfo.promise?.aborted) {
|
||||||
item.sendInfo.status = "canceled";
|
item.sendInfo.status = "canceled";
|
||||||
} else {
|
} else {
|
||||||
console.error("ERROR", ignorederr);
|
console.error("ERROR", error);
|
||||||
item.sendInfo.status = "failed";
|
item.sendInfo.status = "failed";
|
||||||
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
@ -659,6 +663,7 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room
|
||||||
})
|
})
|
||||||
.catch((err: any) => {
|
.catch((err: any) => {
|
||||||
console.error("Upload error", err);
|
console.error("Upload error", err);
|
||||||
|
sendingStatus.value = "failed";
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
room.off(RoomEvent.LocalEchoUpdated, onLocalEchoUpdated);
|
room.off(RoomEvent.LocalEchoUpdated, onLocalEchoUpdated);
|
||||||
|
|
@ -670,6 +675,7 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room
|
||||||
return {
|
return {
|
||||||
sendingStatus,
|
sendingStatus,
|
||||||
sendingRootEventId,
|
sendingRootEventId,
|
||||||
|
sendingRootMessage,
|
||||||
progressPercent,
|
progressPercent,
|
||||||
attachments: unref(attachments),
|
attachments: unref(attachments),
|
||||||
attachmentsSentCount,
|
attachmentsSentCount,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue