diff --git a/src/App.vue b/src/App.vue index 6b26c0b806f..12fc4cfb902 100644 --- a/src/App.vue +++ b/src/App.vue @@ -427,7 +427,6 @@ export default { // this.$store.dispatch('purgeConversationsStore') this.$store.dispatch('addConversation', response.data.ocs.data) - this.$store.dispatch('markConversationRead', token) /** * Emits a global event that is used in App.vue to update the page title once the diff --git a/src/FilesSidebarTabApp.vue b/src/FilesSidebarTabApp.vue index 9b24d4b6263..682be431796 100644 --- a/src/FilesSidebarTabApp.vue +++ b/src/FilesSidebarTabApp.vue @@ -264,7 +264,6 @@ export default { const response = await fetchConversation(this.token) this.$store.dispatch('addConversation', response.data.ocs.data) - this.$store.dispatch('markConversationRead', this.token) }, /** diff --git a/src/PublicShareAuthSidebar.vue b/src/PublicShareAuthSidebar.vue index bf005593c6d..e30f9b70d21 100644 --- a/src/PublicShareAuthSidebar.vue +++ b/src/PublicShareAuthSidebar.vue @@ -167,7 +167,6 @@ export default { try { const response = await fetchConversation(this.token) this.$store.dispatch('addConversation', response.data.ocs.data) - this.$store.dispatch('markConversationRead', this.token) // Although the current participant is automatically added to // the participants store it must be explicitly set in the diff --git a/src/PublicShareSidebar.vue b/src/PublicShareSidebar.vue index 5f39cfd21fb..451daf5dfd7 100644 --- a/src/PublicShareSidebar.vue +++ b/src/PublicShareSidebar.vue @@ -203,7 +203,6 @@ export default { try { const response = await fetchConversation(this.token) this.$store.dispatch('addConversation', response.data.ocs.data) - this.$store.dispatch('markConversationRead', this.token) // Although the current participant is automatically added to // the participants store it must be explicitly set in the diff --git a/src/components/ChatView.vue b/src/components/ChatView.vue index 2f46fb72cf5..105fafa886c 100644 --- a/src/components/ChatView.vue +++ b/src/components/ChatView.vue @@ -47,6 +47,7 @@ :aria-label="t('spreed', 'Conversation messages')" :is-chat-scrolled-to-bottom="isChatScrolledToBottom" :token="token" + :is-visible="isVisible" @setChatScrolledToBottom="setScrollStatus" /> {{ t('spreed', 'Copy link') }} + + {{ t('spreed', 'Mark as read') }} + @@ -313,6 +318,10 @@ export default { } }, + markConversationAsRead() { + this.$store.dispatch('clearLastReadMessage', { token: this.item.token }) + }, + /** * Deletes the conversation. */ diff --git a/src/components/LeftSidebar/ConversationsList/ConversationsList.vue b/src/components/LeftSidebar/ConversationsList/ConversationsList.vue index be79aca565c..66ac7251dee 100644 --- a/src/components/LeftSidebar/ConversationsList/ConversationsList.vue +++ b/src/components/LeftSidebar/ConversationsList/ConversationsList.vue @@ -115,7 +115,6 @@ export default { } if (to.name === 'conversation') { joinConversation(to.params.token) - this.$store.dispatch('markConversationRead', to.params.token) } }, diff --git a/src/components/LeftSidebar/LeftSidebar.vue b/src/components/LeftSidebar/LeftSidebar.vue index 6b827ab10f3..e9ea1415f95 100644 --- a/src/components/LeftSidebar/LeftSidebar.vue +++ b/src/components/LeftSidebar/LeftSidebar.vue @@ -442,9 +442,6 @@ export default { this.$store.dispatch('purgeConversationsStore') conversations.data.ocs.data.forEach(conversation => { this.$store.dispatch('addConversation', conversation) - if (conversation.token === this.$store.getters.getToken()) { - this.$store.dispatch('markConversationRead', this.$store.getters.getToken()) - } }) /** * Emits a global event that is used in App.vue to update the page title once the diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue index fd9106b7aa4..02f4ff4f1c0 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.vue +++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue @@ -28,143 +28,162 @@ the main body of the message as well as a quote.
  • -
    - {{ actorDisplayName }} -
    + :data-message-id="id" + :data-seen="seen" + :data-next-message-id="nextMessageId" + :data-previous-message-id="previousMessageId" + class="message">
    -
    - -
    - {{ message }} -
    -
    -
    - - + :class="{'hover': showActions && !isSystemMessage && !isDeletedMessage, 'system' : isSystemMessage}" + class="message-body" + @mouseover="handleMouseover" + @mouseleave="handleMouseleave"> +
    + {{ actorDisplayName }}
    -
    - -
    -
    - - -
    -
    - {{ messageTime }} - -
    - - +
    +
    + +
    + {{ message }} +
    +
    +
    + +
    -
    -
    - +
    +
    -
    - +
    + +
    - -
    - - - {{ t('spreed', 'Reply') }} - - - - - {{ t('spreed', 'Reply privately') }} - - - {{ t('spreed', 'Copy message link') }} - - - - - + + {{ t('spreed', 'Mark as unread') }} + + + + + +
    +
    +
    + {{ t('spreed', 'Unread messages') }} +
    +
  • @@ -337,6 +356,21 @@ export default { type: String, default: '', }, + + previousMessageId: { + type: [String, Number], + default: 0, + }, + + nextMessageId: { + type: [String, Number], + default: 0, + }, + + lastReadMessageId: { + type: [String, Number], + default: 0, + }, }, data() { @@ -346,16 +380,26 @@ export default { isTallEnough: false, showReloadButton: false, isDeleting: false, + // whether the message was seen, only used if this was marked as last read message + seen: false, } }, computed: { + isLastReadMessage() { + // note: not reading lastReadMessage from the conversation as we want to define it externally + // to have closer control on marker's visibility behavior + return this.id === this.lastReadMessageId + && (!this.conversation.lastMessage + || this.id !== this.conversation.lastMessage.id) + }, + messageObject() { return this.$store.getters.message(this.token, this.id) }, hasActionsMenu() { - return (this.isPrivateReplyable || this.isDeleteable || this.messageActions.length > 0) && !this.isConversationReadOnly + return (this.isPrivateReplyable || this.isReplyable || this.isDeleteable || this.messageActions.length > 0) && !this.isConversationReadOnly }, isConversationReadOnly() { @@ -571,6 +615,12 @@ export default { }, methods: { + lastReadMessageVisibilityChanged(isVisible) { + if (isVisible) { + this.seen = true + } + }, + highlightAnimation() { // trigger CSS highlight animation by setting a class this.$refs.message.classList.add('highlight-animation') @@ -659,6 +709,15 @@ export default { showError(t('spreed', 'The link could not be copied.')) } }, + + handleMarkAsUnread() { + // update in backend + visually + this.$store.dispatch('updateLastReadMessage', { + token: this.token, + id: this.previousMessageId, + updateVisually: true, + }) + }, }, } @@ -667,7 +726,7 @@ export default { @import '../../../../assets/variables'; @import '../../../../assets/buttons'; -.message { +.message-body { padding: 4px; font-size: $chat-font-size; line-height: $chat-line-height; @@ -754,7 +813,7 @@ export default { // Increase the padding for regular messages to improve readability and // allow some space for the reply button -.message:not(.system) { +.message-body:not(.system) { padding: 12px 4px 12px 8px; margin: -6px 0; } @@ -777,6 +836,25 @@ export default { 100% { background-color: rgba(var(--color-background-hover), 0); } } +.new-message-marker { + position: relative; + margin: 20px 15px 20px -45px; + border-top: 1px solid var(--color-border); + + span { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%) translateY(-50%); + padding: 0 7px 0 7px; + text-align: center; + white-space: nowrap; + + border-radius: var(--border-radius); + background-color: var(--color-main-background); + } +} + .message-status { margin: -8px 0; width: $clickable-area; diff --git a/src/components/MessagesList/MessagesGroup/MessagesGroup.vue b/src/components/MessagesList/MessagesGroup/MessagesGroup.vue index 5702e735510..7f0f3501db3 100644 --- a/src/components/MessagesList/MessagesGroup/MessagesGroup.vue +++ b/src/components/MessagesList/MessagesGroup/MessagesGroup.vue @@ -41,6 +41,9 @@ :key="message.id" v-bind="message" :is-first-message="index === 0" + :next-message-id="(messages[index + 1] && messages[index + 1].id) || nextMessageId" + :previous-message-id="(index > 0 && messages[index - 1].id) || previousMessageId" + :last-read-message-id="lastReadMessageId" :actor-type="actorType" :actor-id="actorId" :actor-display-name="actorDisplayName" @@ -86,6 +89,21 @@ export default { type: Array, required: true, }, + + previousMessageId: { + type: [String, Number], + default: 0, + }, + + nextMessageId: { + type: [String, Number], + default: 0, + }, + + lastReadMessageId: { + type: [String, Number], + default: 0, + }, }, computed: { diff --git a/src/components/MessagesList/MessagesList.vue b/src/components/MessagesList/MessagesList.vue index 67e742f6f86..327fb691a34 100644 --- a/src/components/MessagesList/MessagesList.vue +++ b/src/components/MessagesList/MessagesList.vue @@ -40,11 +40,14 @@ get the messagesList array and loop through the list to generate the messages. class="icon-loading" />