+ showMoreMessageOperations({event: selectedEvent, anchor: $event.anchor})
+ " :originalEvent="selectedEvent" :timelineSet="timelineSet" />
@@ -49,10 +58,11 @@
+
-
+
+
-
+
{{ $t('message.unread_messages') }}
@@ -97,6 +115,7 @@
{{ replyToEvent.getContent().body | latestReply }}
+
{{ replyToThreadMessage }}
{{ $t("message.reply_image") }}
{{ $t("message.reply_audio_message") }}
{{ $t("message.reply_video") }}
@@ -121,7 +140,7 @@
{{ typingMembersString }}
-
+
- {{ $t('message.send_attachements_dialog_title') }}
-
-
- {{ $t('message.images') }}
-
-
-
-
-
- {{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}
-
- {{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}
-
- ({{ formatBytes(currentImageInput.scaledSize) }})
-
+
+ {{ this.$t("message.preparing_to_upload")}}
+
+
+
+
+ {{ currentSendErrorExceededFile }}
+ {{ $t('message.send_attachements_dialog_title') }}
+
+
+
+ {{ $t('message.images') }}
+
+
+
+
+
+
+
+
+ {{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}
+
+ {{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}
+
+
+
+ ({{ formatBytes(currentImageInput.scaledSize) }})
+
+
+ ({{ formatBytes(currentImageInput.actualSize) }})
+
+
+
+
-
-
-
-
- {{ $t('message.files') }}
-
-
-
-
{{ $t('message.file') }}: {{ currentImageInputPath.name }}
-
({{ formatBytes(currentImageInputPath.size) }})
+
+
+
+ {{ $t('message.files') }}
+
+
+
+ {{ $t('message.file') }}: {{ currentImageInputPath.name }}
+ ({{ formatBytes(currentImageInputPath.size) }})
+
+
-
-
-
-
-
-
- {{ currentSendError }}
- {{ currentSendProgress }}
-
-
- {{ $t("menu.cancel") }}
-
- {{ $t("menu.send") }}
-
+
+
+
+
+
+ {{ currentSendError }}
+
+
+ {{ $t("menu.cancel") }}
+
+ {{ $t("menu.send") }}
+
+
@@ -282,6 +323,46 @@
+
+
+
+
+
notifications_active
+
+ {{ $t("notification.dialog.title") }}
+
+
{{ $t("notification.dialog.body") }}
+
+
+
+ {{ $t("global.close") }}
+
+
+ {{ $t("notification.dialog.enable") }}
+
+
+
+
+
@@ -292,9 +373,11 @@ import util, { ROOM_TYPE_VOICE_MODE, ROOM_TYPE_FILE_MODE } from "../plugins/util
import MessageOperations from "./messages/MessageOperations.vue";
import AvatarOperations from "./messages/AvatarOperations.vue";
import ChatHeader from "./ChatHeader";
+import ChatHeaderPrivate from "./ChatHeaderPrivate.vue";
import VoiceRecorder from "./VoiceRecorder";
import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
+import DirectChatWelcomeHeader from "./DirectChatWelcomeHeader";
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue";
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
@@ -302,8 +385,12 @@ import BottomSheet from "./BottomSheet.vue";
import ImageResize from "image-resize";
import CreatePollDialog from "./CreatePollDialog.vue";
import chatMixin from "./chatMixin";
+import sendAttachmentsMixin from "./sendAttachmentsMixin";
import AudioLayout from "./AudioLayout.vue";
import FileDropLayout from "./file_mode/FileDropLayout";
+import { requestNotificationPermission, windowNotificationPermission } from "../plugins/notificationAndServiceWorker.js"
+import roomTypeMixin from "./roomTypeMixin";
+import roomMembersMixin from "./roomMembersMixin";
const sizeOf = require("image-size");
const dataUriToBuffer = require("data-uri-to-buffer");
@@ -339,13 +426,15 @@ ScrollPosition.prototype.prepareFor = function (direction) {
export default {
name: "Chat",
- mixins: [chatMixin],
+ mixins: [chatMixin, roomTypeMixin, sendAttachmentsMixin, roomMembersMixin],
components: {
ChatHeader,
+ ChatHeaderPrivate,
MessageOperations,
VoiceRecorder,
RoomInfoBottomSheet,
CreatedRoomWelcomeHeader,
+ DirectChatWelcomeHeader,
NoHistoryRoomWelcomeHeader,
MessageOperationsBottomSheet,
StickerPickerBottomSheet,
@@ -370,10 +459,9 @@ export default {
scrollPosition: null,
currentFileInputs: null,
- currentSendOperation: null,
- currentSendProgress: null,
currentSendShowSendButton: true,
currentSendError: null,
+ currentSendErrorExceededFile: null,
showEmojiPicker: false,
selectedEvent: null,
editedEvent: null,
@@ -411,7 +499,10 @@ export default {
lastRR: null,
/** If we just created this room, show a small welcome header with info */
- showCreatedRoomWelcomeHeader: false,
+ hideCreatedRoomWelcomeHeader: false,
+
+ /** For direct chats, show a small welcome header with info about the other party */
+ hideDirectChatWelcomeHeader: false,
/** An array of recent emojis. Used in the "message operations" popup. */
recentEmojis: [],
@@ -433,7 +524,8 @@ export default {
Symbols: this.$t("emoji.categories.symbols"),
Places: this.$t("emoji.categories.places")
}
- }
+ },
+ notificationDialog: false
};
},
@@ -443,7 +535,7 @@ export default {
if (contentArr[0] === "") {
contentArr.shift();
}
- return contentArr[0].replace(/^> (<.*> )?/g, "");
+ return (contentArr && contentArr.length > 0) ? contentArr[0].replace(/^> (<.*> )?/g, "") : "";
},
},
@@ -471,10 +563,10 @@ export default {
computed: {
nonImageFiles() {
- return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file.type.includes("image/"))
+ return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file?.type.includes("image/"))
},
imageFiles() {
- return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => file.type.includes("image/"))
+ return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => file?.type.includes("image/"))
},
isCurrentFileInputsAnArray() {
return Array.isArray(this.currentFileInputs)
@@ -595,13 +687,13 @@ export default {
useVoiceMode: {
get: function () {
if (!this.$config.experimental_voice_mode) return false;
- return util.roomDisplayType(this.room) === ROOM_TYPE_VOICE_MODE;
+ return (util.roomDisplayTypeOverride(this.room) || this.roomDisplayType) === ROOM_TYPE_VOICE_MODE;
},
},
useFileModeNonAdmin: {
get: function() {
if (!this.$config.experimental_file_mode) return false;
- return util.roomDisplayType(this.room) === ROOM_TYPE_FILE_MODE && !this.canCreatePoll; // TODO - Check user or admin
+ return (util.roomDisplayTypeOverride(this.room) || this.roomDisplayType) === ROOM_TYPE_FILE_MODE && !this.canCreatePoll; // TODO - Check user or admin
}
},
@@ -632,6 +724,88 @@ export default {
}
}
return this.events;
+ },
+
+ roomCreatedByUsRecently() {
+ const createEvent = this.room && this.room.currentState.getStateEvents("m.room.create", "");
+ if (createEvent) {
+ const creatorId = createEvent.getContent().creator;
+ return (creatorId == this.$matrix.currentUserId && createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */);
+ }
+ return false;
+ },
+
+ isDirectRoom() {
+ return this.room && this.room.getJoinRule() == "invite" && this.joinedAndInvitedMembers.length == 2;
+ },
+
+ isPublicRoom() {
+ return this.room && this.room.getJoinRule() == "public";
+ },
+
+ showCreatedRoomWelcomeHeader() {
+ return !this.hideCreatedRoomWelcomeHeader && this.roomCreatedByUsRecently && !this.isDirectRoom;
+ },
+
+ showDirectChatWelcomeHeader() {
+ return !this.hideDirectChatWelcomeHeader && this.roomCreatedByUsRecently && this.isDirectRoom;
+ },
+
+ chatContainerStyle() {
+ if (this.$config.chat_backgrounds && this.room && this.roomId) {
+ const roomType = this.isDirectRoom ? "direct" : this.isPublicRoom ? "public" : "invite";
+ let backgrounds = this.$config.chat_backgrounds[roomType] || this.$config.chat_backgrounds["all"];
+ if (backgrounds) {
+ const numBackgrounds = backgrounds.length;
+
+ // If we have several backgrounds set, use the room ID to calculate
+ // an int hash value, then take mod of that to select a background to use.
+ // That way, we always get the same one, since room IDs don't change.
+
+ // From: https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
+ const hashCode = function (s) {
+ var hash = 0,
+ i, chr;
+ if (s.length === 0) return hash;
+ for (i = 0; i < s.length; i++) {
+ chr = s.charCodeAt(i);
+ hash = ((hash << 5) - hash) + chr;
+ hash |= 0; // Convert to 32bit integer
+ }
+ return hash;
+ }
+
+ // Adapted from: https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url
+ const validUrl = function (s) {
+ let url;
+ try {
+ url = new URL(s, window.location);
+ } catch (err) {
+ return false;
+ }
+ return url.protocol === "http:" || url.protocol === "https:" || url.protocol === "data:";
+ }
+
+ const index = Math.abs(hashCode(this.roomId)) % numBackgrounds;
+ const background = backgrounds[index];
+ if (background && validUrl(background)) {
+ return "background-image: url(" + background + ");background-repeat: repeat";
+ }
+ }
+ }
+ return "";
+ },
+
+ /**
+ * If we are replying to a (media) thread, this is the hint we show when replying.
+ */
+ replyToThreadMessage() {
+ if (this.replyToEvent && this.timelineSet) {
+ return this.$t("message.sent_media", {count: this.timelineSet.relations
+ .getAllChildEventsForEvent(this.replyToEvent.getId())
+ .filter((e) => util.downloadableTypes().includes(e.getContent().msgtype)).length});
+ }
+ return "";
}
},
@@ -640,6 +814,8 @@ export default {
immediate: true,
handler(value, oldValue) {
if (value && !oldValue) {
+ this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
+ this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
console.log("Loading finished!");
}
}
@@ -661,7 +837,8 @@ export default {
this.timelineWindow = null;
this.typingMembers = [];
this.initialLoadDone = false;
- this.showCreatedRoomWelcomeHeader = false;
+ this.hideDirectChatWelcomeHeader = false;
+ this.hideCreatedRoomWelcomeHeader = false;
// Stop RR timer
this.stopRRTimer();
@@ -681,7 +858,7 @@ export default {
}
});
} else {
- this.initialLoadDone = true;
+ this.setInitialLoadDone();
return; // no room
}
},
@@ -708,8 +885,10 @@ export default {
var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
top = rectAnchor.top - rectChat.top - 50;
left = rectAnchor.left - rectChat.left - 75;
- if (left + rectOps.width >= rectChat.right) {
+ if (left + rectOps.width + 10 >= rectChat.right) {
left = rectChat.right - rectOps.width - 10; // No overflow
+ } else if (left < 0) {
+ left = 0;
}
}
}
@@ -726,17 +905,28 @@ export default {
},
methods: {
- onRoomJoined(initialEventId) {
- // Was this room just created (by you)? Show a small info header in
- // that case!
- const createEvent = this.room.currentState.getStateEvents("m.room.create", "");
- if (createEvent) {
- const creatorId = createEvent.getContent().creator;
- if (creatorId == this.$matrix.currentUserId && createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */) {
- this.showCreatedRoomWelcomeHeader = true;
- }
+ /**
+ * Set initialLoadDone to 'true'. First process all events, setting threadParent and replyEvent if needed.
+ */
+ setInitialLoadDone() {
+ this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
+ this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
+ this.initialLoadDone = true;
+ console.log("Loading finished!");
+ },
+ windowNotificationPermission,
+ onNotificationDialog() {
+ if(this.windowNotificationPermission() === 'denied') {
+ alert(this.$t("notification.blocked_message"));
+ } else if(this.windowNotificationPermission() === 'default') {
+ this.notificationDialog = true;
}
-
+ },
+ onNotifyRequest() {
+ requestNotificationPermission()
+ this.notificationDialog = false;
+ },
+ onRoomJoined(initialEventId) {
// Listen to events
this.$matrix.on("Room.timeline", this.onEvent);
this.$matrix.on("RoomMember.typing", this.onUserTyping);
@@ -778,10 +968,24 @@ export default {
console.log("ERROR " + err);
})
.finally(() => {
- self.initialLoadDone = true;
- if (initialEventId && !this.showCreatedRoomWelcomeHeader) {
- self.scrollToEvent(initialEventId);
- } else if (this.showCreatedRoomWelcomeHeader) {
+// const [timelineEvents, threadedEvents, unknownRelations] =
+// this.room.partitionThreadedEvents(self.events);
+// this.$matrix.matrixClient.processAggregatedTimelineEvents(this.room, timelineEvents);
+// //room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
+// this.$matrix.matrixClient.processThreadEvents(this.room, threadedEvents, true);
+// unknownRelations.forEach((event) => this.room.relations.aggregateChildEvent(event));
+
+ this.setInitialLoadDone();
+ if (initialEventId && !this.showCreatedRoomWelcomeHeader) {
+ const event = this.room.findEventById(initialEventId);
+ this.$nextTick(() => {
+ if (event && event.parentThread) {
+ self.scrollToEvent(event.parentThread.getId());
+ } else {
+ self.scrollToEvent(initialEventId);
+ }
+ });
+ } else if (this.showCreatedRoomWelcomeHeader || this.showDirectChatWelcomeHeader) {
self.onScroll();
}
self.restartRRTimer();
@@ -795,7 +999,7 @@ export default {
} else {
// Error. Done loading.
this.events = this.timelineWindow.getEvents();
- this.initialLoadDone = true;
+ this.setInitialLoadDone();
}
})
.finally(() => {
@@ -929,12 +1133,83 @@ export default {
this.restartRRTimer();
},
+
+ setParentThread(event) {
+ const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
+ if (parentEvent) {
+ Vue.set(parentEvent, "isMxThread", true);
+ Vue.set(event, "parentThread", parentEvent);
+ } else {
+ // Try to load from server.
+ this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.threadRootId).then((tl) => {
+ if (tl) {
+ const parentEvent = tl.getEvents().find((e) => e.getId() === event.threadRootId);
+ if (parentEvent) {
+ this.events = this.timelineWindow.getEvents();
+ const fn = () => {
+ Vue.set(parentEvent, "isMxThread", true);
+ Vue.set(event, "parentThread", parentEvent);
+ };
+ if (this.initialLoadDone) {
+ const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
+ const element = document.querySelector(sel);
+ if (element) {
+ this.onLayoutChange(fn, element);
+ } else {
+ fn();
+ }
+ } else {
+ fn();
+ }
+ }
+ }
+ });
+ }
+ },
+
+ setReplyToEvent(event) {
+ const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
+ if (parentEvent) {
+ Vue.set(event, "replyEvent", parentEvent);
+ } else {
+ // Try to load from server.
+ this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.replyEventId)
+ .then((tl) => {
+ if (tl) {
+ const parentEvent = tl.getEvents().find((e) => e.getId() === event.replyEventId);
+ if (parentEvent) {
+ this.events = this.timelineWindow.getEvents();
+ const fn = () => {Vue.set(event, "replyEvent", parentEvent);};
+ if (this.initialLoadDone) {
+ const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
+ const element = document.querySelector(sel);
+ if (element) {
+ this.onLayoutChange(fn, element);
+ } else {
+ fn();
+ }
+ } else {
+ fn();
+ }
+ }
+ }
+ }).catch(e => console.error(e));
+ }
+ },
+
onEvent(event) {
//console.log("OnEvent", JSON.stringify(event));
if (event.getRoomId() !== this.roomId) {
return; // Not for this room
}
+ if (this.initialLoadDone && event.threadRootId && !event.parentThread) {
+ this.setParentThread(event);
+ }
+ if (this.initialLoadDone && event.replyEventId && !event.replyEvent) {
+ this.setReplyToEvent(event);
+ }
+
const loadingDone = this.initialLoadDone;
this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
@@ -942,7 +1217,7 @@ export default {
this.paginateBackIfNeeded();
}
- if (loadingDone && event.forwardLooking && !event.isRelation()) {
+ if (loadingDone && event.forwardLooking && (!event.isRelation() || event.isMxThread || event.threadRootId || event.parentThread )) {
// If we are at bottom, scroll to see new events...
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
const container = this.chatContainer;
@@ -954,7 +1229,7 @@ export default {
this.handleScrolledToBottom(scrollToSeeNew);
// If kick or ban event, redirect to "goodbye"...
- if (event.getType() === "m.room.member" &&
+ if (event.getType() === "m.room.member" &&
event.getStateKey() == this.$matrix.currentUserId &&
(event.getPrevContent() || {}).membership == "join" &&
(
@@ -1015,15 +1290,19 @@ export default {
this.$refs.attachment.click();
},
- optimizeImage(e,event,file) {
- file.image = e.target.result;
- file.dimensions = null;
+ optimizeImage(evt,file) {
+ let fileObj = {}
+ fileObj.image = evt.target.result;
+ fileObj.dimensions = null;
+ fileObj.type = file.type;
+ fileObj.actualSize = file.size;
+ fileObj.actualFile = file
try {
- file.dimensions = sizeOf(dataUriToBuffer(e.target.result));
+ fileObj.dimensions = sizeOf(dataUriToBuffer(evt.target.result));
// Need to resize?
- const w = file.dimensions.width;
- const h = file.dimensions.height;
+ const w = fileObj.dimensions.width;
+ const h = fileObj.dimensions.height;
if (w > 640 || h > 640) {
var aspect = w / h;
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
@@ -1035,19 +1314,19 @@ export default {
outputType: "blob",
});
imageResize
- .play(event.target)
+ .play(evt.target.result)
.then((img) => {
Vue.set(
- file,
+ fileObj,
"scaled",
new File([img], file.name, {
type: img.type,
lastModified: Date.now(),
})
);
- Vue.set(file, "useScaled", true);
- Vue.set(file, "scaledSize", img.size);
- Vue.set(file, "scaledDimensions", {
+ Vue.set(fileObj, "useScaled", true);
+ Vue.set(fileObj, "scaledSize", img.size);
+ Vue.set(fileObj, "scaledDimensions", {
width: newWidth,
height: newHeight,
});
@@ -1059,24 +1338,19 @@ export default {
} catch (error) {
console.error("Failed to get image dimensions: " + error);
}
- return file
+ return fileObj
},
- handleFileReader(event, file) {
+ handleFileReader(file) {
if (file) {
+ let optimizedFileObj;
var reader = new FileReader();
- reader.onload = (e) => {
+ reader.onload = (evt) => {
if (file.type.startsWith("image/")) {
- this.optimizeImage(e, event, file)
+ optimizedFileObj = this.optimizeImage(evt, file)
+ } else {
+ optimizedFileObj = file
}
- this.$matrix.matrixClient.getMediaConfig().then((config) => {
- this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, file] : [file];
- if (config["m.upload.size"] && file.size > config["m.upload.size"]) {
- this.currentSendError = this.$t("message.upload_file_too_large");
- this.currentSendShowSendButton = false;
- } else {
- this.currentSendShowSendButton = true;
- }
- });
+ this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, optimizedFileObj] : [optimizedFileObj];
};
reader.readAsDataURL(file);
}
@@ -1085,66 +1359,62 @@ export default {
* Handle picked attachment
*/
handlePickedAttachment(event) {
- Object.values(event.target.files).forEach(file => this.handleFileReader(event, file));
+ this.currentFileInputs = []
+ const uploadedFiles = Object.values(event.target.files);
+
+ this.$matrix.matrixClient.getMediaConfig().then((config) => {
+ const configUploadSize = config["m.upload.size"];
+ const configFormattedUploadSize = this.formatBytes(configUploadSize);
+
+ uploadedFiles.every(file => {
+ if (configUploadSize && file.size > configUploadSize) {
+ this.currentSendError = this.$t("message.upload_file_too_large");
+ this.currentSendErrorExceededFile = this.$t("message.upload_exceeded_file_limit", { configFormattedUploadSize });
+ this.currentSendShowSendButton = false;
+ return false;
+ } else {
+ this.currentSendShowSendButton = true;
+ }
+ return true;
+ });
+
+ uploadedFiles.forEach(file => this.handleFileReader(file));
+
+ });
},
showStickerPicker() {
this.$refs.stickerPickerSheet.open();
},
- onUploadProgress(p) {
- if (p.total) {
- this.currentSendProgress = this.$t("message.upload_progress_with_total", {
- count: p.loaded || 0,
- total: p.total,
- });
- } else {
- this.currentSendProgress = this.$t("message.upload_progress", {
- count: p.loaded || 0,
- });
- }
- },
sendAttachment(withText) {
this.$refs.attachment.value = null;
if (this.isCurrentFileInputsAnArray) {
- let inputFiles = this.currentFileInputs.map(entry => {
- if (entry.scaled && entry.useScaled) {
- // Send scaled version of image instead!
- return entry.scaled;
- }
- return entry;
+ const text = withText || "";
+ const promise = this.sendAttachments(text, this.currentFileInputs);
+ promise.then(() => {
+ this.currentFileInputs = null;
+ this.sendingStatus = this.sendStatuses.INITIAL;
})
- const promises = inputFiles.map(inputFile => util.sendImage(this.$matrix.matrixClient, this.roomId, inputFile, this.onUploadProgress));
-
- Promise.all(promises).then(() => {
- this.currentSendOperation = null;
- this.currentFileInputs = null;
- this.currentSendProgress = null;
- if (withText) {
- this.sendMessage(withText);
- }
- })
- .catch((err) => {
- if (err.name === "AbortError" || err === "Abort") {
- this.currentSendError = null;
- } else {
- this.currentSendError = err.LocaleString();
- }
- this.currentSendOperation = null;
- this.currentSendProgress = null;
- });
+ .catch((err) => {
+ if (err.name === "AbortError" || err === "Abort") {
+ this.currentSendError = null;
+ this.currentSendErrorExceededFile = null;
+ } else {
+ this.currentSendError = err.LocaleString();
+ this.currentSendErrorExceededFile = err.LocaleString();
+ }
+ });
}
},
cancelSendAttachment() {
this.$refs.attachment.value = null;
- if (this.currentSendOperation) {
- this.currentSendOperation.abort();
- }
- this.currentSendOperation = null;
+ this.cancelSendAttachments();
this.currentFileInputs = null;
- this.currentSendProgress = null;
this.currentSendError = null;
+ this.currentSendErrorExceededFile = null;
+ this.sendingStatus = this.sendStatuses.INITIAL;
},
addAttachment(file) {
@@ -1155,6 +1425,28 @@ export default {
this.cancelSendAttachment();
},
+ /**
+ * Called by message components that need to change their layout. This will avoid "jumping" in the UI, because
+ * we remember scroll position, apply the layout change, then restore the scroll.
+ * NOTE: we use "parentElement" below, because it is expected to be called with "element" set to the message component
+ * and the message component in turn being wrapped by a "message-wrapper" element (see html above).
+ * @param {} action A function that performs desired layout changes.
+ * @param {*} element Root element for the chat message.
+ */
+ onLayoutChange(action, element) {
+ if (!element || !element.parentElemen || this.useVoiceMode || this.useFileModeNonAdmin) {
+ action();
+ return
+ }
+ const container = this.chatContainer;
+ this.scrollPosition.prepareFor(element.parentElement.offsetTop >= container.scrollTop ? "down" : "up");
+ action();
+ this.$nextTick(() => {
+ // restore scroll position!
+ this.scrollPosition.restore();
+ });
+ },
+
handleScrolledToTop() {
if (
this.timelineWindow &&
@@ -1165,7 +1457,7 @@ export default {
this.timelineWindow
.paginate(EventTimeline.BACKWARDS, 10, true)
.then((success) => {
- if (success) {
+ if (success && this.scrollPosition) {
this.scrollPosition.prepareFor("up");
this.events = this.timelineWindow.getEvents();
this.$nextTick(() => {
@@ -1193,7 +1485,7 @@ export default {
.then((success) => {
if (success) {
this.events = this.timelineWindow.getEvents();
- if (!this.useVoiceMode) {
+ if (!this.useVoiceMode && this.scrollPosition) {
this.scrollPosition.prepareFor("down");
this.$nextTick(() => {
// restore scroll position!
@@ -1219,9 +1511,18 @@ export default {
const container = this.chatContainer;
const ref = this.$refs[eventId];
if (container && ref) {
- const targetY = container.clientHeight / 2;
- const sourceY = ref[0].offsetTop;
- container.scrollTo(0, sourceY - targetY);
+ const parent = container.getBoundingClientRect();
+ const item = ref[0].getBoundingClientRect();
+ let offsetY = (parent.bottom - parent.top) / 2;
+ if (ref[0].clientHeight > offsetY) {
+ offsetY = Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight);
+ }
+ const targetY = parent.top + offsetY;
+ const currentY = item.top;
+ const y = container.scrollTop + (currentY - targetY);
+ this.$nextTick(() => {
+ container.scrollTo(0, y);
+ });
}
},
@@ -1273,7 +1574,11 @@ export default {
addReply(event) {
this.replyToEvent = event;
this.$refs.messageInput.focus();
- this.replyToContentType = event.getContent().msgtype || 'm.poll';
+ if (event.parentThread || event.isThreadRoot || event.isMxThread) {
+ this.replyToContentType = 'm.thread';
+ } else {
+ this.replyToContentType = event.getContent().msgtype || 'm.poll';
+ }
this.setReplyToImage(event);
},
@@ -1295,24 +1600,12 @@ export default {
},
download(event) {
- util
- .getAttachment(this.$matrix.matrixClient, event)
- .then((url) => {
- const link = document.createElement("a");
- link.href = url;
- link.target = "_blank";
- link.download = event.getContent().body || this.$t("fallbacks.download_name");
- document.body.appendChild(link);
- link.click();
-
- setTimeout(function () {
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
- }, 200);
- })
- .catch((err) => {
- console.log("Failed to fetch attachment: ", err);
- });
+ if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) {
+ const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
+ children.forEach(child => util.download(this.$matrix.matrixClient, child));
+ } else {
+ util.download(this.$matrix.matrixClient, event);
+ }
},
cancelEditReply() {
@@ -1555,7 +1848,17 @@ export default {
},
closeCreateRoomWelcomeHeader() {
- this.showCreatedRoomWelcomeHeader = false;
+ this.hideCreatedRoomWelcomeHeader = true;
+ this.$nextTick(() => {
+ // We change the layout when removing the welcome header, so call
+ // onScroll here to handle updates (e.g. remove the "scroll to last" if we now
+ // can see all messages).
+ this.onScroll();
+ });
+ },
+
+ closeDirectChatWelcomeHeader() {
+ this.hideDirectChatWelcomeHeader = true;
this.$nextTick(() => {
// We change the layout when removing the welcome header, so call
// onScroll here to handle updates (e.g. remove the "scroll to last" if we now
diff --git a/src/components/ChatHeader.vue b/src/components/ChatHeader.vue
index 0f20da3..f54610f 100644
--- a/src/components/ChatHeader.vue
+++ b/src/components/ChatHeader.vue
@@ -5,7 +5,7 @@
cols="auto"
class="chat-header-members text-start ma-0 pa-0"
>
-
@@ -51,7 +57,12 @@ export default {
i18nCopyLinkKey: {
type: String,
default: 'copy_link'
+ },
+ i18nQrPopupTitleKey: {
+ type: String,
+ default: 'room_info.scan_code'
}
+
},
data() {
return {
@@ -59,6 +70,11 @@ export default {
showFullScreenQR: false,
}
},
+ computed: {
+ popupTitle() {
+ return this.$t(this.i18nQrPopupTitleKey);
+ },
+ },
methods: {
copyRoomLink() {
if(this.locationLinkCopied) return
@@ -87,7 +103,7 @@ export default {
{
type: "image/png",
margin: 1,
- width: 60,
+ width: canvas.getBoundingClientRect().width,
},
function (error) {
if (error) console.error(error);
diff --git a/src/components/CreateRoom.vue b/src/components/CreateRoom.vue
index 0ac27dd..64e9818 100644
--- a/src/components/CreateRoom.vue
+++ b/src/components/CreateRoom.vue
@@ -65,11 +65,15 @@