Quick reactions
This commit is contained in:
parent
5589131c86
commit
29acde8604
15 changed files with 454 additions and 137 deletions
51
package-lock.json
generated
51
package-lock.json
generated
|
|
@ -18,6 +18,7 @@
|
|||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"raw-loader": "^4.0.2",
|
||||
"roboto-fontface": "*",
|
||||
"v-emoji-picker": "^2.3.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.2.11",
|
||||
|
|
@ -13304,6 +13305,18 @@
|
|||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v-emoji-picker": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/v-emoji-picker/-/v-emoji-picker-2.3.1.tgz",
|
||||
"integrity": "sha512-KpFum53KelwRunQhB8c5Jj83YCGWDnBBrFaIHg0NJN0WCGTxSiVjPh+GyrHxssvLB9wvo+1YLKzal+iuJ+08eA==",
|
||||
"dependencies": {
|
||||
"vue-class-component": "^7.2.6",
|
||||
"vue-property-decorator": "^9.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^2.6.11"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz",
|
||||
|
|
@ -13362,6 +13375,14 @@
|
|||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
|
||||
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
||||
},
|
||||
"node_modules/vue-class-component": {
|
||||
"version": "7.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz",
|
||||
"integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==",
|
||||
"peerDependencies": {
|
||||
"vue": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-cli-plugin-vuetify": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.7.tgz",
|
||||
|
|
@ -13557,6 +13578,15 @@
|
|||
"integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vue-property-decorator": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz",
|
||||
"integrity": "sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==",
|
||||
"peerDependencies": {
|
||||
"vue": "*",
|
||||
"vue-class-component": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "3.4.9",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",
|
||||
|
|
@ -25496,6 +25526,15 @@
|
|||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
},
|
||||
"v-emoji-picker": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/v-emoji-picker/-/v-emoji-picker-2.3.1.tgz",
|
||||
"integrity": "sha512-KpFum53KelwRunQhB8c5Jj83YCGWDnBBrFaIHg0NJN0WCGTxSiVjPh+GyrHxssvLB9wvo+1YLKzal+iuJ+08eA==",
|
||||
"requires": {
|
||||
"vue-class-component": "^7.2.6",
|
||||
"vue-property-decorator": "^9.0.2"
|
||||
}
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz",
|
||||
|
|
@ -25544,6 +25583,12 @@
|
|||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
|
||||
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
||||
},
|
||||
"vue-class-component": {
|
||||
"version": "7.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz",
|
||||
"integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==",
|
||||
"requires": {}
|
||||
},
|
||||
"vue-cli-plugin-vuetify": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.7.tgz",
|
||||
|
|
@ -25685,6 +25730,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"vue-property-decorator": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz",
|
||||
"integrity": "sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "3.4.9",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"raw-loader": "^4.0.2",
|
||||
"roboto-fontface": "*",
|
||||
"v-emoji-picker": "^2.3.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.2.11",
|
||||
|
|
|
|||
|
|
@ -185,4 +185,35 @@ $chat-text-size: 0.7pt;
|
|||
color: #1c242a;
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.messageOperations {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
&.incoming {
|
||||
left: -20px;
|
||||
}
|
||||
&.outgoing {
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-reaction-container {
|
||||
.quick-reaction {
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 9px;
|
||||
margin: 0px 2px;
|
||||
padding: 2px;
|
||||
&:hover {
|
||||
border: 1px solid #888888;
|
||||
background-color: #e2e2e2;
|
||||
}
|
||||
.quick-reaction-count {
|
||||
color: #888888;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
.sent {
|
||||
background-color: palegreen;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,29 @@
|
|||
v-on:scroll="onScroll"
|
||||
>
|
||||
<div v-for="event in events" :key="event.getId()">
|
||||
<component :is="componentForEvent(event)" :room="room" :event="event" />
|
||||
<v-hover v-slot="{ hover }" v-if="!event.isRelation()">
|
||||
<div style="position: relative">
|
||||
<component
|
||||
:is="componentForEvent(event)"
|
||||
:room="room"
|
||||
:event="event"
|
||||
:reactions="
|
||||
timelineWindow._timelineSet.getRelationsForEvent(
|
||||
event.getId(),
|
||||
'm.annotation',
|
||||
'm.reaction'
|
||||
)
|
||||
"
|
||||
v-on:send-quick-reaction="sendQuickReaction"
|
||||
/>
|
||||
<message-operations
|
||||
v-if="hover"
|
||||
v-on:addreaction="addReaction"
|
||||
:event="event"
|
||||
:incoming="event.getSender() != $matrix.currentUserId"
|
||||
/>
|
||||
</div>
|
||||
</v-hover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -68,11 +90,23 @@
|
|||
<v-btn color="primary" text @click="cancelSendAttachment"
|
||||
>Cancel</v-btn
|
||||
>
|
||||
<v-btn color="primary" text @click="sendAttachment" :disabled="currentSendOperation != null">Send</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
text
|
||||
@click="sendAttachment"
|
||||
:disabled="currentSendOperation != null"
|
||||
>Send</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<v-dialog v-model="showEmojiPicker" class="ma-0 pa-0" width="50%">
|
||||
<VEmojiPicker style="width: 100%" @select="emojiSelected" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -90,6 +124,7 @@ import DebugEvent from "./messages/DebugEvent.vue";
|
|||
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
|
||||
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
||||
import util from "../plugins/utils";
|
||||
import MessageOperations from "./messages/MessageOperations.vue";
|
||||
|
||||
// from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html
|
||||
function ScrollPosition(node) {
|
||||
|
|
@ -133,6 +168,7 @@ export default {
|
|||
DebugEvent,
|
||||
MessageOutgoingImage,
|
||||
MessageIncomingImage,
|
||||
MessageOperations,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
@ -148,37 +184,24 @@ export default {
|
|||
currentSendOperation: null,
|
||||
currentSendProgress: null,
|
||||
currentSendError: null,
|
||||
joinRoom: null,
|
||||
showEmojiPicker: false,
|
||||
selectedEvent: null,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const container = this.$refs.chatContainer;
|
||||
this.scrollPosition = new ScrollPosition(container);
|
||||
|
||||
this.$matrix.on("Room.timeline", this.onEvent);
|
||||
this.$matrix.on("RoomMember.typing", this.onUserTyping);
|
||||
this.$matrix.on("Matrix.initialized", this.onInitialized);
|
||||
|
||||
if (this.$route.params && this.$route.params.joinRoom) {
|
||||
this.joinRoom = this.$route.params.joinRoom;
|
||||
}
|
||||
|
||||
if (this.$matrix.matrixClientReady) {
|
||||
this.onInitialized(this.$matrix.matrixClient);
|
||||
}
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.$matrix.off("Room.timeline", this.onEvent);
|
||||
this.$matrix.off("RoomMember.typing", this.onUserTyping);
|
||||
this.$matrix.off("Matrix.initialized", this.onInitialized);
|
||||
},
|
||||
|
||||
computed: {
|
||||
myUserId() {
|
||||
return this.$store.state.auth.user.user_id;
|
||||
},
|
||||
roomId() {
|
||||
return this.$matrix.currentRoomId;
|
||||
},
|
||||
|
|
@ -188,69 +211,55 @@ export default {
|
|||
},
|
||||
|
||||
watch: {
|
||||
roomId() {
|
||||
console.log("Chat: Current room changed");
|
||||
roomId: {
|
||||
handler(ignoredNewVal, ignoredOldVal) {
|
||||
console.log("Chat: Current room changed");
|
||||
|
||||
// Clear old events
|
||||
this.events = [];
|
||||
this.timelineWindow = null;
|
||||
this.contactIsTyping = false;
|
||||
// Clear old events
|
||||
this.events = [];
|
||||
this.timelineWindow = null;
|
||||
this.contactIsTyping = false;
|
||||
|
||||
if (!this.roomId) {
|
||||
return; // no room
|
||||
}
|
||||
if (!this.roomId) {
|
||||
return; // no room
|
||||
}
|
||||
|
||||
this.room = this.$matrix.getRoom(this.roomId);
|
||||
if (!this.room) {
|
||||
return; // Not found
|
||||
}
|
||||
this.room = this.$matrix.getRoom(this.roomId);
|
||||
if (!this.room) {
|
||||
return; // Not found
|
||||
}
|
||||
|
||||
this.timelineWindow = new TimelineWindow(
|
||||
this.$matrix.matrixClient,
|
||||
this.room.getUnfilteredTimelineSet(),
|
||||
{}
|
||||
);
|
||||
this.timelineWindow.load(null, 20).then(() => {
|
||||
this.events = this.timelineWindow.getEvents();
|
||||
this.$nextTick(() => {
|
||||
this.paginateBackIfNeeded();
|
||||
this.timelineWindow = new TimelineWindow(
|
||||
this.$matrix.matrixClient,
|
||||
this.room.getUnfilteredTimelineSet(),
|
||||
{}
|
||||
);
|
||||
this.timelineWindow.load(null, 20).then(() => {
|
||||
this.events = this.timelineWindow.getEvents();
|
||||
this.$nextTick(() => {
|
||||
this.paginateBackIfNeeded();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onInitialized(matrixClient) {
|
||||
if (this.joinRoom) {
|
||||
const roomId = this.joinRoom;
|
||||
this.joinRoom = null;
|
||||
matrixClient
|
||||
.joinRoom(roomId)
|
||||
.then((room) => {
|
||||
this.$matrix.setCurrentRoomId(room.roomId);
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO - handle error
|
||||
console.log("Failed to join room", err);
|
||||
});
|
||||
}
|
||||
},
|
||||
componentForEvent(event) {
|
||||
switch (event.getType()) {
|
||||
case "m.room.member":
|
||||
if (event.event.state_key != this.myUserId) {
|
||||
if (event.getContent().membership == "join") {
|
||||
return ContactJoin;
|
||||
} else if (event.getContent().membership == "leave") {
|
||||
return ContactLeave;
|
||||
} else if (event.getContent().membership == "invite") {
|
||||
return ContactInvited;
|
||||
}
|
||||
if (event.getContent().membership == "join") {
|
||||
return ContactJoin;
|
||||
} else if (event.getContent().membership == "leave") {
|
||||
return ContactLeave;
|
||||
} else if (event.getContent().membership == "invite") {
|
||||
return ContactInvited;
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case "m.room.message":
|
||||
if (event.getSender() != this.myUserId) {
|
||||
if (event.getSender() != this.$matrix.currentUserId) {
|
||||
if (event.getContent().msgtype == "m.image") {
|
||||
return MessageIncomingImage;
|
||||
}
|
||||
|
|
@ -319,13 +328,18 @@ export default {
|
|||
|
||||
sendMessage() {
|
||||
if (this.currentInput.length > 0) {
|
||||
util.sendTextMessage(this.$matrix.matrixClient, this.roomId, this.currentInput)
|
||||
.then(() => {
|
||||
console.log("Sent message");
|
||||
})
|
||||
.catch(err => {
|
||||
console.log("Failed to send:", err);
|
||||
})
|
||||
util
|
||||
.sendTextMessage(
|
||||
this.$matrix.matrixClient,
|
||||
this.roomId,
|
||||
this.currentInput
|
||||
)
|
||||
.then(() => {
|
||||
console.log("Sent message");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to send:", err);
|
||||
});
|
||||
this.currentInput = "";
|
||||
}
|
||||
},
|
||||
|
|
@ -356,18 +370,23 @@ export default {
|
|||
sendAttachment() {
|
||||
if (this.currentImageInputPath) {
|
||||
this.currentSendProgress = 0;
|
||||
this.currentSendOperation = util.sendEncyptedImage(this.$matrix.matrixClient, this.roomId, this.currentImageInputPath, this.onUploadProgress);
|
||||
this.currentSendOperation = util.sendImage(
|
||||
this.$matrix.matrixClient,
|
||||
this.roomId,
|
||||
this.currentImageInputPath,
|
||||
this.onUploadProgress
|
||||
);
|
||||
this.currentSendOperation
|
||||
.then(() => {
|
||||
this.currentSendOperation = null;
|
||||
this.currentImageInput = null;
|
||||
this.currentSendProgress = 0;
|
||||
})
|
||||
.catch(err => {
|
||||
this.currentSendError = err.toLocaleString();
|
||||
this.currentSendOperation = null;
|
||||
this.currentSendProgress = 0;
|
||||
});
|
||||
.then(() => {
|
||||
this.currentSendOperation = null;
|
||||
this.currentImageInput = null;
|
||||
this.currentSendProgress = 0;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.currentSendError = err.toLocaleString();
|
||||
this.currentSendOperation = null;
|
||||
this.currentSendProgress = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -441,6 +460,39 @@ export default {
|
|||
}
|
||||
});
|
||||
},
|
||||
|
||||
addReaction(e) {
|
||||
const event = e.event;
|
||||
// Store the event we are reacting to, so that we know where to
|
||||
// send when the picker closes.
|
||||
this.selectedEvent = event;
|
||||
this.showEmojiPicker = true;
|
||||
},
|
||||
|
||||
emojiSelected(e) {
|
||||
this.showEmojiPicker = false;
|
||||
if (this.selectedEvent) {
|
||||
const event = this.selectedEvent;
|
||||
this.selectedEvent = null;
|
||||
this.sendQuickReaction({reaction:e.data, event: event});
|
||||
}
|
||||
},
|
||||
|
||||
sendQuickReaction(e) {
|
||||
util
|
||||
.sendQuickReaction(
|
||||
this.$matrix.matrixClient,
|
||||
this.roomId,
|
||||
e.reaction,
|
||||
e.event
|
||||
)
|
||||
.then(() => {
|
||||
console.log("Quick reaction message");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to send quick reaction:", err);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -38,38 +38,29 @@ export default {
|
|||
handleJoin() {
|
||||
this.loading = true;
|
||||
this.loadingMessage = "Logging in...";
|
||||
var clientPromise;
|
||||
if (this.currentUser) {
|
||||
this.$matrix
|
||||
.getMatrixClient(this.currentUser)
|
||||
.then((ignoreduser) => {
|
||||
this.loadingMessage = "Joining room...";
|
||||
return this.$matrix.matrixClient.joinRoom(this.roomId);
|
||||
})
|
||||
.then((room) => {
|
||||
this.$matrix.setCurrentRoomId(room.roomId);
|
||||
this.loading = false;
|
||||
this.loadingMessage = null;
|
||||
this.$router.replace({ name: "Chat" });
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO - handle error
|
||||
console.log("Failed to join room", err);
|
||||
this.loading = false;
|
||||
this.loadingMessage = err.toString();
|
||||
});
|
||||
clientPromise = this.$matrix.getMatrixClient(this.currentUser);
|
||||
} else {
|
||||
this.$store.dispatch("auth/login", this.guestUser).then(
|
||||
() => {
|
||||
this.loading = false;
|
||||
this.loadingMessage = null;
|
||||
this.$router.replace({ name: "Chat" });
|
||||
},
|
||||
(error) => {
|
||||
this.loading = false;
|
||||
this.loadingMessage = error.toString();
|
||||
}
|
||||
);
|
||||
clientPromise = this.$store.dispatch("auth/login", this.guestUser);
|
||||
}
|
||||
return clientPromise
|
||||
.then((ignoreduser) => {
|
||||
this.loadingMessage = "Joining room...";
|
||||
return this.$matrix.matrixClient.joinRoom(this.roomId);
|
||||
})
|
||||
.then((room) => {
|
||||
this.$matrix.setCurrentRoomId(room.roomId);
|
||||
this.loading = false;
|
||||
this.loadingMessage = null;
|
||||
this.$router.replace({ name: "Chat" });
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO - handle error
|
||||
console.log("Failed to join room", err);
|
||||
this.loading = false;
|
||||
this.loadingMessage = err.toString();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<div class="sender">{{ messageEventDisplayName(event) }}</div>
|
||||
<div class="bubble image-bubble">
|
||||
<v-img :aspect-ratio="16 / 9" ref="image" :src="src" cover />
|
||||
<QuickReactions :event="event" :reactions="reactions" />
|
||||
</div>
|
||||
<v-avatar class="avatar" size="40" color="grey">
|
||||
<img
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
<div class="bubble">
|
||||
<div class="message">{{ event.getContent().body }}</div>
|
||||
<QuickReactions :event="event" :reactions="reactions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="time">
|
||||
|
|
|
|||
40
src/components/messages/MessageOperations.vue
Normal file
40
src/components/messages/MessageOperations.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div :class="{'messageOperations':true,'incoming':incoming,'outgoing':!incoming}">
|
||||
<v-btn icon @click="addReaction" class="ma-0 pa-0">
|
||||
<v-icon>mood</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageMixin from "./messageMixin";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
|
||||
props: {
|
||||
incoming: {
|
||||
type: Boolean,
|
||||
default: function () {
|
||||
return true
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addReaction() {
|
||||
this.$emit("addreaction", {event:this.event});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
<div class="sender">{{ "You" }}</div>
|
||||
<div class="bubble image-bubble">
|
||||
<v-img :aspect-ratio="16/9" ref="image" :src="src" cover />
|
||||
<QuickReactions :event="event" :reactions="reactions" />
|
||||
</div>
|
||||
<div class="status">{{ event.status }}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<div class="sender">{{ "You" }}</div>
|
||||
<div class="bubble">
|
||||
<div class="message">{{ event.getContent().body }}</div>
|
||||
<QuickReactions :event="event" :reactions="reactions" />
|
||||
</div>
|
||||
<div class="status">{{ event.status }}</div>
|
||||
</div>
|
||||
|
|
@ -20,7 +21,6 @@ export default {
|
|||
mixins: [messageMixin],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
81
src/components/messages/QuickReactions.vue
Normal file
81
src/components/messages/QuickReactions.vue
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<div class="quick-reaction-container">
|
||||
<span :class="{'quick-reaction':true,'sent':value.includes($matrix.currentUserId)}" v-for="(value, name) in reactionMap" :key="name" @mousedown="onClickEmoji(name)">
|
||||
{{ name }} <span class="quick-reaction-count">{{ value.length }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
reactions: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reactionMap: {}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.reactions) {
|
||||
this.reactions.off('Relations.add', this.onAddRelation);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClickEmoji(emoji) {
|
||||
this.$bubble('send-quick-reaction', {reaction:emoji, event:this.event});
|
||||
},
|
||||
onAddRelation(ignoredevent) {
|
||||
this.processReactions();
|
||||
},
|
||||
processReactions() {
|
||||
var reactionMap = {};
|
||||
if (this.reactions && this.reactions._eventsCount > 0) {
|
||||
const relations = this.reactions.getRelations();
|
||||
for (const r of relations) {
|
||||
const emoji = r.getRelation().key;
|
||||
const sender = r.getSender();
|
||||
if (reactionMap[emoji]) {
|
||||
const array = reactionMap[emoji];
|
||||
if (!array.includes(sender)) {
|
||||
array.push(sender)
|
||||
}
|
||||
} else {
|
||||
reactionMap[emoji] = [sender];
|
||||
}
|
||||
}
|
||||
}
|
||||
this.reactionMap = reactionMap;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
reactions: {
|
||||
handler(newValue, oldValue) {
|
||||
if (oldValue) {
|
||||
oldValue.off('Relations.add', this.onAddRelation);
|
||||
}
|
||||
if (newValue) {
|
||||
newValue.on('Relations.add', this.onAddRelation);
|
||||
}
|
||||
this.processReactions();
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
import QuickReactions from './QuickReactions.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
QuickReactions
|
||||
},
|
||||
props: {
|
||||
room: {
|
||||
type: Object,
|
||||
|
|
@ -12,6 +17,12 @@ export default {
|
|||
return {}
|
||||
}
|
||||
},
|
||||
reactions: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
|
|
@ -20,6 +31,9 @@ export default {
|
|||
* Get a display name given an event.
|
||||
*/
|
||||
stateEventDisplayName(event) {
|
||||
if (event.getSender() == this.$matrix.currentUserId) {
|
||||
return "You";
|
||||
}
|
||||
if (this.room) {
|
||||
const member = this.room.getMember(event.getSender());
|
||||
if (member) {
|
||||
|
|
@ -64,5 +78,5 @@ export default {
|
|||
}
|
||||
return date.toLocaleString();
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
15
src/main.js
15
src/main.js
|
|
@ -6,11 +6,26 @@ import store from './store'
|
|||
import matrix from './services/matrix.service'
|
||||
import 'roboto-fontface/css/roboto/roboto-fontface.css'
|
||||
import 'material-design-icons-iconfont/dist/material-design-icons.css'
|
||||
import VEmojiPicker from 'v-emoji-picker';
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
Vue.use(VEmojiPicker);
|
||||
Vue.use(matrix, {store: store});
|
||||
|
||||
// Add bubble functionality to custom events.
|
||||
// From here: https://stackoverflow.com/questions/41993508/vuejs-bubbling-custom-events
|
||||
Vue.use((Vue) => {
|
||||
Vue.prototype.$bubble = function $bubble(eventName, ...args) {
|
||||
// Emit the event on all parent components
|
||||
let component = this;
|
||||
do {
|
||||
component.$emit(eventName, ...args);
|
||||
component = component.$parent;
|
||||
} while (component);
|
||||
};
|
||||
});
|
||||
|
||||
new Vue({
|
||||
vuetify,
|
||||
router,
|
||||
|
|
|
|||
|
|
@ -76,12 +76,23 @@ class Util {
|
|||
}
|
||||
|
||||
sendTextMessage(matrixClient, roomId, text) {
|
||||
return this.sendMessage(matrixClient, roomId, ContentHelpers.makeTextMessage(text));
|
||||
return this.sendMessage(matrixClient, roomId, "m.room.message", ContentHelpers.makeTextMessage(text));
|
||||
}
|
||||
|
||||
sendMessage(matrixClient, roomId, content) {
|
||||
sendQuickReaction(matrixClient, roomId, emoji, event) {
|
||||
const content = {
|
||||
'm.relates_to': {
|
||||
key: emoji,
|
||||
rel_type: 'm.annotation',
|
||||
event_id: event.getId()
|
||||
}
|
||||
};
|
||||
return this.sendMessage(matrixClient, roomId, "m.reaction", content);
|
||||
}
|
||||
|
||||
sendMessage(matrixClient, roomId, eventType, content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
matrixClient.sendMessage(roomId, content, undefined, undefined)
|
||||
matrixClient.sendEvent(roomId, eventType, content, undefined, undefined)
|
||||
.then((result) => {
|
||||
console.log("Message sent: ", result);
|
||||
resolve(true);
|
||||
|
|
@ -127,22 +138,57 @@ class Util {
|
|||
});
|
||||
}
|
||||
|
||||
sendEncyptedImage(matrixClient, roomId, file, onUploadProgress) {
|
||||
sendImage(matrixClient, roomId, file, onUploadProgress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const fileContents = e.target.result;
|
||||
var data = new Uint8Array(fileContents);
|
||||
|
||||
const info = {
|
||||
mimetype: file.type,
|
||||
size: file.size
|
||||
};
|
||||
|
||||
const opts = {
|
||||
type: file.type,
|
||||
name: 'Image',
|
||||
progressHandler: onUploadProgress,
|
||||
onlyContentUri: false
|
||||
};
|
||||
|
||||
if (!matrixClient.isRoomEncrypted(roomId)) {
|
||||
// Not encrypted.
|
||||
matrixClient.uploadContent(data, opts)
|
||||
.then((response) => {
|
||||
const messageContent = {
|
||||
body: 'Image',
|
||||
url: response.content_uri,
|
||||
info: info,
|
||||
msgtype: 'm.image'
|
||||
}
|
||||
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent)
|
||||
})
|
||||
.then(result => {
|
||||
resolve(result);
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
return; // Don't fall through
|
||||
}
|
||||
|
||||
const crypto = require('crypto');
|
||||
let key = crypto.randomBytes(256 / 8);
|
||||
let iv = Buffer.concat([crypto.randomBytes(8),Buffer.alloc(8)]); // Initialization vector.
|
||||
|
||||
// Encrypt
|
||||
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
|
||||
const data = new Uint8Array(fileContents);
|
||||
var encryptedBytes = aesCtr.encrypt(data);
|
||||
data = encryptedBytes;
|
||||
|
||||
// Calculate sha256
|
||||
var hash = new Uint8Array(sha256.create().update(encryptedBytes).arrayBuffer());
|
||||
var hash = new Uint8Array(sha256.create().update(data).arrayBuffer());
|
||||
|
||||
const jwk = {
|
||||
kty: 'oct',
|
||||
|
|
@ -159,20 +205,7 @@ class Util {
|
|||
hashes: { sha256: Buffer.from(hash).toString('base64').replace( /=/g, '' )},
|
||||
v: 'v2'
|
||||
};
|
||||
console.log("Encrypted file:", encryptedFile);
|
||||
|
||||
const info = {
|
||||
mimetype: file.type,
|
||||
size: file.size
|
||||
};
|
||||
|
||||
|
||||
const opts = {
|
||||
type: file.type,
|
||||
name: 'Image',
|
||||
progressHandler: onUploadProgress,
|
||||
};
|
||||
|
||||
const messageContent = {
|
||||
body: 'Image',
|
||||
file: encryptedFile,
|
||||
|
|
@ -180,10 +213,10 @@ class Util {
|
|||
msgtype: 'm.image'
|
||||
}
|
||||
|
||||
matrixClient.uploadContent(encryptedBytes, opts)
|
||||
.then((uri) => {
|
||||
encryptedFile.url = uri;
|
||||
return this.sendMessage(matrixClient, roomId, messageContent)
|
||||
matrixClient.uploadContent(data, opts)
|
||||
.then((response) => {
|
||||
encryptedFile.url = response.content_uri;
|
||||
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent)
|
||||
})
|
||||
.then(result => {
|
||||
resolve(result);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ export default {
|
|||
return this.$store.state.auth.user;
|
||||
},
|
||||
|
||||
currentUserId() {
|
||||
const user = this.currentUser || {}
|
||||
return user.user_id;
|
||||
},
|
||||
|
||||
currentRoomId() {
|
||||
return this.$store.state.currentRoomId;
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue