diff --git a/src/main/cache/account.ts b/src/main/cache/account.ts new file mode 100644 index 00000000..b1c5f674 --- /dev/null +++ b/src/main/cache/account.ts @@ -0,0 +1,38 @@ +import { isEmpty } from 'lodash' +import Datastore from 'nedb' +import { CachedAccount } from '~/src/types/cachedAccount' + +export default class AccountCache { + private db: Datastore + + constructor(path: string) { + this.db = new Datastore({ + filename: path, + autoload: true + }) + } + + listAccounts(ownerID: string): Promise> { + return new Promise((resolve, reject) => { + this.db.find({ owner_id: ownerID }, (err, docs) => { + if (err) return reject(err) + resolve(docs) + }) + }) + } + + insertAccount(ownerID: string, acct: string): Promise { + return new Promise((resolve, reject) => { + // At first confirm records for unique. + this.db.findOne({ owner_id: ownerID, acct: acct }, (err, doc) => { + if (err) return err + // Ignore error for unique constraints. + if (!isEmpty(doc)) return err + return this.db.insert({ owner_id: ownerID, acct: acct }, (err, doc) => { + if (err) return reject(err) + return resolve(doc) + }) + }) + }) + } +} diff --git a/src/main/index.ts b/src/main/index.ts index 1ad337f6..2ae73526 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -41,6 +41,8 @@ import { UnreadNotification as UnreadNotificationConfig } from '~/src/types/unre import { Notify } from '~/src/types/notify' import { StreamingError } from '~/src/errors/streamingError' import HashtagCache from './cache/hashtag' +import AccountCache from './cache/account' +import { InsertAccountCache } from '~/src/types/insertAccountCache' /** * Context menu @@ -107,6 +109,9 @@ const preferencesDBPath = process.env.NODE_ENV === 'production' ? userData + './ const hashtagCachePath = process.env.NODE_ENV === 'production' ? userData + '/cache/hashtag.db' : 'cache/hashtag.db' const hashtagCache = new HashtagCache(hashtagCachePath) +const accountCachePath = process.env.NODE_ENV === 'production' ? userData + '/cache/account.db' : 'cache/account.db' +const accountCache = new AccountCache(accountCachePath) + const soundBasePath = process.env.NODE_ENV === 'development' ? path.join(__dirname, '../../build/sounds/') : path.join(process.resourcesPath!, 'build/sounds/') @@ -460,7 +465,7 @@ ipcMain.on('start-all-user-streamings', (event: Event, accounts: Array { + async (update: Status) => { if (!event.sender.isDestroyed()) { event.sender.send(`update-start-all-user-streamings-${id}`, update) } @@ -468,6 +473,8 @@ ipcMain.on('start-all-user-streamings', (event: Event, accounts: Array { await hashtagCache.insertHashtag(tag.name) }) + // Cache account + await accountCache.insertAccount(id, update.account.acct).catch(err => console.error(err)) }, (notification: RemoteNotification) => { const preferences = new Preferences(preferencesDBPath) @@ -1005,7 +1012,20 @@ ipcMain.on('insert-cache-hashtags', (event: Event, tags: Array) => { tags.map(async name => { await hashtagCache.insertHashtag(name) }) - event.sender.send('response-insert-cache-hashtag') + event.sender.send('response-insert-cache-hashtags') +}) + +ipcMain.on('get-cache-accounts', async (event: Event, ownerID: string) => { + const accounts = await accountCache.listAccounts(ownerID) + event.sender.send('response-get-cache-accounts', accounts) +}) + +ipcMain.on('insert-cache-accounts', (event: Event, obj: InsertAccountCache) => { + const { ownerID, accts } = obj + accts.map(async acct => { + await accountCache.insertAccount(ownerID, acct).catch(err => console.error(err)) + }) + event.sender.send('response-insert-cache-accounts') }) // Application control diff --git a/src/renderer/store/TimelineSpace/Modals/NewToot/Status.ts b/src/renderer/store/TimelineSpace/Modals/NewToot/Status.ts index 4f95d4c0..44b0b574 100644 --- a/src/renderer/store/TimelineSpace/Modals/NewToot/Status.ts +++ b/src/renderer/store/TimelineSpace/Modals/NewToot/Status.ts @@ -1,9 +1,11 @@ import { ipcRenderer } from 'electron' import emojilib from 'emojilib' -import Mastodon, { Account, Tag, Response, Results } from 'megalodon' +import Mastodon, { Response, Results } from 'megalodon' import { Module, MutationTree, ActionTree, GetterTree } from 'vuex' import { RootState } from '@/store/index' import { LocalTag } from '~/src/types/localTag' +import { InsertAccountCache } from '~/src/types/insertAccountCache' +import { CachedAccount } from '~/src/types/cachedAccount' type Suggest = { name: string @@ -38,7 +40,7 @@ const state = (): StatusState => ({ }) export const MUTATION_TYPES = { - UPDATE_FILTERED_ACCOUNTS: 'updateFilteredAccounts', + APPEND_FILTERED_ACCOUNTS: 'appendFilteredAccounts', CLEAR_FILTERED_ACCOUNTS: 'clearFilteredAccounts', APPEND_FILTERED_HASHTAGS: 'appendFilteredHashtags', CLEAR_FILTERED_HASHTAGS: 'clearFilteredHashtags', @@ -54,16 +56,17 @@ export const MUTATION_TYPES = { } const mutations: MutationTree = { - [MUTATION_TYPES.UPDATE_FILTERED_ACCOUNTS]: (state, accounts: Array) => { - state.filteredAccounts = accounts.map(a => ({ - name: `@${a.acct}`, + [MUTATION_TYPES.APPEND_FILTERED_ACCOUNTS]: (state, accounts: Array) => { + const appended = accounts.map(a => ({ + name: `@${a}`, image: null })) + state.filteredAccounts = appended.filter((elem, index, self) => self.indexOf(elem) === index) }, [MUTATION_TYPES.CLEAR_FILTERED_ACCOUNTS]: state => { state.filteredAccounts = [] }, - [MUTATION_TYPES.APPEND_FILTERED_HASHTAGS]: (state, tags: Array) => { + [MUTATION_TYPES.APPEND_FILTERED_HASHTAGS]: (state, tags: Array) => { const appended = tags.map(t => ({ name: `#${t}`, image: null @@ -109,15 +112,39 @@ const actions: ActionTree = { commit(MUTATION_TYPES.CLEAR_FILTERED_ACCOUNTS) commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS) const { word, start } = wordStart - const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1') - const res: Response = await client.get('/search', { q: word, resolve: false }) - commit(MUTATION_TYPES.UPDATE_FILTERED_ACCOUNTS, res.data.accounts) - if (res.data.accounts.length === 0) throw new Error('Empty') - commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true) - commit(MUTATION_TYPES.CHANGE_START_INDEX, start) - commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word) - commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS) - return res.data.accounts + const searchCache = () => { + return new Promise(resolve => { + const target = word.replace('@', '') + ipcRenderer.once('response-get-cache-accounts', (_, accounts: Array) => { + console.log(accounts) + const matched = accounts.map(account => account.acct).filter(acct => acct.includes(target)) + if (matched.length === 0) throw new Error('Empty') + commit(MUTATION_TYPES.APPEND_FILTERED_ACCOUNTS, matched) + commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true) + commit(MUTATION_TYPES.CHANGE_START_INDEX, start) + commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word) + commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS) + resolve(matched) + }) + ipcRenderer.send('get-cache-accounts', rootState.TimelineSpace.account._id) + }) + } + const searchAPI = async () => { + const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1') + const res: Response = await client.get('/search', { q: word, resolve: false }) + if (res.data.accounts.length === 0) throw new Error('Empty') + commit(MUTATION_TYPES.APPEND_FILTERED_ACCOUNTS, res.data.accounts.map(account => account.acct)) + ipcRenderer.send('insert-cache-accounts', { + ownerID: rootState.TimelineSpace.account._id!, + accts: res.data.accounts.map(a => a.acct) + } as InsertAccountCache) + commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true) + commit(MUTATION_TYPES.CHANGE_START_INDEX, start) + commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word) + commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS) + return res.data.accounts + } + await Promise.all([searchCache(), searchAPI()]) }, suggestHashtag: async ({ commit, rootState }, wordStart: WordStart) => { commit(MUTATION_TYPES.CLEAR_FILTERED_HASHTAGS) @@ -127,14 +154,14 @@ const actions: ActionTree = { return new Promise(resolve => { const target = word.replace('#', '') ipcRenderer.once('response-get-cache-hashtags', (_, tags: Array) => { - const mached = tags.filter(tag => tag.tagName.includes(target)).map(tag => tag.tagName) - commit(MUTATION_TYPES.APPEND_FILTERED_HASHTAGS, mached) - if (mached.length === 0) throw new Error('Empty') + const matched = tags.map(tag => tag.tagName).filter(tag => tag.includes(target)) + if (matched.length === 0) throw new Error('Empty') + commit(MUTATION_TYPES.APPEND_FILTERED_HASHTAGS, matched) commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true) commit(MUTATION_TYPES.CHANGE_START_INDEX, start) commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word) commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS) - resolve(mached) + resolve(matched) }) ipcRenderer.send('get-cache-hashtags') }) @@ -143,8 +170,8 @@ const actions: ActionTree = { const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1') const res: Response = await client.get('/search', { q: word }) commit(MUTATION_TYPES.APPEND_FILTERED_HASHTAGS, res.data.hashtags) - ipcRenderer.send('insert-cache-hashtags', res.data.hashtags) if (res.data.hashtags.length === 0) throw new Error('Empty') + ipcRenderer.send('insert-cache-hashtags', res.data.hashtags) commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true) commit(MUTATION_TYPES.CHANGE_START_INDEX, start) commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word) diff --git a/src/types/cachedAccount.ts b/src/types/cachedAccount.ts new file mode 100644 index 00000000..7a82b299 --- /dev/null +++ b/src/types/cachedAccount.ts @@ -0,0 +1,5 @@ +export type CachedAccount = { + _id?: string + acct: string + owner_id: string +} diff --git a/src/types/insertAccountCache.ts b/src/types/insertAccountCache.ts new file mode 100644 index 00000000..c4d298f6 --- /dev/null +++ b/src/types/insertAccountCache.ts @@ -0,0 +1,4 @@ +export type InsertAccountCache = { + ownerID: string + accts: Array +}