Merge pull request #3041 from h3poteto/feat/setting-store

Change storage of Settings to json-storage
This commit is contained in:
AkiraFukushima 2022-01-01 20:41:07 +09:00 committed by GitHub
commit 76437ce599
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 226 additions and 232 deletions

View File

@ -3,7 +3,6 @@ import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import { ipcMain, ipcRenderer } from '~/spec/mock/electron'
import TimelineSpace, { TimelineSpaceState, blankAccount } from '~/src/renderer/store/TimelineSpace'
import unreadSettings from '~/src/constants/unreadNotification'
import { MyWindow } from '~/src/types/global'
;((window as any) as MyWindow).ipcRenderer = ipcRenderer
@ -79,10 +78,12 @@ const state = (): TimelineSpaceState => {
loading: false,
emojis: [],
tootMax: 500,
unreadNotification: {
direct: true,
local: true,
public: true
timelineSetting: {
unreadNotification: {
direct: true,
local: true,
public: true
}
},
sns: 'mastodon',
filters: []
@ -246,34 +247,27 @@ describe('TimelineSpace', () => {
describe('loadUnreadNotification', () => {
describe('success', () => {
it('should be updated', async () => {
ipcMain.handle('get-unread-notification', () => {
ipcMain.handle('get-account-setting', () => {
return {
accountID: 'sample',
timeline: {
unreadNotification: {
direct: false,
local: false,
public: false
}
}
}
})
await store.dispatch('TimelineSpace/loadTimelineSetting')
expect(store.state.TimelineSpace.timelineSetting).toEqual({
unreadNotification: {
direct: false,
local: false,
public: false
}
})
await store.dispatch('TimelineSpace/loadUnreadNotification')
expect(store.state.TimelineSpace.unreadNotification).toEqual({
direct: false,
local: false,
public: false
})
ipcMain.removeHandler('get-unread-notification')
})
})
describe('error', () => {
it('should be set default', async () => {
ipcMain.handle('get-unread-notification', async () => {
throw new Error()
})
await store.dispatch('TimelineSpace/loadUnreadNotification')
expect(store.state.TimelineSpace.unreadNotification).toEqual({
direct: unreadSettings.Direct.default,
local: unreadSettings.Local.default,
public: unreadSettings.Public.default
})
ipcMain.removeHandler('get-unread-notification')
ipcMain.removeHandler('get-account-setting')
})
})
})

View File

@ -1,5 +1,5 @@
import TimelineSpace, { TimelineSpaceState, blankAccount, MUTATION_TYPES } from '~/src/renderer/store/TimelineSpace'
import unreadSettings from '~/src/constants/unreadNotification'
import { Base } from '~/src/constants/initializer/setting'
describe('TimelineSpace', () => {
describe('mutations', () => {
@ -11,11 +11,7 @@ describe('TimelineSpace', () => {
loading: false,
emojis: [],
tootMax: 500,
unreadNotification: {
direct: unreadSettings.Direct.default,
local: unreadSettings.Local.default,
public: unreadSettings.Public.default
},
timelineSetting: Base.timeline,
sns: 'mastodon',
filters: []
}

View File

@ -0,0 +1,16 @@
import { Setting, Timeline, UnreadNotification } from '~/src/types/setting'
const unreadNotification: UnreadNotification = {
direct: false,
local: true,
public: false
}
const timeline: Timeline = {
unreadNotification: unreadNotification
}
export const Base: Setting = {
accountID: '',
timeline: timeline
}

View File

@ -1,13 +0,0 @@
export type UnreadNotificationType = {
default: boolean
}
export type UnreadNotificationList = {
Direct: UnreadNotificationType,
Local: UnreadNotificationType,
Public: UnreadNotificationType
}
declare let u: UnreadNotificationList
export default u

View File

@ -1,11 +0,0 @@
export default {
Direct: {
default: false
},
Local: {
default: true
},
Public: {
default: false
}
}

View File

@ -36,13 +36,11 @@ import { StreamingURL, UserStreaming, DirectStreaming, LocalStreaming, PublicStr
import Preferences from './preferences'
import Fonts from './fonts'
import Hashtags from './hashtags'
import UnreadNotification from './unreadNotification'
import i18next from '~/src/config/i18n'
import { i18n as I18n } from 'i18next'
import Language, { LanguageType } from '../constants/language'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalTag } from '~/src/types/localTag'
import { UnreadNotification as UnreadNotificationConfig } from '~/src/types/unreadNotification'
import { Notify } from '~/src/types/notify'
import { StreamingError } from '~/src/errors/streamingError'
import HashtagCache from './cache/hashtag'
@ -56,6 +54,8 @@ import { Menu as MenuPreferences } from '~/src/types/preference'
import { LocalMarker } from '~/src/types/localMarker'
import Marker from './marker'
import newDB from './database'
import Settings from './settings'
import { BaseSettings, Setting } from '~/src/types/setting'
/**
* Context menu
@ -136,9 +136,7 @@ const hashtagsDB = new Datastore({
autoload: true
})
const unreadNotificationDBPath = process.env.NODE_ENV === 'production' ? userData + '/db/unread_notification.db' : 'unread_notification.db'
const unreadNotification = new UnreadNotification(unreadNotificationDBPath)
unreadNotification.initialize().catch((err: Error) => log.error(err))
const settingsDBPath = process.env.NODE_ENV === 'production' ? userData + './db/settings.json' : 'settings.json'
const preferencesDBPath = process.env.NODE_ENV === 'production' ? userData + './db/preferences.json' : 'preferences.json'
@ -1215,18 +1213,24 @@ ipcMain.handle('list-fonts', async (_: IpcMainInvokeEvent) => {
return list
})
// Unread notifications
ipcMain.handle('get-unread-notification', async (_: IpcMainInvokeEvent, accountID: string) => {
const doc = await unreadNotification.findOne({
accountID: accountID
})
return doc
})
// Settings
ipcMain.handle(
'get-account-setting',
async (_: IpcMainInvokeEvent, accountID: string): Promise<Setting> => {
const settings = new Settings(settingsDBPath)
const setting = await settings.get(accountID)
return setting
}
)
ipcMain.handle('update-unread-notification', async (_: IpcMainInvokeEvent, config: UnreadNotificationConfig) => {
const { accountID } = config
await unreadNotification.insertOrUpdate(accountID!, config)
})
ipcMain.handle(
'update-account-setting',
async (_: IpcMainInvokeEvent, setting: Setting): Promise<BaseSettings> => {
const settings = new Settings(settingsDBPath)
const res = await settings.update(setting)
return res
}
)
// Cache
ipcMain.handle('get-cache-hashtags', async (_: IpcMainInvokeEvent) => {

81
src/main/settings.ts Normal file
View File

@ -0,0 +1,81 @@
import storage from 'electron-json-storage'
import log from 'electron-log'
import objectAssignDeep from 'object-assign-deep'
import { BaseSettings, Setting } from '~/src/types/setting'
import { Base } from '~/src/constants/initializer/setting'
import { isEmpty } from 'lodash'
export default class Settings {
private path: string
constructor(path: string) {
this.path = path
}
public async load(): Promise<BaseSettings> {
try {
const settings = await this._get()
if (isEmpty(settings)) {
return []
}
return settings
} catch (err) {
log.error(err)
return []
}
}
public async get(accountID: string): Promise<Setting> {
const current = await this.load()
const find: Setting | undefined = current.find(d => {
return d.accountID === accountID
})
if (find) {
return find
}
const base = objectAssignDeep({}, Base, {
accountID: accountID
})
return base
}
private _get(): Promise<BaseSettings> {
return new Promise((resolve, reject) => {
storage.get(this.path, (err, data) => {
if (err) return reject(err)
return resolve(data as BaseSettings)
})
})
}
private _save(data: BaseSettings): Promise<BaseSettings> {
return new Promise((resolve, reject) => {
storage.set(this.path, data, err => {
if (err) return reject(err)
return resolve(data)
})
})
}
public async update(obj: Setting): Promise<BaseSettings> {
const current = await this.load()
const find = current.find(d => {
return d.accountID === obj.accountID
})
if (find) {
const data = current.map(d => {
if (d.accountID !== obj.accountID) {
return d
}
const newData = objectAssignDeep({}, d, obj)
return newData
})
const result = await this._save(data)
return result
} else {
const data = current.concat([obj])
const result = await this._save(data)
return result
}
}
}

View File

@ -1,61 +0,0 @@
import { isEmpty } from 'lodash'
import Datastore from 'nedb'
import { UnreadNotification as Config } from '~/src/types/unreadNotification'
export default class UnreadNotification {
private db: Datastore
constructor (path: string) {
this.db = new Datastore({
filename: path,
autoload: true
})
}
async initialize () {
await this.updateUnique()
}
updateUnique () {
return new Promise((resolve, reject) => {
// At first, remove old index.
this.db.removeIndex('accountID', (err) => {
if (err) reject(err)
// Add unique index.
this.db.ensureIndex({ fieldName: 'accountID', unique: true, sparse: true }, (err) => {
if (err) reject(err)
resolve({})
})
})
})
}
insertOrUpdate (accountID: string, config: Config): Promise<number> {
return new Promise((resolve, reject) => {
this.db.update(
{
accountID: accountID
},
config,
{
upsert: true
},
(err, num) => {
if (err) return reject(err)
resolve(num)
})
})
}
findOne (obj: any): Promise<Config> {
return new Promise((resolve, reject) => {
this.db.findOne<Config>(obj, (err, doc) => {
if (err) return reject(err)
if (isEmpty(doc)) return reject(new EmptyRecordError('empty'))
resolve(doc)
})
})
}
}
class EmptyRecordError extends Error {}

View File

@ -1,21 +1,21 @@
<template>
<div id="timeline">
<h2>{{ $t('settings.timeline.title') }}</h2>
<el-form class="unread-notification section" size="medium" label-position="right" label-width="250px">
<h3>{{ $t('settings.timeline.unread_notification.title') }}</h3>
<p class="description">{{ $t('settings.timeline.unread_notification.description') }}</p>
<div id="timeline">
<h2>{{ $t('settings.timeline.title') }}</h2>
<el-form class="unread-notification section" size="medium" label-position="right" label-width="250px">
<h3>{{ $t('settings.timeline.unread_notification.title') }}</h3>
<p class="description">{{ $t('settings.timeline.unread_notification.description') }}</p>
<el-form-item for="direct" :label="$t('settings.timeline.unread_notification.direct')">
<el-switch v-model="direct" id="direct" />
</el-form-item>
<el-form-item for="local" :label="$t('settings.timeline.unread_notification.local')">
<el-switch v-model="local" id="local" />
</el-form-item>
<el-form-item for="public" :label="$t('settings.timeline.unread_notification.public')">
<el-switch v-model="public" id="public" />
</el-form-item>
</el-form>
</div>
<el-form-item for="direct" :label="$t('settings.timeline.unread_notification.direct')">
<el-switch v-model="direct" id="direct" />
</el-form-item>
<el-form-item for="local" :label="$t('settings.timeline.unread_notification.local')">
<el-switch v-model="local" id="local" />
</el-form-item>
<el-form-item for="public" :label="$t('settings.timeline.unread_notification.public')">
<el-switch v-model="public" id="public" />
</el-form-item>
</el-form>
</div>
</template>
<script>
@ -23,38 +23,38 @@ export default {
name: 'Timeline',
computed: {
direct: {
get () {
return this.$store.state.Settings.Timeline.unreadNotification.direct
get() {
return this.$store.state.Settings.Timeline.setting.unreadNotification.direct
},
set (value) {
set(value) {
this.$store.dispatch('Settings/Timeline/changeUnreadNotification', {
direct: value
})
}
},
local: {
get () {
return this.$store.state.Settings.Timeline.unreadNotification.local
get() {
return this.$store.state.Settings.Timeline.setting.unreadNotification.local
},
set (value) {
set(value) {
this.$store.dispatch('Settings/Timeline/changeUnreadNotification', {
local: value
})
}
},
public: {
get () {
return this.$store.state.Settings.Timeline.unreadNotification.public
get() {
return this.$store.state.Settings.Timeline.setting.unreadNotification.public
},
set (value) {
set(value) {
this.$store.dispatch('Settings/Timeline/changeUnreadNotification', {
public: value
})
}
}
},
async created () {
await this.$store.dispatch('Settings/Timeline/loadUnreadNotification')
async created() {
await this.$store.dispatch('Settings/Timeline/loadTimelineSetting')
}
}
</script>

View File

@ -59,7 +59,7 @@ export default {
openSideBar: state => state.TimelineSpace.Contents.SideBar.openSideBar,
backgroundColor: state => state.App.theme.background_color,
startReload: state => state.TimelineSpace.HeaderMenu.reload,
unreadNotification: state => state.TimelineSpace.unreadNotification
unreadNotification: state => state.TimelineSpace.timelineSetting.unreadNotification
}),
...mapGetters('TimelineSpace/Contents/Public', ['filters']),
...mapGetters('TimelineSpace/Modals', ['modalOpened']),

View File

@ -1,55 +1,46 @@
import unreadSettings from '~/src/constants/unreadNotification'
import { Module, MutationTree, ActionTree } from 'vuex'
import { RootState } from '@/store'
import { UnreadNotification } from '~/src/types/unreadNotification'
import { MyWindow } from '~/src/types/global'
import { Setting, UnreadNotification, Timeline as TimelineSetting } from '~src/types/setting'
import { Base } from '~/src/constants/initializer/setting'
const win = (window as any) as MyWindow
export type TimelineState = {
unreadNotification: UnreadNotification
setting: TimelineSetting
}
const state = (): TimelineState => ({
unreadNotification: {
direct: unreadSettings.Direct.default,
local: unreadSettings.Local.default,
public: unreadSettings.Public.default
}
setting: Base.timeline
})
export const MUTATION_TYPES = {
UPDATE_UNREAD_NOTIFICATION: 'updateUnreadNotification'
UPDATE_TIMELINE_SETTING: 'updateTimelineSetting'
}
const mutations: MutationTree<TimelineState> = {
[MUTATION_TYPES.UPDATE_UNREAD_NOTIFICATION]: (state, settings: UnreadNotification) => {
state.unreadNotification = settings
[MUTATION_TYPES.UPDATE_TIMELINE_SETTING]: (state, setting: TimelineSetting) => {
state.setting = setting
}
}
const actions: ActionTree<TimelineState, RootState> = {
loadUnreadNotification: async ({ commit, rootState }): Promise<boolean> => {
try {
const settings: UnreadNotification = await win.ipcRenderer.invoke('get-unread-notification', rootState.Settings.accountID)
commit(MUTATION_TYPES.UPDATE_UNREAD_NOTIFICATION, settings)
return true
} catch (err) {
const settings: UnreadNotification = {
direct: unreadSettings.Direct.default,
local: unreadSettings.Local.default,
public: unreadSettings.Public.default
}
commit(MUTATION_TYPES.UPDATE_UNREAD_NOTIFICATION, settings)
return false
}
loadTimelineSetting: async ({ commit, rootState }): Promise<boolean> => {
const setting: Setting = await win.ipcRenderer.invoke('get-account-setting', rootState.Settings.accountID)
commit(MUTATION_TYPES.UPDATE_TIMELINE_SETTING, setting.timeline)
return true
},
changeUnreadNotification: async ({ dispatch, state, rootState }, timeline: { key: boolean }): Promise<boolean> => {
const settings: UnreadNotification = Object.assign({}, state.unreadNotification, timeline, {
accountID: rootState.Settings.accountID
const unread: UnreadNotification = Object.assign({}, state.setting.unreadNotification, timeline)
const tl: TimelineSetting = Object.assign({}, state.setting, {
unreadNotification: unread
})
await win.ipcRenderer.invoke('update-unread-notification', settings)
dispatch('loadUnreadNotification')
const setting: Setting = {
accountID: rootState.Settings.accountID!,
timeline: tl
}
await win.ipcRenderer.invoke('update-account-setting', setting)
dispatch('loadTimelineSetting')
return true
}
}

View File

@ -3,14 +3,14 @@ import SideMenu, { SideMenuState } from './TimelineSpace/SideMenu'
import HeaderMenu, { HeaderMenuState } from './TimelineSpace/HeaderMenu'
import Modals, { ModalsModuleState } from './TimelineSpace/Modals'
import Contents, { ContentsModuleState } from './TimelineSpace/Contents'
import unreadSettings from '~/src/constants/unreadNotification'
import { Module, MutationTree, ActionTree } from 'vuex'
import { LocalAccount } from '~/src/types/localAccount'
import { RootState } from '@/store'
import { UnreadNotification } from '~/src/types/unreadNotification'
import { AccountLoadError } from '@/errors/load'
import { TimelineFetchError } from '@/errors/fetch'
import { MyWindow } from '~/src/types/global'
import { Timeline, Setting } from '~src/types/setting'
import { Base } from '~/src/constants/initializer/setting'
const win = (window as any) as MyWindow
@ -20,7 +20,7 @@ export type TimelineSpaceState = {
loading: boolean
emojis: Array<Entity.Emoji>
tootMax: number
unreadNotification: UnreadNotification
timelineSetting: Timeline
sns: 'mastodon' | 'pleroma' | 'misskey'
filters: Array<Entity.Filter>
}
@ -45,11 +45,7 @@ const state = (): TimelineSpaceState => ({
loading: false,
emojis: [],
tootMax: 500,
unreadNotification: {
direct: unreadSettings.Direct.default,
local: unreadSettings.Local.default,
public: unreadSettings.Public.default
},
timelineSetting: Base.timeline,
sns: 'mastodon',
filters: []
})
@ -60,7 +56,7 @@ export const MUTATION_TYPES = {
CHANGE_LOADING: 'changeLoading',
UPDATE_EMOJIS: 'updateEmojis',
UPDATE_TOOT_MAX: 'updateTootMax',
UPDATE_UNREAD_NOTIFICATION: 'updateUnreadNotification',
UPDATE_TIMELINE_SETTING: 'updateTimelineSetting',
CHANGE_SNS: 'changeSNS',
UPDATE_FILTERS: 'updateFilters'
}
@ -85,8 +81,8 @@ const mutations: MutationTree<TimelineSpaceState> = {
state.tootMax = 500
}
},
[MUTATION_TYPES.UPDATE_UNREAD_NOTIFICATION]: (state, settings: UnreadNotification) => {
state.unreadNotification = settings
[MUTATION_TYPES.UPDATE_TIMELINE_SETTING]: (state, setting: Timeline) => {
state.timelineSetting = setting
},
[MUTATION_TYPES.CHANGE_SNS]: (state, sns: 'mastodon' | 'pleroma' | 'misskey') => {
state.sns = sns
@ -109,7 +105,7 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
dispatch('TimelineSpace/SideMenu/fetchLists', account, { root: true })
dispatch('TimelineSpace/SideMenu/fetchFollowRequests', account, { root: true })
dispatch('TimelineSpace/SideMenu/confirmTimelines', account, { root: true })
await dispatch('loadUnreadNotification', accountId)
await dispatch('loadTimelineSetting', accountId)
await dispatch('fetchFilters')
commit(MUTATION_TYPES.CHANGE_LOADING, false)
await dispatch('fetchContentsTimelines').catch(_ => {
@ -204,17 +200,9 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
commit(MUTATION_TYPES.UPDATE_TOOT_MAX, res.data.max_toot_chars)
return true
},
loadUnreadNotification: async ({ commit }, accountID: string) => {
try {
const settings: UnreadNotification = await win.ipcRenderer.invoke('get-unread-notification', accountID)
commit(MUTATION_TYPES.UPDATE_UNREAD_NOTIFICATION, settings)
} catch (err) {
commit(MUTATION_TYPES.UPDATE_UNREAD_NOTIFICATION, {
direct: unreadSettings.Direct.default,
local: unreadSettings.Local.default,
public: unreadSettings.Public.default
} as UnreadNotification)
}
loadTimelineSetting: async ({ commit }, accountID: string) => {
const setting: Setting = await win.ipcRenderer.invoke('get-account-setting', accountID)
commit(MUTATION_TYPES.UPDATE_TIMELINE_SETTING, setting.timeline)
},
fetchContentsTimelines: async ({ dispatch, state }) => {
dispatch('TimelineSpace/Contents/changeLoading', true, { root: true })
@ -224,13 +212,13 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
await dispatch('TimelineSpace/Contents/Notifications/fetchNotifications', {}, { root: true })
await dispatch('TimelineSpace/Contents/Mentions/fetchMentions', {}, { root: true })
if (state.unreadNotification.direct) {
if (state.timelineSetting.unreadNotification.direct) {
await dispatch('TimelineSpace/Contents/DirectMessages/fetchTimeline', {}, { root: true })
}
if (state.unreadNotification.local) {
if (state.timelineSetting.unreadNotification.local) {
await dispatch('TimelineSpace/Contents/Local/fetchLocalTimeline', {}, { root: true })
}
if (state.unreadNotification.public) {
if (state.timelineSetting.unreadNotification.public) {
await dispatch('TimelineSpace/Contents/Public/fetchPublicTimeline', {}, { root: true })
}
},
@ -244,24 +232,24 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
},
bindStreamings: ({ dispatch, state }) => {
dispatch('bindUserStreaming')
if (state.unreadNotification.direct) {
if (state.timelineSetting.unreadNotification.direct) {
dispatch('bindDirectMessagesStreaming')
}
if (state.unreadNotification.local) {
if (state.timelineSetting.unreadNotification.local) {
dispatch('bindLocalStreaming')
}
if (state.unreadNotification.public) {
if (state.timelineSetting.unreadNotification.public) {
dispatch('bindPublicStreaming')
}
},
startStreamings: ({ dispatch, state }) => {
if (state.unreadNotification.direct) {
if (state.timelineSetting.unreadNotification.direct) {
dispatch('startDirectMessagesStreaming')
}
if (state.unreadNotification.local) {
if (state.timelineSetting.unreadNotification.local) {
dispatch('startLocalStreaming')
}
if (state.unreadNotification.public) {
if (state.timelineSetting.unreadNotification.public) {
dispatch('startPublicStreaming')
}
},
@ -429,13 +417,13 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
commit('TimelineSpace/Contents/Home/updateToot', status, { root: true })
commit('TimelineSpace/Contents/Notifications/updateToot', status, { root: true })
commit('TimelineSpace/Contents/Mentions/updateToot', status, { root: true })
if (state.unreadNotification.direct) {
if (state.timelineSetting.unreadNotification.direct) {
commit('TimelineSpace/Contents/DirectMessages/updateToot', status, { root: true })
}
if (state.unreadNotification.local) {
if (state.timelineSetting.unreadNotification.local) {
commit('TimelineSpace/Contents/Local/updateToot', status, { root: true })
}
if (state.unreadNotification.public) {
if (state.timelineSetting.unreadNotification.public) {
commit('TimelineSpace/Contents/Public/updateToot', status, { root: true })
}
return true

15
src/types/setting.ts Normal file
View File

@ -0,0 +1,15 @@
export type UnreadNotification = {
direct: boolean
local: boolean
public: boolean
}
export type Timeline = {
unreadNotification: UnreadNotification
}
export type Setting = {
accountID: string
timeline: Timeline
}
export type BaseSettings = Array<Setting>

View File

@ -1,6 +0,0 @@
export type UnreadNotification = {
accountID?: string,
direct: boolean,
local: boolean,
public: boolean
}