Quick reactions

This commit is contained in:
N-Pex 2020-11-25 14:42:50 +01:00
parent 5589131c86
commit 29acde8604
15 changed files with 454 additions and 137 deletions

51
package-lock.json generated
View file

@ -18,6 +18,7 @@
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"roboto-fontface": "*", "roboto-fontface": "*",
"v-emoji-picker": "^2.3.1",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-router": "^3.2.0", "vue-router": "^3.2.0",
"vuetify": "^2.2.11", "vuetify": "^2.2.11",
@ -13304,6 +13305,18 @@
"uuid": "bin/uuid" "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": { "node_modules/v8-compile-cache": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==" "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": { "node_modules/vue-cli-plugin-vuetify": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.7.tgz", "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.7.tgz",
@ -13557,6 +13578,15 @@
"integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=", "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
"dev": true "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": { "node_modules/vue-router": {
"version": "3.4.9", "version": "3.4.9",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz", "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", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" "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": { "v8-compile-cache": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==" "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": { "vue-cli-plugin-vuetify": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.7.tgz", "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": { "vue-router": {
"version": "3.4.9", "version": "3.4.9",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",

View file

@ -19,6 +19,7 @@
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"roboto-fontface": "*", "roboto-fontface": "*",
"v-emoji-picker": "^2.3.1",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-router": "^3.2.0", "vue-router": "^3.2.0",
"vuetify": "^2.2.11", "vuetify": "^2.2.11",

View file

@ -185,4 +185,35 @@ $chat-text-size: 0.7pt;
color: #1c242a; color: #1c242a;
text-align: center; text-align: center;
margin: 20px; 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;
}
} }

View file

@ -7,7 +7,29 @@
v-on:scroll="onScroll" v-on:scroll="onScroll"
> >
<div v-for="event in events" :key="event.getId()"> <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>
</div> </div>
@ -68,11 +90,23 @@
<v-btn color="primary" text @click="cancelSendAttachment" <v-btn color="primary" text @click="cancelSendAttachment"
>Cancel</v-btn >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-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
</div> </div>
<div>
<v-dialog v-model="showEmojiPicker" class="ma-0 pa-0" width="50%">
<VEmojiPicker style="width: 100%" @select="emojiSelected" />
</v-dialog>
</div>
</div> </div>
</template> </template>
@ -90,6 +124,7 @@ import DebugEvent from "./messages/DebugEvent.vue";
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue"; import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
import MessageIncomingImage from "./messages/MessageIncomingImage.vue"; import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
import util from "../plugins/utils"; import util from "../plugins/utils";
import MessageOperations from "./messages/MessageOperations.vue";
// from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html // from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html
function ScrollPosition(node) { function ScrollPosition(node) {
@ -133,6 +168,7 @@ export default {
DebugEvent, DebugEvent,
MessageOutgoingImage, MessageOutgoingImage,
MessageIncomingImage, MessageIncomingImage,
MessageOperations,
}, },
data() { data() {
@ -148,37 +184,24 @@ export default {
currentSendOperation: null, currentSendOperation: null,
currentSendProgress: null, currentSendProgress: null,
currentSendError: null, currentSendError: null,
joinRoom: null, showEmojiPicker: false,
selectedEvent: null,
}; };
}, },
mounted() { mounted() {
const container = this.$refs.chatContainer; const container = this.$refs.chatContainer;
this.scrollPosition = new ScrollPosition(container); this.scrollPosition = new ScrollPosition(container);
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);
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() { destroyed() {
this.$matrix.off("Room.timeline", this.onEvent); this.$matrix.off("Room.timeline", this.onEvent);
this.$matrix.off("RoomMember.typing", this.onUserTyping); this.$matrix.off("RoomMember.typing", this.onUserTyping);
this.$matrix.off("Matrix.initialized", this.onInitialized);
}, },
computed: { computed: {
myUserId() {
return this.$store.state.auth.user.user_id;
},
roomId() { roomId() {
return this.$matrix.currentRoomId; return this.$matrix.currentRoomId;
}, },
@ -188,69 +211,55 @@ export default {
}, },
watch: { watch: {
roomId() { roomId: {
console.log("Chat: Current room changed"); handler(ignoredNewVal, ignoredOldVal) {
console.log("Chat: Current room changed");
// 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) {
return; // no room return; // no room
} }
this.room = this.$matrix.getRoom(this.roomId); this.room = this.$matrix.getRoom(this.roomId);
if (!this.room) { if (!this.room) {
return; // Not found return; // Not found
} }
this.timelineWindow = new TimelineWindow( this.timelineWindow = new TimelineWindow(
this.$matrix.matrixClient, this.$matrix.matrixClient,
this.room.getUnfilteredTimelineSet(), this.room.getUnfilteredTimelineSet(),
{} {}
); );
this.timelineWindow.load(null, 20).then(() => { this.timelineWindow.load(null, 20).then(() => {
this.events = this.timelineWindow.getEvents(); this.events = this.timelineWindow.getEvents();
this.$nextTick(() => { this.$nextTick(() => {
this.paginateBackIfNeeded(); this.paginateBackIfNeeded();
});
}); });
}); },
immediate: true,
}, },
}, },
methods: { 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) { componentForEvent(event) {
switch (event.getType()) { switch (event.getType()) {
case "m.room.member": case "m.room.member":
if (event.event.state_key != this.myUserId) { if (event.getContent().membership == "join") {
if (event.getContent().membership == "join") { return ContactJoin;
return ContactJoin; } else if (event.getContent().membership == "leave") {
} else if (event.getContent().membership == "leave") { return ContactLeave;
return ContactLeave; } else if (event.getContent().membership == "invite") {
} else if (event.getContent().membership == "invite") { return ContactInvited;
return ContactInvited;
}
} }
break; break;
case "m.room.message": case "m.room.message":
if (event.getSender() != this.myUserId) { if (event.getSender() != this.$matrix.currentUserId) {
if (event.getContent().msgtype == "m.image") { if (event.getContent().msgtype == "m.image") {
return MessageIncomingImage; return MessageIncomingImage;
} }
@ -319,13 +328,18 @@ export default {
sendMessage() { sendMessage() {
if (this.currentInput.length > 0) { if (this.currentInput.length > 0) {
util.sendTextMessage(this.$matrix.matrixClient, this.roomId, this.currentInput) util
.then(() => { .sendTextMessage(
console.log("Sent message"); this.$matrix.matrixClient,
}) this.roomId,
.catch(err => { this.currentInput
console.log("Failed to send:", err); )
}) .then(() => {
console.log("Sent message");
})
.catch((err) => {
console.log("Failed to send:", err);
});
this.currentInput = ""; this.currentInput = "";
} }
}, },
@ -356,18 +370,23 @@ export default {
sendAttachment() { sendAttachment() {
if (this.currentImageInputPath) { if (this.currentImageInputPath) {
this.currentSendProgress = 0; 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 this.currentSendOperation
.then(() => { .then(() => {
this.currentSendOperation = null; this.currentSendOperation = null;
this.currentImageInput = null; this.currentImageInput = null;
this.currentSendProgress = 0; this.currentSendProgress = 0;
}) })
.catch(err => { .catch((err) => {
this.currentSendError = err.toLocaleString(); this.currentSendError = err.toLocaleString();
this.currentSendOperation = null; this.currentSendOperation = null;
this.currentSendProgress = 0; 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> </script>

View file

@ -38,38 +38,29 @@ export default {
handleJoin() { handleJoin() {
this.loading = true; this.loading = true;
this.loadingMessage = "Logging in..."; this.loadingMessage = "Logging in...";
var clientPromise;
if (this.currentUser) { if (this.currentUser) {
this.$matrix clientPromise = this.$matrix.getMatrixClient(this.currentUser);
.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();
});
} else { } else {
this.$store.dispatch("auth/login", this.guestUser).then( clientPromise = this.$store.dispatch("auth/login", this.guestUser);
() => {
this.loading = false;
this.loadingMessage = null;
this.$router.replace({ name: "Chat" });
},
(error) => {
this.loading = false;
this.loadingMessage = error.toString();
}
);
} }
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();
});
}, },
}, },
}; };

View file

@ -4,6 +4,7 @@
<div class="sender">{{ messageEventDisplayName(event) }}</div> <div class="sender">{{ messageEventDisplayName(event) }}</div>
<div class="bubble image-bubble"> <div class="bubble image-bubble">
<v-img :aspect-ratio="16 / 9" ref="image" :src="src" cover /> <v-img :aspect-ratio="16 / 9" ref="image" :src="src" cover />
<QuickReactions :event="event" :reactions="reactions" />
</div> </div>
<v-avatar class="avatar" size="40" color="grey"> <v-avatar class="avatar" size="40" color="grey">
<img <img

View file

@ -14,6 +14,7 @@
<div class="bubble"> <div class="bubble">
<div class="message">{{ event.getContent().body }}</div> <div class="message">{{ event.getContent().body }}</div>
<QuickReactions :event="event" :reactions="reactions" />
</div> </div>
</div> </div>
<div class="time"> <div class="time">

View 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>

View file

@ -4,6 +4,7 @@
<div class="sender">{{ "You" }}</div> <div class="sender">{{ "You" }}</div>
<div class="bubble image-bubble"> <div class="bubble image-bubble">
<v-img :aspect-ratio="16/9" ref="image" :src="src" cover /> <v-img :aspect-ratio="16/9" ref="image" :src="src" cover />
<QuickReactions :event="event" :reactions="reactions" />
</div> </div>
<div class="status">{{ event.status }}</div> <div class="status">{{ event.status }}</div>
</div> </div>

View file

@ -4,6 +4,7 @@
<div class="sender">{{ "You" }}</div> <div class="sender">{{ "You" }}</div>
<div class="bubble"> <div class="bubble">
<div class="message">{{ event.getContent().body }}</div> <div class="message">{{ event.getContent().body }}</div>
<QuickReactions :event="event" :reactions="reactions" />
</div> </div>
<div class="status">{{ event.status }}</div> <div class="status">{{ event.status }}</div>
</div> </div>
@ -20,7 +21,6 @@ export default {
mixins: [messageMixin], mixins: [messageMixin],
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import "@/assets/css/chat.scss"; @import "@/assets/css/chat.scss";
</style> </style>

View 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>

View file

@ -1,4 +1,9 @@
import QuickReactions from './QuickReactions.vue';
export default { export default {
components: {
QuickReactions
},
props: { props: {
room: { room: {
type: Object, type: Object,
@ -12,6 +17,12 @@ export default {
return {} return {}
} }
}, },
reactions: {
type: Object,
default: function () {
return null
}
}
}, },
computed: { computed: {
}, },
@ -20,6 +31,9 @@ export default {
* Get a display name given an event. * Get a display name given an event.
*/ */
stateEventDisplayName(event) { stateEventDisplayName(event) {
if (event.getSender() == this.$matrix.currentUserId) {
return "You";
}
if (this.room) { if (this.room) {
const member = this.room.getMember(event.getSender()); const member = this.room.getMember(event.getSender());
if (member) { if (member) {
@ -64,5 +78,5 @@ export default {
} }
return date.toLocaleString(); return date.toLocaleString();
}, },
} },
} }

View file

@ -6,11 +6,26 @@ import store from './store'
import matrix from './services/matrix.service' import matrix from './services/matrix.service'
import 'roboto-fontface/css/roboto/roboto-fontface.css' import 'roboto-fontface/css/roboto/roboto-fontface.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css' import 'material-design-icons-iconfont/dist/material-design-icons.css'
import VEmojiPicker from 'v-emoji-picker';
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.use(VEmojiPicker);
Vue.use(matrix, {store: store}); 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({ new Vue({
vuetify, vuetify,
router, router,

View file

@ -76,12 +76,23 @@ class Util {
} }
sendTextMessage(matrixClient, roomId, text) { 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) => { return new Promise((resolve, reject) => {
matrixClient.sendMessage(roomId, content, undefined, undefined) matrixClient.sendEvent(roomId, eventType, content, undefined, undefined)
.then((result) => { .then((result) => {
console.log("Message sent: ", result); console.log("Message sent: ", result);
resolve(true); resolve(true);
@ -127,22 +138,57 @@ class Util {
}); });
} }
sendEncyptedImage(matrixClient, roomId, file, onUploadProgress) { sendImage(matrixClient, roomId, file, onUploadProgress) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var reader = new FileReader(); var reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
const fileContents = e.target.result; 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'); const crypto = require('crypto');
let key = crypto.randomBytes(256 / 8); let key = crypto.randomBytes(256 / 8);
let iv = Buffer.concat([crypto.randomBytes(8),Buffer.alloc(8)]); // Initialization vector. let iv = Buffer.concat([crypto.randomBytes(8),Buffer.alloc(8)]); // Initialization vector.
// Encrypt // Encrypt
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv)); var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
const data = new Uint8Array(fileContents);
var encryptedBytes = aesCtr.encrypt(data); var encryptedBytes = aesCtr.encrypt(data);
data = encryptedBytes;
// Calculate sha256 // Calculate sha256
var hash = new Uint8Array(sha256.create().update(encryptedBytes).arrayBuffer()); var hash = new Uint8Array(sha256.create().update(data).arrayBuffer());
const jwk = { const jwk = {
kty: 'oct', kty: 'oct',
@ -159,20 +205,7 @@ class Util {
hashes: { sha256: Buffer.from(hash).toString('base64').replace( /=/g, '' )}, hashes: { sha256: Buffer.from(hash).toString('base64').replace( /=/g, '' )},
v: 'v2' 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 = { const messageContent = {
body: 'Image', body: 'Image',
file: encryptedFile, file: encryptedFile,
@ -180,10 +213,10 @@ class Util {
msgtype: 'm.image' msgtype: 'm.image'
} }
matrixClient.uploadContent(encryptedBytes, opts) matrixClient.uploadContent(data, opts)
.then((uri) => { .then((response) => {
encryptedFile.url = uri; encryptedFile.url = response.content_uri;
return this.sendMessage(matrixClient, roomId, messageContent) return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent)
}) })
.then(result => { .then(result => {
resolve(result); resolve(result);

View file

@ -38,6 +38,11 @@ export default {
return this.$store.state.auth.user; return this.$store.state.auth.user;
}, },
currentUserId() {
const user = this.currentUser || {}
return user.user_id;
},
currentRoomId() { currentRoomId() {
return this.$store.state.currentRoomId; return this.$store.state.currentRoomId;
}, },