Merge branch 'desktop-notifications-for-chat' into 'develop'

Desktop chat notifications

Closes #893

See merge request pleroma/pleroma-fe!1185
This commit is contained in:
Shpuld Shpludson 2020-07-20 14:34:50 +00:00
commit a06ab08f96
6 changed files with 83 additions and 49 deletions

View File

@ -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)
} }
} }
) )

View File

@ -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)

View File

@ -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 })

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)