Style quick reactions.

Issue #65. Move "edit" and "delete" to emoji picker dialog, accessible through the "..." button.
This commit is contained in:
N-Pex 2021-04-09 14:03:40 +02:00
parent 779e53c3b2
commit 61dbcad131
6 changed files with 193 additions and 71 deletions

View file

@ -377,15 +377,24 @@ $admin-fg: white;
.message-operations { .message-operations {
position: absolute; position: absolute;
width: fit-content; width: fit-content;
background-color: #e2e2e2; background-color: white;
height: 40px;
border-radius: 20px;
box-shadow: 4px 4px 8px #888888;
// &.incoming { // &.incoming {
// right: 30%; // right: 30%;
// } // }
// &.outgoing { // &.outgoing {
// left: 30%; // left: unset !important;
// right: 10px !important;
// } // }
} }
.message-operations-picker {
background-color: white;
text-align: center;
}
.quick-reaction-container { .quick-reaction-container {
position: absolute; position: absolute;
background-color: #000000; background-color: #000000;

View file

@ -13,16 +13,19 @@
> >
<div ref="messageOperationsStrut" class="message-operations-strut"> <div ref="messageOperationsStrut" class="message-operations-strut">
<message-operations <message-operations
ref="messageOperations"
:style="opStyle" :style="opStyle"
:emojis="recentEmojis"
v-on:close="showContextMenu = false" v-on:close="showContextMenu = false"
v-if="selectedEvent && showContextMenu" v-if="selectedEvent && showContextMenu"
v-on:addreaction="addReaction" v-on:addreaction="addReaction"
v-on:addquickreaction="addQuickReaction"
v-on:addreply="addReply(selectedEvent)" v-on:addreply="addReply(selectedEvent)"
v-on:edit="edit(selectedEvent)" v-on:edit="edit(selectedEvent)"
v-on:redact="redact(selectedEvent)" v-on:redact="redact(selectedEvent)"
v-on:download="download(selectedEvent)" v-on:download="download(selectedEvent)"
v-on:more="showMoreMessageOperations"
:event="selectedEvent" :event="selectedEvent"
:incoming="selectedEvent.getSender() != $matrix.currentUserId"
/> />
</div> </div>
@ -255,11 +258,22 @@
</v-dialog> </v-dialog>
</div> </div>
<div v-if="showEmojiPicker"> <v-dialog v-model="showEmojiPicker" class="ma-0 pa-0" eager>
<v-dialog v-model="showEmojiPicker" class="ma-0 pa-0"> <div>
<VEmojiPicker style="width: 100%" @select="emojiSelected" /> <MessageOperationsPicker
</v-dialog> v-on:close="showEmojiPicker = false"
</div> v-if="selectedEvent"
v-on:addreaction="addReaction"
v-on:addquickreaction="addQuickReaction"
v-on:addreply="addReply(selectedEvent)"
v-on:edit="edit(selectedEvent)"
v-on:redact="redact(selectedEvent)"
v-on:download="download(selectedEvent)"
:event="selectedEvent"
/>
<VEmojiPicker ref="emojiPicker" style="width: 100%" @select="emojiSelected" />
</div>
</v-dialog>
<!-- "NOT ALLOWED FOR GUEST ACCOUNTS" dialog --> <!-- "NOT ALLOWED FOR GUEST ACCOUNTS" dialog -->
<v-dialog v-model="showNotAllowedForGuests" class="ma-0 pa-0" width="50%"> <v-dialog v-model="showNotAllowedForGuests" class="ma-0 pa-0" width="50%">
@ -321,6 +335,7 @@ import RoomJoinRules from "./messages/RoomJoinRules.vue";
import DebugEvent from "./messages/DebugEvent.vue"; import DebugEvent from "./messages/DebugEvent.vue";
import util from "../plugins/utils"; import util from "../plugins/utils";
import MessageOperations from "./messages/MessageOperations.vue"; import MessageOperations from "./messages/MessageOperations.vue";
import MessageOperationsPicker from "./messages/MessageOperationsPicker.vue";
import ChatHeader from "./ChatHeader"; import ChatHeader from "./ChatHeader";
import VoiceRecorder from "./VoiceRecorder"; import VoiceRecorder from "./VoiceRecorder";
import RoomInfoBottomSheet from "./RoomInfoBottomSheet"; import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
@ -379,6 +394,7 @@ export default {
RoomJoinRules, RoomJoinRules,
DebugEvent, DebugEvent,
MessageOperations, MessageOperations,
MessageOperationsPicker,
VoiceRecorder, VoiceRecorder,
RoomInfoBottomSheet, RoomInfoBottomSheet,
CreatedRoomWelcomeHeader CreatedRoomWelcomeHeader
@ -430,7 +446,10 @@ export default {
lastRR: null, lastRR: null,
/** If we just created this room, show a small welcome header with info */ /** If we just created this room, show a small welcome header with info */
showCreatedRoomWelcomeHeader: false showCreatedRoomWelcomeHeader: false,
/** An array of recent emojis. Used in the "message operations" popup. */
recentEmojis: []
}; };
}, },
@ -522,6 +541,9 @@ export default {
var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect(); var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect();
top = rectAnchor.top - rectChat.top; top = rectAnchor.top - rectChat.top;
left = rectAnchor.left - rectChat.left; left = rectAnchor.left - rectChat.left;
if (left + 250 > rectChat.right) {
left = rectChat.right - 250; // Pretty ugly, but we want to make sure it does not escape the screen, and we don't have the exakt width of it (yet)!
}
} }
} }
return "top:" + top + "px;left:" + left + "px"; return "top:" + top + "px;left:" + left + "px";
@ -713,6 +735,7 @@ export default {
* Triggered when our "long tap" timer hits. * Triggered when our "long tap" timer hits.
*/ */
touchTimerElapsed() { touchTimerElapsed() {
this.updateRecentEmojis();
this.showContextMenu = true; this.showContextMenu = true;
}, },
@ -1040,6 +1063,10 @@ export default {
}); });
}, },
showMoreMessageOperations(e) {
this.addReaction(e);
},
addReaction(e) { addReaction(e) {
const event = e.event; const event = e.event;
// Store the event we are reacting to, so that we know where to // Store the event we are reacting to, so that we know where to
@ -1048,6 +1075,10 @@ export default {
this.showEmojiPicker = true; this.showEmojiPicker = true;
}, },
addQuickReaction(e) {
this.sendQuickReaction({ reaction: e.emoji, event: e.event });
},
addReply(event) { addReply(event) {
this.replyToEvent = event; this.replyToEvent = event;
this.$refs.messageInput.focus(); this.$refs.messageInput.focus();
@ -1124,6 +1155,7 @@ export default {
console.log("Got the ref", ref); console.log("Got the ref", ref);
} }
this.selectedEvent = event; this.selectedEvent = event;
this.updateRecentEmojis();
this.showContextMenu = true; this.showContextMenu = true;
this.showContextMenuAnchor = e.anchor; this.showContextMenuAnchor = e.anchor;
}, },
@ -1258,7 +1290,16 @@ export default {
// can see all messages). // can see all messages).
this.onScroll(); this.onScroll();
}); });
},
updateRecentEmojis() {
if (this.$refs.emojiPicker) {
this.recentEmojis = this.$refs.emojiPicker.mapEmojis["Frequently"];
return;
}
this.recentEmojis = [];
} }
}, },
}; };
</script> </script>

View file

@ -1,86 +1,54 @@
<template> <template>
<div :class="{'message-operations':true,'incoming':incoming,'outgoing':!incoming}"> <div :class="{'message-operations':true,'incoming':incoming,'outgoing':!incoming}">
<v-btn icon @click.stop="addReaction" class="ma-0 pa-0"> <template v-for="(item,index) in emojis">
<v-icon>mood</v-icon> <v-btn v-if="index < maxRecents" :key="item.data" icon @click.stop="addQuickReaction(item.data)" class="ma-0 pa-0">
</v-btn> <span class="recent-emoji" >{{ item.data }}</span>
</v-btn>
</template>
<v-btn v-if="incoming" icon @click.stop="addReply" class="ma-0 pa-0"> <v-btn v-if="incoming" icon @click.stop="addReply" class="ma-0 pa-0">
<v-icon>reply</v-icon> <v-icon>reply</v-icon>
</v-btn> </v-btn>
<v-btn v-if="isEditable" icon @click.stop="edit" class="ma-0 pa-0"> <v-btn icon @click.stop="more" class="ma-0 pa-0">
<v-icon>edit</v-icon> <v-icon>more_horiz</v-icon>
</v-btn> </v-btn> </div>
<v-btn v-if="isRedactable" icon @click.stop="redact" class="ma-0 pa-0">
<v-icon>delete</v-icon>
</v-btn>
<v-btn v-if="isDownloadable" icon @click.stop="download" class="ma-0 pa-0">
<v-icon>get_app</v-icon>
</v-btn>
</div>
</template> </template>
<script> <script>
import messageMixin from "./messageMixin"; import messageMixin from "./messageMixin";
import messageOperationsMixin from "./messageOperationsMixin";
export default { export default {
mixins: [messageMixin], mixins: [messageMixin, messageOperationsMixin],
data() {
return {
maxRecents: 5
}
},
props: { props: {
incoming: { emojis: {
type: Boolean, type: Array,
default: function () { default: function () {
return true return []
} }
}, }
event: {
type: Object,
default: function () {
return {}
}
},
}, },
computed: { watch: {
isEditable() { emojis: {
return !this.incoming && this.event.getContent().msgtype == "m.text"; immediate: true,
}, handler(newVal, oldVal) {
isDownloadable() { console.log("Emojis changed", newVal, oldVal);
const msgtype = this.event.getContent().msgtype;
return ['m.video','m.audio','m.image','m.file'].includes(msgtype);
},
isRedactable() {
const room = this.$matrix.matrixClient.getRoom(this.event.getRoomId());
if (room && room.currentState && room.currentState.maySendRedactionForEvent(this.event, this.$matrix.currentUserId)) {
return true;
} }
return false; },
}
}, },
methods: {
addReaction() {
this.$emit("close");
this.$emit("addreaction", {event:this.event});
},
addReply() {
this.$emit("close");
this.$emit("addreply", {event:this.event});
},
edit() {
this.$emit("close");
this.$emit("edit", {event:this.event});
},
redact() {
this.$emit("close");
this.$emit("redact", {event:this.event});
},
download() {
this.$emit("close");
this.$emit("download", {event:this.event});
}
}
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import "@/assets/css/chat.scss"; @import "@/assets/css/chat.scss";
// .recent-emoji {
// width: 30px;
// }
</style> </style>

View file

@ -0,0 +1,51 @@
<template>
<div
:class="{
'message-operations-picker': true,
incoming: incoming,
outgoing: !incoming,
}"
>
<v-container fluid>
<v-row>
<v-col v-if="incoming">
<v-btn icon @click.stop="addReply" class="ma-0 pa-0">
<v-icon>reply</v-icon>
</v-btn>
<div>Reply</div>
</v-col>
<v-col v-if="isEditable">
<v-btn icon @click.stop="edit" class="ma-0 pa-0">
<v-icon>edit</v-icon>
</v-btn>
<div>Edit</div>
</v-col>
<v-col v-if="isRedactable">
<v-btn icon @click.stop="redact" class="ma-0 pa-0">
<v-icon>delete</v-icon>
</v-btn>
<div>Delete</div>
</v-col>
<v-col v-if="isDownloadable">
<v-btn icon @click.stop="download" class="ma-0 pa-0">
<v-icon>get_app</v-icon>
</v-btn>
<div>Download</div>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
import messageMixin from "./messageMixin";
import messageOperationsMixin from "./messageOperationsMixin";
export default {
mixins: [messageMixin, messageOperationsMixin],
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
</style>

View file

@ -62,6 +62,10 @@ export default {
} }
}, },
computed: { computed: {
incoming() {
return this.event && this.event.getSender() != this.$matrix.currentUserId;
},
/** /**
* Don't show sender and time if the next event is within 2 minutes and also from us (= back to back messages) * Don't show sender and time if the next event is within 2 minutes and also from us (= back to back messages)
*/ */

View file

@ -0,0 +1,49 @@
export default {
computed: {
isEditable() {
return !this.incoming && this.event.getContent().msgtype == "m.text";
},
isDownloadable() {
const msgtype = this.event.getContent().msgtype;
return ['m.video','m.audio','m.image','m.file'].includes(msgtype);
},
isRedactable() {
const room = this.$matrix.matrixClient.getRoom(this.event.getRoomId());
if (room && room.currentState && room.currentState.maySendRedactionForEvent(this.event, this.$matrix.currentUserId)) {
return true;
}
return false;
}
},
methods: {
addReaction() {
this.$emit("close");
this.$emit("addreaction", {event:this.event});
},
addQuickReaction(emoji) {
this.$emit("close");
this.$emit("addquickreaction", {event:this.event,emoji:emoji});
},
addReply() {
this.$emit("close");
this.$emit("addreply", {event:this.event});
},
edit() {
this.$emit("close");
this.$emit("edit", {event:this.event});
},
redact() {
this.$emit("close");
this.$emit("redact", {event:this.event});
},
download() {
this.$emit("close");
this.$emit("download", {event:this.event});
},
more() {
this.$emit("close");
this.$emit("more", {event:this.event});
},
}
}