forked from AkkomaGang/akkoma-fe
Add desktop chat notifications
This commit is contained in:
parent
3e09a708f6
commit
2c35afeebf
6 changed files with 82 additions and 49 deletions
|
@ -1,5 +1,6 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { WSConnectionStatus } from '../services/api/api.service.js'
|
import { WSConnectionStatus } from '../services/api/api.service.js'
|
||||||
|
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
|
||||||
import { Socket } from 'phoenix'
|
import { Socket } from 'phoenix'
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
|
@ -77,6 +78,7 @@ const api = {
|
||||||
messages: [message.chatUpdate.lastMessage]
|
messages: [message.chatUpdate.lastMessage]
|
||||||
})
|
})
|
||||||
dispatch('updateChat', { chat: message.chatUpdate })
|
dispatch('updateChat', { chat: message.chatUpdate })
|
||||||
|
maybeShowChatNotification(store, message.chatUpdate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||||
import { find, omitBy, orderBy, sumBy } from 'lodash'
|
import { find, omitBy, orderBy, sumBy } from 'lodash'
|
||||||
import chatService from '../services/chat_service/chat_service.js'
|
import chatService from '../services/chat_service/chat_service.js'
|
||||||
import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
|
import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
|
||||||
|
|
||||||
const emptyChatList = () => ({
|
const emptyChatList = () => ({
|
||||||
data: [],
|
data: [],
|
||||||
|
@ -59,8 +60,12 @@ const chats = {
|
||||||
return chats
|
return chats
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addNewChats ({ rootState, commit, dispatch, rootGetters }, { chats }) {
|
addNewChats (store, { chats }) {
|
||||||
commit('addNewChats', { dispatch, chats, rootGetters })
|
const { commit, dispatch, rootGetters } = store
|
||||||
|
const newChatMessageSideEffects = (chat) => {
|
||||||
|
maybeShowChatNotification(store, chat)
|
||||||
|
}
|
||||||
|
commit('addNewChats', { dispatch, chats, rootGetters, newChatMessageSideEffects })
|
||||||
},
|
},
|
||||||
updateChat ({ commit }, { chat }) {
|
updateChat ({ commit }, { chat }) {
|
||||||
commit('updateChat', { chat })
|
commit('updateChat', { chat })
|
||||||
|
@ -130,13 +135,17 @@ const chats = {
|
||||||
setCurrentChatId (state, { chatId }) {
|
setCurrentChatId (state, { chatId }) {
|
||||||
state.currentChatId = chatId
|
state.currentChatId = chatId
|
||||||
},
|
},
|
||||||
addNewChats (state, { _dispatch, chats, _rootGetters }) {
|
addNewChats (state, { chats, newChatMessageSideEffects }) {
|
||||||
chats.forEach((updatedChat) => {
|
chats.forEach((updatedChat) => {
|
||||||
const chat = getChatById(state, updatedChat.id)
|
const chat = getChatById(state, updatedChat.id)
|
||||||
|
|
||||||
if (chat) {
|
if (chat) {
|
||||||
|
const isNewMessage = (chat.lastMessage && chat.lastMessage.id) !== (updatedChat.lastMessage && updatedChat.lastMessage.id)
|
||||||
chat.lastMessage = updatedChat.lastMessage
|
chat.lastMessage = updatedChat.lastMessage
|
||||||
chat.unread = updatedChat.unread
|
chat.unread = updatedChat.unread
|
||||||
|
if (isNewMessage && chat.unread) {
|
||||||
|
newChatMessageSideEffects(updatedChat)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
state.chatList.data.push(updatedChat)
|
state.chatList.data.push(updatedChat)
|
||||||
Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
|
Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
|
||||||
|
|
|
@ -13,9 +13,8 @@ import {
|
||||||
omitBy
|
omitBy
|
||||||
} from 'lodash'
|
} from 'lodash'
|
||||||
import { set } from 'vue'
|
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 apiService from '../services/api/api.service.js'
|
||||||
import { muteWordHits } from '../services/status_parser/status_parser.js'
|
|
||||||
|
|
||||||
const emptyTl = (userId = 0) => ({
|
const emptyTl = (userId = 0) => ({
|
||||||
statuses: [],
|
statuses: [],
|
||||||
|
@ -77,17 +76,6 @@ export const prepareStatus = (status) => {
|
||||||
return 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 mergeOrAdd = (arr, obj, item) => {
|
||||||
const oldItem = obj[item.id]
|
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) => {
|
each(notifications, (notification) => {
|
||||||
if (isStatusNotification(notification.type)) {
|
if (isStatusNotification(notification.type)) {
|
||||||
notification.action = addStatusToGlobalStorage(state, notification.action).item
|
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.data.push(notification)
|
||||||
state.notifications.idStore[notification.id] = notification
|
state.notifications.idStore[notification.id] = notification
|
||||||
|
|
||||||
if ('Notification' in window && window.Notification.permission === 'granted') {
|
newNotificationSideEffects(notification)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (notification.seen) {
|
} else if (notification.seen) {
|
||||||
state.notifications.idStore[notification.id].seen = true
|
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 }) {
|
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
|
||||||
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
|
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
|
||||||
},
|
},
|
||||||
addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
|
addNewNotifications (store, { notifications, older }) {
|
||||||
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
|
const { commit, dispatch, rootGetters } = store
|
||||||
|
|
||||||
|
const newNotificationSideEffects = (notification) => {
|
||||||
|
maybeShowNotification(store, notification)
|
||||||
|
}
|
||||||
|
commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects })
|
||||||
},
|
},
|
||||||
setError ({ rootState, commit }, { value }) {
|
setError ({ rootState, commit }, { value }) {
|
||||||
commit('setError', { value })
|
commit('setError', { value })
|
||||||
|
|
18
src/services/chat_utils/chat_utils.js
Normal file
18
src/services/chat_utils/chat_utils.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
|
||||||
|
|
||||||
|
export const maybeShowChatNotification = (store, chat) => {
|
||||||
|
if (!chat.lastMessage) 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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -1,16 +1,22 @@
|
||||||
import { filter, sortBy, includes } from 'lodash'
|
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 notificationsFromStore = store => store.state.statuses.notifications.data
|
||||||
|
|
||||||
export const visibleTypes = store => ([
|
export const visibleTypes = store => {
|
||||||
store.state.config.notificationVisibility.likes && 'like',
|
const rootState = store.rootState || store.state
|
||||||
store.state.config.notificationVisibility.mentions && 'mention',
|
|
||||||
store.state.config.notificationVisibility.repeats && 'repeat',
|
return ([
|
||||||
store.state.config.notificationVisibility.follows && 'follow',
|
rootState.config.notificationVisibility.likes && 'like',
|
||||||
store.state.config.notificationVisibility.followRequest && 'follow_request',
|
rootState.config.notificationVisibility.mentions && 'mention',
|
||||||
store.state.config.notificationVisibility.moves && 'move',
|
rootState.config.notificationVisibility.repeats && 'repeat',
|
||||||
store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
|
rootState.config.notificationVisibility.follows && 'follow',
|
||||||
].filter(_ => _))
|
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']
|
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) => {
|
export const filteredNotificationsFromStore = (store, types) => {
|
||||||
// map is just to clone the array since sort mutates it and it causes some issues
|
// map is just to clone the array since sort mutates it and it causes some issues
|
||||||
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
|
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
|
||||||
|
|
Loading…
Reference in a new issue