import sanitizeHtml from 'sanitize-html' import { ipcRenderer } from 'electron' import Mastodon, { Account, Emoji, Instance, Status, Notification as NotificationType } from 'megalodon' import SideMenu from './TimelineSpace/SideMenu' import HeaderMenu from './TimelineSpace/HeaderMenu' import Modals from './TimelineSpace/Modals' import Contents from './TimelineSpace/Contents' import router from '@/router' import unreadSettings from '~/src/constants/unreadNotification' import { Module, MutationTree, ActionTree } from 'vuex' import AccountType from '~/src/types/account' import { Notify } from './App' declare var Notification: any interface MyEmoji { name: string, image: string } interface UnreadNotification { direct: boolean, local: boolean, public: boolean } export interface TimelineSpaceState { account: AccountType, loading: boolean, emojis: Array, tootMax: number, unreadNotification: UnreadNotification, useWebsocket: boolean, pleroma: boolean } export const blankAccount: AccountType = { _id: '', baseURL: '', domain: '', username: '', clientId: '', clientSecret: '', accessToken: null, refreshToken: null, accountId: null, avatar: null, order: 0 } const state = (): TimelineSpaceState => ({ account: blankAccount, loading: false, emojis: [], tootMax: 500, unreadNotification: { direct: unreadSettings.Direct.default, local: unreadSettings.Local.default, public: unreadSettings.Public.default }, useWebsocket: false, pleroma: false }) export const MUTATION_TYPES = { UPDATE_ACCOUNT: 'updateAccount', CHANGE_LOADING: 'changeLoading', UPDATE_EMOJIS: 'updateEmojis', UPDATE_TOOT_MAX: 'updateTootMax', UPDATE_UNREAD_NOTIFICATION: 'updateUnreadNotification', CHANGE_PLEROMA: 'changePleroma', CHANGE_USE_WEBSOCKET: 'changeUseWebsocket' } const mutations: MutationTree = { [MUTATION_TYPES.UPDATE_ACCOUNT]: (state, account: AccountType) => { state.account = account }, [MUTATION_TYPES.CHANGE_LOADING]: (state, value: boolean) => { state.loading = value }, [MUTATION_TYPES.UPDATE_EMOJIS]: (state, emojis: Array) => { state.emojis = emojis.map((e) => { return { name: `:${e.shortcode}:`, image: e.url } }) }, [MUTATION_TYPES.UPDATE_TOOT_MAX]: (state, value: number | null) => { if (value) { state.tootMax = value } else { state.tootMax = 500 } }, [MUTATION_TYPES.UPDATE_UNREAD_NOTIFICATION]: (state, settings: UnreadNotification) => { state.unreadNotification = settings }, [MUTATION_TYPES.CHANGE_PLEROMA]: (state, pleroma: boolean) => { state.pleroma = pleroma }, [MUTATION_TYPES.CHANGE_USE_WEBSOCKET]: (state, use: boolean) => { state.useWebsocket = use } } const actions: ActionTree = { // ------------------------------------------------- // Accounts // ------------------------------------------------- localAccount: ({ dispatch, commit }, id: string): Promise => { return new Promise((resolve, reject) => { ipcRenderer.send('get-local-account', id) ipcRenderer.once('error-get-local-account', (_, err: Error) => { ipcRenderer.removeAllListeners('response-get-local-account') reject(err) }) ipcRenderer.once('response-get-local-account', (_, account: AccountType) => { ipcRenderer.removeAllListeners('error-get-local-account') if (account.username === undefined || account.username === null || account.username === '') { dispatch('fetchAccount', account) .then((acct: AccountType) => { commit(MUTATION_TYPES.UPDATE_ACCOUNT, acct) resolve(acct) }) .catch((err) => { reject(err) }) } else { commit(MUTATION_TYPES.UPDATE_ACCOUNT, account) resolve(account) } }) }) }, fetchAccount: (_, account: AccountType): Promise => { return new Promise((resolve, reject) => { ipcRenderer.send('update-account', account) ipcRenderer.once('error-update-account', (_, err: Error) => { ipcRenderer.removeAllListeners('response-update-account') reject(err) }) ipcRenderer.once('response-update-account', (_, account: AccountType) => { ipcRenderer.removeAllListeners('error-update-account') resolve(account) }) }) }, clearAccount: async ({ commit }) => { commit(MUTATION_TYPES.UPDATE_ACCOUNT, blankAccount) return true }, detectPleroma: async ({ commit, state }) => { const res = await Mastodon.get('/instance', {}, state.account.baseURL + '/api/v1') if (res.data.version.includes('Pleroma')) { commit(MUTATION_TYPES.CHANGE_PLEROMA, true) commit(MUTATION_TYPES.CHANGE_USE_WEBSOCKET, true) } else { commit(MUTATION_TYPES.CHANGE_PLEROMA, false) commit(MUTATION_TYPES.CHANGE_USE_WEBSOCKET, false) } }, // ----------------------------------------------- // Shortcuts // ----------------------------------------------- watchShortcutEvents: ({ commit, dispatch }) => { ipcRenderer.on('CmdOrCtrl+N', () => { dispatch('TimelineSpace/Modals/NewToot/openModal', {}, { root: true }) }) ipcRenderer.on('CmdOrCtrl+K', () => { commit('TimelineSpace/Modals/Jump/changeModal', true, { root: true }) }) }, removeShortcutEvents: async () => { ipcRenderer.removeAllListeners('CmdOrCtrl+N') ipcRenderer.removeAllListeners('CmdOrCtrl+K') return true }, /** * clearUnread */ clearUnread: async ({ dispatch }) => { dispatch('TimelineSpace/SideMenu/clearUnread', {}, { root: true }) }, /** * fetchEmojis */ fetchEmojis: async ({ commit }, account: AccountType): Promise> => { const res = await Mastodon.get>('/custom_emojis', {}, account.baseURL + '/api/v1') commit(MUTATION_TYPES.UPDATE_EMOJIS, res.data) return res.data }, /** * fetchInstance */ fetchInstance: async ({ commit }, account: AccountType) => { const res = await Mastodon.get('/instance', {}, account.baseURL + '/api/v1') commit(MUTATION_TYPES.UPDATE_TOOT_MAX, res.data.max_toot_chars) return true }, loadUnreadNotification: ({ commit }, accountID: string) => { return new Promise(resolve => { ipcRenderer.once('response-get-unread-notification', (_, settings: UnreadNotification) => { ipcRenderer.removeAllListeners('error-get-unread-notification') commit(MUTATION_TYPES.UPDATE_UNREAD_NOTIFICATION, settings) resolve(settings) }) ipcRenderer.once('error-get-unread-notification', () => { ipcRenderer.removeAllListeners('response-get-unread-notification') commit(MUTATION_TYPES.UPDATE_UNREAD_NOTIFICATION, { direct: unreadSettings.Direct.default, local: unreadSettings.Local.default, public: unreadSettings.Public.default } as UnreadNotification) resolve({}) }) ipcRenderer.send('get-unread-notification', accountID) }) }, fetchContentsTimelines: async ({ dispatch, state }) => { await dispatch('TimelineSpace/Contents/Home/fetchTimeline', {}, { root: true }) await dispatch('TimelineSpace/Contents/Notifications/fetchNotifications', {}, { root: true }) await dispatch('TimelineSpace/Contents/Mentions/fetchMentions', {}, { root: true }) if (state.unreadNotification.direct) { await dispatch('TimelineSpace/Contents/DirectMessages/fetchTimeline', {}, { root: true }) } if (state.unreadNotification.local) { await dispatch('TimelineSpace/Contents/Local/fetchLocalTimeline', {}, { root: true }) } if (state.unreadNotification.public) { await dispatch('TimelineSpace/Contents/Public/fetchPublicTimeline', {}, { root: true }) } }, clearContentsTimelines: ({ commit }) => { commit('TimelineSpace/Contents/Home/clearTimeline', {}, { root: true }) commit('TimelineSpace/Contents/Local/clearTimeline', {}, { root: true }) commit('TimelineSpace/Contents/DirectMessages/clearTimeline', {}, { root: true }) commit('TimelineSpace/Contents/Notifications/clearNotifications', {}, { root: true }) commit('TimelineSpace/Contents/Public/clearTimeline', {}, { root: true }) commit('TimelineSpace/Contents/Mentions/clearMentions', {}, { root: true }) }, bindStreamings: ({ dispatch, state }, account: AccountType) => { dispatch('bindUserStreaming', account) if (state.unreadNotification.direct) { dispatch('bindDirectMessagesStreaming') } if (state.unreadNotification.local) { dispatch('bindLocalStreaming') } if (state.unreadNotification.public) { dispatch('bindPublicStreaming') } }, startStreamings: ({ dispatch, state }) => { dispatch('startUserStreaming') if (state.unreadNotification.direct) { dispatch('startDirectMessagesStreaming') } if (state.unreadNotification.local) { dispatch('startLocalStreaming') } if (state.unreadNotification.public) { dispatch('startPublicStreaming') } }, stopStreamings: ({ dispatch }) => { dispatch('stopUserStreaming') dispatch('stopDirectMessagesStreaming') dispatch('stopLocalStreaming') dispatch('stopPublicStreaming') }, unbindStreamings: ({ dispatch }) => { dispatch('unbindUserStreaming') dispatch('unbindDirectMessagesStreaming') dispatch('unbindLocalStreaming') dispatch('unbindPublicStreaming') }, // ------------------------------------------------ // Each streaming methods // ------------------------------------------------ bindUserStreaming: ({ commit, rootState }, account: AccountType) => { ipcRenderer.on('update-start-user-streaming', (_, update: Status) => { commit('TimelineSpace/Contents/Home/appendTimeline', update, { root: true }) // Sometimes archive old statuses if (rootState.TimelineSpace.Contents.Home.heading && Math.random() > 0.8) { commit('TimelineSpace/Contents/Home/archiveTimeline', null, { root: true }) } 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`) } } 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) => { 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 }) }) }, 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 }) if (rootState.TimelineSpace.Contents.Local.heading && Math.random() > 0.8) { commit('TimelineSpace/Contents/Local/archiveTimeline', {}, { root: true }) } commit('TimelineSpace/SideMenu/changeUnreadLocalTimeline', true, { root: true }) }) }, startLocalStreaming: ({ state }) => { // @ts-ignore return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars ipcRenderer.send('start-local-streaming', { account: state.account, useWebsocket: state.useWebsocket }) ipcRenderer.once('error-start-local-streaming', (_, err: Error) => { reject(err) }) }) }, bindPublicStreaming: ({ commit, rootState }) => { ipcRenderer.on('update-start-public-streaming', (_, update: Status) => { commit('TimelineSpace/Contents/Public/appendTimeline', update, { root: true }) if (rootState.TimelineSpace.Contents.Public.heading && Math.random() > 0.8) { commit('TimelineSpace/Contents/Public/archiveTimeline', {}, { root: true }) } commit('TimelineSpace/SideMenu/changeUnreadPublicTimeline', true, { root: true }) }) }, startPublicStreaming: ({ state }) => { // @ts-ignore return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars ipcRenderer.send('start-public-streaming', { account: state.account, useWebsocket: state.useWebsocket }) ipcRenderer.once('error-start-public-streaming', (_, err: Error) => { reject(err) }) }) }, bindDirectMessagesStreaming: ({ commit, rootState }) => { ipcRenderer.on('update-start-directmessages-streaming', (_, update: Status) => { commit('TimelineSpace/Contents/DirectMessages/appendTimeline', update, { root: true }) if (rootState.TimelineSpace.Contents.DirectMessages.heading && Math.random() > 0.8) { commit('TimelineSpace/Contents/DirectMessages/archiveTimeline', {}, { root: true }) } commit('TimelineSpace/SideMenu/changeUnreadDirectMessagesTimeline', true, { root: true }) }) }, startDirectMessagesStreaming: ({ state }) => { // @ts-ignore return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars ipcRenderer.send('start-directmessages-streaming', { account: state.account, useWebsocket: state.useWebsocket }) ipcRenderer.once('error-start-directmessages-streaming', (_, err: Error) => { reject(err) }) }) }, unbindUserStreaming: () => { ipcRenderer.removeAllListeners('update-start-user-streaming') ipcRenderer.removeAllListeners('notification-start-user-streaming') ipcRenderer.removeAllListeners('error-start-user-streaming') }, stopUserStreaming: () => { ipcRenderer.send('stop-user-streaming') }, unbindLocalStreaming: () => { ipcRenderer.removeAllListeners('error-start-local-streaming') ipcRenderer.removeAllListeners('update-start-local-streaming') }, stopLocalStreaming: () => { ipcRenderer.send('stop-local-streaming') }, unbindPublicStreaming: () => { ipcRenderer.removeAllListeners('error-start-public-streaming') ipcRenderer.removeAllListeners('update-start-public-streaming') }, stopPublicStreaming: () => { ipcRenderer.send('stop-public-streaming') }, unbindDirectMessagesStreaming: () => { ipcRenderer.removeAllListeners('error-start-directmessages-streaming') ipcRenderer.removeAllListeners('update-start-directmessages-streaming') }, stopDirectMessagesStreaming: () => { ipcRenderer.send('stop-directmessages-streaming') } } const TimelineSpace: Module = { namespaced: true, modules: { SideMenu, HeaderMenu, Modals, Contents }, state: state, mutations: mutations, actions: actions } 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 } }