1
0
mirror of https://github.com/h3poteto/whalebird-desktop synced 2025-02-07 04:43:52 +01:00

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 { LocalAccount } from '~/src/types/localAccount'
import { LocalTag } from '~/src/types/localTag' import { LocalTag } from '~/src/types/localTag'
import { UnreadNotification as UnreadNotificationConfig } from '~/src/types/unreadNotification' import { UnreadNotification as UnreadNotificationConfig } from '~/src/types/unreadNotification'
import { AccountNotification } from '~/src/types/accountNotification'
/** /**
* Context menu * 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 // streaming
let userStreaming: StreamingManager | null = null let userStreaming: StreamingManager | null = null

View File

@ -9,15 +9,12 @@
:background-color="themeColor" :background-color="themeColor"
text-color="#909399" text-color="#909399"
active-text-color="#ffffff" 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"> <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> <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 v-else :src="account.avatar" class="avatar" :title="account.username + '@' + account.domain" />
<FailoverImg <FailoverImg :src="`${account.baseURL}/favicon.ico`" :failoverSrc="`${account.baseURL}/favicon.png`" class="instance-icon" />
:src="`${account.baseURL}/favicon.ico`"
:failoverSrc="`${account.baseURL}/favicon.png`"
class="instance-icon"
/>
<span slot="title">{{ account.domain }}</span> <span slot="title">{{ account.domain }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="/login" :title="$t('global_header.add_new_account')" role="menuitem"> <el-menu-item index="/login" :title="$t('global_header.add_new_account')" role="menuitem">
@ -29,7 +26,6 @@
<router-view :key="$route.params.id"></router-view> <router-view :key="$route.params.id"></router-view>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -58,11 +54,8 @@ export default {
return this.$route.path return this.$route.path
}, },
async initialize() { async initialize() {
await this.$store.dispatch('GlobalHeader/removeShortcutEvents')
await this.$store.dispatch('GlobalHeader/loadHide')
this.$store.dispatch('GlobalHeader/watchShortcutEvents')
try { try {
const accounts = await this.$store.dispatch('GlobalHeader/listAccounts') const accounts = await this.$store.dispatch('GlobalHeader/initLoad')
if (this.$route.params.id === undefined) { if (this.$route.params.id === undefined) {
return this.$router.push({ path: `/${accounts[0]._id}/home` }) 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 { ipcRenderer } from 'electron'
import router from '@/router' import router from '@/router'
import { LocalAccount } from '~/src/types/localAccount' import { LocalAccount } from '~/src/types/localAccount'
import { Module, MutationTree, ActionTree } from 'vuex' import { Module, MutationTree, ActionTree } from 'vuex'
import { RootState } from '@/store' import { RootState } from '@/store'
import { Notify } from '~/src/types/notify'
import { AccountNotification } from '~/src/types/accountNotification'
declare var Notification: any
export type GlobalHeaderState = { export type GlobalHeaderState = {
accounts: Array<LocalAccount> accounts: Array<LocalAccount>
@ -35,6 +41,24 @@ const mutations: MutationTree<GlobalHeaderState> = {
} }
const actions: ActionTree<GlobalHeaderState, RootState> = { 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>> => { listAccounts: ({ dispatch, commit }): Promise<Array<LocalAccount>> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ipcRenderer.send('list-accounts', 'list') ipcRenderer.send('list-accounts', 'list')
@ -104,6 +128,30 @@ const actions: ActionTree<GlobalHeaderState, RootState> = {
resolve(true) 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 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 { ipcRenderer } from 'electron'
import Mastodon, { Account, Emoji, Instance, Status, Notification as NotificationType } from 'megalodon' import Mastodon, { Account, Emoji, Instance, Status, Notification as NotificationType } from 'megalodon'
import SideMenu, { SideMenuState } from './TimelineSpace/SideMenu' import SideMenu, { SideMenuState } from './TimelineSpace/SideMenu'
import HeaderMenu, { HeaderMenuState } from './TimelineSpace/HeaderMenu' import HeaderMenu, { HeaderMenuState } from './TimelineSpace/HeaderMenu'
import Modals, { ModalsModuleState } from './TimelineSpace/Modals' import Modals, { ModalsModuleState } from './TimelineSpace/Modals'
import Contents, { ContentsModuleState } from './TimelineSpace/Contents' import Contents, { ContentsModuleState } from './TimelineSpace/Contents'
import router from '@/router'
import unreadSettings from '~/src/constants/unreadNotification' import unreadSettings from '~/src/constants/unreadNotification'
import { Module, MutationTree, ActionTree } from 'vuex' import { Module, MutationTree, ActionTree } from 'vuex'
import { LocalAccount } from '~/src/types/localAccount' import { LocalAccount } from '~/src/types/localAccount'
import { Notify } from '~/src/types/notify'
import { RootState } from '@/store' import { RootState } from '@/store'
import { UnreadNotification } from '~/src/types/unreadNotification' import { UnreadNotification } from '~/src/types/unreadNotification'
import { AccountLoadError } from '@/errors/load' import { AccountLoadError } from '@/errors/load'
import { TimelineFetchError } from '@/errors/fetch' import { TimelineFetchError } from '@/errors/fetch'
declare var Notification: any
type MyEmoji = { type MyEmoji = {
name: string name: string
image: string image: string
@ -121,8 +116,8 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
throw new TimelineFetchError() throw new TimelineFetchError()
}) })
await dispatch('unbindStreamings') await dispatch('unbindStreamings')
await dispatch('bindStreamings', account) await dispatch('bindStreamings')
dispatch('startStreamings', account) dispatch('startStreamings')
dispatch('fetchEmojis', account) dispatch('fetchEmojis', account)
dispatch('fetchInstance', account) dispatch('fetchInstance', account)
return account return account
@ -266,8 +261,8 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
commit('TimelineSpace/Contents/Public/clearTimeline', {}, { root: true }) commit('TimelineSpace/Contents/Public/clearTimeline', {}, { root: true })
commit('TimelineSpace/Contents/Mentions/clearMentions', {}, { root: true }) commit('TimelineSpace/Contents/Mentions/clearMentions', {}, { root: true })
}, },
bindStreamings: ({ dispatch, state }, account: LocalAccount) => { bindStreamings: ({ dispatch, state }) => {
dispatch('bindUserStreaming', account) dispatch('bindUserStreaming')
if (state.unreadNotification.direct) { if (state.unreadNotification.direct) {
dispatch('bindDirectMessagesStreaming') dispatch('bindDirectMessagesStreaming')
} }
@ -279,7 +274,6 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
} }
}, },
startStreamings: ({ dispatch, state }) => { startStreamings: ({ dispatch, state }) => {
dispatch('startUserStreaming')
if (state.unreadNotification.direct) { if (state.unreadNotification.direct) {
dispatch('startDirectMessagesStreaming') dispatch('startDirectMessagesStreaming')
} }
@ -291,7 +285,6 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
} }
}, },
stopStreamings: ({ dispatch }) => { stopStreamings: ({ dispatch }) => {
dispatch('stopUserStreaming')
dispatch('stopDirectMessagesStreaming') dispatch('stopDirectMessagesStreaming')
dispatch('stopLocalStreaming') dispatch('stopLocalStreaming')
dispatch('stopPublicStreaming') dispatch('stopPublicStreaming')
@ -305,8 +298,8 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
// ------------------------------------------------ // ------------------------------------------------
// Each streaming methods // Each streaming methods
// ------------------------------------------------ // ------------------------------------------------
bindUserStreaming: ({ commit, rootState }, account: LocalAccount) => { bindUserStreaming: ({ commit, state, rootState }) => {
ipcRenderer.on('update-start-user-streaming', (_, update: Status) => { ipcRenderer.on(`update-start-all-user-streamings-${state.account._id!}`, (_, update: Status) => {
commit('TimelineSpace/Contents/Home/appendTimeline', update, { root: true }) commit('TimelineSpace/Contents/Home/appendTimeline', update, { root: true })
// Sometimes archive old statuses // Sometimes archive old statuses
if (rootState.TimelineSpace.Contents.Home.heading && Math.random() > 0.8) { 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 }) commit('TimelineSpace/SideMenu/changeUnreadHomeTimeline', true, { root: true })
}) })
ipcRenderer.on('notification-start-user-streaming', (_, notification: NotificationType) => { ipcRenderer.on(`notification-start-all-user-streamings-${state.account._id!}`, (_, notification: NotificationType) => {
let notify = createNotification(notification, rootState.App.notify as Notify)
if (notify) {
notify.onclick = () => {
router.push(`/${account._id}/notifications`)
}
}
commit('TimelineSpace/Contents/Notifications/appendNotifications', notification, { root: true }) commit('TimelineSpace/Contents/Notifications/appendNotifications', notification, { root: true })
if (rootState.TimelineSpace.Contents.Notifications.heading && Math.random() > 0.8) { if (rootState.TimelineSpace.Contents.Notifications.heading && Math.random() > 0.8) {
commit('TimelineSpace/Contents/Notifications/archiveNotifications', null, { root: true }) commit('TimelineSpace/Contents/Notifications/archiveNotifications', null, { root: true })
} }
commit('TimelineSpace/SideMenu/changeUnreadNotifications', true, { 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 }) commit('TimelineSpace/Contents/Mentions/appendMentions', mention, { root: true })
if (rootState.TimelineSpace.Contents.Mentions.heading && Math.random() > 0.8) { if (rootState.TimelineSpace.Contents.Mentions.heading && Math.random() > 0.8) {
commit('TimelineSpace/Contents/Mentions/archiveMentions', null, { root: true }) commit('TimelineSpace/Contents/Mentions/archiveMentions', null, { root: true })
} }
commit('TimelineSpace/SideMenu/changeUnreadMentions', true, { 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/Home/deleteToot', id, { root: true })
commit('TimelineSpace/Contents/Notifications/deleteToot', id, { root: true }) commit('TimelineSpace/Contents/Notifications/deleteToot', id, { root: true })
commit('TimelineSpace/Contents/Mentions/deleteToot', id, { root: true }) commit('TimelineSpace/Contents/Mentions/deleteToot', id, { root: true })
}) })
}, },
startUserStreaming: ({ state }): Promise<{}> => { // startUserStreaming: ({ state }): Promise<{}> => {
// @ts-ignore // // @ts-ignore
return new Promise((resolve, reject) => { // return new Promise((resolve, reject) => {
// eslint-disable-line no-unused-vars // // eslint-disable-line no-unused-vars
ipcRenderer.send('start-user-streaming', { // ipcRenderer.send('start-user-streaming', {
account: state.account, // account: state.account,
useWebsocket: state.useWebsocket // useWebsocket: state.useWebsocket
}) // })
ipcRenderer.once('error-start-user-streaming', (_, err: Error) => { // ipcRenderer.once('error-start-user-streaming', (_, err: Error) => {
reject(err) // reject(err)
}) // })
}) // })
}, // },
bindLocalStreaming: ({ commit, rootState }) => { bindLocalStreaming: ({ commit, rootState }) => {
ipcRenderer.on('update-start-local-streaming', (_, update: Status) => { ipcRenderer.on('update-start-local-streaming', (_, update: Status) => {
commit('TimelineSpace/Contents/Local/appendTimeline', update, { root: true }) commit('TimelineSpace/Contents/Local/appendTimeline', update, { root: true })
@ -428,16 +415,15 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
}) })
}) })
}, },
unbindUserStreaming: () => { unbindUserStreaming: ({ state }) => {
ipcRenderer.removeAllListeners('update-start-user-streaming') ipcRenderer.removeAllListeners(`update-start-all-user-streamings-${state.account._id!}`)
ipcRenderer.removeAllListeners('mention-start-user-streaming') ipcRenderer.removeAllListeners(`mention-start-all-user-streamings-${state.account._id!}`)
ipcRenderer.removeAllListeners('notification-start-user-streaming') ipcRenderer.removeAllListeners(`notification-start-all-user-streamings-${state.account._id!}`)
ipcRenderer.removeAllListeners('delete-start-user-streaming') ipcRenderer.removeAllListeners(`delete-start-all-user-streamings-${state.account._id!}`)
ipcRenderer.removeAllListeners('error-start-user-streaming')
},
stopUserStreaming: () => {
ipcRenderer.send('stop-user-streaming')
}, },
// stopUserStreaming: () => {
// ipcRenderer.send('stop-user-streaming')
// },
unbindLocalStreaming: () => { unbindLocalStreaming: () => {
ipcRenderer.removeAllListeners('error-start-local-streaming') ipcRenderer.removeAllListeners('error-start-local-streaming')
ipcRenderer.removeAllListeners('update-start-local-streaming') ipcRenderer.removeAllListeners('update-start-local-streaming')
@ -502,48 +488,3 @@ const TimelineSpace: Module<TimelineSpaceState, RootState> = {
} }
export default TimelineSpace 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
}