diff --git a/src/assets/css/chat.scss b/src/assets/css/chat.scss index c52fa26..4cfec2b 100644 --- a/src/assets/css/chat.scss +++ b/src/assets/css/chat.scss @@ -61,6 +61,10 @@ $chat-text-size: 0.7pt; margin: 0; padding-left: $chat-standard-padding-s; padding-right: $chat-standard-padding-s; + .currentImage { + width: 6 * $chat-standard-padding-s; + height: 6 * $chat-standard-padding-s; + } } .input-message { @@ -72,7 +76,7 @@ $chat-text-size: 0.7pt; padding: 0 10px 0 10px; margin: $chat-standard-padding-xs 0 0 0; color: #999999; - background-color: #ffffff; + //background-color: #ffffff; overflow: hidden; border: 1px solid #cccccc; border-radius: 0; @@ -88,6 +92,9 @@ $chat-text-size: 0.7pt; textarea { line-height: 1.1rem; } + .v-input__prepend-outer { + margin-top: 0px; + } } } diff --git a/src/components/Chat.vue b/src/components/Chat.vue index a33619b..37531eb 100644 --- a/src/components/Chat.vue +++ b/src/components/Chat.vue @@ -51,8 +51,13 @@
{{ messageEventDisplayName(event) }}
- - {{messageEventDisplayName(event).substring(0,1).toUpperCase()}} + + {{ + messageEventDisplayName(event).substring(0, 1).toUpperCase() + }}
@@ -78,12 +83,14 @@
- {{ stateEventDisplayName(event) }} changed room name to {{ event.getContent().name }} + {{ stateEventDisplayName(event) }} changed room name to + {{ event.getContent().name }}
- {{ stateEventDisplayName(event) }} changed topic to {{ event.getContent().topic }} + {{ stateEventDisplayName(event) }} changed topic to + {{ event.getContent().topic }}
@@ -91,15 +98,14 @@ {{ stateEventDisplayName(event) }} changed the room avatar
-
Event: {{ event.getType() }} -
+
Event: {{ event.getType() }}
- -
Someone is typing...
-
+
+ +
Someone is typing...
+ background-color="white" + > + +
+ +
+ + + + +
{{ currentSendError }}
+
{{ currentSendProgress }}
+
+ + + + Cancel + Send + +
+
+
@@ -128,6 +174,7 @@ import { TimelineWindow, EventTimeline } from "matrix-js-sdk"; function ScrollPosition(node) { this.node = node; this.previousScrollHeightMinusTop = 0; + this.previousScrollTop = 0; this.readyFor = "up"; } @@ -135,18 +182,19 @@ ScrollPosition.prototype.restore = function () { if (this.readyFor === "up") { this.node.scrollTop = this.node.scrollHeight - this.previousScrollHeightMinusTop; + } else { + this.node.scrollTop = this.previousScrollTop; } - - // 'down' doesn't need to be special cased unless the - // content was flowing upwards, which would only happen - // if the container is position: absolute, bottom: 0 for - // a Facebook messages effect }; ScrollPosition.prototype.prepareFor = function (direction) { this.readyFor = direction || "up"; - this.previousScrollHeightMinusTop = - this.node.scrollHeight - this.node.scrollTop; + if (this.readyFor === "up") { + this.previousScrollHeightMinusTop = + this.node.scrollHeight - this.node.scrollTop; + } else { + this.previousScrollTop = this.node.scrollTop; + } }; export default { @@ -159,6 +207,11 @@ export default { contactIsTyping: false, timelineWindow: null, scrollPosition: null, + currentImageInput: null, + currentImageInputPath: null, + currentSendOperation: null, + currentSendProgress: null, + currentSendError: null, }), mounted() { @@ -167,7 +220,6 @@ export default { this.$matrix.on("Room.timeline", this.onEvent); this.$matrix.on("RoomMember.typing", this.onUserTyping); - }, destroyed() { @@ -193,7 +245,7 @@ export default { // Clear old events this.events = []; - this.timelineWindow = null; + this.timelineWindow = null; this.contactIsTyping = false; if (!this.roomId) { @@ -219,12 +271,12 @@ export default { methods: { paginateBackIfNeeded() { - this.$nextTick(() => { - const container = this.$refs.chatContainer; - if (container.scrollHeight <= container.clientHeight) { - this.handleScrolledToTop(); - } - }) + this.$nextTick(() => { + const container = this.$refs.chatContainer; + if (container.scrollHeight <= container.clientHeight) { + this.handleScrolledToTop(); + } + }); }, onScroll(ignoredevent) { const container = this.$refs.chatContainer; @@ -235,7 +287,7 @@ export default { container.scrollHeight - container.scrollTop == container.clientHeight ) { - this.handleScrolledToBottom(); + this.handleScrolledToBottom(false); } }, onEvent(event) { @@ -243,6 +295,15 @@ export default { return; // Not for this room } this.paginateBackIfNeeded(); + + // If we are at bottom, scroll to see new events... + const container = this.$refs.chatContainer; + if ( + container.scrollHeight - container.scrollTop == + container.clientHeight + ) { + this.handleScrolledToBottom(true); + } }, onUserTyping(event) { @@ -273,7 +334,13 @@ export default { if (this.room) { const member = this.room.getMember(event.getSender()); if (member) { - return member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true); + return member.getAvatarUrl( + this.$matrix.matrixClient.getHomeserverUrl(), + 40, + 40, + "scale", + true + ); } } return null; @@ -286,6 +353,98 @@ export default { } }, + /** + * Show attachment picker to select image + */ + pickAttachment(event) { + if (event.target.files && event.target.files[0]) { + var reader = new FileReader(); + reader.onload = (e) => { + this.currentImageInput = e.target.result; + this.currentImageInputPath = event.target.files[0]; + }; + reader.readAsDataURL(event.target.files[0]); + } + }, + + onUploadProgress(p) { + if (p.total) { + this.currentSendProgress = + "Uploaded " + (p.loaded || 0) + " of " + p.total; + } else { + this.currentSendProgress = "Uploaded " + (p.loaded || 0); + } + }, + + sendAttachment() { + if (this.currentImageInputPath) { + const opts = { + progressHandler: this.onUploadProgress, + }; + this.currentSendOperation = this.$matrix.uploadFile( + this.currentImageInputPath, + opts + ); + var matrixUri; + this.currentSendOperation + .then((uri) => { + matrixUri = uri; + return this.$matrix.matrixClient.sendImageMessage( + this.roomId, + matrixUri, + "", + "Image", + null + ); + }) + .then((result) => { + console.log("Image sent: ", result); + }) + .catch((err) => { + console.log("Image send error: ", err); + if (err && err.name == "UnknownDeviceError") { + console.log("Unknown devices. Mark as known before retrying."); + var setAsKnownPromises = []; + for (var user of Object.keys(err.devices)) { + const userDevices = err.devices[user]; + for (var deviceId of Object.keys(userDevices)) { + const deviceInfo = userDevices[deviceId]; + if (!deviceInfo.known) { + setAsKnownPromises.push( + this.$matrix.matrixClient.setDeviceKnown( + user, + deviceId, + true + ) + ); + } + } + } + Promise.all(setAsKnownPromises) + .then(() => { + // All devices now marked as "known", try to resend + return this.$matrix.matrixClient.sendImageMessage( + this.roomId, + matrixUri, + "", + "Image", + null + ); + }) + .then((result) => { + console.log("Image sent: ", result); + }) + .catch((err) => { + // Still error, abort + this.currentSendError = err.toLocaleString(); + }); + } else { + this.currentSendError = err.toLocaleString(); + } + }); + } + }, + sendMatrixMessage(body) { var content = { body: body, @@ -320,7 +479,6 @@ export default { handleScrolledToTop() { console.log("@top"); - // const room = this.$matrix.getRoom(this.roomId); if ( this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.BACKWARDS) @@ -341,8 +499,46 @@ export default { } }, - handleScrolledToBottom() { + handleScrolledToBottom(scrollToEnd) { console.log("@bottom"); + if ( + this.timelineWindow && + this.timelineWindow.canPaginate(EventTimeline.FORWARDS) + ) { + this.timelineWindow + .paginate(EventTimeline.FORWARDS, 10, true) + .then((success) => { + if (success) { + this.scrollPosition.prepareFor("down"); + this.events = this.timelineWindow.getEvents(); + this.$nextTick(() => { + // restore scroll position! + console.log("Restore scroll!"); + this.scrollPosition.restore(); + if (scrollToEnd) { + this.smoothScrollToEnd(); + } + }); + } + }); + } + }, + + smoothScrollToEnd() { + this.$nextTick(function () { + const container = this.$refs.chatContainer; + if (container.children.length > 0) { + const lastChild = container.children[container.children.length - 1]; + console.log("Scroll into view", lastChild); + window.requestAnimationFrame(() => { + lastChild.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest", + }); + }); + } + }); }, }, }; diff --git a/src/plugins/utils.tsx b/src/plugins/utils.tsx new file mode 100644 index 0000000..0b18ab9 --- /dev/null +++ b/src/plugins/utils.tsx @@ -0,0 +1,16 @@ +class Util { + readFileAsArrayBuffer(file: File | Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function(e) { + resolve(e.target.result as ArrayBuffer); + }; + reader.onerror = function(e) { + reject(e); + }; + reader.readAsArrayBuffer(file); + }) + }; +}; +export default new Util(); + diff --git a/src/services/matrix.service.js b/src/services/matrix.service.js index 2945c32..2bad182 100644 --- a/src/services/matrix.service.js +++ b/src/services/matrix.service.js @@ -195,7 +195,11 @@ export default { if (this.matrixClient) { this.matrixClient.off(event, handler); } - } + }, + + uploadFile(file, opts) { + return this.matrixClient.uploadContent(file, opts); + }, } })