Lots of channel related fixes and updates

This commit is contained in:
N-Pex 2024-10-11 17:04:32 +02:00
parent e3bfede77e
commit ca777a83be
17 changed files with 508 additions and 59 deletions

View file

@ -1,7 +1,7 @@
<template>
<!-- BASE CLASS FOR INCOMING MESSAGE -->
<div :class="messageClasses">
<div v-if="showSenderAndTime" class="senderAndTime">
<div v-if="showSenderAndTime || room.displayType == ROOM_TYPE_CHANNEL" class="senderAndTime">
<div class="sender">{{ eventSenderDisplayName(event) }}</div>
<div class="time">
{{ utils.formatTime(event.event.origin_server_ts) }}
@ -17,24 +17,31 @@
<span ref="messageInOutRef" class="content">
<slot></slot>
</span>
<div class="op-button" ref="opbutton" v-if="!event.isRedacted()">
<div class="pin-icon" v-if="event.isPinned"><v-icon>$vuetify.icons.ic_pin_filled</v-icon></div>
<div class="op-button" ref="opbutton" v-if="!event.isRedacted() && room.displayType != ROOM_TYPE_CHANNEL">
<v-btn id="btn-more" icon @click.stop="showContextMenu($refs.opbutton)">
<v-icon>more_vert</v-icon>
</v-btn>
</div>
<QuickReactions :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
<SeenBy :room="room" :event="event"/>
<QuickReactionsChannel v-if="room.displayType == ROOM_TYPE_CHANNEL" :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
<QuickReactions v-else :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
<SeenBy v-if="room.displayType != ROOM_TYPE_CHANNEL" :room="room" :event="event"/>
</div>
</template>
<script>
import SeenBy from "./SeenBy.vue";
import messageMixin from "./messageMixin";
import util from "../../plugins/utils";
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
import QuickReactions from "./QuickReactions.vue";
import QuickReactionsChannel from "./channel/QuickReactionsChannel.vue";
export default {
mixins: [messageMixin],
components: { SeenBy },
components: { QuickReactions, QuickReactionsChannel, SeenBy },
data() {
return { ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL }
},
mounted() {
if(util.isMobileOrTabletBrowser() && this.$refs.messageInOutRef) {
this.initMsgHammerJs(this.$refs.messageInOutRef);

View file

@ -1,7 +1,8 @@
<template>
<!-- BASE CLASS FOR OUTGOING MESSAGE -->
<div class="messageOut">
<div :class="messageClasses">
<div class="senderAndTime">
<div class="sender" v-if="room.displayType == ROOM_TYPE_CHANNEL">{{ eventSenderDisplayName(event) }}</div>
<div class="time">
{{ utils.formatTime(event.event.origin_server_ts) }}
</div>
@ -13,6 +14,8 @@
<v-icon>more_vert</v-icon>
</v-btn>
</div>
<div class="pin-icon" v-if="event.isPinned"><v-icon>$vuetify.icons.ic_pin_filled</v-icon></div>
<!-- SLOT FOR CONTENT -->
<span ref="messageInOutRef" class="content">
<slot></slot>
@ -26,19 +29,25 @@
<img v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text headline">{{ userAvatarLetter }}</span>
</v-avatar>
<QuickReactions :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
<SeenBy :room="room" :event="event"/>
<QuickReactionsChannel v-if="room.displayType == ROOM_TYPE_CHANNEL" :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
<QuickReactions v-else :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
<SeenBy v-if="room.displayType != ROOM_TYPE_CHANNEL" :room="room" :event="event"/>
</div>
</template>
<script>
import SeenBy from "./SeenBy.vue";
import messageMixin from "./messageMixin";
import util from "../../plugins/utils";
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
import QuickReactions from "./QuickReactions.vue";
import QuickReactionsChannel from "./channel/QuickReactionsChannel.vue";
export default {
mixins: [messageMixin],
components: { SeenBy },
components: { QuickReactions, QuickReactionsChannel, SeenBy },
data() {
return { ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL }
},
mounted() {
if(util.isMobileOrTabletBrowser() && this.$refs.messageInOutRef) {
this.initMsgHammerJs(this.$refs.messageInOutRef);

View file

@ -0,0 +1,17 @@
<template>
<div class="read-marker">
<div class="line"></div>
<div class="text">{{ $t('message.unread_messages') }}</div>
<div class="line"></div>
</div>
</template>
<script>
export default {
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
</style>

View file

@ -0,0 +1,34 @@
<template>
<div :class="{'message-operations':true,'incoming':incoming,'outgoing':!incoming}">
<v-btn id="btn-pin" icon @click.stop="pin" class="ma-0 pa-0" v-if="userCanPin && !event.isPinned">
<v-icon small>$vuetify.icons.ic_pin_filled</v-icon>
</v-btn>
<v-btn id="btn-unpin" icon @click.stop="unpin" class="ma-0 pa-0" v-if="userCanPin && event.isPinned">
<v-icon small>$vuetify.icons.ic_pin</v-icon>
</v-btn>
</div>
</template>
<script>
import messageMixin from "../messageMixin";
import messageOperationsMixin from "../messageOperationsMixin";
export default {
mixins: [messageMixin, messageOperationsMixin],
data() {
return {}
},
props: {
userCanPin: {
type: Boolean,
default: function () {
return false;
}
},
},
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
</style>

View file

@ -0,0 +1,127 @@
<template>
<div class="quick-reaction-container">
<div
class="emoji"
v-for="(value, name) in reactionMap"
:key="name"
v-show="name == '❤️'"
>
<v-tooltip top v-if="value.includes($matrix.currentUserId)">
<template v-slot:activator="{ on, attrs }">
<v-icon class="ma-1 ml-0 clickable" v-bind="attrs" v-on="on" @click="onClickEmoji(name)">$vuetify.icons.ic_like_filled</v-icon>
</template>
<span>{{ $t("global.click_to_remove") }}</span>
</v-tooltip>
<v-icon v-else class="ma-1 ml-0 clickable" @click="onClickEmoji(name)">$vuetify.icons.ic_like</v-icon> {{ value.length }}
</div>
</div>
</template>
<script>
import messageOperationsMixin from "../messageOperationsMixin";
export default {
mixins: [messageOperationsMixin],
props: {
event: {
type: Object,
default: function () {
return {}
}
},
timelineSet: {
type: Object,
default: function () {
return null
}
},
},
data() {
return {
reactionMap: {"❤️": []},
reactions: null,
REACTION_LIMIT: 5,
showAllReaction: false
}
},
mounted() {
this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction');
this.event.on("Event.relationsCreated", this.onRelationsCreated);
},
beforeDestroy() {
this.event.off("Event.relationsCreated", this.onRelationsCreated);
if (this.reactions) {
this.reactions.off('Relations.add', this.onAddRelation);
}
},
computed: {
totalReaction() {
return Object.keys(this.reactionMap).length
},
otherReactionText() {
return this.showAllReaction ? this.$t("global.show_less") : this.$t("message.reaction_count_more", { reactionCount: this.totalReaction - this.REACTION_LIMIT })
}
},
methods: {
onRelationsCreated() {
this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction');
},
onClickEmoji(emoji) {
this.$bubble('send-quick-reaction', {reaction:emoji, event:this.event});
},
onAddRelation(ignoredevent) {
this.processReactions();
},
onRemoveRelation(ignoredevent) {
this.processReactions();
},
onRedactRelation(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 (r.isRedacted()) {
delete array[sender];
}
if (!array.includes(sender)) {
array.push(sender)
}
} else if (!r.isRedacted()) {
reactionMap[emoji] = [sender];
}
}
}
this.reactionMap = reactionMap;
}
},
watch: {
reactions: {
handler(newValue, oldValue) {
if (oldValue) {
oldValue.off('Relations.add', this.onAddRelation);
oldValue.off('Relations.remove', this.onRemoveRelation);
oldValue.off('Relations.redaction', this.onRedactRelation);
}
if (newValue) {
newValue.on('Relations.add', this.onAddRelation);
newValue.on('Relations.remove', this.onRemoveRelation);
newValue.on('Relations.redaction', this.onRedactRelation);
}
this.processReactions();
},
immediate: true
}
}
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
</style>

View file

@ -109,7 +109,7 @@ export default {
* Don't show sender and time if the next event is within 2 minutes and also from us (= back to back messages)
*/
showSenderAndTime() {
if (this.nextEvent && this.nextEvent.getSender() == this.event.getSender()) {
if (!this.event.isPinned && this.nextEvent && this.nextEvent.getSender() == this.event.getSender()) {
const ts1 = this.nextEvent.event.origin_server_ts;
const ts2 = this.event.event.origin_server_ts;
return ts1 - ts2 < 2 * 60 * 1000; // less than 2 minutes
@ -181,11 +181,15 @@ export default {
},
/**
* Classes to set for the message. Currently only for "messageIn", TODO: - detect messageIn or messageOut.
* Classes to set for the message. Currently only for "messageIn"
*/
messageClasses() {
return { messageIn: true, "from-admin": this.senderIsAdminOrModerator(this.event) };
if (this.incoming) {
return { messageIn: true, "from-admin": this.senderIsAdminOrModerator(this.event), "pinned": this.event.isPinned };
} else {
return { messageOut: true, "pinned": this.event.isPinned };
}
},
userAvatar() {

View file

@ -50,5 +50,13 @@ export default {
this.$emit("close");
this.$emit("more", {event:this.event});
},
pin() {
this.$emit("close");
this.$emit("pin", {event:this.event});
},
unpin() {
this.$emit("close");
this.$emit("unpin", {event:this.event});
},
}
}