diff --git a/.electron-vue/webpack.renderer.config.js b/.electron-vue/webpack.renderer.config.js index 304b3c72..ac54832a 100644 --- a/.electron-vue/webpack.renderer.config.js +++ b/.electron-vue/webpack.renderer.config.js @@ -32,7 +32,7 @@ let rendererConfig = { module: { rules: [ { - test: /\.(js|vue)$/, + test: /\.(js|vue|ts)$/, enforce: 'pre', exclude: /node_modules/, use: { diff --git a/package-lock.json b/package-lock.json index 1065cdb3..e513cfe6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13466,9 +13466,9 @@ "dev": true }, "megalodon": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-0.6.0.tgz", - "integrity": "sha512-SXk6DqM02NJGLppr4XpCKDX3HqsPBS1fyuxm19IPmMlHRerChVyIDoWlmxlAEq3jdcCQrMmVVC/E5QuoO5uc0Q==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-0.6.2.tgz", + "integrity": "sha512-EmNs0M6e2AiX9hutoiXo0FUkghZ1HdyLpS8mVkrMMN8btBR2x1hVrAF/8WAFePeJQrEMYjRyQSEJfykJ/4rwaQ==", "requires": { "@types/oauth": "0.9.1", "@types/request": "2.48.1", diff --git a/package.json b/package.json index dcbef799..381b43ec 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "i18next-sync-fs-backend": "^1.1.0", "is-empty": "^1.2.0", "lodash": "^4.17.11", - "megalodon": "0.6.0", + "megalodon": "0.6.2", "moment": "^2.21.0", "mousetrap": "^1.6.2", "nedb": "^1.8.0", diff --git a/spec/renderer/integration/store/TimelineSpace.spec.ts b/spec/renderer/integration/store/TimelineSpace.spec.ts index 43ca4c48..62d4c4a3 100644 --- a/spec/renderer/integration/store/TimelineSpace.spec.ts +++ b/spec/renderer/integration/store/TimelineSpace.spec.ts @@ -1,19 +1,35 @@ -import Mastodon from 'megalodon' +import Mastodon, { Emoji, Instance, Response } from 'megalodon' import { createLocalVue } from '@vue/test-utils' import Vuex from 'vuex' import { ipcMain } from '~/spec/mock/electron' -import TimelineSpace from '~/src/renderer/store/TimelineSpace' +import TimelineSpace, { TimelineSpaceState, blankAccount } from '~/src/renderer/store/TimelineSpace' import unreadSettings from '~/src/constants/unreadNotification' jest.mock('megalodon') -const state = () => { +const mockedInstance: Instance = { + uri: 'http://pleroma.io', + title: 'pleroma', + description: '', + email: 'test@example.com', + version: '2.5.0 (compatible; Pleroma 0.9.0-3363-g7c5d2dc7)', + thumbnail: null, + urls: { + streaming_api: 'wss://pleroma.io' + }, + stats: { + user_count: 10, + status_count: 1000, + domain_count: 100 + }, + languages: ['en'], + contact_account: null, + max_toot_chars: 5000 +} + +const state = (): TimelineSpaceState => { return { - account: { - domain: '', - _id: '', - username: '' - }, + account: blankAccount, loading: false, emojis: [], tootMax: 500, @@ -110,7 +126,7 @@ describe('TimelineSpace', () => { describe('localAccount', () => { describe('account already exists', () => { beforeEach(() => { - ipcMain.once('get-local-account', (event, _id) => { + ipcMain.once('get-local-account', (event: any) => { event.sender.send('response-get-local-account', { username: 'test' }) @@ -124,10 +140,10 @@ describe('TimelineSpace', () => { describe('account does not exist', () => { beforeEach(() => { - ipcMain.once('get-local-account', (event, _id) => { + ipcMain.once('get-local-account', (event: any) => { event.sender.send('response-get-local-account', {}) }) - ipcMain.once('update-account', (event, _account) => { + ipcMain.once('update-account', (event: any) => { event.sender.send('response-update-account', { username: 'fetched' }) @@ -142,23 +158,52 @@ describe('TimelineSpace', () => { describe('detectPleroma', () => { describe('API is pleroma', () => { + let mockedResponse: Response + beforeEach(() => { + mockedResponse = { + data: mockedInstance, + status: 200, + statusText: 'OK', + headers: {} + } + }) it('should be detected', async () => { - // const mockResponse = { - // version: 'Pleroma v0.9.9' - // } - (Mastodon.get as any).mockResolvedValue({ - version: 'Pleroma v0.9.9' - }) + (Mastodon.get as any).mockResolvedValue(mockedResponse) await store.dispatch('TimelineSpace/detectPleroma') expect(store.state.TimelineSpace.pleroma).toEqual(true) expect(store.state.TimelineSpace.useWebsocket).toEqual(true) }) }) describe('API is not pleroma', () => { + let mockedResponse: Response + beforeEach(() => { + const instance: Instance = { + uri: 'http://mstdn.io', + title: 'mstnd.io', + description: '', + email: 'test@example.com', + version: '2.7.0', + thumbnail: null, + urls: { + streaming_api: 'wss://mstdn.io' + }, + stats: { + user_count: 10, + status_count: 1000, + domain_count: 100 + }, + languages: ['en'], + contact_account: null + } + mockedResponse = { + data: instance, + status: 200, + statusText: 'OK', + headers: {} + } + }) it('should be detected', async () => { - (Mastodon.get as any).mockResolvedValue({ - version: '2.7.0' - }) + (Mastodon.get as any).mockResolvedValue(mockedResponse) await store.dispatch('TimelineSpace/detectPleroma') expect(store.state.TimelineSpace.pleroma).toEqual(false) expect(store.state.TimelineSpace.useWebsocket).toEqual(false) @@ -167,36 +212,59 @@ describe('TimelineSpace', () => { }) describe('fetchEmojis', () => { + let emacsEmoji: Emoji + let rubyEmoji: Emoji + let mockedResponse: Response> + beforeEach(() => { + emacsEmoji = { + shortcode: 'emacs', + url: 'http://example.com/emacs', + static_url: 'http://example.com/emacs', + visible_in_picker: true + } + rubyEmoji = { + shortcode: 'ruby', + url: 'http://example.com/ruby', + static_url: 'http://example.com/ruby', + visible_in_picker: true + } + mockedResponse = { + data: [ + emacsEmoji, + rubyEmoji + ], + status: 200, + statusText: 'OK', + headers: {} + } + }) it('should be updated', async () => { - (Mastodon.get as any).mockResolvedValue([ - { - shortcode: 'emacs', - url: 'http://example.com/emacs' - }, - { - shortcode: 'ruby', - url: 'http://example.com/ruby' - } - ]) + (Mastodon.get as any).mockResolvedValue(mockedResponse) await store.dispatch('TimelineSpace/fetchEmojis', {}) expect(store.state.TimelineSpace.emojis).toEqual([ { - name: ':emacs:', - image: 'http://example.com/emacs' + image: 'http://example.com/emacs', + name: ':emacs:' }, { - name: ':ruby:', - image: 'http://example.com/ruby' - } - ]) + image: 'http://example.com/ruby', + name: ':ruby:' + }]) }) }) describe('fetchInstance', () => { + let mockedResponse: Response + beforeEach(() => { + mockedResponse = { + data: mockedInstance, + status: 200, + statusText: 'OK', + headers: {} + } + }) it('should be updated', async () => { - (Mastodon.get as any).mockResolvedValue({ - max_toot_chars: 5000 - }) + (Mastodon.get as any).mockResolvedValue(mockedResponse) await store.dispatch('TimelineSpace/fetchInstance', {}) expect(store.state.TimelineSpace.tootMax).toEqual(5000) }) @@ -205,7 +273,7 @@ describe('TimelineSpace', () => { describe('loadUnreadNotification', () => { describe('success', () => { it('should be updated', async () => { - ipcMain.once('get-unread-notification', (event, _) => { + ipcMain.once('get-unread-notification', (event: any) => { event.sender.send('response-get-unread-notification', { direct: false, local: false, @@ -222,7 +290,7 @@ describe('TimelineSpace', () => { }) describe('error', () => { it('should be set default', async () => { - ipcMain.once('get-unread-notification', (event, _) => { + ipcMain.once('get-unread-notification', (event: any) => { event.sender.send('error-get-unread-notification', new Error()) }) await store.dispatch('TimelineSpace/loadUnreadNotification') diff --git a/spec/renderer/unit/store/TimelineSpace.spec.ts b/spec/renderer/unit/store/TimelineSpace.spec.ts index 25e975cf..71c0c17f 100644 --- a/spec/renderer/unit/store/TimelineSpace.spec.ts +++ b/spec/renderer/unit/store/TimelineSpace.spec.ts @@ -1,16 +1,13 @@ -import TimelineSpace from '~/src/renderer/store/TimelineSpace' +import TimelineSpace, { TimelineSpaceState, blankAccount, MUTATION_TYPES } from '~/src/renderer/store/TimelineSpace' +import { Emoji } from 'megalodon' import unreadSettings from '~/src/constants/unreadNotification' describe('TimelineSpace', () => { describe('mutations', () => { - let state + let state: TimelineSpaceState beforeEach(() => { state = { - account: { - domain: '', - _id: '', - username: '' - }, + account: blankAccount, loading: false, emojis: [], tootMax: 500, @@ -26,39 +23,41 @@ describe('TimelineSpace', () => { describe('updateEmojis', () => { it('should be updated', () => { - TimelineSpace.mutations.updateEmojis(state, [ - { - shortcode: 'emacs', - url: 'http://example.com/emacs' - }, - { - shortcode: 'ruby', - url: 'http://example.com/ruby' - } - ]) + const emacsEmoji: Emoji = { + shortcode: 'emacs', + url: 'http://example.com/emacs', + static_url: 'http://example.com/emacs', + visible_in_picker: true + } + const rubyEmoji: Emoji = { + shortcode: 'ruby', + url: 'http://example.com/ruby', + static_url: 'http://example.com/ruby', + visible_in_picker: true + } + TimelineSpace.mutations![MUTATION_TYPES.UPDATE_EMOJIS](state, [emacsEmoji, rubyEmoji]) expect(state.emojis).toEqual([ { - name: ':emacs:', - image: 'http://example.com/emacs' + image: 'http://example.com/emacs', + name: ':emacs:' }, { - name: ':ruby:', - image: 'http://example.com/ruby' - } - ]) + image: 'http://example.com/ruby', + name: ':ruby:' + }]) }) }) describe('updateTootMax', () => { describe('value is null', () => { it('should be updated with 500', () => { - TimelineSpace.mutations.updateTootMax(state, null) + TimelineSpace.mutations![MUTATION_TYPES.UPDATE_TOOT_MAX](state, null) expect(state.tootMax).toEqual(500) }) }) describe('value is not null', () => { it('should be updated', () => { - TimelineSpace.mutations.updateTootMax(state, 1200) + TimelineSpace.mutations![MUTATION_TYPES.UPDATE_TOOT_MAX](state, 1200) expect(state.tootMax).toEqual(1200) }) }) diff --git a/src/renderer/store/TimelineSpace.js b/src/renderer/store/TimelineSpace.js deleted file mode 100644 index 3f8b5581..00000000 --- a/src/renderer/store/TimelineSpace.js +++ /dev/null @@ -1,428 +0,0 @@ -import sanitizeHtml from 'sanitize-html' -import { ipcRenderer } from 'electron' -import Mastodon 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' - -const TimelineSpace = { - namespaced: true, - modules: { - SideMenu, - HeaderMenu, - Modals, - Contents - }, - state: { - account: { - domain: '', - _id: '', - username: '' - }, - loading: false, - emojis: [], - tootMax: 500, - unreadNotification: { - direct: unreadSettings.Direct.default, - local: unreadSettings.Local.default, - public: unreadSettings.Public.default - }, - useWebsocket: false, - pleroma: false - }, - mutations: { - updateAccount (state, account) { - state.account = account - }, - changeLoading (state, value) { - state.loading = value - }, - updateEmojis (state, emojis) { - state.emojis = emojis.map((e) => { - return { - name: `:${e.shortcode}:`, - image: e.url - } - }) - }, - updateTootMax (state, value) { - if (value) { - state.tootMax = value - } else { - state.tootMax = 500 - } - }, - updateUnreadNotification (state, settings) { - state.unreadNotification = settings - }, - changePleroma (state, pleroma) { - state.pleroma = pleroma - }, - changeUseWebsocket (state, use) { - state.useWebsocket = use - } - }, - actions: { - // ------------------------------------------------- - // Accounts - // ------------------------------------------------- - localAccount ({ dispatch, commit }, id) { - return new Promise((resolve, reject) => { - ipcRenderer.send('get-local-account', id) - ipcRenderer.once('error-get-local-account', (event, err) => { - ipcRenderer.removeAllListeners('response-get-local-account') - reject(err) - }) - ipcRenderer.once('response-get-local-account', (event, account) => { - ipcRenderer.removeAllListeners('error-get-local-account') - - if (account.username === undefined || account.username === null || account.username === '') { - dispatch('fetchAccount', account) - .then((acct) => { - commit('updateAccount', acct) - resolve(acct) - }) - .catch((err) => { - reject(err) - }) - } else { - commit('updateAccount', account) - resolve(account) - } - }) - }) - }, - fetchAccount (_, account) { - return new Promise((resolve, reject) => { - ipcRenderer.send('update-account', account) - ipcRenderer.once('error-update-account', (event, err) => { - ipcRenderer.removeAllListeners('response-update-account') - reject(err) - }) - ipcRenderer.once('response-update-account', (event, account) => { - ipcRenderer.removeAllListeners('error-update-account') - resolve(account) - }) - }) - }, - async clearAccount ({ commit }) { - commit( - 'updateAccount', - { - domain: '', - _id: '', - username: '' - } - ) - return 'clearAccount' - }, - async detectPleroma ({ commit, state }) { - const data = await Mastodon.get('/instance', {}, state.account.baseURL + '/api/v1') - if (data.version.includes('Pleroma')) { - commit('changePleroma', true) - commit('changeUseWebsocket', true) - } else { - commit('changePleroma', false) - commit('changeUseWebsocket', 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 }) - }) - }, - async removeShortcutEvents () { - ipcRenderer.removeAllListeners('CmdOrCtrl+N') - ipcRenderer.removeAllListeners('CmdOrCtrl+K') - return 'removeShortcutEvents' - }, - /** - * clearUnread - */ - async clearUnread ({ dispatch }) { - dispatch('TimelineSpace/SideMenu/clearUnread', {}, { root: true }) - }, - /** - * fetchEmojis - */ - async fetchEmojis ({ commit }, account) { - const data = await Mastodon.get('/custom_emojis', {}, account.baseURL + '/api/v1') - commit('updateEmojis', data) - return data - }, - /** - * fetchInstance - */ - async fetchInstance ({ commit }, account) { - const data = await Mastodon.get('/instance', {}, account.baseURL + '/api/v1') - commit('updateTootMax', data.max_toot_chars) - return data - }, - loadUnreadNotification ({ commit }, accountID) { - return new Promise(resolve => { - ipcRenderer.once('response-get-unread-notification', (event, settings) => { - ipcRenderer.removeAllListeners('error-get-unread-notification') - commit('updateUnreadNotification', settings) - resolve(settings) - }) - ipcRenderer.once('error-get-unread-notification', () => { - ipcRenderer.removeAllListeners('response-get-unread-notification') - commit('updateUnreadNotification', { - direct: unreadSettings.Direct.default, - local: unreadSettings.Local.default, - public: unreadSettings.Public.default - }) - resolve(null) - }) - ipcRenderer.send('get-unread-notification', accountID) - }) - }, - async fetchContentsTimelines ({ dispatch, state }, account) { - await dispatch('TimelineSpace/Contents/Home/fetchTimeline', account, { root: true }) - await dispatch('TimelineSpace/Contents/Notifications/fetchNotifications', account, { 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) { - 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) { - ipcRenderer.on('update-start-user-streaming', (event, update) => { - 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', (event, notification) => { - let notify = createNotification(notification, rootState.App.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', (event, mention) => { - 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 }) { - return new Promise((resolve, reject) => { - ipcRenderer.send('start-user-streaming', { - account: state.account, - useWebsocket: state.useWebsocket - }) - ipcRenderer.once('error-start-user-streaming', (event, err) => { - reject(err) - }) - }) - }, - bindLocalStreaming ({ commit, rootState }) { - ipcRenderer.on('update-start-local-streaming', (event, update) => { - 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 }) { - return new Promise((resolve, reject) => { - ipcRenderer.send('start-local-streaming', { - account: state.account, - useWebsocket: state.useWebsocket - }) - ipcRenderer.once('error-start-local-streaming', (event, err) => { - reject(err) - }) - }) - }, - bindPublicStreaming ({ commit, rootState }) { - ipcRenderer.on('update-start-public-streaming', (event, update) => { - 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 }) { - return new Promise((resolve, reject) => { - ipcRenderer.send('start-public-streaming', { - account: state.account, - useWebsocket: state.useWebsocket - }) - ipcRenderer.once('error-start-public-streaming', (event, err) => { - reject(err) - }) - }) - }, - bindDirectMessagesStreaming ({ commit, rootState }) { - ipcRenderer.on('update-start-directmessages-streaming', (event, update) => { - 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 }) { - return new Promise((resolve, reject) => { - ipcRenderer.send('start-directmessages-streaming', { - account: state.account, - useWebsocket: state.useWebsocket - }) - ipcRenderer.once('error-start-directmessages-streaming', (event, err) => { - 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') - } - } -} - -export default TimelineSpace - -function createNotification (notification, notifyConfig) { - 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) { - if (account.display_name !== '') { - return account.display_name - } else { - return account.username - } -} diff --git a/src/renderer/store/TimelineSpace.ts b/src/renderer/store/TimelineSpace.ts new file mode 100644 index 00000000..d8fbae10 --- /dev/null +++ b/src/renderer/store/TimelineSpace.ts @@ -0,0 +1,477 @@ +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 + } +} diff --git a/src/renderer/store/TimelineSpace/Contents/Notifications.js b/src/renderer/store/TimelineSpace/Contents/Notifications.js index 383997e9..ae1ba0e7 100644 --- a/src/renderer/store/TimelineSpace/Contents/Notifications.js +++ b/src/renderer/store/TimelineSpace/Contents/Notifications.js @@ -59,10 +59,10 @@ const Notifications = { } }, actions: { - fetchNotifications ({ commit }, account) { + fetchNotifications ({ commit, rootState }) { const client = new Mastodon( - account.accessToken, - account.baseURL + '/api/v1' + rootState.TimelineSpace.account.accessToken, + rootState.TimelineSpace.account.baseURL + '/api/v1' ) return client.get('/notifications', { limit: 30 }) .then(res => {