refs #921 Run all userstreaming and notify for all accounts

This commit is contained in:
AkiraFukushima 2019-06-27 23:03:30 +09:00
parent ae56ef451c
commit 80e9e6368c
5 changed files with 198 additions and 105 deletions

View File

@ -34,6 +34,7 @@ import Language from '../constants/language'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalTag } from '~/src/types/localTag'
import { UnreadNotification as UnreadNotificationConfig } from '~/src/types/unreadNotification'
import { AccountNotification } from '~/src/types/accountNotification'
/**
* Context menu
@ -422,6 +423,65 @@ ipcMain.on('reset-badge', () => {
}
})
// user streaming
let userStreamings: { [key: string]: StreamingManager | null } = {}
ipcMain.on('start-all-user-streamings', (event: Event, accounts: Array<LocalAccount>) => {
accounts.map(account => {
const id: string = account._id!
accountManager
.getAccount(id)
.then(acct => {
// Stop old user streaming
if (userStreamings[id]) {
userStreamings[id]!.stop()
userStreamings[id] = null
}
userStreamings[id] = new StreamingManager(acct, true)
userStreamings[id]!.startUser(
(update: Status) => {
event.sender.send(`update-start-all-user-streamings-${id}`, update)
},
(notification: Notification) => {
const accountNotification: AccountNotification = {
id: id,
notification: notification
}
// To notiy badge
event.sender.send('notification-start-all-user-streamings', accountNotification)
// To update notification timeline
event.sender.send(`notification-start-all-user-streamings-${id}`, notification)
// Does not exist a endpoint for only mention. And mention is a part of notification.
// So we have to get mention from notification.
if (notification.type === 'mention') {
event.sender.send(`mention-start-all-user-streamings-${id}`, notification)
}
if (process.platform === 'darwin') {
app.dock.setBadge('•')
}
},
(id: string) => {
event.sender.send(`delete-start-all-user-streamings-${id}`, id)
},
(err: Error) => {
log.error(err)
// In macOS, sometimes window is closed (not quit).
// When window is closed, we can not send event to webContents; because it is destroyed.
// So we have to guard it.
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-all-user-streamings', err)
}
}
)
})
.catch(err => {
log.error(err)
event.sender.send('error-start-all-user-streamings', err)
})
})
})
// streaming
let userStreaming: StreamingManager | null = null

View File

@ -3,21 +3,18 @@
<el-menu
v-if="!hide"
:default-active="activeRoute()"
class="el-menu-vertical account-menu"
class="el-menu-vertical account-menu"
:collapse="true"
:router="true"
:background-color="themeColor"
text-color="#909399"
active-text-color="#ffffff"
role="menubar">
role="menubar"
>
<el-menu-item :index="`/${account._id}/home`" v-for="(account, index) in accounts" v-bind:key="account._id" role="menuitem">
<i v-if="account.avatar === undefined || account.avatar === null || account.avatar === ''" class="el-icon-menu"></i>
<FailoverImg v-else :src="account.avatar" class="avatar" :title="account.username + '@' + account.domain" />
<FailoverImg
:src="`${account.baseURL}/favicon.ico`"
:failoverSrc="`${account.baseURL}/favicon.png`"
class="instance-icon"
/>
<FailoverImg :src="`${account.baseURL}/favicon.ico`" :failoverSrc="`${account.baseURL}/favicon.png`" class="instance-icon" />
<span slot="title">{{ account.domain }}</span>
</el-menu-item>
<el-menu-item index="/login" :title="$t('global_header.add_new_account')" role="menuitem">
@ -25,11 +22,10 @@
<span slot="new">New</span>
</el-menu-item>
</el-menu>
<div :class="hide ? 'space no-global-header':'space with-global-header' ">
<div :class="hide ? 'space no-global-header' : 'space with-global-header'">
<router-view :key="$route.params.id"></router-view>
</div>
</div>
</div>
</template>
<script>
@ -50,19 +46,16 @@ export default {
themeColor: state => state.App.theme.global_header_color
})
},
created () {
created() {
this.initialize()
},
methods: {
activeRoute () {
activeRoute() {
return this.$route.path
},
async initialize () {
await this.$store.dispatch('GlobalHeader/removeShortcutEvents')
await this.$store.dispatch('GlobalHeader/loadHide')
this.$store.dispatch('GlobalHeader/watchShortcutEvents')
async initialize() {
try {
const accounts = await this.$store.dispatch('GlobalHeader/listAccounts')
const accounts = await this.$store.dispatch('GlobalHeader/initLoad')
if (this.$route.params.id === undefined) {
return this.$router.push({ path: `/${accounts[0]._id}/home` })
}

View File

@ -1,8 +1,14 @@
import sanitizeHtml from 'sanitize-html'
import { Account, Notification as NotificationType } from 'megalodon'
import { ipcRenderer } from 'electron'
import router from '@/router'
import { LocalAccount } from '~/src/types/localAccount'
import { Module, MutationTree, ActionTree } from 'vuex'
import { RootState } from '@/store'
import { Notify } from '~/src/types/notify'
import { AccountNotification } from '~/src/types/accountNotification'
declare var Notification: any
export type GlobalHeaderState = {
accounts: Array<LocalAccount>
@ -35,6 +41,24 @@ const mutations: MutationTree<GlobalHeaderState> = {
}
const actions: ActionTree<GlobalHeaderState, RootState> = {
initLoad: async ({ dispatch }): Promise<Array<LocalAccount>> => {
// Ignore error
try {
await dispatch('removeShortcutEvents')
await dispatch('loadHide')
dispatch('watchShortcutEvents')
} catch (err) {
console.error(err)
}
const accounts = await dispatch('listAccounts')
try {
dispatch('bindUserStreamingsForNotify')
dispatch('startUserStreamings')
} catch (err) {
console.error(err)
}
return accounts
},
listAccounts: ({ dispatch, commit }): Promise<Array<LocalAccount>> => {
return new Promise((resolve, reject) => {
ipcRenderer.send('list-accounts', 'list')
@ -104,6 +128,30 @@ const actions: ActionTree<GlobalHeaderState, RootState> = {
resolve(true)
})
})
},
startUserStreamings: ({ state }): Promise<{}> => {
// @ts-ignore
return new Promise((resolve, reject) => {
ipcRenderer.once('error-start-all-user-streamings', (_, err: Error) => {
reject(err)
})
ipcRenderer.send('start-all-user-streamings', state.accounts)
})
},
bindUserStreamingsForNotify: ({ rootState }) => {
ipcRenderer.on('notification-start-all-user-streamings', (_, accountNotification: AccountNotification) => {
const { id, notification } = accountNotification
let notify = createNotification(notification, rootState.App.notify as Notify)
if (notify) {
notify.onclick = () => {
router.push(`/${id}/notifications`)
}
}
})
},
unbindUserStreamings: () => {
ipcRenderer.removeAllListeners('notification-start-all-user-streamings')
ipcRenderer.removeAllListeners('error-start-all-user-streamings')
}
}
@ -115,3 +163,48 @@ const GlobalHeader: Module<GlobalHeaderState, RootState> = {
}
export default GlobalHeader
function createNotification(notification: NotificationType, notifyConfig: Notify) {
switch (notification.type) {
case 'favourite':
if (notifyConfig.favourite) {
return new Notification('Favourite', {
body: `${username(notification.account)} favourited your status`
})
}
break
case 'follow':
if (notifyConfig.follow) {
return new Notification('Follow', {
body: `${username(notification.account)} is now following you`
})
}
break
case 'mention':
if (notifyConfig.reply) {
// Clean html tags
return new Notification(`${notification.status!.account.display_name}`, {
body: sanitizeHtml(notification.status!.content, {
allowedTags: [],
allowedAttributes: []
})
})
}
break
case 'reblog':
if (notifyConfig.reblog) {
return new Notification('Reblog', {
body: `${username(notification.account)} boosted your status`
})
}
break
}
}
function username(account: Account) {
if (account.display_name !== '') {
return account.display_name
} else {
return account.username
}
}

View File

@ -1,22 +1,17 @@
import sanitizeHtml from 'sanitize-html'
import { ipcRenderer } from 'electron'
import Mastodon, { Account, Emoji, Instance, Status, Notification as NotificationType } from 'megalodon'
import SideMenu, { SideMenuState } from './TimelineSpace/SideMenu'
import HeaderMenu, { HeaderMenuState } from './TimelineSpace/HeaderMenu'
import Modals, { ModalsModuleState } from './TimelineSpace/Modals'
import Contents, { ContentsModuleState } from './TimelineSpace/Contents'
import router from '@/router'
import unreadSettings from '~/src/constants/unreadNotification'
import { Module, MutationTree, ActionTree } from 'vuex'
import { LocalAccount } from '~/src/types/localAccount'
import { Notify } from '~/src/types/notify'
import { RootState } from '@/store'
import { UnreadNotification } from '~/src/types/unreadNotification'
import { AccountLoadError } from '@/errors/load'
import { TimelineFetchError } from '@/errors/fetch'
declare var Notification: any
type MyEmoji = {
name: string
image: string
@ -121,8 +116,8 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
throw new TimelineFetchError()
})
await dispatch('unbindStreamings')
await dispatch('bindStreamings', account)
dispatch('startStreamings', account)
await dispatch('bindStreamings')
dispatch('startStreamings')
dispatch('fetchEmojis', account)
dispatch('fetchInstance', account)
return account
@ -266,8 +261,8 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
commit('TimelineSpace/Contents/Public/clearTimeline', {}, { root: true })
commit('TimelineSpace/Contents/Mentions/clearMentions', {}, { root: true })
},
bindStreamings: ({ dispatch, state }, account: LocalAccount) => {
dispatch('bindUserStreaming', account)
bindStreamings: ({ dispatch, state }) => {
dispatch('bindUserStreaming')
if (state.unreadNotification.direct) {
dispatch('bindDirectMessagesStreaming')
}
@ -279,7 +274,6 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
}
},
startStreamings: ({ dispatch, state }) => {
dispatch('startUserStreaming')
if (state.unreadNotification.direct) {
dispatch('startDirectMessagesStreaming')
}
@ -291,7 +285,6 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
}
},
stopStreamings: ({ dispatch }) => {
dispatch('stopUserStreaming')
dispatch('stopDirectMessagesStreaming')
dispatch('stopLocalStreaming')
dispatch('stopPublicStreaming')
@ -305,8 +298,8 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
// ------------------------------------------------
// Each streaming methods
// ------------------------------------------------
bindUserStreaming: ({ commit, rootState }, account: LocalAccount) => {
ipcRenderer.on('update-start-user-streaming', (_, update: Status) => {
bindUserStreaming: ({ commit, state, rootState }) => {
ipcRenderer.on(`update-start-all-user-streamings-${state.account._id!}`, (_, update: Status) => {
commit('TimelineSpace/Contents/Home/appendTimeline', update, { root: true })
// Sometimes archive old statuses
if (rootState.TimelineSpace.Contents.Home.heading && Math.random() > 0.8) {
@ -314,45 +307,39 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
}
commit('TimelineSpace/SideMenu/changeUnreadHomeTimeline', true, { root: true })
})
ipcRenderer.on('notification-start-user-streaming', (_, notification: NotificationType) => {
let notify = createNotification(notification, rootState.App.notify as Notify)
if (notify) {
notify.onclick = () => {
router.push(`/${account._id}/notifications`)
}
}
ipcRenderer.on(`notification-start-all-user-streamings-${state.account._id!}`, (_, notification: NotificationType) => {
commit('TimelineSpace/Contents/Notifications/appendNotifications', notification, { root: true })
if (rootState.TimelineSpace.Contents.Notifications.heading && Math.random() > 0.8) {
commit('TimelineSpace/Contents/Notifications/archiveNotifications', null, { root: true })
}
commit('TimelineSpace/SideMenu/changeUnreadNotifications', true, { root: true })
})
ipcRenderer.on('mention-start-user-streaming', (_, mention: NotificationType) => {
ipcRenderer.on(`mention-start-all-user-streamings-${state.account._id!}`, (_, mention: NotificationType) => {
commit('TimelineSpace/Contents/Mentions/appendMentions', mention, { root: true })
if (rootState.TimelineSpace.Contents.Mentions.heading && Math.random() > 0.8) {
commit('TimelineSpace/Contents/Mentions/archiveMentions', null, { root: true })
}
commit('TimelineSpace/SideMenu/changeUnreadMentions', true, { root: true })
})
ipcRenderer.on('delete-start-user-streaming', (_, id: string) => {
ipcRenderer.on(`delete-start-all-user-streamings-${state.account._id!}`, (_, id: string) => {
commit('TimelineSpace/Contents/Home/deleteToot', id, { root: true })
commit('TimelineSpace/Contents/Notifications/deleteToot', id, { root: true })
commit('TimelineSpace/Contents/Mentions/deleteToot', id, { root: true })
})
},
startUserStreaming: ({ state }): Promise<{}> => {
// @ts-ignore
return new Promise((resolve, reject) => {
// eslint-disable-line no-unused-vars
ipcRenderer.send('start-user-streaming', {
account: state.account,
useWebsocket: state.useWebsocket
})
ipcRenderer.once('error-start-user-streaming', (_, err: Error) => {
reject(err)
})
})
},
// startUserStreaming: ({ state }): Promise<{}> => {
// // @ts-ignore
// return new Promise((resolve, reject) => {
// // eslint-disable-line no-unused-vars
// ipcRenderer.send('start-user-streaming', {
// account: state.account,
// useWebsocket: state.useWebsocket
// })
// ipcRenderer.once('error-start-user-streaming', (_, err: Error) => {
// reject(err)
// })
// })
// },
bindLocalStreaming: ({ commit, rootState }) => {
ipcRenderer.on('update-start-local-streaming', (_, update: Status) => {
commit('TimelineSpace/Contents/Local/appendTimeline', update, { root: true })
@ -428,16 +415,15 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
})
})
},
unbindUserStreaming: () => {
ipcRenderer.removeAllListeners('update-start-user-streaming')
ipcRenderer.removeAllListeners('mention-start-user-streaming')
ipcRenderer.removeAllListeners('notification-start-user-streaming')
ipcRenderer.removeAllListeners('delete-start-user-streaming')
ipcRenderer.removeAllListeners('error-start-user-streaming')
},
stopUserStreaming: () => {
ipcRenderer.send('stop-user-streaming')
unbindUserStreaming: ({ state }) => {
ipcRenderer.removeAllListeners(`update-start-all-user-streamings-${state.account._id!}`)
ipcRenderer.removeAllListeners(`mention-start-all-user-streamings-${state.account._id!}`)
ipcRenderer.removeAllListeners(`notification-start-all-user-streamings-${state.account._id!}`)
ipcRenderer.removeAllListeners(`delete-start-all-user-streamings-${state.account._id!}`)
},
// stopUserStreaming: () => {
// ipcRenderer.send('stop-user-streaming')
// },
unbindLocalStreaming: () => {
ipcRenderer.removeAllListeners('error-start-local-streaming')
ipcRenderer.removeAllListeners('update-start-local-streaming')
@ -502,48 +488,3 @@ const TimelineSpace: Module<TimelineSpaceState, RootState> = {
}
export default TimelineSpace
function createNotification(notification: NotificationType, notifyConfig: Notify) {
switch (notification.type) {
case 'favourite':
if (notifyConfig.favourite) {
return new Notification('Favourite', {
body: `${username(notification.account)} favourited your status`
})
}
break
case 'follow':
if (notifyConfig.follow) {
return new Notification('Follow', {
body: `${username(notification.account)} is now following you`
})
}
break
case 'mention':
if (notifyConfig.reply) {
// Clean html tags
return new Notification(`${notification.status!.account.display_name}`, {
body: sanitizeHtml(notification.status!.content, {
allowedTags: [],
allowedAttributes: []
})
})
}
break
case 'reblog':
if (notifyConfig.reblog) {
return new Notification('Reblog', {
body: `${username(notification.account)} boosted your status`
})
}
break
}
}
function username(account: Account) {
if (account.display_name !== '') {
return account.display_name
} else {
return account.username
}
}

View File

@ -0,0 +1,6 @@
import { Notification } from 'megalodon'
export type AccountNotification = {
id: string
notification: Notification
}