Lots of channel related fixes and updates
This commit is contained in:
parent
e3bfede77e
commit
ca777a83be
17 changed files with 508 additions and 59 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ export default {
|
|||
this.cancelled = true;
|
||||
},
|
||||
async getEvents() {
|
||||
// TODO - Handle pinned messages?
|
||||
const eventsPerBatch = 100;
|
||||
let batchToken = null;
|
||||
var nToFetch = null;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
17
src/components/messages/ReadMarker.vue
Normal file
17
src/components/messages/ReadMarker.vue
Normal 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>
|
||||
34
src/components/messages/channel/MessageOperationsChannel.vue
Normal file
34
src/components/messages/channel/MessageOperationsChannel.vue
Normal 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>
|
||||
127
src/components/messages/channel/QuickReactionsChannel.vue
Normal file
127
src/components/messages/channel/QuickReactionsChannel.vue
Normal 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>
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue