diff --git a/src/modules/api.js b/src/modules/api.js
index 684026027ad3882c88adaa86283dae52e0c727ca..5e213f0db2ef543802633054d4a178e56d4a2ff9 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -1,5 +1,6 @@
 import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
 import { WSConnectionStatus } from '../services/api/api.service.js'
+import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
 import { Socket } from 'phoenix'
 
 const api = {
@@ -77,6 +78,7 @@ const api = {
                   messages: [message.chatUpdate.lastMessage]
                 })
                 dispatch('updateChat', { chat: message.chatUpdate })
+                maybeShowChatNotification(store, message.chatUpdate)
               }
             }
           )
diff --git a/src/modules/chats.js b/src/modules/chats.js
index 228d6256456027bed76ead616f8539697a00d8ca..c7609018090bc19ad3d66ef7af4d02f9718a5505 100644
--- a/src/modules/chats.js
+++ b/src/modules/chats.js
@@ -2,6 +2,7 @@ import Vue from 'vue'
 import { find, omitBy, orderBy, sumBy } from 'lodash'
 import chatService from '../services/chat_service/chat_service.js'
 import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
+import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
 
 const emptyChatList = () => ({
   data: [],
@@ -59,8 +60,12 @@ const chats = {
           return chats
         })
     },
-    addNewChats ({ rootState, commit, dispatch, rootGetters }, { chats }) {
-      commit('addNewChats', { dispatch, chats, rootGetters })
+    addNewChats (store, { chats }) {
+      const { commit, dispatch, rootGetters } = store
+      const newChatMessageSideEffects = (chat) => {
+        maybeShowChatNotification(store, chat)
+      }
+      commit('addNewChats', { dispatch, chats, rootGetters, newChatMessageSideEffects })
     },
     updateChat ({ commit }, { chat }) {
       commit('updateChat', { chat })
@@ -130,13 +135,17 @@ const chats = {
     setCurrentChatId (state, { chatId }) {
       state.currentChatId = chatId
     },
-    addNewChats (state, { _dispatch, chats, _rootGetters }) {
+    addNewChats (state, { chats, newChatMessageSideEffects }) {
       chats.forEach((updatedChat) => {
         const chat = getChatById(state, updatedChat.id)
 
         if (chat) {
+          const isNewMessage = (chat.lastMessage && chat.lastMessage.id) !== (updatedChat.lastMessage && updatedChat.lastMessage.id)
           chat.lastMessage = updatedChat.lastMessage
           chat.unread = updatedChat.unread
+          if (isNewMessage && chat.unread) {
+            newChatMessageSideEffects(updatedChat)
+          }
         } else {
           state.chatList.data.push(updatedChat)
           Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 64f5b587ca21a11d31295792dc59028fa1ab2dfe..e108b2a799497c78c4763b8f97be31f49e20e723 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -13,9 +13,8 @@ import {
   omitBy
 } from 'lodash'
 import { set } from 'vue'
-import { isStatusNotification, prepareNotificationObject } from '../services/notification_utils/notification_utils.js'
+import { isStatusNotification, maybeShowNotification } from '../services/notification_utils/notification_utils.js'
 import apiService from '../services/api/api.service.js'
-import { muteWordHits } from '../services/status_parser/status_parser.js'
 
 const emptyTl = (userId = 0) => ({
   statuses: [],
@@ -77,17 +76,6 @@ export const prepareStatus = (status) => {
   return status
 }
 
-const visibleNotificationTypes = (rootState) => {
-  return [
-    rootState.config.notificationVisibility.likes && 'like',
-    rootState.config.notificationVisibility.mentions && 'mention',
-    rootState.config.notificationVisibility.repeats && 'repeat',
-    rootState.config.notificationVisibility.follows && 'follow',
-    rootState.config.notificationVisibility.moves && 'move',
-    rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions'
-  ].filter(_ => _)
-}
-
 const mergeOrAdd = (arr, obj, item) => {
   const oldItem = obj[item.id]
 
@@ -325,7 +313,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
   }
 }
 
-const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
+const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => {
   each(notifications, (notification) => {
     if (isStatusNotification(notification.type)) {
       notification.action = addStatusToGlobalStorage(state, notification.action).item
@@ -348,27 +336,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
       state.notifications.data.push(notification)
       state.notifications.idStore[notification.id] = notification
 
-      if ('Notification' in window && window.Notification.permission === 'granted') {
-        const notifObj = prepareNotificationObject(notification, rootGetters.i18n)
-
-        const reasonsToMuteNotif = (
-          notification.seen ||
-            state.notifications.desktopNotificationSilence ||
-            !visibleNotificationTypes.includes(notification.type) ||
-            (
-              notification.type === 'mention' && status && (
-                status.muted ||
-                  muteWordHits(status, rootGetters.mergedConfig.muteWords).length === 0
-              )
-            )
-        )
-        if (!reasonsToMuteNotif) {
-          let desktopNotification = new window.Notification(notifObj.title, notifObj)
-          // Chrome is known for not closing notifications automatically
-          // according to MDN, anyway.
-          setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
-        }
-      }
+      newNotificationSideEffects(notification)
     } else if (notification.seen) {
       state.notifications.idStore[notification.id].seen = true
     }
@@ -609,8 +577,13 @@ const statuses = {
     addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
       commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
     },
-    addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
-      commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
+    addNewNotifications (store, { notifications, older }) {
+      const { commit, dispatch, rootGetters } = store
+
+      const newNotificationSideEffects = (notification) => {
+        maybeShowNotification(store, notification)
+      }
+      commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects })
     },
     setError ({ rootState, commit }, { value }) {
       commit('setError', { value })
diff --git a/src/services/chat_utils/chat_utils.js b/src/services/chat_utils/chat_utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..ab898cede1c7c6593da9d759b2ad4a4fa7b4e52d
--- /dev/null
+++ b/src/services/chat_utils/chat_utils.js
@@ -0,0 +1,19 @@
+import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
+
+export const maybeShowChatNotification = (store, chat) => {
+  if (!chat.lastMessage) return
+  if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return
+
+  const opts = {
+    tag: chat.lastMessage.id,
+    title: chat.account.name,
+    icon: chat.account.profile_image_url,
+    body: chat.lastMessage.content
+  }
+
+  if (chat.lastMessage.attachment && chat.lastMessage.attachment.type === 'image') {
+    opts.image = chat.lastMessage.attachment.preview_url
+  }
+
+  showDesktopNotification(store.rootState, opts)
+}
diff --git a/src/services/desktop_notification_utils/desktop_notification_utils.js b/src/services/desktop_notification_utils/desktop_notification_utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..b84a1f7506fdd5efe610b17c2c37a806088cf247
--- /dev/null
+++ b/src/services/desktop_notification_utils/desktop_notification_utils.js
@@ -0,0 +1,9 @@
+export const showDesktopNotification = (rootState, desktopNotificationOpts) => {
+  if (!('Notification' in window && window.Notification.permission === 'granted')) return
+  if (rootState.statuses.notifications.desktopNotificationSilence) { return }
+
+  const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts)
+  // Chrome is known for not closing notifications automatically
+  // according to MDN, anyway.
+  setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
+}
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index 5cc19215aaf388c1c7852d47455b6cb9525fb1e3..d912d19f2f8ebd5ebc7b29b43711d4b09a450076 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -1,16 +1,22 @@
 import { filter, sortBy, includes } from 'lodash'
+import { muteWordHits } from '../status_parser/status_parser.js'
+import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
 
 export const notificationsFromStore = store => store.state.statuses.notifications.data
 
-export const visibleTypes = store => ([
-  store.state.config.notificationVisibility.likes && 'like',
-  store.state.config.notificationVisibility.mentions && 'mention',
-  store.state.config.notificationVisibility.repeats && 'repeat',
-  store.state.config.notificationVisibility.follows && 'follow',
-  store.state.config.notificationVisibility.followRequest && 'follow_request',
-  store.state.config.notificationVisibility.moves && 'move',
-  store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
-].filter(_ => _))
+export const visibleTypes = store => {
+  const rootState = store.rootState || store.state
+
+  return ([
+    rootState.config.notificationVisibility.likes && 'like',
+    rootState.config.notificationVisibility.mentions && 'mention',
+    rootState.config.notificationVisibility.repeats && 'repeat',
+    rootState.config.notificationVisibility.follows && 'follow',
+    rootState.config.notificationVisibility.followRequest && 'follow_request',
+    rootState.config.notificationVisibility.moves && 'move',
+    rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
+  ].filter(_ => _))
+}
 
 const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
 
@@ -32,6 +38,22 @@ const sortById = (a, b) => {
   }
 }
 
+const isMutedNotification = (store, notification) => {
+  if (!notification.status) return
+  return notification.status.muted || muteWordHits(notification.status, store.rootGetters.mergedConfig.muteWords).length > 0
+}
+
+export const maybeShowNotification = (store, notification) => {
+  const rootState = store.rootState || store.state
+
+  if (notification.seen) return
+  if (!visibleTypes(store).includes(notification.type)) return
+  if (notification.type === 'mention' && isMutedNotification(store, notification)) return
+
+  const notificationObject = prepareNotificationObject(notification, store.rootGetters.i18n)
+  showDesktopNotification(rootState, notificationObject)
+}
+
 export const filteredNotificationsFromStore = (store, types) => {
   // map is just to clone the array since sort mutates it and it causes some issues
   let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)