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,5 +1,5 @@
<template>
<div :class="{'chat-root': true, 'fill-height': true, 'd-flex': true, 'flex-column': true, 'channel': roomDisplayType == 'im.keanu.room_type_channel'}" :style="chatContainerStyle">
<div :class="{'chat-root': true, 'fill-height': true, 'd-flex': true, 'flex-column': true, 'channel': roomDisplayType == ROOM_TYPE_CHANNEL}" :style="chatContainerStyle">
<ChatHeaderPrivate class="chat-header flex-grow-0 flex-shrink-0"
v-on:header-click="onHeaderClick"
v-on:view-room-details="viewRoomDetails"
@ -35,9 +35,14 @@
<div v-if="!useVoiceMode && !useFileModeNonAdmin" :class="{'chat-content': true, 'flex-grow-1': true, 'flex-shrink-1': true, 'invisible': !initialLoadDone}" ref="chatContainer"
v-on:scroll="onScroll" @click="closeContextMenusIfOpen">
<div ref="messageOperationsStrut" class="message-operations-strut">
<message-operations-channel ref="messageOperations" :style="opStyle" v-on:close="showContextMenu = false;" v-if="showMessageOperations && room.displayType == ROOM_TYPE_CHANNEL" :userCanPin="true"
:originalEvent="selectedEvent" :timelineSet="timelineSet"
v-on:pin="pin(selectedEvent)"
v-on:unpin="unpin(selectedEvent)"
/>
<message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
showContextMenu = false;
" v-if="showMessageOperations" v-on:addreaction="addReaction" v-on:addquickreaction="addQuickReaction"
" v-else-if="showMessageOperations" 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)" v-on:more="
isEmojiQuickReaction= true
@ -56,11 +61,12 @@
<!-- If we have a retention timer, it means we have active message retention. Show header. -->
<WelcomeHeaderChannelUser v-if="retentionTimer && !roomWelcomeHeader && newlyJoinedRoom" />
<div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()">
<div v-for="(event) in filteredEvents" :key="event.getId()" :eventId="event !== ROOM_READ_MARKER_EVENT_PLACEHOLDER ? event.getId() : undefined">
<!-- DAY Marker, shown for every new day in the timeline -->
<div v-if="!reverseOrder && showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
<div v-if="!reverseOrder && showDayMarkerBeforeEvent(event) && !!event.component && event !== ROOM_READ_MARKER_EVENT_PLACEHOLDER" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
<div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
<div :ref="event.getId()">
<MessageErrorHandler>
<div class="message-wrapper" v-on:touchstart="
(e) => {
@ -68,14 +74,12 @@
}
" v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
<div v-if="reverseOrder && event.getId() == readMarker && index > 0" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line"></div></div>
<!-- Note: For threaded media messages, IF there is only one item we show that media item as a single component.
We might therefore get calls to v-on:context-menu that has the event set to that single media item, not the top level thread event
that is really displayed in the flow. Therefore, we rewrite these events with "{event: event, anchor: $event.anchor}",
see below. Otherwise things like context menus won't work as designed.
-->
<component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="filteredEvents[index + 1]"
<component :is="event.component" :room="room" :originalEvent="event" :nextEvent="event.nextDisplayedEvent"
:timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
:componentFn="componentForEvent"
v-on:context-menu="showContextMenuForEvent({event: event, anchor: $event.anchor})"
@ -92,13 +96,12 @@
/>
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}<br /><br /></div> -->
<div v-if="!reverseOrder && event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line"></div></div>
</div>
</MessageErrorHandler>
</div>
<!-- Day marker when reverseOrder is set -->
<div v-if="reverseOrder && showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
<div v-if="reverseOrder && showDayMarkerBeforeEvent(event) && !!event.component && event !== ROOM_READ_MARKER_EVENT_PLACEHOLDER" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
</div>
@ -369,7 +372,7 @@ import UserProfileDialog from "./UserProfileDialog.vue"
import BottomSheet from "./BottomSheet.vue";
import ImageResize from "image-resize";
import CreatePollDialog from "./CreatePollDialog.vue";
import chatMixin from "./chatMixin";
import chatMixin, { ROOM_READ_MARKER_EVENT_PLACEHOLDER } from "./chatMixin";
import sendAttachmentsMixin from "./sendAttachmentsMixin";
import AudioLayout from "./AudioLayout.vue";
import FileDropLayout from "./file_mode/FileDropLayout";
@ -377,6 +380,7 @@ import roomTypeMixin from "./roomTypeMixin";
import roomMembersMixin from "./roomMembersMixin";
import PurgeRoomDialog from "../components/PurgeRoomDialog";
import MessageErrorHandler from "./MessageErrorHandler";
import MessageOperationsChannel from './messages/channel/MessageOperationsChannel.vue';
const sizeOf = require("image-size");
const dataUriToBuffer = require("data-uri-to-buffer");
@ -431,11 +435,15 @@ export default {
UserProfileDialog,
PurgeRoomDialog,
WelcomeHeaderChannelUser,
MessageErrorHandler
MessageErrorHandler,
MessageOperationsChannel
},
data() {
return {
ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
ROOM_READ_MARKER_EVENT_PLACEHOLDER: ROOM_READ_MARKER_EVENT_PLACEHOLDER,
waitingForRoomObject: false,
events: [],
currentInput: "",
@ -690,6 +698,8 @@ export default {
},
filteredEvents() {
let events = this.events;
if (this.room && this.$matrix.matrixClient.isRoomEncrypted(this.room.roomId)) {
if (this.room.getHistoryVisibility() == "joined") {
// For encrypted rooms where history is set to "joined" we can't read old events.
@ -702,12 +712,37 @@ export default {
(!e.getPrevContent() || e.getPrevContent().membership != "join") &&
e.getStateKey() == this.$matrix.currentUserId) {
// Our own join event.
return this.reverseOrder ? this.events.slice(idx + 1).toReversed() : this.events.slice(idx + 1);
events = this.events.slice(idx + 1);
}
}
}
}
return this.reverseOrder ? this.events.toReversed() : this.events;
// Filter out relations and redactions
events = this.events.toReversed().filter((e) => !e.isRelation() && !e.isRedaction());
// Add read marker, if it is not newer than the "latest" message we are going to display
//
let lastDisplayedEvent = undefined;
events = events.flatMap((e) => {
let result = [];
Vue.set(e, "component", this.componentForEvent(e, false));
if (e.getId() == this.readMarker && lastDisplayedEvent !== undefined) {
const readMarkerEvent = ROOM_READ_MARKER_EVENT_PLACEHOLDER;
Vue.set(readMarkerEvent, "component", this.componentForEvent(readMarkerEvent, false));
if (readMarkerEvent.component) {
Vue.set(e, "nextDisplayedEvent", lastDisplayedEvent);
}
result.push(readMarkerEvent);
}
if (e.component) {
Vue.set(e, "nextDisplayedEvent", lastDisplayedEvent);
lastDisplayedEvent = e;
}
result.push(e);
return result;
})
return (this.reverseOrder ? events : events.toReversed()) // Reverse back if needed
},
roomCreatedByUsRecently() {
@ -886,6 +921,12 @@ export default {
var rectAnchor = this.showContextMenuAnchor.getBoundingClientRect();
var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect();
var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
if (this.room.displayType == ROOM_TYPE_CHANNEL) {
top = rectAnchor.top - rectChat.top;
let right = rectChat.right - rectAnchor.right;
this.opStyle = "top:" + top + "px;right:" + right + "px";
return;
} else {
top = rectAnchor.top - rectChat.top - 50;
left = rectAnchor.left - rectChat.left - 75;
if (left + rectOps.width + 10 >= rectChat.right) {
@ -894,6 +935,7 @@ export default {
left = 0;
}
}
}
}
this.opStyle = "top:" + top + "px;left:" + left + "px";
});
@ -916,10 +958,30 @@ export default {
},
/**
* Set events to display. At the same time, filter out messages that are past rentention period etc.
* Set events to display. At the same time, filter out messages that are past rentention period etc. Also, filter pinned events "at the top"
*/
setEvents(events) {
this.events = this.filterOutOldAndInvisible(events);
setEvents(events, onlyIfLengthChanges = false) {
let updated = this.filterOutOldAndInvisible(events);
// Handle pinning
//
const pinnedEvents = this.$matrix.getPinnedEvents(this.room);
console.log("Pinned events in room", JSON.stringify(pinnedEvents));
events.forEach((e) => {
Vue.set(e, "isPinned", pinnedEvents.includes(e.getId()));
});
updated = updated.sort((e1, e2) => {
if (!e1.isPinned && !e2.isPinned) return 0;
else if (e1.isPinned && !e2.isPinned) return this.reverseOrder ? 1 : -1;
else if (e2.isPinned && !e1.isPinned) return this.reverseOrder ? -1 : 1;
else {
// Look at order in "pinned" value in the m.room.pinned_events event!
return pinnedEvents.indexOf(e1.getId()) < pinnedEvents.indexOf(e2.getId()) ? (this.reverseOrder ? 1 : -1) : (this.reverseOrder ? -1 : 1)
}
});
if (!onlyIfLengthChanges || updated.length != this.events.length) {
this.events = updated; // Changed
}
},
filterOutOldAndInvisible(events) {
@ -962,10 +1024,7 @@ export default {
},
onRetentionTimer() {
const events = this.filterOutOldAndInvisible(this.events);
if (events.length != this.events.length) {
this.events = events; // Changed
}
this.setEvents(this.events, true);
},
onRoomJoined(initialEventId) {
@ -980,6 +1039,7 @@ export default {
}
this.reverseOrder = (this.room && this.roomDisplayType == ROOM_TYPE_CHANNEL);
Vue.set(this.room, "displayType", this.roomDisplayType);
// Listen to events
this.$matrix.on("Room.timeline", this.onEvent);
@ -1280,7 +1340,7 @@ export default {
this.paginateBackIfNeeded();
}
if (loadingDone && event.forwardLooking && (!event.isRelation() || event.isMxThread || event.threadRootId || event.parentThread)) {
if (loadingDone && event.forwardLooking && (!(event.isRelation() || event.isRedaction()) || event.isMxThread || event.threadRootId || event.parentThread)) {
// If we are at bottom, scroll to see new events...
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
const container = this.chatContainer;
@ -1704,6 +1764,14 @@ export default {
}
},
pin(event) {
this.$matrix.setEventPinned(this.room, event, true);
},
unpin(event) {
this.$matrix.setEventPinned(this.room, event, false);
},
cancelEditReply() {
this.currentInput = "";
this.editedEvent = null;
@ -2006,9 +2074,9 @@ export default {
if (!this.useVoiceMode) { // Voice mode has own autoplay handling inside "AudioLayout"!
// Auto play consecutive audio messages, either incoming or sent.
const filteredEvents = this.filteredEvents;
const index = filteredEvents.findIndex(e => e.getId() === matrixEventId);
if (index >= 0 && index < (filteredEvents.length - 1)) {
const nextEvent = filteredEvents[index + 1];
const event = filteredEvents.find(e => e.getId() === matrixEventId);
if (event && event.nextDisplayedEvent) {
const nextEvent = event.nextDisplayedEvent;
if (nextEvent.getContent().msgtype === "m.audio") {
// Yes, audio event!
this.$audioPlayer.play(nextEvent, this.timelineSet);

View file

@ -187,6 +187,7 @@ export default {
this.cancelled = true;
},
async getEvents() {
// TODO - Handle pinned messages?
const eventsPerBatch = 100;
let batchToken = null;
var nToFetch = null;

View file

@ -53,9 +53,12 @@ import RoomGuestAccessChanged from "./messages/RoomGuestAccessChanged.vue";
import RoomEncrypted from "./messages/RoomEncrypted.vue";
import RoomDeletionNotice from "./messages/RoomDeletionNotice.vue";
import DebugEvent from "./messages/DebugEvent.vue";
import ReadMarker from "./messages/ReadMarker.vue";
import roomDisplayOptionsMixin from "./roomDisplayOptionsMixin";
import roomTypeMixin from "./roomTypeMixin";
export const ROOM_READ_MARKER_EVENT_PLACEHOLDER = { getId: () => "ROOM_READ_MARKER" };
export default {
mixins: [ roomDisplayOptionsMixin, roomTypeMixin ],
components: {
@ -101,6 +104,7 @@ export default {
StickerPickerBottomSheet,
BottomSheet,
CreatePollDialog,
ReadMarker
},
methods: {
showDayMarkerBeforeEvent(event) {
@ -127,6 +131,9 @@ export default {
componentForEvent(event, isForExport = false) {
const isChannel = this.roomDisplayType === ROOM_TYPE_CHANNEL;
if (event === ROOM_READ_MARKER_EVENT_PLACEHOLDER) {
return ReadMarker;
}
switch (event.getType()) {
case "m.room.member":
if (isChannel) break;

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});
},
}
}