diff --git a/src/main/database.ts b/src/main/database.ts index 0d82b6d485..ad8976b9d3 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -36,6 +36,18 @@ FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE)', } } ) + db.run( + 'CREATE TABLE IF NOT EXISTS hashtags(\ +id INTEGER PRIMARY KEY, \ +tag TEXT NOT NULL, \ +account_id INTEGER UNIQUE NOT NULL, \ +FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE)', + err => { + if (err) { + console.error('failed to create hashtags: ', err) + } + } + ) }) return db diff --git a/src/main/hashtags.ts b/src/main/hashtags.ts index 38e607902b..6d4b96fd78 100644 --- a/src/main/hashtags.ts +++ b/src/main/hashtags.ts @@ -1,44 +1,63 @@ -import Datastore from 'nedb' +import sqlite3 from 'sqlite3' import { LocalTag } from '~/src/types/localTag' -export default class Hashtags { - private db: Datastore +export const listTags = (db: sqlite3.Database, accountId: number): Promise> => { + return new Promise((resolve, reject) => { + db.all('SELECT * FROM hashtags WHERE account_id = ?', accountId, (err, rows) => { + if (err) { + reject(err) + } + resolve( + rows.map(r => ({ + id: r.id, + tagName: r.tag, + accountId: r.account_id + })) + ) + }) + }) +} - constructor(db: Datastore) { - this.db = db - this.db.ensureIndex({ fieldName: 'tagName', unique: true }, _ => {}) - } +export const insertTag = (db: sqlite3.Database, accountId: number, tag: string): Promise => { + return new Promise((resolve, reject) => { + db.serialize(() => { + db.run('BEGIN TRANSACTION') - listTags(): Promise> { - return new Promise((resolve, reject) => { - this.db.find({}, (err, docs) => { - if (err) return reject(err) - resolve(docs) - }) - }) - } + db.get('SELECT * FROM hashtags WHERE id = ? AND tag = ?', [accountId, tag], (err, row) => { + if (err) { + reject(err) + } + if (row) { + resolve({ + id: row.id, + tagName: row.tag, + accountId: row.account_id + }) + } - insertTag(tag: string): Promise { - return new Promise((resolve, reject) => { - this.db.insert({ tagName: tag }, (err, doc) => { - if (err) return reject(err) - resolve(doc) + db.run('INSERT INTO hashtags(tag, account_id) VALUES (?, ?)', [accountId, tag], function (err) { + if (err) { + reject(err) + } + db.run('COMMIT') + resolve({ + id: this.lastID, + tagName: tag, + accountId: accountId + }) + }) }) }) - } + }) +} - removeTag(localTag: LocalTag): Promise { - return new Promise((resolve, reject) => { - this.db.remove( - { - tagName: localTag.tagName - }, - { multi: true }, - (err, numRemoved) => { - if (err) return reject(err) - resolve(numRemoved) - } - ) +export const removeTag = (db: sqlite3.Database, tag: LocalTag): Promise => { + return new Promise((resolve, reject) => { + db.run('DELETE FROM hashtags WHERE id = ?', tag.id, err => { + if (err) { + reject(err) + } + resolve(null) }) - } + }) } diff --git a/src/main/index.ts b/src/main/index.ts index 7b7e3a6979..25bf0fbfe9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -35,7 +35,6 @@ import { getAccount, insertAccount, listAccounts } from './account' // import { StreamingURL, UserStreaming, DirectStreaming, LocalStreaming, PublicStreaming, ListStreaming, TagStreaming } from './websocket' import Preferences from './preferences' import Fonts from './fonts' -import Hashtags from './hashtags' import i18next from '~/src/config/i18n' import { i18n as I18n } from 'i18next' import Language, { LanguageType } from '../constants/language' @@ -54,6 +53,7 @@ import Settings from './settings' import { BaseSettings, Setting } from '~/src/types/setting' import { insertServer } from './server' import { LocalServer } from '~src/types/localServer' +import { insertTag, listTags, removeTag } from './hashtags' /** * Context menu @@ -125,12 +125,6 @@ const db = newDB(databasePath) const preferencesDBPath = process.env.NODE_ENV === 'production' ? userData + './db/preferences.json' : 'preferences.json' -const hashtagsDBPath = process.env.NODE_ENV === 'production' ? userData + '/db/hashtags.db' : 'hashtags.db' -const hashtagsDB = new Datastore({ - filename: hashtagsDBPath, - autoload: true -}) - const settingsDBPath = process.env.NODE_ENV === 'production' ? userData + './db/settings.json' : 'settings.json' /** @@ -1113,20 +1107,17 @@ ipcMain.handle('update-spellchecker-languages', async (_: IpcMainInvokeEvent, la }) // hashtag -ipcMain.handle('save-hashtag', async (_: IpcMainInvokeEvent, tag: string) => { - const hashtags = new Hashtags(hashtagsDB) - await hashtags.insertTag(tag) +ipcMain.handle('save-hashtag', async (_: IpcMainInvokeEvent, req: { accountId: number; tag: string }) => { + await insertTag(db, req.accountId, req.tag) }) -ipcMain.handle('list-hashtags', async (_: IpcMainInvokeEvent) => { - const hashtags = new Hashtags(hashtagsDB) - const tags = await hashtags.listTags() +ipcMain.handle('list-hashtags', async (_: IpcMainInvokeEvent, accountId: number) => { + const tags = await listTags(db, accountId) return tags }) ipcMain.handle('remove-hashtag', async (_: IpcMainInvokeEvent, tag: LocalTag) => { - const hashtags = new Hashtags(hashtagsDB) - await hashtags.removeTag(tag) + await removeTag(db, tag) }) // Fonts diff --git a/src/renderer/store/TimelineSpace/Contents/Hashtag.ts b/src/renderer/store/TimelineSpace/Contents/Hashtag.ts index f62fe8a2a4..865192866f 100644 --- a/src/renderer/store/TimelineSpace/Contents/Hashtag.ts +++ b/src/renderer/store/TimelineSpace/Contents/Hashtag.ts @@ -18,8 +18,8 @@ export type HashtagModuleState = HashtagModule & HashtagState const state = (): HashtagState => ({}) const actions: ActionTree = { - saveTag: async ({ dispatch }, tag: string) => { - await win.ipcRenderer.invoke('save-hashtag', tag) + saveTag: async ({ dispatch, rootState }, tag: string) => { + await win.ipcRenderer.invoke('save-hashtag', { accountId: rootState.TimelineSpace.account!.id, tag }) dispatch('TimelineSpace/SideMenu/listTags', {}, { root: true }) } } diff --git a/src/renderer/store/TimelineSpace/Contents/Hashtag/List.ts b/src/renderer/store/TimelineSpace/Contents/Hashtag/List.ts index 0d265a8639..f20658f9e8 100644 --- a/src/renderer/store/TimelineSpace/Contents/Hashtag/List.ts +++ b/src/renderer/store/TimelineSpace/Contents/Hashtag/List.ts @@ -4,7 +4,7 @@ import { RootState } from '@/store' import { MyWindow } from '~/src/types/global' import { toRaw } from 'vue' -const win = window as any as MyWindow +const win = (window as any) as MyWindow export type ListState = { tags: Array @@ -30,14 +30,14 @@ export const ACTION_TYPES = { } const actions: ActionTree = { - [ACTION_TYPES.LIST_TAGS]: async ({ commit }) => { - const tags: Array = await win.ipcRenderer.invoke('list-hashtags') + [ACTION_TYPES.LIST_TAGS]: async ({ rootState, commit }) => { + const tags: Array = await win.ipcRenderer.invoke('list-hashtags', rootState.TimelineSpace.account!.id) commit(MUTATION_TYPES.UPDATE_TAGS, tags) return tags }, [ACTION_TYPES.REMOVE_TAG]: async ({ dispatch }, tag: LocalTag) => { await win.ipcRenderer.invoke('remove-hashtag', toRaw(tag)) - dispatch('listTags') + dispatch(ACTION_TYPES.LIST_TAGS) dispatch('TimelineSpace/SideMenu/listTags', {}, { root: true }) return 'deleted' } diff --git a/src/renderer/store/TimelineSpace/SideMenu.ts b/src/renderer/store/TimelineSpace/SideMenu.ts index c3e4f90469..9a24e415dc 100644 --- a/src/renderer/store/TimelineSpace/SideMenu.ts +++ b/src/renderer/store/TimelineSpace/SideMenu.ts @@ -223,8 +223,10 @@ const actions: ActionTree = { commit(MUTATION_TYPES.CHANGE_COLLAPSE, value) return value }, - [ACTION_TYPES.LIST_TAGS]: async ({ commit }) => { - const tags: Array = await win.ipcRenderer.invoke('list-hashtags') + [ACTION_TYPES.LIST_TAGS]: async ({ rootState, commit }) => { + // TODO: Can not get account because too early. + // It should be executed after TimelineSpace obtain local account. + const tags: Array = await win.ipcRenderer.invoke('list-hashtags', rootState.TimelineSpace.account!.id) commit(MUTATION_TYPES.UPDATE_TAGS, tags) return tags } diff --git a/src/types/localTag.ts b/src/types/localTag.ts index 8c916fdd1e..c9870113ff 100644 --- a/src/types/localTag.ts +++ b/src/types/localTag.ts @@ -1,4 +1,5 @@ export type LocalTag = { + id: number tagName: string - _id?: string + accountId: number }