From ba1574116fc6df795c902ad35629ab2de61e3ca1 Mon Sep 17 00:00:00 2001 From: AkiraFukushima Date: Thu, 10 Jun 2021 12:03:51 +0900 Subject: [PATCH 1/3] refs #574 Save marker in local db every time receive a new status in home --- src/main/index.ts | 14 ++++ src/main/marker.ts | 67 +++++++++++++++++++ .../TimelineSpace/Contents/Home.vue | 5 ++ .../store/TimelineSpace/Contents/Home.ts | 12 ++++ src/types/localMarker.ts | 5 ++ 5 files changed, 103 insertions(+) create mode 100644 src/main/marker.ts create mode 100644 src/types/localMarker.ts diff --git a/src/main/index.ts b/src/main/index.ts index 8aa58d3c..a7f8e86d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -54,6 +54,8 @@ import ProxyConfiguration from './proxy' import confirm from './timelines' import { EnabledTimelines } from '~/src/types/enabledTimelines' import { Menu as MenuPreferences } from '~/src/types/preference' +import { LocalMarker } from '~/src/types/localMarker' +import Marker from './marker' /** * Context menu @@ -140,6 +142,12 @@ unreadNotification.initialize().catch((err: Error) => log.error(err)) const preferencesDBPath = process.env.NODE_ENV === 'production' ? userData + './db/preferences.json' : 'preferences.json' +const markerDBPath = process.env.NODE_ENV === 'production' ? userData + '/db/marker.db' : 'marker.db' +const markerDB = new Datastore({ + filename: markerDBPath, + autoload: true +}) + /** * Cache path */ @@ -1137,6 +1145,12 @@ ipcMain.handle('update-spellchecker-languages', async (_: IpcMainInvokeEvent, la return conf.language.spellchecker.languages }) +// marker +ipcMain.handle('save-marker', async (_: IpcMainInvokeEvent, marker: LocalMarker) => { + const repo = new Marker(markerDB) + await repo.save(marker) +}) + // hashtag ipcMain.handle('save-hashtag', async (_: IpcMainInvokeEvent, tag: string) => { const hashtags = new Hashtags(hashtagsDB) diff --git a/src/main/marker.ts b/src/main/marker.ts new file mode 100644 index 00000000..a000a484 --- /dev/null +++ b/src/main/marker.ts @@ -0,0 +1,67 @@ +import { isEmpty } from 'lodash' +import Datastore from 'nedb' +import { LocalMarker } from '~/src/types/localMarker' + +export default class Marker { + private db: Datastore + + constructor(db: Datastore) { + this.db = db + this.db.persistence.setAutocompactionInterval(60000) // milliseconds + } + + private insert(marker: LocalMarker): Promise { + return new Promise((resolve, reject) => { + this.db.insert(marker, (err, doc) => { + if (err) return reject(err) + resolve(doc) + }) + }) + } + + private update(marker: LocalMarker): Promise { + // @ts-ignore + return new Promise((resolve, reject) => { + // eslint-disable-line no-unused-vars + this.db.update( + { + acct: marker.acct, + timeline: marker.timeline + }, + { $set: marker }, + { multi: false }, + err => { + if (err) return reject(err) + return this.get(marker.acct, marker.timeline) + } + ) + }) + } + + public async save(marker: LocalMarker): Promise { + return this.get(marker.acct, marker.timeline).then(l => { + if (isEmpty(l)) return this.insert(marker) + return this.update(marker) + }) + } + + public async get(acct: string, timeline: 'home' | 'notifications'): Promise { + return new Promise((resolve, reject) => { + this.db.findOne({ acct: acct, timeline: timeline }, (err, doc) => { + if (err) return reject(err) + resolve(doc) + }) + }) + } + + public async list(acct: string): Promise> { + return new Promise((resolve, reject) => { + this.db + .find({ acct: acct }) + .exec((err, docs) => { + if (err) return reject(err) + resolve(docs) + }) + }) + } +} diff --git a/src/renderer/components/TimelineSpace/Contents/Home.vue b/src/renderer/components/TimelineSpace/Contents/Home.vue index 413ac1e7..b463bad1 100644 --- a/src/renderer/components/TimelineSpace/Contents/Home.vue +++ b/src/renderer/components/TimelineSpace/Contents/Home.vue @@ -127,6 +127,11 @@ export default { this.$store.commit('TimelineSpace/Contents/Home/changeHeading', true) this.$store.commit('TimelineSpace/Contents/Home/mergeTimeline') } + }, + timeline: function (newState, _oldState) { + if (this.heading && newState.length > 0) { + this.$store.dispatch('TimelineSpace/Contents/Home/saveMarker', newState[0].id) + } } }, methods: { diff --git a/src/renderer/store/TimelineSpace/Contents/Home.ts b/src/renderer/store/TimelineSpace/Contents/Home.ts index feadab7f..56659587 100644 --- a/src/renderer/store/TimelineSpace/Contents/Home.ts +++ b/src/renderer/store/TimelineSpace/Contents/Home.ts @@ -1,6 +1,10 @@ import generator, { Entity, FilterContext } from 'megalodon' import { Module, MutationTree, ActionTree, GetterTree } from 'vuex' import { RootState } from '@/store' +import { LocalMarker } from '~/src/types/localMarker' +import { MyWindow } from '~/src/types/global' + +const win = (window as any) as MyWindow export type HomeState = { lazyLoading: boolean @@ -135,6 +139,14 @@ const actions: ActionTree = { .finally(() => { commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, false) }) + }, + saveMarker: async ({ rootState }, id: string) => { + const acct = `@${rootState.TimelineSpace.account.username}@${rootState.TimelineSpace.account.domain}` + await win.ipcRenderer.invoke('save-marker', { + acct: acct, + timeline: 'home', + lastReadID: id + } as LocalMarker) } } diff --git a/src/types/localMarker.ts b/src/types/localMarker.ts new file mode 100644 index 00000000..95712762 --- /dev/null +++ b/src/types/localMarker.ts @@ -0,0 +1,5 @@ +export type LocalMarker = { + acct: string + timeline: 'home' | 'notifications' + lastReadID: string +} From 0e354c474df4cecbd81dff92c03c5af3e69dbf96 Mon Sep 17 00:00:00 2001 From: AkiraFukushima Date: Thu, 10 Jun 2021 15:27:04 +0900 Subject: [PATCH 2/3] refs #574 Save marker in local db every time receive a new status in notifications --- src/main/index.ts | 4 ++-- src/main/marker.ts | 14 +++++++------- .../TimelineSpace/Contents/Home.vue | 4 ++++ .../TimelineSpace/Contents/Notifications.vue | 19 +++++++++++++++---- .../store/TimelineSpace/Contents/Home.ts | 5 ++--- .../TimelineSpace/Contents/Notifications.ts | 8 ++++++++ src/types/localMarker.ts | 4 ++-- 7 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index a7f8e86d..0dc0220e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -147,6 +147,7 @@ const markerDB = new Datastore({ filename: markerDBPath, autoload: true }) +const markerRepo = new Marker(markerDB) /** * Cache path @@ -1147,8 +1148,7 @@ ipcMain.handle('update-spellchecker-languages', async (_: IpcMainInvokeEvent, la // marker ipcMain.handle('save-marker', async (_: IpcMainInvokeEvent, marker: LocalMarker) => { - const repo = new Marker(markerDB) - await repo.save(marker) + await markerRepo.save(marker) }) // hashtag diff --git a/src/main/marker.ts b/src/main/marker.ts index a000a484..700c125e 100644 --- a/src/main/marker.ts +++ b/src/main/marker.ts @@ -25,39 +25,39 @@ export default class Marker { // eslint-disable-line no-unused-vars this.db.update( { - acct: marker.acct, + owner_id: marker.owner_id, timeline: marker.timeline }, { $set: marker }, { multi: false }, err => { if (err) return reject(err) - return this.get(marker.acct, marker.timeline) + return this.get(marker.owner_id, marker.timeline) } ) }) } public async save(marker: LocalMarker): Promise { - return this.get(marker.acct, marker.timeline).then(l => { + return this.get(marker.owner_id, marker.timeline).then(l => { if (isEmpty(l)) return this.insert(marker) return this.update(marker) }) } - public async get(acct: string, timeline: 'home' | 'notifications'): Promise { + public async get(owner_id: string, timeline: 'home' | 'notifications'): Promise { return new Promise((resolve, reject) => { - this.db.findOne({ acct: acct, timeline: timeline }, (err, doc) => { + this.db.findOne({ owner_id: owner_id, timeline: timeline }, (err, doc) => { if (err) return reject(err) resolve(doc) }) }) } - public async list(acct: string): Promise> { + public async list(owner_id: string): Promise> { return new Promise((resolve, reject) => { this.db - .find({ acct: acct }) + .find({ owner_id: owner_id }) .exec((err, docs) => { if (err) return reject(err) resolve(docs) diff --git a/src/renderer/components/TimelineSpace/Contents/Home.vue b/src/renderer/components/TimelineSpace/Contents/Home.vue index b463bad1..d9b598df 100644 --- a/src/renderer/components/TimelineSpace/Contents/Home.vue +++ b/src/renderer/components/TimelineSpace/Contents/Home.vue @@ -94,6 +94,10 @@ export default { this.focusedId = previousFocusedId }) }) + + if (this.heading && this.timeline.length > 0) { + this.$store.dispatch('TimelineSpace/Contents/Home/saveMarker', this.timeline[0].id) + } }, beforeUpdate() { if (this.$store.state.TimelineSpace.SideMenu.unreadHomeTimeline && this.heading) { diff --git a/src/renderer/components/TimelineSpace/Contents/Notifications.vue b/src/renderer/components/TimelineSpace/Contents/Notifications.vue index 2e68fd56..b15fde97 100644 --- a/src/renderer/components/TimelineSpace/Contents/Notifications.vue +++ b/src/renderer/components/TimelineSpace/Contents/Notifications.vue @@ -47,10 +47,12 @@ export default { ...mapState({ openSideBar: state => state.TimelineSpace.Contents.SideBar.openSideBar, startReload: state => state.TimelineSpace.HeaderMenu.reload, - backgroundColor: state => state.App.theme.background_color, - lazyLoading: state => state.TimelineSpace.Contents.Notifications.lazyLoading, - heading: state => state.TimelineSpace.Contents.Notifications.heading, - unread: state => state.TimelineSpace.Contents.Notifications.unreadNotifications + backgroundColor: state => state.App.theme.background_color + }), + ...mapState('TimelineSpace/Contents/Notifications', { + lazyLoading: state => state.lazyLoading, + heading: state => state.heading, + unread: state => state.unreadNotifications }), ...mapGetters('TimelineSpace/Contents/Notifications', ['handledNotifications', 'filters']), ...mapGetters('TimelineSpace/Modals', ['modalOpened']), @@ -79,6 +81,10 @@ export default { this.focusedId = previousFocusedId }) }) + + if (this.heading && this.handledNotifications.length > 0) { + this.$store.dispatch('TimelineSpace/Contents/Notifications/saveMarker', this.handledNotifications[0].id) + } }, beforeUpdate() { if (this.$store.state.TimelineSpace.SideMenu.unreadNotifications) { @@ -113,6 +119,11 @@ export default { this.$store.commit('TimelineSpace/Contents/Notifications/mergeNotifications') this.$store.dispatch('TimelineSpace/Contents/Notifications/resetBadge') } + }, + handledNotifications: function (newState, _oldState) { + if (this.heading && newState.length > 0) { + this.$store.dispatch('TimelineSpace/Contents/Notifications/saveMarker', newState[0].id) + } } }, methods: { diff --git a/src/renderer/store/TimelineSpace/Contents/Home.ts b/src/renderer/store/TimelineSpace/Contents/Home.ts index 56659587..8b5aebaa 100644 --- a/src/renderer/store/TimelineSpace/Contents/Home.ts +++ b/src/renderer/store/TimelineSpace/Contents/Home.ts @@ -141,11 +141,10 @@ const actions: ActionTree = { }) }, saveMarker: async ({ rootState }, id: string) => { - const acct = `@${rootState.TimelineSpace.account.username}@${rootState.TimelineSpace.account.domain}` await win.ipcRenderer.invoke('save-marker', { - acct: acct, + owner_id: rootState.TimelineSpace.account._id, timeline: 'home', - lastReadID: id + last_read_id: id } as LocalMarker) } } diff --git a/src/renderer/store/TimelineSpace/Contents/Notifications.ts b/src/renderer/store/TimelineSpace/Contents/Notifications.ts index 1b7fe833..4dfd1562 100644 --- a/src/renderer/store/TimelineSpace/Contents/Notifications.ts +++ b/src/renderer/store/TimelineSpace/Contents/Notifications.ts @@ -1,6 +1,7 @@ import generator, { Entity, FilterContext, NotificationType } from 'megalodon' import { Module, MutationTree, ActionTree, GetterTree } from 'vuex' import { RootState } from '@/store' +import { LocalMarker } from '~/src/types/localMarker' import { MyWindow } from '~/src/types/global' const win = (window as any) as MyWindow @@ -135,6 +136,13 @@ const actions: ActionTree = { }, resetBadge: () => { win.ipcRenderer.send('reset-badge') + }, + saveMarker: async ({ rootState }, id: string) => { + await win.ipcRenderer.invoke('save-marker', { + owner_id: rootState.TimelineSpace.account._id, + timeline: 'notifications', + last_read_id: id + } as LocalMarker) } } diff --git a/src/types/localMarker.ts b/src/types/localMarker.ts index 95712762..c32e5330 100644 --- a/src/types/localMarker.ts +++ b/src/types/localMarker.ts @@ -1,5 +1,5 @@ export type LocalMarker = { - acct: string + owner_id: string timeline: 'home' | 'notifications' - lastReadID: string + last_read_id: string } From adb4ddb1f1c35a65d78b62a8459a5e2576b1a84a Mon Sep 17 00:00:00 2001 From: AkiraFukushima Date: Thu, 10 Jun 2021 22:12:21 +0900 Subject: [PATCH 3/3] refs #574 Sync local marker to servers --- src/main/index.ts | 78 +++++++++++++++++++++++++++++++++++----------- src/main/marker.ts | 2 +- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 0dc0220e..de39cb42 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -25,7 +25,7 @@ import path from 'path' import ContextMenu from 'electron-context-menu' import { initSplashScreen, Config } from '@trodi/electron-splashscreen' import openAboutWindow from 'about-window' -import { Entity, detector, NotificationType } from 'megalodon' +import generator, { Entity, detector, NotificationType } from 'megalodon' import sanitizeHtml from 'sanitize-html' import AutoLaunch from 'auto-launch' import minimist from 'minimist' @@ -127,8 +127,8 @@ const accountDB = new Datastore({ filename: accountDBPath, autoload: true }) -const accountManager = new Account(accountDB) -accountManager.initialize().catch((err: Error) => log.error(err)) +const accountRepo = new Account(accountDB) +accountRepo.initialize().catch((err: Error) => log.error(err)) const hashtagsDBPath = process.env.NODE_ENV === 'production' ? userData + '/db/hashtags.db' : 'hashtags.db' const hashtagsDB = new Datastore({ @@ -181,7 +181,7 @@ if (process.platform !== 'darwin') { async function listAccounts(): Promise> { try { - const accounts = await accountManager.listAccounts() + const accounts = await accountRepo.listAccounts() return accounts } catch (err) { return [] @@ -450,7 +450,7 @@ app.on('activate', () => { } }) -const auth = new Authentication(accountManager) +const auth = new Authentication(accountRepo) type AuthRequest = { instance: string @@ -506,23 +506,23 @@ ipcMain.handle('get-and-update-access-token', async (_: IpcMainInvokeEvent, requ // nedb ipcMain.handle('list-accounts', async (_: IpcMainInvokeEvent) => { - const accounts = await accountManager.listAccounts() + const accounts = await accountRepo.listAccounts() return accounts }) ipcMain.handle('get-local-account', async (_: IpcMainInvokeEvent, id: string) => { - const account = await accountManager.getAccount(id) + const account = await accountRepo.getAccount(id) return account }) ipcMain.handle('update-account', async (_: IpcMainInvokeEvent, acct: LocalAccount) => { const proxy = await proxyConfiguration.forMastodon() - const ac: LocalAccount = await accountManager.refresh(acct, proxy) + const ac: LocalAccount = await accountRepo.refresh(acct, proxy) return ac }) ipcMain.handle('remove-account', async (_: IpcMainInvokeEvent, id: string) => { - const accountId = await accountManager.removeAccount(id) + const accountId = await accountRepo.removeAccount(id) const accounts = await listAccounts() const accountsChange: Array = accounts.map((a, index) => { @@ -543,22 +543,22 @@ ipcMain.handle('remove-account', async (_: IpcMainInvokeEvent, id: string) => { }) ipcMain.handle('forward-account', async (_: IpcMainInvokeEvent, acct: LocalAccount) => { - await accountManager.forwardAccount(acct) + await accountRepo.forwardAccount(acct) }) ipcMain.handle('backward-account', async (_: IpcMainInvokeEvent, acct: LocalAccount) => { - await accountManager.backwardAccount(acct) + await accountRepo.backwardAccount(acct) }) ipcMain.handle('refresh-accounts', async (_: IpcMainInvokeEvent) => { const proxy = await proxyConfiguration.forMastodon() - const accounts = await accountManager.refreshAccounts(proxy) + const accounts = await accountRepo.refreshAccounts(proxy) return accounts }) ipcMain.handle('remove-all-accounts', async (_: IpcMainInvokeEvent) => { - await accountManager.removeAll() + await accountRepo.removeAll() const accounts = await listAccounts() const accountsChange: Array = accounts.map((a, index) => { @@ -614,7 +614,7 @@ ipcMain.on('start-all-user-streamings', (event: IpcMainEvent, accounts: Array { const id: string = account._id! try { - const acct = await accountManager.getAccount(id) + const acct = await accountRepo.getAccount(id) // Stop old user streaming if (userStreamings[id]) { userStreamings[id]!.stop() @@ -725,7 +725,7 @@ let directMessagesStreaming: DirectStreaming | null = null ipcMain.on('start-directmessages-streaming', async (event: IpcMainEvent, obj: StreamingSetting) => { const { account } = obj try { - const acct = await accountManager.getAccount(account._id!) + const acct = await accountRepo.getAccount(account._id!) // Stop old directmessages streaming if (directMessagesStreaming !== null) { @@ -774,7 +774,7 @@ let localStreaming: LocalStreaming | null = null ipcMain.on('start-local-streaming', async (event: IpcMainEvent, obj: StreamingSetting) => { const { account } = obj try { - const acct = await accountManager.getAccount(account._id!) + const acct = await accountRepo.getAccount(account._id!) // Stop old local streaming if (localStreaming !== null) { @@ -823,7 +823,7 @@ let publicStreaming: PublicStreaming | null = null ipcMain.on('start-public-streaming', async (event: IpcMainEvent, obj: StreamingSetting) => { const { account } = obj try { - const acct = await accountManager.getAccount(account._id!) + const acct = await accountRepo.getAccount(account._id!) // Stop old public streaming if (publicStreaming !== null) { @@ -876,7 +876,7 @@ type ListID = { ipcMain.on('start-list-streaming', async (event: IpcMainEvent, obj: ListID & StreamingSetting) => { const { listID, account } = obj try { - const acct = await accountManager.getAccount(account._id!) + const acct = await accountRepo.getAccount(account._id!) // Stop old list streaming if (listStreaming !== null) { @@ -930,7 +930,7 @@ type Tag = { ipcMain.on('start-tag-streaming', async (event: IpcMainEvent, obj: Tag & StreamingSetting) => { const { tag, account } = obj try { - const acct = await accountManager.getAccount(account._id!) + const acct = await accountRepo.getAccount(account._id!) // Stop old tag streaming if (tagStreaming !== null) { @@ -1148,9 +1148,49 @@ ipcMain.handle('update-spellchecker-languages', async (_: IpcMainInvokeEvent, la // marker ipcMain.handle('save-marker', async (_: IpcMainInvokeEvent, marker: LocalMarker) => { + if (marker.owner_id === null || marker.owner_id === undefined || marker.owner_id === '') { + return + } await markerRepo.save(marker) }) +setTimeout(async () => { + try { + const accounts = await accountRepo.listAccounts() + accounts.map(async acct => { + const proxy = await proxyConfiguration.forMastodon() + const sns = await detector(acct.baseURL, proxy) + if (sns === 'misskey') { + return + } + const client = generator(sns, acct.baseURL, acct.accessToken, 'Whalebird', proxy) + const home = await markerRepo.get(acct._id!, 'home') + const notifications = await markerRepo.get(acct._id!, 'notifications') + let params = {} + if (home !== null && home !== undefined) { + params = Object.assign({}, params, { + home: { + last_read_id: home.last_read_id + } + }) + } + if (notifications !== null && notifications !== undefined) { + params = Object.assign({}, params, { + notifications: { + last_read_id: notifications.last_read_id + } + }) + } + if (isEmpty(params)) { + return + } + await client.saveMarkers(params) + }) + } catch (err) { + console.error(err) + } +}, 120000) + // hashtag ipcMain.handle('save-hashtag', async (_: IpcMainInvokeEvent, tag: string) => { const hashtags = new Hashtags(hashtagsDB) diff --git a/src/main/marker.ts b/src/main/marker.ts index 700c125e..ef3d0c4c 100644 --- a/src/main/marker.ts +++ b/src/main/marker.ts @@ -45,7 +45,7 @@ export default class Marker { }) } - public async get(owner_id: string, timeline: 'home' | 'notifications'): Promise { + public async get(owner_id: string, timeline: 'home' | 'notifications'): Promise { return new Promise((resolve, reject) => { this.db.findOne({ owner_id: owner_id, timeline: timeline }, (err, doc) => { if (err) return reject(err)