Send images
This commit is contained in:
parent
072a685c1a
commit
d221d94b6c
4 changed files with 254 additions and 31 deletions
|
|
@ -61,6 +61,10 @@ $chat-text-size: 0.7pt;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: $chat-standard-padding-s;
|
padding-left: $chat-standard-padding-s;
|
||||||
padding-right: $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 {
|
.input-message {
|
||||||
|
|
@ -72,7 +76,7 @@ $chat-text-size: 0.7pt;
|
||||||
padding: 0 10px 0 10px;
|
padding: 0 10px 0 10px;
|
||||||
margin: $chat-standard-padding-xs 0 0 0;
|
margin: $chat-standard-padding-xs 0 0 0;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
background-color: #ffffff;
|
//background-color: #ffffff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #cccccc;
|
border: 1px solid #cccccc;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
@ -88,6 +92,9 @@ $chat-text-size: 0.7pt;
|
||||||
textarea {
|
textarea {
|
||||||
line-height: 1.1rem;
|
line-height: 1.1rem;
|
||||||
}
|
}
|
||||||
|
.v-input__prepend-outer {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,13 @@
|
||||||
<div class="messageIn">
|
<div class="messageIn">
|
||||||
<div class="sender">{{ messageEventDisplayName(event) }}</div>
|
<div class="sender">{{ messageEventDisplayName(event) }}</div>
|
||||||
<v-avatar class="avatar" size="40" color="grey">
|
<v-avatar class="avatar" size="40" color="grey">
|
||||||
<img v-if="messageEventAvatar(event)" :src="messageEventAvatar(event)" />
|
<img
|
||||||
<span v-else class="white--text headline">{{messageEventDisplayName(event).substring(0,1).toUpperCase()}}</span>
|
v-if="messageEventAvatar(event)"
|
||||||
|
:src="messageEventAvatar(event)"
|
||||||
|
/>
|
||||||
|
<span v-else class="white--text headline">{{
|
||||||
|
messageEventDisplayName(event).substring(0, 1).toUpperCase()
|
||||||
|
}}</span>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
|
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
|
|
@ -78,12 +83,14 @@
|
||||||
|
|
||||||
<!-- ROOM NAME CHANGED -->
|
<!-- ROOM NAME CHANGED -->
|
||||||
<div v-else-if="event.getType() == 'm.room.name'" class="statusEvent">
|
<div v-else-if="event.getType() == 'm.room.name'" class="statusEvent">
|
||||||
{{ stateEventDisplayName(event) }} changed room name to {{ event.getContent().name }}
|
{{ stateEventDisplayName(event) }} changed room name to
|
||||||
|
{{ event.getContent().name }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ROOM TOPIC CHANGED -->
|
<!-- ROOM TOPIC CHANGED -->
|
||||||
<div v-else-if="event.getType() == 'm.room.topic'" class="statusEvent">
|
<div v-else-if="event.getType() == 'm.room.topic'" class="statusEvent">
|
||||||
{{ stateEventDisplayName(event) }} changed topic to {{ event.getContent().topic }}
|
{{ stateEventDisplayName(event) }} changed topic to
|
||||||
|
{{ event.getContent().topic }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ROOM AVATAR CHANGED -->
|
<!-- ROOM AVATAR CHANGED -->
|
||||||
|
|
@ -91,15 +98,14 @@
|
||||||
{{ stateEventDisplayName(event) }} changed the room avatar
|
{{ stateEventDisplayName(event) }} changed the room avatar
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="statusEvent">Event: {{ event.getType() }}
|
<div v-else class="statusEvent">Event: {{ event.getType() }}</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- CONTACT IS TYPING -->
|
|
||||||
<div v-show="contactIsTyping" class="typing">Someone is typing...</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input area -->
|
<!-- Input area -->
|
||||||
<div class="input-area flex-grow-0 flex-shrink-0">
|
<div v-if="room" class="input-area flex-grow-0 flex-shrink-0">
|
||||||
|
<!-- CONTACT IS TYPING -->
|
||||||
|
<div v-show="contactIsTyping" class="typing">Someone is typing...</div>
|
||||||
<v-textarea
|
<v-textarea
|
||||||
ref="messageInput"
|
ref="messageInput"
|
||||||
full-width
|
full-width
|
||||||
|
|
@ -108,7 +114,22 @@
|
||||||
class="input-message"
|
class="input-message"
|
||||||
placeholder="Send message"
|
placeholder="Send message"
|
||||||
hide-details
|
hide-details
|
||||||
></v-textarea>
|
background-color="white"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<label icon flat>
|
||||||
|
<v-icon>attachment</v-icon>
|
||||||
|
<input
|
||||||
|
ref="attachment"
|
||||||
|
type="file"
|
||||||
|
name="attachment"
|
||||||
|
@change="pickAttachment($event)"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</v-textarea>
|
||||||
<div align-self="end" class="text-right">
|
<div align-self="end" class="text-right">
|
||||||
<v-btn
|
<v-btn
|
||||||
elevation="0"
|
elevation="0"
|
||||||
|
|
@ -118,6 +139,31 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentImageInput">
|
||||||
|
<v-dialog v-model="currentImageInput" class="ma-0 pa-0" width="50%">
|
||||||
|
<v-card class="ma-0 pa-0">
|
||||||
|
<v-card-text class="ma-0 pa-0">
|
||||||
|
<v-img
|
||||||
|
:aspect-ratio="1"
|
||||||
|
:src="currentImageInput"
|
||||||
|
contain
|
||||||
|
style="max-height: 50vh"
|
||||||
|
/>
|
||||||
|
<div v-if="currentSendError">{{ currentSendError }}</div>
|
||||||
|
<div v-else>{{ currentSendProgress }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" text @click="currentImageInput = null"
|
||||||
|
>Cancel</v-btn
|
||||||
|
>
|
||||||
|
<v-btn color="primary" text @click="sendAttachment">Send</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -128,6 +174,7 @@ import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
|
||||||
function ScrollPosition(node) {
|
function ScrollPosition(node) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
this.previousScrollHeightMinusTop = 0;
|
this.previousScrollHeightMinusTop = 0;
|
||||||
|
this.previousScrollTop = 0;
|
||||||
this.readyFor = "up";
|
this.readyFor = "up";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,18 +182,19 @@ ScrollPosition.prototype.restore = function () {
|
||||||
if (this.readyFor === "up") {
|
if (this.readyFor === "up") {
|
||||||
this.node.scrollTop =
|
this.node.scrollTop =
|
||||||
this.node.scrollHeight - this.previousScrollHeightMinusTop;
|
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) {
|
ScrollPosition.prototype.prepareFor = function (direction) {
|
||||||
this.readyFor = direction || "up";
|
this.readyFor = direction || "up";
|
||||||
this.previousScrollHeightMinusTop =
|
if (this.readyFor === "up") {
|
||||||
this.node.scrollHeight - this.node.scrollTop;
|
this.previousScrollHeightMinusTop =
|
||||||
|
this.node.scrollHeight - this.node.scrollTop;
|
||||||
|
} else {
|
||||||
|
this.previousScrollTop = this.node.scrollTop;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -159,6 +207,11 @@ export default {
|
||||||
contactIsTyping: false,
|
contactIsTyping: false,
|
||||||
timelineWindow: null,
|
timelineWindow: null,
|
||||||
scrollPosition: null,
|
scrollPosition: null,
|
||||||
|
currentImageInput: null,
|
||||||
|
currentImageInputPath: null,
|
||||||
|
currentSendOperation: null,
|
||||||
|
currentSendProgress: null,
|
||||||
|
currentSendError: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
@ -167,7 +220,6 @@ export default {
|
||||||
|
|
||||||
this.$matrix.on("Room.timeline", this.onEvent);
|
this.$matrix.on("Room.timeline", this.onEvent);
|
||||||
this.$matrix.on("RoomMember.typing", this.onUserTyping);
|
this.$matrix.on("RoomMember.typing", this.onUserTyping);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
destroyed() {
|
destroyed() {
|
||||||
|
|
@ -193,7 +245,7 @@ export default {
|
||||||
|
|
||||||
// Clear old events
|
// Clear old events
|
||||||
this.events = [];
|
this.events = [];
|
||||||
this.timelineWindow = null;
|
this.timelineWindow = null;
|
||||||
this.contactIsTyping = false;
|
this.contactIsTyping = false;
|
||||||
|
|
||||||
if (!this.roomId) {
|
if (!this.roomId) {
|
||||||
|
|
@ -219,12 +271,12 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
paginateBackIfNeeded() {
|
paginateBackIfNeeded() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const container = this.$refs.chatContainer;
|
const container = this.$refs.chatContainer;
|
||||||
if (container.scrollHeight <= container.clientHeight) {
|
if (container.scrollHeight <= container.clientHeight) {
|
||||||
this.handleScrolledToTop();
|
this.handleScrolledToTop();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
onScroll(ignoredevent) {
|
onScroll(ignoredevent) {
|
||||||
const container = this.$refs.chatContainer;
|
const container = this.$refs.chatContainer;
|
||||||
|
|
@ -235,7 +287,7 @@ export default {
|
||||||
container.scrollHeight - container.scrollTop ==
|
container.scrollHeight - container.scrollTop ==
|
||||||
container.clientHeight
|
container.clientHeight
|
||||||
) {
|
) {
|
||||||
this.handleScrolledToBottom();
|
this.handleScrolledToBottom(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEvent(event) {
|
onEvent(event) {
|
||||||
|
|
@ -243,6 +295,15 @@ export default {
|
||||||
return; // Not for this room
|
return; // Not for this room
|
||||||
}
|
}
|
||||||
this.paginateBackIfNeeded();
|
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) {
|
onUserTyping(event) {
|
||||||
|
|
@ -273,7 +334,13 @@ export default {
|
||||||
if (this.room) {
|
if (this.room) {
|
||||||
const member = this.room.getMember(event.getSender());
|
const member = this.room.getMember(event.getSender());
|
||||||
if (member) {
|
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;
|
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) {
|
sendMatrixMessage(body) {
|
||||||
var content = {
|
var content = {
|
||||||
body: body,
|
body: body,
|
||||||
|
|
@ -320,7 +479,6 @@ export default {
|
||||||
|
|
||||||
handleScrolledToTop() {
|
handleScrolledToTop() {
|
||||||
console.log("@top");
|
console.log("@top");
|
||||||
// const room = this.$matrix.getRoom(this.roomId);
|
|
||||||
if (
|
if (
|
||||||
this.timelineWindow &&
|
this.timelineWindow &&
|
||||||
this.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
|
this.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
|
||||||
|
|
@ -341,8 +499,46 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleScrolledToBottom() {
|
handleScrolledToBottom(scrollToEnd) {
|
||||||
console.log("@bottom");
|
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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
16
src/plugins/utils.tsx
Normal file
16
src/plugins/utils.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
class Util {
|
||||||
|
readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
||||||
|
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();
|
||||||
|
|
||||||
|
|
@ -195,7 +195,11 @@ export default {
|
||||||
if (this.matrixClient) {
|
if (this.matrixClient) {
|
||||||
this.matrixClient.off(event, handler);
|
this.matrixClient.off(event, handler);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
uploadFile(file, opts) {
|
||||||
|
return this.matrixClient.uploadContent(file, opts);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue