Merge pull request #3967 from h3poteto/iss-2500/account

refs #2500 Change account database to sqlite3
This commit is contained in:
AkiraFukushima 2023-01-13 19:14:16 +09:00 committed by GitHub
commit ddb76ca20e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
145 changed files with 2631 additions and 10610 deletions

View File

@ -108,6 +108,7 @@
"rc": "^1.2.7",
"sanitize-html": "^2.8.1",
"simplayer": "0.0.8",
"sqlite3": "^5.1.4",
"system-font-families": "^0.6.0",
"tunnel-agent": "^0.6.0",
"unicode-emoji-json": "^0.4.0",

View File

@ -3,7 +3,7 @@ import { createStore, Store } from 'vuex'
import { ipcMain, ipcRenderer } from '~/spec/mock/electron'
import GlobalHeader, { GlobalHeaderState } from '~/src/renderer/store/GlobalHeader'
import { MyWindow } from '~/src/types/global'
;(window as any as MyWindow).ipcRenderer = ipcRenderer
;((window as any) as MyWindow).ipcRenderer = ipcRenderer
const state = (): GlobalHeaderState => {
return {
@ -58,21 +58,6 @@ describe('GlobalHeader', () => {
})
})
describe('refreshAccounts', () => {
beforeEach(() => {
ipcMain.handle('refresh-accounts', () => {
return ['accounts']
})
})
afterEach(() => {
ipcMain.removeHandler('refresh-accounts')
})
it('should be refreshed', async () => {
await store.dispatch('GlobalHeader/refreshAccounts')
expect(store.state.GlobalHeader.accounts).toEqual(['accounts'])
})
})
describe('removeShortcutEvents', () => {
it('should be removed', async () => {
const removed = await store.dispatch('GlobalHeader/removeShortcutEvents')

View File

@ -1,9 +1,9 @@
import { createStore, Store } from 'vuex'
import { ipcMain, ipcRenderer } from '~/spec/mock/electron'
import { ipcRenderer } from '~/spec/mock/electron'
import Login, { LoginState } from '@/store/Login'
import { MyWindow } from '~/src/types/global'
import { RootState } from '@/store'
;(window as any as MyWindow).ipcRenderer = ipcRenderer
;((window as any) as MyWindow).ipcRenderer = ipcRenderer
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
@ -13,8 +13,10 @@ jest.mock('megalodon', () => ({
const state = (): LoginState => {
return {
selectedInstance: null,
domain: null,
searching: false,
server: null,
appData: null,
sns: 'mastodon'
}
}
@ -47,34 +49,10 @@ describe('Login', () => {
})
})
describe('fetchLogin', () => {
describe('error', () => {
it('should return error', async () => {
ipcMain.handle('get-auth-url', () => {
throw new Error()
})
await store.dispatch('Login/fetchLogin', 'pleroma.io').catch((err: Error) => {
expect(err instanceof Error).toEqual(true)
})
ipcMain.removeHandler('get-auth-url')
})
})
describe('success', () => {
it('should return url', async () => {
ipcMain.handle('get-auth-url', () => {
return 'http://example.com/auth'
})
const url = await store.dispatch('Login/fetchLogin', 'pleroma.io')
expect(url).toEqual('http://example.com/auth')
ipcMain.removeHandler('get-auth-url')
})
})
})
describe('pageBack', () => {
it('should reset instance', () => {
store.dispatch('Login/pageBack')
expect(store.state.Login.selectedInstance).toEqual(null)
expect(store.state.Login.domain).toEqual(null)
})
})
@ -82,7 +60,7 @@ describe('Login', () => {
it('should change instance', async () => {
const result = await store.dispatch('Login/confirmInstance', 'pleroma.io')
expect(result).toEqual(true)
expect(store.state.Login.selectedInstance).toEqual('pleroma.io')
expect(store.state.Login.domain).toEqual('pleroma.io')
})
})
})

View File

@ -1,157 +0,0 @@
import { createStore, Store } from 'vuex'
import { ipcMain, ipcRenderer } from '~/spec/mock/electron'
import Account, { AccountState } from '@/store/Preferences/Account'
import { LocalAccount } from '~/src/types/localAccount'
import { MyWindow } from '~/src/types/global'
import { RootState } from '@/store'
;(window as any as MyWindow).ipcRenderer = ipcRenderer
const account: LocalAccount = {
_id: 'sample',
baseURL: 'http://example.com',
domain: 'example.com',
clientId: 'hoge',
clientSecret: 'hogehoge',
accessToken: null,
refreshToken: null,
username: null,
accountId: null,
avatar: null,
order: 1
}
const state = (): AccountState => {
return {
accounts: [],
accountLoading: false
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Account.actions,
mutations: Account.mutations
}
}
const preferencesStore = () => ({
namespaced: true,
modules: {
Account: initStore()
}
})
describe('Account', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
Preferences: preferencesStore()
}
})
})
describe('loadAccounts', () => {
it('error', async () => {
ipcMain.handle('list-accounts', async () => {
throw new Error()
})
await store.dispatch('Preferences/Account/loadAccounts').catch((err: Error) => {
expect(err instanceof Error).toEqual(true)
})
ipcMain.removeHandler('list-accounts')
})
it('success', async () => {
ipcMain.handle('list-accounts', () => {
return [account]
})
await store.dispatch('Preferences/Account/loadAccounts')
expect(store.state.Preferences.Account.accounts).toEqual([account])
ipcMain.removeHandler('list-accounts')
})
})
describe('removeAccount', () => {
it('error', async () => {
ipcMain.handle('remove-account', async () => {
throw new Error()
})
await store.dispatch('Preferences/Account/removeAccount', account).catch((err: Error) => {
expect(err instanceof Error).toEqual(true)
})
ipcMain.removeHandler('remove-account')
})
it('success', async () => {
ipcMain.handle('remove-account', () => {
return true
})
const res = await store.dispatch('Preferences/Account/removeAccount', account)
expect(res).toEqual(undefined)
ipcMain.removeHandler('remove-account')
})
})
describe('forwardAccount', () => {
it('error', async () => {
ipcMain.handle('forward-account', async () => {
throw new Error()
})
await store.dispatch('Preferences/Account/forwardAccount', account).catch((err: Error) => {
expect(err instanceof Error).toEqual(true)
})
ipcMain.removeHandler('forward-account')
})
it('success', async () => {
ipcMain.handle('forward-account', () => {
return {}
})
const res = await store.dispatch('Preferences/Account/forwardAccount', account)
expect(res).toEqual(undefined)
ipcMain.removeHandler('forward-account')
})
})
describe('backwardAccount', () => {
it('error', async () => {
ipcMain.handle('backward-account', () => {
throw new Error()
})
await store.dispatch('Preferences/Account/backwardAccount', account).catch((err: Error) => {
expect(err instanceof Error).toEqual(true)
})
ipcMain.removeHandler('backward-account')
})
it('success', async () => {
ipcMain.handle('backward-account', () => {
return {}
})
const res = await store.dispatch('Preferences/Account/backwardAccount', account)
expect(res).toEqual(undefined)
ipcMain.removeHandler('backward-account')
})
})
describe('removeAllAccounts', () => {
it('error', async () => {
ipcMain.handle('remove-all-accounts', () => {
throw new Error()
})
await store.dispatch('Preferences/Account/removeAllAccounts', account).catch((err: Error) => {
expect(err instanceof Error).toEqual(true)
})
ipcMain.removeHandler('remove-all-accounts')
})
it('success', async () => {
ipcMain.handle('remove-all-accounts', () => {
return {}
})
const res = await store.dispatch('Preferences/Account/removeAllAccounts', account)
expect(res).toEqual(undefined)
ipcMain.removeHandler('remove-all-accounts')
})
})
})

View File

@ -1,286 +0,0 @@
import { RootState } from '@/store'
import { Entity, Response } from 'megalodon'
import { createStore, Store } from 'vuex'
import { ipcMain, ipcRenderer } from '~/spec/mock/electron'
import TimelineSpace, { TimelineSpaceState, blankAccount } from '~/src/renderer/store/TimelineSpace'
import { MyWindow } from '~/src/types/global'
;((window as any) as MyWindow).ipcRenderer = ipcRenderer
const emacsEmoji: Entity.Emoji = {
shortcode: 'emacs',
url: 'http://example.com/emacs',
static_url: 'http://example.com/emacs',
visible_in_picker: true
}
const rubyEmoji: Entity.Emoji = {
shortcode: 'ruby',
url: 'http://example.com/ruby',
static_url: 'http://example.com/ruby',
visible_in_picker: true
}
const mockedInstance: Entity.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 mockClient = {
getInstance: () => {
return new Promise<Response<Entity.Instance>>(resolve => {
const res: Response<Entity.Instance> = {
data: mockedInstance,
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
},
getInstanceCustomEmojis: () => {
return new Promise<Response<Array<Entity.Emoji>>>(resolve => {
const res: Response<Array<Entity.Emoji>> = {
data: [emacsEmoji, rubyEmoji],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
detector: jest.fn(() => 'pleroma'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const state = (): TimelineSpaceState => {
return {
account: blankAccount,
bindingAccount: null,
loading: false,
emojis: [],
tootMax: 500,
timelineSetting: {
unreadNotification: {
direct: true,
local: true,
public: true
},
useMarker: {
home: false,
notifications: true
}
},
sns: 'mastodon',
filters: []
}
}
const homeStore = {
namespaced: true,
actions: {
fetchTimeline: jest.fn()
}
}
const notificationStore = {
namespaced: true,
actions: {
fetchNotifications: jest.fn()
}
}
const DMStore = {
namespaced: true,
actions: {
fetchTimeline: jest.fn()
}
}
const LocalStore = {
namespaced: true,
actions: {
fetchLocalTimeline: jest.fn()
}
}
const PublicStore = {
namespaced: true,
actions: {
fetchPublicTimeline: jest.fn()
}
}
const MentionStore = {
namespaced: true,
actions: {
fetchMentions: jest.fn()
}
}
const contentsStore = {
namespaced: true,
modules: {
Home: homeStore,
Notifications: notificationStore,
DirectMessages: DMStore,
Local: LocalStore,
Public: PublicStore,
Mentions: MentionStore
},
actions: {
changeLoading: jest.fn()
}
}
const initStore = () => {
return {
namespaced: true,
modules: {
Contents: contentsStore
},
state: state(),
actions: TimelineSpace.actions,
mutations: TimelineSpace.mutations
}
}
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('TimelineSpace', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: initStore(),
App: appState
}
})
})
describe('localAccount', () => {
describe('account already exists', () => {
beforeEach(() => {
ipcMain.handle('get-local-account', () => {
return {
username: 'test'
}
})
})
afterEach(() => {
ipcMain.removeHandler('get-local-account')
})
it('should be updated', async () => {
await store.dispatch('TimelineSpace/localAccount', 1)
expect(store.state.TimelineSpace.account.username).toEqual('test')
})
})
describe('account does not exist', () => {
beforeEach(() => {
ipcMain.handle('get-local-account', () => {
return {}
})
ipcMain.handle('update-account', () => {
return {
username: 'fetched'
}
})
})
afterEach(() => {
ipcMain.removeHandler('get-local-account')
ipcMain.removeHandler('update-account')
})
it('should be fetched', async () => {
await store.dispatch('TimelineSpace/localAccount', 1)
expect(store.state.TimelineSpace.account.username).toEqual('fetched')
})
})
})
describe('detectSNS', () => {
describe('API is pleroma', () => {
it('should be detected', async () => {
await store.dispatch('TimelineSpace/detectSNS')
expect(store.state.TimelineSpace.sns).toEqual('pleroma')
})
})
})
describe('fetchEmojis', () => {
it('should be updated', async () => {
await store.dispatch('TimelineSpace/fetchEmojis', {})
expect(store.state.TimelineSpace.emojis).toEqual([emacsEmoji, rubyEmoji])
})
})
describe('fetchInstance', () => {
it('should be updated', async () => {
await store.dispatch('TimelineSpace/fetchInstance', {})
expect(store.state.TimelineSpace.tootMax).toEqual(mockedInstance.max_toot_chars)
})
})
describe('loadUnreadNotification', () => {
describe('success', () => {
it('should be updated', async () => {
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
}
})
ipcMain.removeHandler('get-account-setting')
})
})
})
describe('fetchContentsTimelines', () => {
it('should be called', async () => {
await store.dispatch('TimelineSpace/fetchContentsTimelines', {})
expect(homeStore.actions.fetchTimeline).toHaveBeenCalled()
expect(notificationStore.actions.fetchNotifications).toHaveBeenCalled()
expect(DMStore.actions.fetchTimeline).toHaveBeenCalled()
expect(LocalStore.actions.fetchLocalTimeline).toHaveBeenCalled()
expect(PublicStore.actions.fetchPublicTimeline).toHaveBeenCalled()
})
})
})

View File

@ -1,228 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import DirectMessages, { DirectMessagesState } from '@/store/TimelineSpace/Contents/DirectMessages'
import { RootState } from '@/store'
const mockClient = {
getConversationTimeline: () => {
return new Promise<Response<Array<Entity.Conversation>>>(resolve => {
const res: Response<Array<Entity.Conversation>> = {
data: [conversation1],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const conversation1: Entity.Conversation = {
id: '1',
accounts: [account],
last_status: status1,
unread: false
}
const conversation2: Entity.Conversation = {
id: '2',
accounts: [account],
last_status: status2,
unread: false
}
let state = (): DirectMessagesState => {
return {
lazyLoading: false,
heading: true,
timeline: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: DirectMessages.actions,
mutations: DirectMessages.mutations
}
}
const contentsStore = () => ({
namespaced: true,
modules: {
DirectMessages: initStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
sns: 'mastodon'
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Home', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchTimeline', () => {
it('should be updated', async () => {
const statuses = await store.dispatch('TimelineSpace/Contents/DirectMessages/fetchTimeline')
expect(statuses).toEqual([status1])
expect(store.state.TimelineSpace.Contents.DirectMessages.timeline).toEqual([status1])
})
})
describe('lazyFetchTimeline', () => {
describe('success', () => {
beforeAll(() => {
state = () => {
return {
lazyLoading: false,
heading: true,
timeline: [status1]
}
}
})
it('should be updated', async () => {
mockClient.getConversationTimeline = () => {
return new Promise<Response<Array<Entity.Conversation>>>(resolve => {
const res: Response<Array<Entity.Conversation>> = {
data: [conversation2],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/DirectMessages/lazyFetchTimeline', status1)
expect(store.state.TimelineSpace.Contents.DirectMessages.lazyLoading).toEqual(false)
expect(store.state.TimelineSpace.Contents.DirectMessages.timeline).toEqual([status1, status2])
})
})
})
})

View File

@ -1,321 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Favourites, { FavouritesState } from '@/store/TimelineSpace/Contents/Favourites'
import { LocalAccount } from '~/src/types/localAccount'
import { RootState } from '@/store'
const mockClient = {
getFavourites: () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status1],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const localAccount: LocalAccount = {
_id: '1',
baseURL: 'http://localhost',
domain: 'localhost',
clientId: 'id',
clientSecret: 'secret',
accessToken: 'token',
refreshToken: null,
username: 'hoge',
accountId: '1',
avatar: null,
order: 1
}
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
let state = (): FavouritesState => {
return {
favourites: [],
lazyLoading: false,
maxId: null
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Favourites.actions,
mutations: Favourites.mutations
}
}
const contentsStore = () => ({
namespaced: true,
modules: {
Favourites: initStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
sns: 'mastodon'
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Favourites', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchFavourites', () => {
it('does not exist header', async () => {
await store.dispatch('TimelineSpace/Contents/Favourites/fetchFavourites', localAccount)
expect(store.state.TimelineSpace.Contents.Favourites.favourites).toEqual([status1])
expect(store.state.TimelineSpace.Contents.Favourites.maxId).toEqual(null)
})
it('link is null', async () => {
mockClient.getFavourites = () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status1],
status: 200,
statusText: 'OK',
headers: {
link: null
}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/Favourites/fetchFavourites', localAccount)
expect(store.state.TimelineSpace.Contents.Favourites.favourites).toEqual([status1])
expect(store.state.TimelineSpace.Contents.Favourites.maxId).toEqual(null)
})
it('link exists in header', async () => {
mockClient.getFavourites = () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status1],
status: 200,
statusText: 'OK',
headers: {
link: '<http://localhost?max_id=2>; rel="next"'
}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/Favourites/fetchFavourites', localAccount)
expect(store.state.TimelineSpace.Contents.Favourites.favourites).toEqual([status1])
expect(store.state.TimelineSpace.Contents.Favourites.maxId).toEqual('2')
})
})
describe('lazyFetchFavourites', () => {
describe('lazyLoading', () => {
beforeAll(() => {
state = () => {
return {
favourites: [],
lazyLoading: true,
maxId: null
}
}
})
it('should not be updated', async () => {
const res = await store.dispatch('TimelineSpace/Contents/Favourites/lazyFetchFavourites')
expect(res).toEqual(null)
})
})
describe('does not exist maxId', () => {
beforeAll(() => {
state = () => {
return {
favourites: [],
lazyLoading: false,
maxId: null
}
}
})
it('should not be updated', async () => {
const res = await store.dispatch('TimelineSpace/Contents/Favourites/lazyFetchFavourites')
expect(res).toEqual(null)
})
})
describe('fetch', () => {
beforeAll(() => {
state = () => {
return {
favourites: [status1],
lazyLoading: false,
maxId: '2'
}
}
})
it('link is null', async () => {
mockClient.getFavourites = () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status2],
status: 200,
statusText: 'OK',
headers: {
link: null
}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/Favourites/lazyFetchFavourites')
expect(store.state.TimelineSpace.Contents.Favourites.favourites).toEqual([status1, status2])
expect(store.state.TimelineSpace.Contents.Favourites.maxId).toEqual(null)
})
it('link exists in header', async () => {
mockClient.getFavourites = () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status2],
status: 200,
statusText: 'OK',
headers: {
link: '<http://localhost?max_id=3>; rel="next"'
}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/Favourites/lazyFetchFavourites')
expect(store.state.TimelineSpace.Contents.Favourites.favourites).toEqual([status1, status2])
expect(store.state.TimelineSpace.Contents.Favourites.maxId).toEqual('3')
})
})
})
})

View File

@ -1,221 +0,0 @@
import { Entity, Response } from 'megalodon'
import { createStore, Store } from 'vuex'
import FollowRequests, { FollowRequestsState } from '@/store/TimelineSpace/Contents/FollowRequests'
import { SideMenuState } from '@/store/TimelineSpace/SideMenu'
import { RootState } from '@/store'
const mockClient = {
getFollowRequests: () => {
return new Promise<Response<Array<Entity.Account>>>(resolve => {
const res: Response<Array<Entity.Account>> = {
data: [account],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
},
acceptFollowRequest: () => {
return new Promise<Response<{}>>(resolve => {
const res: Response<{}> = {
data: {},
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
},
rejectFollowRequest: () => {
return new Promise<Response<{}>>(resolve => {
const res: Response<{}> = {
data: {},
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
let state = (): FollowRequestsState => {
return {
requests: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: FollowRequests.actions,
mutations: FollowRequests.mutations
}
}
const sideMenuState = (): SideMenuState => {
return {
unreadHomeTimeline: false,
unreadNotifications: false,
unreadMentions: false,
unreadLocalTimeline: false,
unreadDirectMessagesTimeline: false,
unreadPublicTimeline: false,
unreadFollowRequests: false,
lists: [],
tags: [],
collapse: false,
enabledTimelines: {
home: true,
notification: true,
mention: true,
direct: true,
favourite: true,
bookmark: true,
local: true,
public: true,
tag: true,
list: true
}
}
}
const sideMenuStore = () => ({
namespaced: true,
state: sideMenuState(),
actions: {
fetchFollowRequests: jest.fn()
},
mutations: {}
})
const contentsStore = () => ({
namespaced: true,
modules: {
FollowRequests: initStore()
}
})
const timelineStore = () => ({
namespaced: true,
modules: {
SideMenu: sideMenuStore(),
Contents: contentsStore()
},
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
sns: 'mastodon'
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Home', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchRequests', () => {
it('should be updated', async () => {
await store.dispatch('TimelineSpace/Contents/FollowRequests/fetchRequests')
expect(store.state.TimelineSpace.Contents.FollowRequests.requests).toEqual([account])
})
})
describe('acceptRequest', () => {
beforeAll(() => {
state = () => {
return {
requests: [account]
}
}
})
it('should be succeed', async () => {
mockClient.getFollowRequests = () => {
return new Promise<Response<Array<Entity.Account>>>(resolve => {
const res: Response<Array<Entity.Account>> = {
data: [],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/FollowRequests/acceptRequest', account)
expect(store.state.TimelineSpace.Contents.FollowRequests.requests).toEqual([])
})
})
describe('rejectRequest', () => {
beforeAll(() => {
state = () => {
return {
requests: [account]
}
}
})
it('should be succeed', async () => {
mockClient.getFollowRequests = () => {
return new Promise<Response<Array<Entity.Account>>>(resolve => {
const res: Response<Array<Entity.Account>> = {
data: [],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/FollowRequests/rejectRequest', account)
expect(store.state.TimelineSpace.Contents.FollowRequests.requests).toEqual([])
})
})
})

View File

@ -1,96 +0,0 @@
import { IpcMainInvokeEvent } from 'electron'
import { createStore, Store } from 'vuex'
import { ipcMain, ipcRenderer } from '~/spec/mock/electron'
import { LocalTag } from '~/src/types/localTag'
import List, { ListState } from '@/store/TimelineSpace/Contents/Hashtag/List'
import { MyWindow } from '~/src/types/global'
import { RootState } from '@/store'
;(window as any as MyWindow).ipcRenderer = ipcRenderer
const tag1: LocalTag = {
tagName: 'tag1',
_id: '1'
}
const tag2: LocalTag = {
tagName: 'tag2',
_id: '2'
}
const state = (): ListState => {
return {
tags: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: List.actions,
mutations: List.mutations
}
}
const hashtagStore = () => ({
namespaced: true,
modules: {
List: initStore()
}
})
const contentsStore = () => ({
namespaced: true,
modules: {
Hashtag: hashtagStore()
}
})
const sideMenuStore = {
namespaced: true,
actions: {
listTags: jest.fn()
}
}
const timelineStore = () => ({
namespaced: true,
modules: {
SideMenu: sideMenuStore,
Contents: contentsStore()
}
})
describe('Hashtag/List', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore()
}
})
})
describe('listTags', () => {
it('should be updated', async () => {
ipcMain.handle('list-hashtags', () => {
return [tag1, tag2]
})
afterEach(() => {
ipcMain.removeHandler('list-hashtags')
})
await store.dispatch('TimelineSpace/Contents/Hashtag/List/listTags')
expect(store.state.TimelineSpace.Contents.Hashtag.List.tags).toEqual([tag1, tag2])
})
})
describe('removeTag', () => {
it('should be updated', async () => {
ipcMain.handle('remove-hashtag', (_: IpcMainInvokeEvent, tag: LocalTag) => {
expect(tag).toEqual(tag1)
})
await store.dispatch('TimelineSpace/Contents/Hashtag/List/removeTag', tag1)
})
})
})

View File

@ -1,225 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Tag, { TagState } from '@/store/TimelineSpace/Contents/Hashtag/Tag'
import { LoadPositionWithTag } from '@/types/loadPosition'
import { RootState } from '@/store'
const mockClient = {
getTagTimeline: () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status1],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
let state = (): TagState => {
return {
lazyLoading: false,
heading: true,
timeline: [],
unreads: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Tag.actions,
mutations: Tag.mutations
}
}
const hashtagStore = () => ({
namespaced: true,
modules: {
Tag: initStore()
}
})
const contentsStore = () => ({
namespaced: true,
modules: {
Hashtag: hashtagStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
}
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Home', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetch', () => {
it('should be updated', async () => {
const statuses = await store.dispatch('TimelineSpace/Contents/Hashtag/Tag/fetch', 'tag')
expect(statuses).toEqual([status1])
expect(store.state.TimelineSpace.Contents.Hashtag.Tag.timeline).toEqual([status1])
})
})
describe('lazyFetchTimeline', () => {
describe('success', () => {
beforeAll(() => {
state = () => {
return {
lazyLoading: false,
heading: true,
timeline: [status1],
unreads: []
}
}
})
it('should be updated', async () => {
mockClient.getTagTimeline = () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status2],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
const loadPositionWithTag: LoadPositionWithTag = {
status: status1,
tag: 'tag'
}
await store.dispatch('TimelineSpace/Contents/Hashtag/Tag/lazyFetchTimeline', loadPositionWithTag)
expect(store.state.TimelineSpace.Contents.Hashtag.Tag.lazyLoading).toEqual(false)
expect(store.state.TimelineSpace.Contents.Hashtag.Tag.timeline).toEqual([status1, status2])
})
})
})
})

View File

@ -1,224 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Home, { HomeState } from '@/store/TimelineSpace/Contents/Home'
import { RootState } from '@/store'
const mockClient = {
getHomeTimeline: () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status1],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
let state = (): HomeState => {
return {
lazyLoading: false,
heading: true,
timeline: [],
unreads: [],
showReblogs: true,
showReplies: true
}
}
const homeStore = () => {
return {
namespaced: true,
state: state(),
actions: Home.actions,
mutations: Home.mutations
}
}
const contentsStore = () => ({
namespaced: true,
modules: {
Home: homeStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
timelineSetting: {
useMarker: {
home: false,
notifications: false
}
}
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Home', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchTimeline', () => {
it('should be updated', async () => {
const statuses = await store.dispatch('TimelineSpace/Contents/Home/fetchTimeline')
expect(statuses).toEqual([status1])
expect(store.state.TimelineSpace.Contents.Home.timeline).toEqual([status1])
})
})
describe('lazyFetchTimeline', () => {
describe('success', () => {
beforeAll(() => {
state = () => {
return {
lazyLoading: false,
heading: true,
timeline: [status1],
unreads: [],
showReblogs: true,
showReplies: true
}
}
})
it('should be updated', async () => {
mockClient.getHomeTimeline = () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status2],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/Home/lazyFetchTimeline', status1)
expect(store.state.TimelineSpace.Contents.Home.lazyLoading).toEqual(false)
expect(store.state.TimelineSpace.Contents.Home.timeline).toEqual([status1, status2])
})
})
})
})

View File

@ -1,139 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Edit, { EditState } from '@/store/TimelineSpace/Contents/Lists/Edit'
import { RemoveAccountFromList } from '@/types/removeAccountFromList'
import { RootState } from '@/store'
const mockClient = {
getAccountsInList: () => {
return new Promise<Response<Entity.Account[]>>(resolve => {
const res: Response<Entity.Account[]> = {
data: [account],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
},
deleteAccountsFromList: () => {
return new Promise<Response<{}>>(resolve => {
const res: Response<{}> = {
data: {},
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const state = (): EditState => {
return {
members: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Edit.actions,
mutations: Edit.mutations
}
}
const listsStore = () => ({
namespaced: true,
modules: {
Edit: initStore()
}
})
const contentsStore = () => ({
namespaced: true,
modules: {
Lists: listsStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
sns: 'mastodon'
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Lists/Edit', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchMembers', () => {
it('should get', async () => {
await store.dispatch('TimelineSpace/Contents/Lists/Edit/fetchMembers', 'id')
expect(store.state.TimelineSpace.Contents.Lists.Edit.members).toEqual([account])
})
})
describe('removeAccount', () => {
it('should be removed', async () => {
const removeFromList: RemoveAccountFromList = {
account: account,
listId: 'id'
}
const res = await store.dispatch('TimelineSpace/Contents/Lists/Edit/removeAccount', removeFromList)
expect(res.data).toEqual({})
})
})
})

View File

@ -1,117 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Index, { IndexState } from '@/store/TimelineSpace/Contents/Lists/Index'
import { RootState } from '@/store'
const mockClient = {
getLists: () => {
return new Promise<Response<Array<Entity.List>>>(resolve => {
const res: Response<Array<Entity.List>> = {
data: [list],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
},
createList: () => {
return new Promise<Response<Entity.List>>(resolve => {
const res: Response<Entity.List> = {
data: list,
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const list: Entity.List = {
id: '1',
title: 'list1'
}
const state = (): IndexState => {
return {
lists: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Index.actions,
mutations: Index.mutations
}
}
const listsStore = () => ({
namespaced: true,
modules: {
Index: initStore()
}
})
const contentsStore = () => ({
namespaced: true,
modules: {
Lists: listsStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
sns: 'mastodon'
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Lists/Index', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchLists', () => {
it('should be updated', async () => {
await store.dispatch('TimelineSpace/Contents/Lists/Index/fetchLists')
expect(store.state.TimelineSpace.Contents.Lists.Index.lists).toEqual([list])
})
})
describe('createList', () => {
it('should be created', async () => {
const res: Entity.List = await store.dispatch('TimelineSpace/Contents/Lists/Index/createList', 'list1')
expect(res.title).toEqual('list1')
})
})
})

View File

@ -1,225 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Show, { ShowState } from '@/store/TimelineSpace/Contents/Lists/Show'
import { LoadPositionWithList } from '@/types/loadPosition'
import { RootState } from '@/store'
const mockClient = {
getListTimeline: () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status1],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
let state = (): ShowState => {
return {
lazyLoading: false,
heading: true,
timeline: [],
unreads: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Show.actions,
mutations: Show.mutations
}
}
const listsStore = () => ({
namespaced: true,
modules: {
Show: initStore()
}
})
const contentsStore = () => ({
namespaced: true,
modules: {
Lists: listsStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
}
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Lists/Show', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchTimeline', () => {
it('should be updated', async () => {
await store.dispatch('TimelineSpace/Contents/Lists/Show/fetchTimeline', '1')
expect(store.state.TimelineSpace.Contents.Lists.Show.timeline).toEqual([status1])
})
})
describe('lazyFetchTimeline', () => {
beforeAll(() => {
state = () => {
return {
lazyLoading: false,
heading: true,
timeline: [status1],
unreads: []
}
}
})
it('should be updated', async () => {
mockClient.getListTimeline = () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status2],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
const loadPosition: LoadPositionWithList = {
status: status1,
list_id: '1'
}
await store.dispatch('TimelineSpace/Contents/Lists/Show/lazyFetchTimeline', loadPosition)
expect(store.state.TimelineSpace.Contents.Lists.Show.timeline).toEqual([status1, status2])
expect(store.state.TimelineSpace.Contents.Lists.Show.lazyLoading).toEqual(false)
})
})
})

View File

@ -1,214 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Local, { LocalState } from '@/store/TimelineSpace/Contents/Local'
import { RootState } from '@/store'
const mockClient = {
getLocalTimeline: () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status1],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
let state = (): LocalState => {
return {
lazyLoading: false,
heading: true,
timeline: [],
unreads: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Local.actions,
mutations: Local.mutations
}
}
const contentsStore = () => ({
namespaced: true,
modules: {
Local: initStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
}
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Home', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchLocalTimeline', () => {
it('should be updated', async () => {
const statuses = await store.dispatch('TimelineSpace/Contents/Local/fetchLocalTimeline')
expect(statuses).toEqual([status1])
expect(store.state.TimelineSpace.Contents.Local.timeline).toEqual([status1])
})
})
describe('lazyFetchTimeline', () => {
describe('success', () => {
beforeAll(() => {
state = () => {
return {
lazyLoading: false,
heading: true,
timeline: [status1],
unreads: []
}
}
})
it('should be updated', async () => {
mockClient.getLocalTimeline = () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status2],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/Local/lazyFetchTimeline', status1)
expect(store.state.TimelineSpace.Contents.Local.lazyLoading).toEqual(false)
expect(store.state.TimelineSpace.Contents.Local.timeline).toEqual([status1, status2])
})
})
})
})

View File

@ -1,248 +0,0 @@
import { RootState } from '@/store'
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Mentions from '~/src/renderer/store/TimelineSpace/Contents/Mentions'
const mockClient = {
getNotifications: () => {
return new Promise<Response<Entity.Notification[]>>(resolve => {
const res: Response<Entity.Notification[]> = {
data: [mention, reblog, favourite, follow],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const mention: Entity.Notification = {
account: account,
created_at: '2019-03-26T21:40:32',
id: '1',
status: status,
type: 'mention'
}
const reblog: Entity.Notification = {
account: account,
created_at: '2019-03-26T21:40:32',
id: '2',
status: status,
type: 'reblog'
}
const favourite: Entity.Notification = {
account: account,
created_at: '2019-03-26T21:40:32',
id: '3',
status: status,
type: 'favourite'
}
const follow: Entity.Notification = {
account: account,
created_at: '2019-03-26T21:40:32',
id: '4',
type: 'follow'
}
let state: Function = () => {
return {
lazyLoading: false,
heading: true,
mentions: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Mentions.actions,
mutations: Mentions.mutations,
getters: Mentions.getters
}
}
const contentsStore = () => ({
namespaced: true,
modules: {
Mentions: initStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
timelineSetting: {
useMarker: {
home: false,
notifications: false,
mentions: false
}
}
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Mentions', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchMentions', () => {
it('should be updated', async () => {
await store.dispatch('TimelineSpace/Contents/Mentions/fetchMentions')
expect(store.state.TimelineSpace.Contents.Mentions.mentions).toEqual([mention, reblog, favourite, follow])
})
})
describe('lazyFetchMentions', () => {
describe('loading', () => {
beforeAll(() => {
state = () => {
return {
lazyLoading: true,
heading: true,
mentions: []
}
}
})
it('should not be updated', async () => {
const result = await store.dispatch('TimelineSpace/Contents/Mentions/lazyFetchMentions', {})
expect(result).toEqual(null)
})
})
describe('success', () => {
beforeAll(() => {
state = () => {
return {
lazyLoading: false,
heading: true,
mentions: [mention, reblog]
}
}
})
it('should be updated', async () => {
mockClient.getNotifications = () => {
return new Promise<Response<Entity.Notification[]>>(resolve => {
const res: Response<Entity.Notification[]> = {
data: [favourite, follow],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/Mentions/lazyFetchMentions', { id: 1 })
expect(store.state.TimelineSpace.Contents.Mentions.mentions).toEqual([mention, reblog, favourite, follow])
expect(store.state.TimelineSpace.Contents.Mentions.lazyLoading).toEqual(false)
})
})
})
describe('mentions', () => {
beforeAll(() => {
state = () => {
return {
lazyLoading: false,
heading: true,
mentions: [mention, favourite, reblog, follow]
}
}
})
it('should return only mentions', () => {
const mentions = store.getters['TimelineSpace/Contents/Mentions/mentions']
expect(mentions).toEqual([mention])
})
})
})

View File

@ -1,293 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Notifications, { NotificationsState } from '@/store/TimelineSpace/Contents/Notifications'
import { RootState } from '@/store'
const mockClient = {
getNotifications: () => {
return new Promise<Response<Array<Entity.Notification>>>(resolve => {
const res: Response<Array<Entity.Notification>> = {
data: [notification1],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account1: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const account2: Entity.Account = {
id: '2',
username: 'h3poteto',
acct: 'h3poteto@mstdn.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://mstdn.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account1,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account1,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const rebloggedStatus: Entity.Status = {
id: '3',
uri: 'http://example.com',
url: 'http://example.com',
account: account1,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: status2,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const notification1: Entity.Notification = {
id: '1',
account: account2,
status: status1,
type: 'favourite',
created_at: '2019-04-01T17:01:32'
}
const notification2: Entity.Notification = {
id: '2',
account: account2,
status: rebloggedStatus,
type: 'mention',
created_at: '2019-04-01T17:01:32'
}
let state = (): NotificationsState => {
return {
lazyLoading: false,
heading: true,
notifications: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Notifications.actions,
mutations: Notifications.mutations
}
}
const contentsStore = () => ({
namespaced: true,
modules: {
Notifications: initStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
timelineSetting: {
useMarker: {
home: false,
notifications: false,
mentions: false
}
}
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false,
useMarkerTimeline: []
}
}
describe('Notifications', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchNotifications', () => {
it('should be updated', async () => {
const response = await store.dispatch('TimelineSpace/Contents/Notifications/fetchNotifications')
expect(response).toEqual([notification1])
expect(store.state.TimelineSpace.Contents.Notifications.notifications).toEqual([notification1])
})
})
describe('lazyFetchNotifications', () => {
beforeAll(() => {
state = () => {
return {
lazyLoading: false,
heading: true,
notifications: [notification1]
}
}
})
it('should be updated', async () => {
mockClient.getNotifications = () => {
return new Promise<Response<Array<Entity.Notification>>>(resolve => {
const res: Response<Array<Entity.Notification>> = {
data: [notification2],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/Notifications/lazyFetchNotifications', notification1)
expect(store.state.TimelineSpace.Contents.Notifications.lazyLoading).toEqual(false)
expect(store.state.TimelineSpace.Contents.Notifications.notifications).toEqual([notification1, notification2])
})
})
})

View File

@ -1,213 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Public, { PublicState } from '@/store/TimelineSpace/Contents/Public'
import { RootState } from '@/store'
const mockClient = {
getPublicTimeline: () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status1],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
let state = (): PublicState => {
return {
lazyLoading: false,
heading: true,
timeline: [],
unreads: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Public.actions,
mutations: Public.mutations
}
}
const contentsStore = () => ({
namespaced: true,
modules: {
Public: initStore()
}
})
const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
}
},
modules: {
Contents: contentsStore()
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Home', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('fetchPublicTimeline', () => {
it('should be updated', async () => {
const statuses = await store.dispatch('TimelineSpace/Contents/Public/fetchPublicTimeline')
expect(statuses).toEqual([status1])
expect(store.state.TimelineSpace.Contents.Public.timeline).toEqual([status1])
})
})
describe('lazyFetchTimeline', () => {
describe('success', () => {
beforeAll(() => {
state = () => {
return {
lazyLoading: false,
heading: true,
timeline: [status1],
unreads: []
}
}
})
it('should be updated', async () => {
mockClient.getPublicTimeline = () => {
return new Promise<Response<Array<Entity.Status>>>(resolve => {
const res: Response<Array<Entity.Status>> = {
data: [status2],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
await store.dispatch('TimelineSpace/Contents/Public/lazyFetchTimeline', status1)
expect(store.state.TimelineSpace.Contents.Public.lazyLoading).toEqual(false)
expect(store.state.TimelineSpace.Contents.Public.timeline).toEqual([status1, status2])
})
})
})
})

View File

@ -1,121 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import AccountStore, { AccountState } from '@/store/TimelineSpace/Contents/Search/Account'
import { RootState } from '@/store'
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const mockClient = {
searchAccount: () => {
return new Promise<Response<Array<Entity.Account>>>(resolve => {
const res: Response<Array<Entity.Account>> = {
data: [account],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const state = (): AccountState => {
return {
results: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: AccountStore.actions,
mutations: AccountStore.mutations
}
}
const searchStore = () => ({
namespaced: true,
modules: {
Account: initStore()
}
})
const contentsStore = () => ({
namespaced: true,
state: {},
mutations: {
changeLoading: jest.fn()
},
actions: {},
modules: {
Search: searchStore()
}
})
const timelineStore = () => ({
namespaced: true,
modules: {
Contents: contentsStore()
},
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
sns: 'mastodon'
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Search/Account', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('search', () => {
it('should be updated', async () => {
await store.dispatch('TimelineSpace/Contents/Search/Account/search', 'query')
expect(store.state.TimelineSpace.Contents.Search.Account.results).toEqual([account])
})
})
})

View File

@ -1,109 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import TagStore, { TagState } from '@/store/TimelineSpace/Contents/Search/Tag'
import { RootState } from '@/store'
const tag1: Entity.Tag = {
name: 'tag1',
url: 'http://example.com/tag1',
history: null
}
const mockClient = {
search: () => {
return new Promise<Response<Entity.Results>>(resolve => {
const res: Response<Entity.Results> = {
data: {
accounts: [],
statuses: [],
hashtags: [tag1]
},
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const state = (): TagState => {
return {
results: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: TagStore.actions,
mutations: TagStore.mutations
}
}
const searchStore = () => ({
namespaced: true,
modules: {
Tag: initStore()
}
})
const contentsStore = () => ({
namespaced: true,
state: {},
mutations: {
changeLoading: jest.fn()
},
actions: {},
modules: {
Search: searchStore()
}
})
const timelineStore = () => ({
namespaced: true,
modules: {
Contents: contentsStore()
},
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
sns: 'mastodon'
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Search/Tag', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('search', () => {
it('should be updated', async () => {
await store.dispatch('TimelineSpace/Contents/Search/Tag/search', 'query')
expect(store.state.TimelineSpace.Contents.Search.Tag.results).toEqual([tag1])
})
})
})

View File

@ -1,161 +0,0 @@
import { Response, Entity } from 'megalodon'
import { createStore, Store } from 'vuex'
import Toots, { TootsState } from '@/store/TimelineSpace/Contents/Search/Toots'
import { RootState } from '@/store'
const mockClient = {
search: () => {
return new Promise<Response<Entity.Results>>(resolve => {
const res: Response<Entity.Results> = {
data: {
accounts: [],
statuses: [status],
hashtags: []
},
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const state = (): TootsState => {
return {
results: []
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: Toots.actions,
mutations: Toots.mutations
}
}
const searchStore = () => ({
namespaced: true,
modules: {
Toots: initStore()
}
})
const contentsStore = () => ({
namespaced: true,
state: {},
mutations: {
changeLoading: jest.fn()
},
actions: {},
modules: {
Search: searchStore()
}
})
const timelineStore = () => ({
namespaced: true,
modules: {
Contents: contentsStore()
},
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
},
sns: 'mastodon'
}
})
const appState = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
describe('Search/Account', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
TimelineSpace: timelineStore(),
App: appState
}
})
})
describe('search', () => {
it('should be updated', async () => {
await store.dispatch('TimelineSpace/Contents/Search/Toots/search', 'query')
expect(store.state.TimelineSpace.Contents.Search.Toots.results).toEqual([status])
})
})
})

View File

@ -40,16 +40,13 @@ const timelineStore = (account: Entity.Account | null) => ({
namespaced: true,
state: {
account: {
baseURL: 'https://example.com',
domain: 'example.com',
clientId: 'sampleId',
clientSecret: 'sampleSecret',
accessToken: 'sampleAccessToken',
refreshToken: null,
username: 'h3poteto',
accountID: '1',
avatar: null,
order: 1
id: 1,
username: 'h3poteto'
},
server: {
sns: 'mastodon',
baseURL: 'https://example.com'
}
},
modules: {

View File

@ -49,10 +49,12 @@ const timelineStore = () => ({
namespaced: true,
state: {
account: {
accessToken: 'token',
baseURL: 'http://localhost'
accessToken: 'token'
},
sns: 'mastodon'
server: {
sns: 'mastodon',
baseURL: 'http://localhost'
}
},
modules: {
HeaderMenu: initStore()

View File

@ -84,9 +84,12 @@ const timelineStore = () => ({
namespaced: true,
state: {
account: {
_id: '0'
id: 0,
accessToken: 'token'
},
sns: 'mastodon'
server: {
sns: 'mastodon'
}
},
modules: {
Modals: modalsStore()

View File

@ -42,8 +42,6 @@ const state = (): JumpState => {
path: 'direct-messages'
}
],
listChannelList: [],
tagChannelList: [],
selectedChannel: {
name: i18n.t('side_menu.home'),
path: 'home'
@ -70,7 +68,7 @@ const timelineStore = () => ({
namespaced: true,
state: {
account: {
_id: '0'
id: 0
}
},
modules: {

View File

@ -130,7 +130,12 @@ const timelineStore = () => ({
namespaced: true,
state: {
account: {
_id: '0'
id: 0,
accessToken: 'token'
},
server: {
sns: 'mastodon',
baseURL: 'http://localhost'
}
},
modules: {

View File

@ -1,164 +0,0 @@
import { Entity, Response } from 'megalodon'
import { createStore, Store } from 'vuex'
import { ipcMain, ipcRenderer } from '~/spec/mock/electron'
import SideMenu, { SideMenuState } from '~/src/renderer/store/TimelineSpace/SideMenu'
import { LocalTag } from '~/src/types/localTag'
import { MyWindow } from '~/src/types/global'
import { RootState } from '@/store'
;(window as any as MyWindow).ipcRenderer = ipcRenderer
const mockClient = {
getLists: () => {
return new Promise<Response<Entity.List[]>>(resolve => {
const res: Response<Entity.List[]> = {
data: [list1, list2],
status: 200,
statusText: 'OK',
headers: {}
}
resolve(res)
})
}
}
jest.mock('megalodon', () => ({
...jest.requireActual<object>('megalodon'),
default: jest.fn(() => mockClient),
__esModule: true
}))
// import mockedMegalodon from '~/spec/mock/megalodon'
const list1: Entity.List = {
id: '1',
title: 'example1'
}
const list2: Entity.List = {
id: '2',
title: 'example2'
}
const state = (): SideMenuState => {
return {
unreadHomeTimeline: false,
unreadNotifications: false,
unreadMentions: false,
unreadLocalTimeline: false,
unreadDirectMessagesTimeline: false,
unreadPublicTimeline: false,
unreadFollowRequests: false,
lists: [],
tags: [],
collapse: false,
enabledTimelines: {
home: true,
notification: true,
mention: true,
direct: true,
favourite: true,
bookmark: true,
local: true,
public: true,
tag: true,
list: true
}
}
}
const initStore = () => {
return {
namespaced: true,
state: state(),
actions: SideMenu.actions,
mutations: SideMenu.mutations
}
}
const appStore = {
namespaced: true,
state: {
proxyConfiguration: false
}
}
const timelineStore = () => ({
namespaced: true,
state: {
sns: 'mastodon'
},
modules: {
SideMenu: initStore()
}
})
describe('SideMenu', () => {
let store: Store<RootState>
beforeEach(() => {
store = createStore({
modules: {
App: appStore,
TimelineSpace: timelineStore()
}
})
// mockedMegalodon.mockClear()
})
describe('fetchLists', () => {
it('should be updated', async () => {
// mockedMegalodon.mockImplementation(() => mockClient)
const account = {
accessToken: 'token',
baseURL: 'http://localhost'
}
const lists = await store.dispatch('TimelineSpace/SideMenu/fetchLists', account)
expect(store.state.TimelineSpace.SideMenu.lists).toEqual([list1, list2])
expect(lists).toEqual([list1, list2])
})
})
describe('clearUnread', () => {
it('should be reset', () => {
store.dispatch('TimelineSpace/SideMenu/clearUnread')
expect(store.state.TimelineSpace.SideMenu.unreadHomeTimeline).toEqual(false)
expect(store.state.TimelineSpace.SideMenu.unreadNotifications).toEqual(false)
expect(store.state.TimelineSpace.SideMenu.unreadLocalTimeline).toEqual(false)
expect(store.state.TimelineSpace.SideMenu.unreadDirectMessagesTimeline).toEqual(false)
expect(store.state.TimelineSpace.SideMenu.unreadPublicTimeline).toEqual(false)
})
})
describe('changeCollapse', () => {
it('should be changed', () => {
store.dispatch('TimelineSpace/SideMenu/changeCollapse', true)
expect(store.state.TimelineSpace.SideMenu.collapse).toEqual(true)
})
})
describe('readCollapse', () => {
it('should be read', async () => {
ipcMain.handle('get-collapse', () => {
return true
})
await store.dispatch('TimelineSpace/SideMenu/readCollapse')
expect(store.state.TimelineSpace.SideMenu.collapse).toEqual(true)
})
})
describe('listTags', () => {
it('should be listed', async () => {
const tag1: LocalTag = {
tagName: 'tag1'
}
const tag2: LocalTag = {
tagName: 'tag2'
}
ipcMain.handle('list-hashtags', () => {
return [tag1, tag2]
})
await store.dispatch('TimelineSpace/SideMenu/listTags')
expect(store.state.TimelineSpace.SideMenu.tags).toEqual([tag1, tag2])
})
})
})

View File

@ -5,15 +5,17 @@ describe('Login', () => {
let state: LoginState
beforeEach(() => {
state = {
selectedInstance: null,
domain: null,
searching: false,
server: null,
appData: null,
sns: 'mastodon'
}
})
describe('changeInstance', () => {
it('should be changed', () => {
Login.mutations![MUTATION_TYPES.CHANGE_INSTANCE](state, 'pleroma.io')
expect(state.selectedInstance).toEqual('pleroma.io')
Login.mutations![MUTATION_TYPES.CHANGE_DOMAIN](state, 'pleroma.io')
expect(state.domain).toEqual('pleroma.io')
})
})
describe('changeSearching', () => {

View File

@ -1,40 +0,0 @@
import Account, { AccountState, MUTATION_TYPES } from '@/store/Preferences/Account'
import { LocalAccount } from '~/src/types/localAccount'
const account: LocalAccount = {
_id: 'sample',
baseURL: 'http://example.com',
domain: 'example.com',
clientId: 'hoge',
clientSecret: 'hogehoge',
accessToken: null,
refreshToken: null,
username: null,
accountId: null,
avatar: null,
order: 1
}
describe('Preferences/Account', () => {
describe('mutations', () => {
let state: AccountState
beforeEach(() => {
state = {
accounts: [],
accountLoading: false
}
})
describe('updateAccounts', () => {
it('should be updated', () => {
Account.mutations![MUTATION_TYPES.UPDATE_ACCOUNTS](state, [account])
expect(state.accounts).toEqual([account])
})
})
describe('updateAccountLoading', () => {
it('should be update', () => {
Account.mutations![MUTATION_TYPES.UPDATE_ACCOUNT_LOADING](state, true)
expect(state.accountLoading).toEqual(true)
})
})
})
})

View File

@ -1,40 +0,0 @@
import Theme from '~/src/constants/theme'
import DisplayStyle from '~/src/constants/displayStyle'
import TimeFormat from '~/src/constants/timeFormat'
import { LightTheme } from '~/src/constants/themeColor'
import DefaultFonts from '@/utils/fonts'
import Appearance, { AppearanceState, MUTATION_TYPES } from '@/store/Preferences/Appearance'
describe('Preferences/Appearance', () => {
let state: AppearanceState
beforeEach(() => {
state = {
appearance: {
theme: Theme.Light.key,
fontSize: 14,
displayNameStyle: DisplayStyle.DisplayNameAndUsername.value,
timeFormat: TimeFormat.Absolute.value,
customThemeColor: LightTheme,
font: DefaultFonts[0],
tootPadding: 8
},
fonts: []
}
})
describe('mutations', () => {
describe('updateAppearance', () => {
it('should be changed', () => {
Appearance.mutations![MUTATION_TYPES.UPDATE_APPEARANCE](state, {
theme: Theme.Dark.key
})
expect(state.appearance.theme).toEqual(Theme.Dark.key)
})
})
describe('updateFonts', () => {
it('should be changed', () => {
Appearance.mutations![MUTATION_TYPES.UPDATE_FONTS](state, ['font'])
expect(state.fonts).toEqual(['font'])
})
})
})
})

View File

@ -1,38 +0,0 @@
import General, { GeneralState, MUTATION_TYPES } from '@/store/Preferences/General'
describe('Preferences/General', () => {
let state: GeneralState
beforeEach(() => {
state = {
general: {
sound: {
fav_rb: true,
toot: true
},
timeline: {
cw: false,
nsfw: false,
hideAllAttachments: false
},
other: {
launch: false,
hideOnLaunch: false
}
},
loading: false
}
})
describe('mutations', () => {
it('updateGeneral', () => {
General.mutations![MUTATION_TYPES.UPDATE_GENERAL](state, {
sound: {
fav_rb: false,
toot: false
}
})
expect(state.general.sound.fav_rb).toEqual(false)
expect(state.general.sound.toot).toEqual(false)
})
})
})

View File

@ -1,33 +0,0 @@
import Language, { LanguageState, MUTATION_TYPES } from '@/store/Preferences/Language'
import DefaultLanguage from '~/src/constants/language'
describe('Preferences/Language', () => {
let state: LanguageState
beforeEach(() => {
state = {
language: {
language: DefaultLanguage.en.key,
spellchecker: {
enabled: true,
languages: []
}
}
}
})
describe('mutations', () => {
describe('updateLanguage', () => {
it('should be updated', () => {
Language.mutations![MUTATION_TYPES.UPDATE_LANGUAGE](state, {
language: DefaultLanguage.ja.key
})
expect(state.language.language).toEqual(DefaultLanguage.ja.key)
})
})
describe('changeLanguage', () => {
it('should be changed', () => {
Language.mutations![MUTATION_TYPES.CHANGE_LANGUAGE](state, DefaultLanguage.ja.key)
expect(state.language.language).toEqual(DefaultLanguage.ja.key)
})
})
})
})

View File

@ -1,50 +0,0 @@
import Notification, { NotificationState, MUTATION_TYPES } from '@/store/Preferences/Notification'
describe('Preferences/Notification', () => {
let state: NotificationState
beforeEach(() => {
state = {
notification: {
notify: {
reply: true,
reblog: true,
favourite: true,
follow: true,
follow_request: true,
reaction: true,
status: true,
poll_vote: true,
poll_expired: true
}
}
}
})
describe('mutations', () => {
it('updateNotification', () => {
Notification.mutations![MUTATION_TYPES.UPDATE_NOTIFICATION](state, {
notify: {
reply: false,
reblog: false,
favourite: false,
follow: false,
follow_request: false,
reaction: false,
status: false,
poll_vote: false,
poll_expired: false
}
})
expect(state.notification.notify).toEqual({
reply: false,
reblog: false,
favourite: false,
follow: false,
follow_request: false,
reaction: false,
status: false,
poll_vote: false,
poll_expired: false
})
})
})
})

View File

@ -1,4 +1,4 @@
import TimelineSpace, { TimelineSpaceState, blankAccount, MUTATION_TYPES } from '~/src/renderer/store/TimelineSpace'
import TimelineSpace, { TimelineSpaceState, MUTATION_TYPES } from '~/src/renderer/store/TimelineSpace'
import { DefaultSetting } from '~/src/constants/initializer/setting'
describe('TimelineSpace', () => {
@ -6,13 +6,12 @@ describe('TimelineSpace', () => {
let state: TimelineSpaceState
beforeEach(() => {
state = {
account: blankAccount,
bindingAccount: null,
account: null,
server: null,
loading: false,
emojis: [],
tootMax: 500,
timelineSetting: DefaultSetting.timeline,
sns: 'mastodon',
setting: DefaultSetting,
filters: []
}
})

View File

@ -1,228 +0,0 @@
import { Entity } from 'megalodon'
import DirectMessages, { DirectMessagesState, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/DirectMessages'
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const rebloggedStatus: Entity.Status = {
id: '3',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: status1,
content: '',
plain_content: null,
created_at: '2019-03-31T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
describe('TimelineSpace/Contents/DirectMessages', () => {
describe('mutations', () => {
let state: DirectMessagesState
describe('deleteToot', () => {
describe('message is not reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1]
}
})
it('should be deleted', () => {
DirectMessages.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
describe('message is reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, rebloggedStatus]
}
})
it('should be deleted', () => {
DirectMessages.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
})
describe('appendTimeline', () => {
describe('heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1]
}
})
it('should be updated timeline', () => {
DirectMessages.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [rebloggedStatus, status2, status1]
}
})
it('should not be updated timeline', () => {
DirectMessages.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
})
describe('not heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [status2, status1]
}
})
it('should be updated timeline', () => {
DirectMessages.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [rebloggedStatus, status2, status1]
}
})
it('should not be updated timeline', () => {
DirectMessages.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
})
})
})
})

View File

@ -1,235 +0,0 @@
import { Entity } from 'megalodon'
import Tag, { TagState, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Hashtag/Tag'
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const rebloggedStatus: Entity.Status = {
id: '3',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: status1,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-31T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
describe('TimelineSpace/Contents/Hashtag/Tag', () => {
describe('mutations', () => {
let state: TagState
describe('deleteToot', () => {
describe('message is not reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1],
unreads: []
}
})
it('should be deleted', () => {
Tag.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
describe('message is reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, rebloggedStatus],
unreads: []
}
})
it('should be deleted', () => {
Tag.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
})
describe('appendTimeline', () => {
describe('heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1],
unreads: []
}
})
it('should be updated timeline', () => {
Tag.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [rebloggedStatus, status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Tag.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
})
describe('not heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Tag.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([status2, status1])
expect(state.unreads).toEqual([rebloggedStatus])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [rebloggedStatus, status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Tag.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
})
})
})
})

View File

@ -1,363 +0,0 @@
import { Entity } from 'megalodon'
import Home, { HomeState, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Home'
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
describe('TimelineSpace/Contents/Home', () => {
describe('mutations', () => {
let state: HomeState
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [],
showReblogs: true,
showReplies: true,
unreads: []
}
})
describe('changeLazyLoading', () => {
it('should be change', () => {
Home.mutations![MUTATION_TYPES.CHANGE_LAZY_LOADING](state, true)
expect(state.lazyLoading).toEqual(true)
})
})
describe('changeHeading', () => {
it('should be change', () => {
Home.mutations![MUTATION_TYPES.CHANGE_HEADING](state, false)
expect(state.heading).toEqual(false)
})
})
describe('appendTimeline', () => {
describe('heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status1],
showReblogs: true,
showReplies: true,
unreads: []
}
})
it('should update timeline', () => {
Home.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, status2)
expect(state.timeline).toEqual([status2, status1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1],
showReblogs: true,
showReplies: true,
unreads: []
}
})
it('should not update timeline', () => {
Home.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, status2)
expect(state.timeline).toEqual([status2, status1])
})
})
})
describe('not heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [status1],
showReblogs: true,
showReplies: true,
unreads: []
}
})
it('should update timeline', () => {
Home.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, status2)
expect(state.timeline).toEqual([status1])
expect(state.unreads).toEqual([status2])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [status2, status1],
showReblogs: true,
showReplies: true,
unreads: []
}
})
it('should not update timeline', () => {
Home.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, status2)
expect(state.timeline).toEqual([status2, status1])
})
})
})
})
describe('insertTimeline', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status1],
showReblogs: true,
showReplies: true,
unreads: []
}
})
it('should be inserted', () => {
Home.mutations![MUTATION_TYPES.INSERT_TIMELINE](state, [status2])
expect(state.timeline).toEqual([status1, status2])
})
})
describe('updateToot', () => {
describe('message is not reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status1, status2],
showReblogs: true,
showReplies: true,
unreads: []
}
})
const favouritedStatus: Entity.Status = Object.assign(status1, {
favourited: true
})
it('should be updated', () => {
Home.mutations![MUTATION_TYPES.UPDATE_TOOT](state, favouritedStatus)
expect(state.timeline).toEqual([favouritedStatus, status2])
})
})
describe('message is reblogged', () => {
const rebloggedStatus: Entity.Status = {
id: '3',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: status1,
content: '',
plain_content: null,
created_at: '2019-03-31T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const favouritedStatus: Entity.Status = Object.assign(status1, {
favourited: true
})
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [rebloggedStatus, status2],
showReblogs: true,
showReplies: true,
unreads: []
}
})
it('should be updated', () => {
Home.mutations![MUTATION_TYPES.UPDATE_TOOT](state, favouritedStatus)
expect((state.timeline[0] as Entity.Status).reblog).not.toBeNull()
expect((state.timeline[0] as Entity.Status).reblog!.favourited).toEqual(true)
})
})
})
describe('deleteToot', () => {
describe('message is not reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status1, status2],
showReblogs: true,
showReplies: true,
unreads: []
}
})
it('should be deleted', () => {
Home.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
describe('message is reblogged', () => {
beforeEach(() => {
const rebloggedStatus: Entity.Status = {
id: '3',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: status1,
content: '',
plain_content: null,
created_at: '2019-03-31T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
state = {
lazyLoading: false,
heading: true,
timeline: [rebloggedStatus, status2],
showReblogs: true,
showReplies: true,
unreads: []
}
})
it('should be deleted', () => {
Home.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
})
})
})

View File

@ -1,235 +0,0 @@
import { Entity } from 'megalodon'
import Show, { ShowState, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Lists/Show'
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const rebloggedStatus: Entity.Status = {
id: '3',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: status1,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-31T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
describe('TimelineSpace/Contents/Lists/Show', () => {
describe('mutations', () => {
let state: ShowState
describe('deleteToot', () => {
describe('message is not reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1],
unreads: []
}
})
it('should be deleted', () => {
Show.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
describe('message is reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, rebloggedStatus],
unreads: []
}
})
it('should be deleted', () => {
Show.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
})
describe('appendTimeline', () => {
describe('heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1],
unreads: []
}
})
it('should be updated timeline', () => {
Show.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [rebloggedStatus, status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Show.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
})
describe('not heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Show.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([status2, status1])
expect(state.unreads).toEqual([rebloggedStatus])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [rebloggedStatus, status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Show.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
})
})
})
})

View File

@ -1,234 +0,0 @@
import { Entity } from 'megalodon'
import Local, { LocalState, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Local'
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const rebloggedStatus: Entity.Status = {
id: '3',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: status1,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-31T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
describe('TimelineSpace/Contents/Local', () => {
describe('mutations', () => {
let state: LocalState
describe('deleteToot', () => {
describe('message is not reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1],
unreads: []
}
})
it('should be deleted', () => {
Local.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
describe('message is reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, rebloggedStatus],
unreads: []
}
})
it('should be deleted', () => {
Local.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
})
describe('appendTimeline', () => {
describe('heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1],
unreads: []
}
})
it('should be updated timeline', () => {
Local.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [rebloggedStatus, status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Local.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
})
describe('not heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [status2, status1],
unreads: []
}
})
it('should be updated timeline', () => {
Local.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([status2, status1])
expect(state.unreads).toEqual([rebloggedStatus])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [rebloggedStatus, status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Local.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
})
})
})
})

View File

@ -1,306 +0,0 @@
import { Entity } from 'megalodon'
import Mentions, { MentionsState, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Mentions'
const account1: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const account2: Entity.Account = {
id: '2',
username: 'h3poteto',
acct: 'h3poteto@mstdn.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://mstdn.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account1,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account1,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const rebloggedStatus: Entity.Status = {
id: '3',
uri: 'http://example.com',
url: 'http://example.com',
account: account1,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: status2,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const notification1: Entity.Notification = {
id: '1',
account: account2,
status: status1,
type: 'mention',
created_at: '2019-04-01T17:01:32'
}
const notification2: Entity.Notification = {
id: '2',
account: account2,
status: rebloggedStatus,
type: 'mention',
created_at: '2019-04-01T17:01:32'
}
describe('TimelineSpace/Contents/Mentions', () => {
describe('mutations', () => {
let state: MentionsState
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
mentions: []
}
})
describe('appendMentions', () => {
describe('heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
mentions: [notification1]
}
})
it('should update mentions', () => {
Mentions.mutations![MUTATION_TYPES.APPEND_MENTIONS](state, notification2)
expect(state.mentions).toEqual([notification2, notification1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
mentions: [notification2, notification1]
}
})
it('should not be updated mentions', () => {
Mentions.mutations![MUTATION_TYPES.APPEND_MENTIONS](state, notification2)
expect(state.mentions).toEqual([notification2, notification1])
})
})
})
describe('not heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
mentions: [notification1]
}
})
it('should update mentions', () => {
Mentions.mutations![MUTATION_TYPES.APPEND_MENTIONS](state, notification2)
expect(state.mentions).toEqual([notification2, notification1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
mentions: [notification2, notification1]
}
})
it('should not be updated mentions', () => {
Mentions.mutations![MUTATION_TYPES.APPEND_MENTIONS](state, notification2)
expect(state.mentions).toEqual([notification2, notification1])
})
})
})
})
describe('insertMentions', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
mentions: [notification2]
}
})
it('should be inserted', () => {
Mentions.mutations![MUTATION_TYPES.INSERT_MENTIONS](state, [notification1])
expect(state.mentions).toEqual([notification2, notification1])
})
})
describe('updateToot', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
mentions: [notification2, notification1]
}
})
it('should be updated', () => {
const favourited: Entity.Status = Object.assign(status1, {
favourited: true
})
Mentions.mutations![MUTATION_TYPES.UPDATE_TOOT](state, favourited)
expect((state.mentions[0] as Entity.Notification).status!.favourited).toEqual(null)
expect((state.mentions[1] as Entity.Notification).status!.favourited).toEqual(true)
})
})
describe('deleteToot', () => {
describe('message is not reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
mentions: [notification2, notification1]
}
})
it('should be deleted', () => {
Mentions.mutations![MUTATION_TYPES.DELETE_TOOT](state, notification1.status!.id)
expect(state.mentions.length).toEqual(1)
})
})
describe('message is reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
mentions: [notification2, notification1]
}
})
it('should be deleted', () => {
Mentions.mutations![MUTATION_TYPES.DELETE_TOOT](state, notification2.status!.id)
expect(state.mentions.length).toEqual(1)
})
})
})
})
})

View File

@ -1,259 +0,0 @@
import { Entity } from 'megalodon'
import Notifications, { NotificationsState, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Notifications'
const account1: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const account2: Entity.Account = {
id: '2',
username: 'h3poteto',
acct: 'h3poteto@mstdn.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://mstdn.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account1,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account1,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const rebloggedStatus: Entity.Status = {
id: '3',
uri: 'http://example.com',
url: 'http://example.com',
account: account1,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: status2,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const notification1: Entity.Notification = {
id: '1',
account: account2,
status: status1,
type: 'favourite',
created_at: '2019-04-01T17:01:32'
}
const notification2: Entity.Notification = {
id: '2',
account: account2,
status: rebloggedStatus,
type: 'mention',
created_at: '2019-04-01T17:01:32'
}
describe('TimelineSpace/Contents/Notifications', () => {
describe('mutations', () => {
let state: NotificationsState
describe('deleteToot', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
notifications: [notification2, notification1]
}
})
describe('message is not reblogged', () => {
it('should be deleted', () => {
Notifications.mutations![MUTATION_TYPES.DELETE_TOOT](state, notification1.status!.id)
expect(state.notifications.length).toEqual(1)
})
})
describe('message is reblogged', () => {
it('should be deleted', () => {
Notifications.mutations![MUTATION_TYPES.DELETE_TOOT](state, notification2.status!.id)
expect(state.notifications.length).toEqual(1)
})
})
})
describe('appendTimeline', () => {
describe('heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
notifications: [notification1]
}
})
it('should update timeline', () => {
Notifications.mutations![MUTATION_TYPES.APPEND_NOTIFICATIONS](state, notification2)
expect(state.notifications).toEqual([notification2, notification1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
notifications: [notification2, notification1]
}
})
it('should not update timeline', () => {
Notifications.mutations![MUTATION_TYPES.APPEND_NOTIFICATIONS](state, notification2)
expect(state.notifications).toEqual([notification2, notification1])
})
})
})
describe('not heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
notifications: [notification1]
}
})
it('should update timeline', () => {
Notifications.mutations![MUTATION_TYPES.APPEND_NOTIFICATIONS](state, notification2)
expect(state.notifications).toEqual([notification2, notification1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
notifications: [notification2, notification1]
}
})
it('should not update timeline', () => {
Notifications.mutations![MUTATION_TYPES.APPEND_NOTIFICATIONS](state, notification2)
expect(state.notifications).toEqual([notification2, notification1])
})
})
})
})
})
})

View File

@ -1,235 +0,0 @@
import { Entity } from 'megalodon'
import Public, { PublicState, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Public'
const account: Entity.Account = {
id: '1',
username: 'h3poteto',
acct: 'h3poteto@pleroma.io',
display_name: 'h3poteto',
locked: false,
created_at: '2019-03-26T21:30:32',
followers_count: 10,
following_count: 10,
statuses_count: 100,
note: 'engineer',
url: 'https://pleroma.io',
avatar: '',
avatar_static: '',
header: '',
header_static: '',
emojis: [],
moved: null,
fields: null,
bot: false
}
const status1: Entity.Status = {
id: '1',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const status2: Entity.Status = {
id: '2',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: null,
content: 'fuga',
plain_content: 'fuga',
created_at: '2019-03-26T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
const rebloggedStatus: Entity.Status = {
id: '3',
uri: 'http://example.com',
url: 'http://example.com',
account: account,
in_reply_to_id: null,
in_reply_to_account_id: null,
reblog: status1,
content: '',
plain_content: null,
created_at: '2019-03-31T21:40:32',
emojis: [],
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
reblogged: null,
favourited: null,
muted: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
media_attachments: [],
mentions: [],
tags: [],
card: null,
poll: null,
application: {
name: 'Web'
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
bookmarked: false,
quote: false
}
describe('TimelineSpace/Contents/Local', () => {
describe('mutations', () => {
let state: PublicState
describe('deleteToot', () => {
describe('message is not reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1],
unreads: []
}
})
it('should be deleted', () => {
Public.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
describe('message is reblogged', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, rebloggedStatus],
unreads: []
}
})
it('should be deleted', () => {
Public.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1.id)
expect(state.timeline).toEqual([status2])
})
})
})
describe('appendTimeline', () => {
describe('heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [status2, status1],
unreads: []
}
})
it('should be updated timeline', () => {
Public.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: true,
timeline: [rebloggedStatus, status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Public.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
})
describe('not heading', () => {
describe('normal', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Public.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([status2, status1])
expect(state.unreads).toEqual([rebloggedStatus])
})
})
describe('duplicated status', () => {
beforeEach(() => {
state = {
lazyLoading: false,
heading: false,
timeline: [rebloggedStatus, status2, status1],
unreads: []
}
})
it('should not be updated timeline', () => {
Public.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, rebloggedStatus)
expect(state.timeline).toEqual([rebloggedStatus, status2, status1])
})
})
})
})
})
})

View File

@ -1,110 +0,0 @@
import i18n from '~/src/config/i18n'
import Jump, { JumpState, MUTATION_TYPES, Channel } from '@/store/TimelineSpace/Modals/Jump'
import { LocalTag } from '~/src/types/localTag'
import { Entity } from 'megalodon'
describe('TimelineSpace/Modals/Jump', () => {
describe('mutations', () => {
let state: JumpState
beforeEach(() => {
state = {
modalOpen: true,
channel: '',
defaultChannelList: [
{
name: i18n.t('side_menu.home'),
path: 'home'
},
{
name: i18n.t('side_menu.notification'),
path: 'notifications'
},
{
name: i18n.t('side_menu.favourite'),
path: 'favourites'
},
{
name: i18n.t('side_menu.local'),
path: 'local'
},
{
name: i18n.t('side_menu.public'),
path: 'public'
},
{
name: i18n.t('side_menu.hashtag'),
path: 'hashtag'
},
{
name: i18n.t('side_menu.search'),
path: 'search'
},
{
name: i18n.t('side_menu.direct'),
path: 'direct-messages'
}
],
listChannelList: [],
tagChannelList: [],
selectedChannel: {
name: i18n.t('side_menu.home'),
path: 'home'
}
}
})
describe('updateListChannel', () => {
it('should be updated', () => {
const admin: Entity.List = {
id: '0',
title: 'admin'
}
const engineer: Entity.List = {
id: '1',
title: 'engineer'
}
const designer: Entity.List = {
id: '2',
title: 'designer'
}
const channelList = [admin, engineer, designer]
Jump.mutations![MUTATION_TYPES.UPDATE_LIST_CHANNEL](state, channelList)
const adminChannel: Channel = {
path: 'lists/0',
name: '#admin'
}
const engineerChannel: Channel = {
path: 'lists/1',
name: '#engineer'
}
const designerChannel: Channel = {
path: 'lists/2',
name: '#designer'
}
expect(state.listChannelList).toEqual([adminChannel, engineerChannel, designerChannel])
})
})
describe('updateTagChannel', () => {
it('should be updated', () => {
const whalebird: LocalTag = {
tagName: 'whalebird'
}
const tqrk: LocalTag = {
tagName: 'tqrk'
}
const channelList = [whalebird, tqrk]
Jump.mutations![MUTATION_TYPES.UPDATE_TAG_CHANNEL](state, channelList)
const whalebirdChannel: Channel = {
name: '#whalebird',
path: 'hashtag/whalebird'
}
const tqrkChannel: Channel = {
name: '#tqrk',
path: 'hashtag/tqrk'
}
expect(state.tagChannelList).toEqual([whalebirdChannel, tqrkChannel])
})
})
})
})

View File

@ -50,7 +50,6 @@
"expand": "Expand",
"home": "Home",
"notification": "Notification",
"mention": "Mention",
"direct": "Direct messages",
"follow_requests": "Follow Requests",
"favourite": "Favourite",
@ -64,7 +63,6 @@
"header_menu": {
"home": "Home",
"notification": "Notification",
"mention": "Mention",
"favourite": "Favourite",
"bookmark": "Bookmark",
"follow_requests": "Follow Requests",
@ -463,10 +461,9 @@
"timeline_fetch_error": "Failed to fetch timeline",
"notification_fetch_error": "Failed to fetch notification",
"favourite_fetch_error": "Failed to fetch favorite",
"bookmark_fetch_error": "Failed to fetch bookmarks",
"follow_request_accept_error": "Failed to accept the request",
"follow_request_reject_error": "Failed to reject the request",
"start_streaming_error": "Failed to start streaming",
"start_all_streamings_error": "Failed to start streaming of {{domain}}",
"attach_error": "Could not attach the file",
"authorize_duplicate_error": "Can not login the same account of the same domain",
"authorize_error": "Failed to authorize",

View File

@ -1,22 +1,7 @@
import { Setting, Timeline, UnreadNotification, UseMarker } from '~/src/types/setting'
const unreadNotification: UnreadNotification = {
direct: false,
local: true,
public: false
}
const useMarker: UseMarker = {
home: false,
notifications: true
}
const timeline: Timeline = {
unreadNotification: unreadNotification,
useMarker: useMarker
}
import { Setting } from '~/src/types/setting'
export const DefaultSetting: Setting = {
accountID: '',
timeline: timeline
accountId: 0,
markerHome: false,
markerNotifications: true
}

View File

@ -1,354 +0,0 @@
import { isEmpty } from 'lodash'
import generator, { detector, Entity, ProxyConfig } from 'megalodon'
import Datastore from 'nedb'
import log from 'electron-log'
import { LocalAccount } from '~/src/types/localAccount'
export default class Account {
private db: Datastore
constructor(db: Datastore) {
this.db = db
}
async initialize() {
await this.cleanup()
await this.reorder()
await this.updateUnique()
}
updateUnique(): Promise<{}> {
return new Promise((resolve, reject) => {
// At first, remove old index.
this.db.removeIndex('order', err => {
if (err) reject(err)
// Add unique index.
this.db.ensureIndex({ fieldName: 'order', unique: true, sparse: true }, err => {
if (err) reject(err)
resolve({})
})
})
})
}
/**
* Reorder accounts, because sometimes the order of accounts is duplicated.
*/
async reorder() {
const accounts = await this.listAllAccounts()
await Promise.all(
accounts.map(async (account, index) => {
const update = await this.updateAccount(account._id!, Object.assign(account, { order: index + 1 }))
return update
})
)
const ordered = await this.listAllAccounts()
return ordered
}
/**
* Check order of all accounts, and fix if order is negative value or over the length.
*/
async cleanup() {
const accounts = await this.listAccounts()
if (accounts.length < 1) {
return accounts.length
}
if (accounts[0].order < 1 || accounts[accounts.length - 1].order > accounts.length) {
await Promise.all(
accounts.map(async (element, index) => {
const update = await this.updateAccount(element._id!, Object.assign(element, { order: index + 1 }))
return update
})
)
}
return null
}
insertAccount(localAccount: LocalAccount): Promise<LocalAccount> {
return new Promise((resolve, reject) => {
this.db.insert<LocalAccount>(localAccount, (err, doc) => {
if (err) return reject(err)
if (isEmpty(doc)) return reject(new EmptyRecordError('empty'))
resolve(doc)
})
})
}
/**
* List up all accounts either authenticated or not authenticated.
*/
listAllAccounts(order = 1): Promise<Array<LocalAccount>> {
return new Promise((resolve, reject) => {
this.db
.find<LocalAccount>({})
.sort({ order: order })
.exec((err, docs) => {
if (err) return reject(err)
if (isEmpty(docs)) return reject(new EmptyRecordError('empty'))
resolve(docs)
})
})
}
/**
* List up authenticated accounts.
*/
listAccounts(): Promise<Array<LocalAccount>> {
return new Promise((resolve, reject) => {
this.db
.find<LocalAccount>({ $and: [{ accessToken: { $ne: '' } }, { accessToken: { $ne: null } }] })
.sort({ order: 1 })
.exec((err, docs) => {
if (err) return reject(err)
if (isEmpty(docs)) return reject(new EmptyRecordError('empty'))
resolve(docs)
})
})
}
// Get the last account.
async lastAccount(): Promise<LocalAccount> {
const accounts = await this.listAllAccounts(-1)
return accounts[0]
}
getAccount(id: string): Promise<LocalAccount> {
return new Promise((resolve, reject) => {
this.db.findOne<LocalAccount>(
{
_id: id
},
(err, doc) => {
if (err) return reject(err)
if (isEmpty(doc)) return reject(new EmptyRecordError('empty'))
resolve(doc)
}
)
})
}
searchAccount(obj: any): Promise<LocalAccount> {
return new Promise((resolve, reject) => {
this.db.findOne<LocalAccount>(obj, (err, doc) => {
if (err) return reject(err)
if (isEmpty(doc)) return reject(new EmptyRecordError('empty'))
resolve(doc)
})
})
}
searchAccounts(obj: any, order = 1): Promise<Array<LocalAccount>> {
return new Promise((resolve, reject) => {
this.db
.find<LocalAccount>(obj)
.sort({ order: order })
.exec((err, docs) => {
if (err) return reject(err)
resolve(docs)
})
})
}
updateAccount(id: string, obj: any): Promise<LocalAccount> {
return new Promise((resolve, reject) => {
this.db.update(
{
_id: id
},
{ $set: Object.assign(obj, { _id: id }) },
{ multi: true },
(err, _numReplaced) => {
if (err) return reject(err)
this.db.findOne<LocalAccount>(
{
_id: id
},
(err, doc) => {
if (err) return reject(err)
if (isEmpty(doc)) return reject(new EmptyRecordError('empty'))
resolve(doc)
}
)
}
)
})
}
removeAccount(id: string): Promise<string> {
return new Promise((resolve, reject) => {
this.db.remove(
{
_id: id
},
{ multi: true },
(err, _numRemoved) => {
if (err) return reject(err)
resolve(id)
}
)
})
}
removeAll(): Promise<number> {
return new Promise((resolve, reject) => {
this.db.remove({}, { multi: true }, (err, numRemoved) => {
if (err) return reject(err)
resolve(numRemoved)
})
})
}
async forwardAccount(ac: LocalAccount): Promise<LocalAccount | {}> {
// Find account which is the previous of the target account.
const accounts = await this.searchAccounts({ order: { $lt: ac.order } }, -1).catch(err => {
console.log(err)
return []
})
if (accounts.length < 1) {
return Promise.resolve({})
}
const previousAccount = accounts[0]
const targetOrder = ac.order
const previousOrder = previousAccount.order
// At first, we need to update the previous account with dummy order.
// Because this column is uniqued, so can not update with same order.
await this.updateAccount(
previousAccount._id!,
Object.assign(previousAccount, {
order: -1
})
)
// Change order of the target account.
const updated = await this.updateAccount(
ac._id!,
Object.assign(ac, {
order: previousOrder
})
)
// Update the previous account with right order.
await this.updateAccount(
previousAccount._id!,
Object.assign(previousAccount, {
order: targetOrder
})
)
return updated
}
async backwardAccount(ac: LocalAccount): Promise<LocalAccount | {}> {
// Find account which is the next of the target account.
const accounts = await this.searchAccounts({ order: { $gt: ac.order } }, 1).catch(err => {
console.log(err)
return []
})
if (accounts.length < 1) {
return Promise.resolve({})
}
const nextAccount = accounts[0]
const targetOrder = ac.order
const nextOrder = nextAccount.order
// At first, we need to update the next account with dummy order.
// Because this column is uniqued, so can not update with same order.
await this.updateAccount(
nextAccount._id!,
Object.assign(nextAccount, {
order: -1
})
)
// Change order of the target account/
const updated = await this.updateAccount(
ac._id!,
Object.assign(ac, {
order: nextOrder
})
)
// Update the next account with right order.
await this.updateAccount(
nextAccount._id!,
Object.assign(nextAccount, {
order: targetOrder
})
)
return updated
}
async refreshAccounts(proxy: ProxyConfig | false): Promise<Array<LocalAccount>> {
const accounts = await this.listAccounts()
if (accounts.length < 1) {
return accounts
}
const results = await Promise.all(
accounts.map(async account => {
const refresh = await this.refresh(account, proxy)
return refresh
})
)
return results
}
/**
* refresh: Refresh an account which is already saved at local
* @param {LocalAccount} account is an local account
* @return {LocalAccount} updated account
*/
async refresh(account: LocalAccount, proxy: ProxyConfig | false): Promise<LocalAccount> {
const sns = await detector(account.baseURL, proxy)
let client = generator(sns, account.baseURL, account.accessToken, 'Whalebird', proxy)
let json = {}
try {
const res = await client.verifyAccountCredentials()
json = {
username: res.data.username,
accountId: res.data.id,
avatar: res.data.avatar
}
} catch (err) {
log.error(err)
log.info('Get new access token using refresh token...')
// If failed to fetch account, get new access token using refresh token.
if (!account.refreshToken) {
throw new RefreshTokenDoesNotExist()
}
const token = await client.refreshToken(account.clientId, account.clientSecret, account.refreshToken)
client = generator(sns, account.baseURL, token.access_token, 'Whalebird', proxy)
const res = await client.verifyAccountCredentials()
json = {
username: res.data.username,
accountId: res.data.id,
avatar: res.data.avatar,
accessToken: token.accessToken,
refreshToken: token.refreshToken
}
}
return this.updateAccount(account._id!, json)
}
// Confirm the access token, and check duplicate
async fetchAccount(
sns: 'mastodon' | 'pleroma' | 'misskey',
account: LocalAccount,
accessToken: string,
proxy: ProxyConfig | false
): Promise<Entity.Account> {
const client = generator(sns, account.baseURL, accessToken, 'Whalebird', proxy)
const res = await client.verifyAccountCredentials()
const query = {
baseURL: account.baseURL,
username: res.data.username
}
const duplicates = await this.searchAccounts(query)
if (duplicates.length > 0) {
throw new DuplicateRecordError(`${res.data.username}@${account.baseURL} is duplicated`)
}
return res.data
}
}
class EmptyRecordError extends Error {}
class DuplicateRecordError extends Error {}
class RefreshTokenDoesNotExist extends Error {}

View File

@ -1,119 +0,0 @@
import generator, { ProxyConfig } from 'megalodon'
import crypto from 'crypto'
import Account from './account'
import { LocalAccount } from '~/src/types/localAccount'
const appName = 'Whalebird'
const appURL = 'https://whalebird.social'
export default class Authentication {
private db: Account
private baseURL: string | null = null
private domain: string | null = null
private clientId: string | null = null
private clientSecret: string | null = null
private sessionToken: string | null = null
private protocol: 'http' | 'https'
constructor(accountDB: Account) {
this.db = accountDB
this.protocol = 'https'
}
setOtherInstance(domain: string) {
this.baseURL = `${this.protocol}://${domain}`
this.domain = domain
this.clientId = null
this.clientSecret = null
}
async getAuthorizationUrl(
sns: 'mastodon' | 'pleroma' | 'misskey',
domain: string = 'mastodon.social',
proxy: ProxyConfig | false
): Promise<string> {
this.setOtherInstance(domain)
if (!this.baseURL || !this.domain) {
throw new Error('domain is required')
}
const client = generator(sns, this.baseURL, null, 'Whalebird', proxy)
const res = await client.registerApp(appName, {
website: appURL
})
this.clientId = res.clientId
this.clientSecret = res.clientSecret
this.sessionToken = res.session_token
const order = await this.db
.lastAccount()
.then(account => account.order + 1)
.catch(err => {
console.log(err)
return 1
})
const local: LocalAccount = {
baseURL: this.baseURL,
domain: this.domain,
clientId: this.clientId,
clientSecret: this.clientSecret,
accessToken: null,
refreshToken: null,
username: null,
accountId: null,
avatar: null,
order: order
}
await this.db.insertAccount(local)
if (res.url === null) {
throw new AuthenticationURLError('Can not get url')
}
return res.url
}
async getAndUpdateAccessToken(sns: 'mastodon' | 'pleroma' | 'misskey', code: string | null, proxy: ProxyConfig | false): Promise<string> {
if (!this.baseURL) {
throw new Error('domain is required')
}
if (!this.clientSecret) {
throw new Error('client secret is required')
}
const client = generator(sns, this.baseURL, null, 'Whalebird', proxy)
// In Misskey session token is required instead of authentication code.
let authCode = code
if (!code) {
authCode = this.sessionToken
}
if (!authCode) {
throw new Error('auth code is required')
}
const tokenData = await client.fetchAccessToken(this.clientId, this.clientSecret, authCode, 'urn:ietf:wg:oauth:2.0:oob')
const search = {
baseURL: this.baseURL,
domain: this.domain,
clientId: this.clientId,
clientSecret: this.clientSecret
}
const rec = await this.db.searchAccount(search)
let accessToken = tokenData.accessToken
// In misskey, access token is sha256(userToken + clientSecret)
if (sns === 'misskey') {
accessToken = crypto
.createHash('sha256')
.update(tokenData.accessToken + this.clientSecret, 'utf8')
.digest('hex')
}
const refreshToken = tokenData.refreshToken
const data = await this.db.fetchAccount(sns, rec, accessToken, proxy)
await this.db.updateAccount(rec._id!, {
username: data.username,
accountId: data.id,
avatar: data.avatar,
accessToken: accessToken,
refreshToken: refreshToken
})
return accessToken
}
}
class AuthenticationURLError extends Error {}

View File

@ -1,48 +0,0 @@
import { isEmpty } from 'lodash'
import Datastore from 'nedb'
import fs from 'fs'
import { CachedAccount } from '~/src/types/cachedAccount'
export default class AccountCache {
private db: Datastore
constructor(path: string) {
this.db = new Datastore({
filename: path,
autoload: true,
onload: (err: Error) => {
if (err) {
fs.unlink(path, err => {
if (err) {
console.error(err)
}
})
}
}
})
}
listAccounts(ownerID: string): Promise<Array<CachedAccount>> {
return new Promise((resolve, reject) => {
this.db.find<CachedAccount>({ owner_id: ownerID }, (err, docs) => {
if (err) return reject(err)
resolve(docs)
})
})
}
insertAccount(ownerID: string, acct: string): Promise<CachedAccount> {
return new Promise((resolve, reject) => {
// At first confirm records for unique.
this.db.findOne<CachedAccount>({ 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<CachedAccount>({ owner_id: ownerID, acct: acct }, (err, doc) => {
if (err) return reject(err)
return resolve(doc)
})
})
})
}
}

View File

@ -1,45 +0,0 @@
import Datastore from 'nedb'
import fs from 'fs'
import { LocalTag } from '~/src/types/localTag'
export default class HashtagCache {
private db: Datastore
constructor(path: string) {
this.db = new Datastore({
filename: path,
autoload: true,
onload: (err: Error) => {
if (err) {
fs.unlink(path, err => {
if (err) {
console.error(err)
}
})
}
}
})
this.db.ensureIndex({ fieldName: 'tagName', unique: true, sparse: true }, err => {
if (err) console.error(err)
})
}
listTags(): Promise<Array<LocalTag>> {
return new Promise((resolve, reject) => {
this.db.find<LocalTag>({}, (err, docs) => {
if (err) return reject(err)
resolve(docs)
})
})
}
insertHashtag(tag: string): Promise<LocalTag> {
return new Promise((resolve, reject) => {
// Ignore error for unique constraints.
this.db.insert({ tagName: tag }, (err, doc) => {
if (err) return reject(err)
resolve(doc)
})
})
}
}

View File

@ -1,22 +1,69 @@
import Loki from 'lokijs'
import sqlite3 from 'sqlite3'
const newDB = (file: string): Promise<Loki> => {
return new Promise(resolve => {
const databaseInitializer = () => {
let markers = db.getCollection('markers')
if (markers === null) {
markers = db.addCollection('markers')
const newDB = (file: string): sqlite3.Database => {
const db = new sqlite3.Database(file, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE)
// migration
db.serialize(() => {
db.run(
'CREATE TABLE IF NOT EXISTS accounts(\
id INTEGER PRIMARY KEY, \
username TEXT NOT NULL, \
account_id TEXT NOT NULL, \
avatar TEXT NOT NULL, \
client_id TEXT DEFAULT NULL, \
client_secret TEXT NOT NULL, \
access_token TEXT NOT NULL, \
refresh_token TEXT DEFAULT NULL, \
sort INTEGER UNIQUE NOT NULL)',
err => {
if (err) {
console.error('failed to create accounts: ', err)
}
}
resolve(db)
}
const db = new Loki(file, {
autoload: true,
autosave: true,
autosaveInterval: 4000,
autoloadCallback: databaseInitializer
})
)
db.run(
'CREATE TABLE IF NOT EXISTS servers(\
id INTEGER PRIMARY KEY, \
domain TEXT NOT NULL, \
base_url TEXT NOT NULL, \
sns TEXT NOT NULL, \
account_id INTEGER UNIQUE DEFAULT NULL, \
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE)',
err => {
if (err) {
console.error('failed to create servers: ', err)
}
}
)
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)
}
}
)
db.run(
'CREATE TABLE IF NOT EXISTS settings(\
id INTEGER PRIMARY KEY, \
account_id INTEGER UNIQUE NOT NULL, \
marker_home BOOLEAN NOT NULL DEFAULT false, \
marker_notifications BOOLEAN NOT NULL DEFAULT true, \
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE)',
err => {
if (err) {
console.error('failed to create settings: ', err)
}
}
)
})
return db
}
export default newDB

285
src/main/db/account.ts Normal file
View File

@ -0,0 +1,285 @@
import sqlite3 from 'sqlite3'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~src/types/localServer'
export const insertAccount = (
db: sqlite3.Database,
username: string,
accountId: string,
avatar: string,
clientId: string,
clientSecret: string,
accessToken: string,
refreshToken: string | null,
server: LocalServer
): Promise<LocalAccount> => {
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('BEGIN TRANSACTION')
db.get('SELECT * FROM accounts ORDER BY sort DESC', (err, row) => {
if (err) {
reject(err)
}
let order = 1
if (row) {
order = row.sort + 1
}
db.run(
'INSERT INTO accounts(username, account_id, avatar, client_id, client_secret, access_token, refresh_token, sort) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[username, accountId, avatar, clientId, clientSecret, accessToken, refreshToken, order],
function (err) {
if (err) {
reject(err)
}
const id = this.lastID
db.run('UPDATE servers SET account_id = ? WHERE id = ?', [id, server.id], err => {
if (err) {
reject(err)
}
db.run('COMMIT')
resolve({
id,
username,
accountId,
avatar,
clientId,
clientSecret,
accessToken,
refreshToken,
order
})
})
}
)
})
})
})
}
/**
* List up authenticated accounts.
*/
export const listAccounts = (db: sqlite3.Database): Promise<Array<[LocalAccount, LocalServer]>> => {
return new Promise((resolve, reject) => {
db.all(
'SELECT \
accounts.id as id, \
accounts.username as username, \
accounts.account_id as remote_account_id, \
accounts.avatar as avatar, \
accounts.client_id as client_id, \
accounts.client_secret as client_secret, \
accounts.access_token as access_token, \
accounts.refresh_token as refresh_token, \
accounts.sort as sort, \
servers.id as server_id, \
servers.base_url as base_url, \
servers.domain as domain, \
servers.sns as sns, \
servers.account_id as account_id \
FROM accounts INNER JOIN servers ON servers.account_id = accounts.id ORDER BY accounts.sort',
(err, rows) => {
if (err) {
reject(err)
}
resolve(
rows.map(r => [
{
id: r.id,
username: r.username,
accountId: r.remote_account_id,
avatar: r.avatar,
clientId: r.client_id,
clientSecret: r.client_secret,
accessToken: r.access_token,
refreshToken: r.refresh_token,
order: r.sort
} as LocalAccount,
{
id: r.server_id,
baseURL: r.base_url,
domain: r.domain,
sns: r.sns,
accountId: r.account_id
} as LocalServer
])
)
}
)
})
}
export const getAccount = (db: sqlite3.Database, id: number): Promise<[LocalAccount, LocalServer]> => {
return new Promise((resolve, reject) => {
db.get(
'SELECT \
accounts.id as id, \
accounts.username as username, \
accounts.account_id as remote_account_id, \
accounts.avatar as avatar, \
accounts.client_id as client_id, \
accounts.client_secret as client_secret, \
accounts.access_token as access_token, \
accounts.refresh_token as refresh_token, \
accounts.sort as sort, \
servers.id as server_id, \
servers.base_url as base_url, \
servers.domain as domain, \
servers.sns as sns, \
servers.account_id as account_id \
FROM accounts INNER JOIN servers ON servers.account_id = accounts.id WHERE accounts.id = ?',
id,
(err, r) => {
if (err) {
reject(err)
}
resolve([
{
id: r.id,
username: r.username,
accountId: r.remote_account_id,
avatar: r.avatar,
clientId: r.client_id,
clientSecret: r.client_secret,
accessToken: r.access_token,
refreshToken: r.refresh_token,
order: r.sort
} as LocalAccount,
{
id: r.server_id,
baseURL: r.base_url,
domain: r.domain,
sns: r.sns,
accountId: r.account_id
} as LocalServer
])
}
)
})
}
export const removeAccount = (db: sqlite3.Database, id: number): Promise<null> => {
return new Promise((resolve, reject) => {
db.run('DELETE FROM accounts WHERE id = ?', id, err => {
if (err) {
reject(err)
}
resolve(null)
})
})
}
export const removeAllAccounts = (db: sqlite3.Database): Promise<null> => {
return new Promise((resolve, reject) => {
db.run('DELETE FROM accounts', err => {
if (err) {
reject(err)
}
resolve(null)
})
})
}
export const forwardAccount = (db: sqlite3.Database, id: number): Promise<null> => {
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('BEGIN TRANSACTION')
db.all('SELECT * FROM accounts ORDER BY sort', (err, rows) => {
if (err) {
console.error(err)
db.run('ROLLBACK TRANSACTION')
return reject(err)
}
const index = rows.findIndex(r => r.id === id)
if (index < 0 || index >= rows.length - 1) {
db.run('ROLLBACK TRANSACTION')
return resolve(null)
}
const target = rows[index + 1]
const base = rows[index]
db.serialize(() => {
db.run('UPDATE accounts SET sort = ? WHERE id = ?', [-100, base.id], err => {
if (err) {
console.error(err)
db.run('ROLLBACK TRANSACTION')
return reject(err)
}
})
db.run('UPDATE accounts SET sort = ? WHERE id = ?', [base.sort, target.id], err => {
if (err) {
console.error(err)
db.run('ROLLBACK TRANSACTION')
return reject(err)
}
})
db.run('UPDATE accounts SET sort = ? WHERE id = ?', [target.sort, base.id], err => {
if (err) {
console.error(err)
db.run('ROLLBACK TRANSACTION')
return reject(err)
}
db.run('COMMIT')
return resolve(null)
})
})
})
})
})
}
export const backwardAccount = (db: sqlite3.Database, id: number): Promise<null> => {
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('BEGIN TRANSACTION')
db.all('SELECT * FROM accounts ORDER BY sort', (err, rows) => {
if (err) {
console.error(err)
db.run('ROLLBACK TRANSACTION')
return reject(err)
}
const index = rows.findIndex(r => r.id === id)
if (index < 1) {
db.run('ROLLBACK TRANSACTION')
return resolve(null)
}
const target = rows[index - 1]
const base = rows[index]
db.serialize(() => {
db.run('UPDATE accounts SET sort = ? WHERE id = ?', [-100, base.id], err => {
if (err) {
console.error(err)
db.run('ROLLBACK TRANSACTION')
return reject(err)
}
})
db.run('UPDATE accounts SET sort = ? WHERE id = ?', [base.sort, target.id], err => {
if (err) {
console.error(err)
db.run('ROLLBACK TRANSACTION')
return reject(err)
}
})
db.run('UPDATE accounts SET sort = ? WHERE id = ?', [target.sort, base.id], err => {
if (err) {
console.error(err)
db.run('ROLLBACK TRANSACTION')
return reject(err)
}
db.run('COMMIT')
return resolve(null)
})
})
})
})
})
}

63
src/main/db/hashtags.ts Normal file
View File

@ -0,0 +1,63 @@
import sqlite3 from 'sqlite3'
import { LocalTag } from '~/src/types/localTag'
export const listTags = (db: sqlite3.Database, accountId: number): Promise<Array<LocalTag>> => {
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
}))
)
})
})
}
export const insertTag = (db: sqlite3.Database, accountId: number, tag: string): Promise<LocalTag> => {
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('BEGIN TRANSACTION')
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
})
}
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
})
})
})
})
})
}
export const removeTag = (db: sqlite3.Database, tag: LocalTag): Promise<null> => {
return new Promise((resolve, reject) => {
db.run('DELETE FROM hashtags WHERE id = ?', tag.id, err => {
if (err) {
reject(err)
}
resolve(null)
})
})
}

29
src/main/db/server.ts Normal file
View File

@ -0,0 +1,29 @@
import sqlite3 from 'sqlite3'
import { LocalServer } from '~/src/types/localServer'
export const insertServer = (
db: sqlite3.Database,
baseURL: string,
domain: string,
sns: 'mastodon' | 'pleroma' | 'misskey',
accountId: number | null
): Promise<LocalServer> => {
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('INSERT INTO servers(domain, base_url, sns, account_id) values (?, ?, ?, ?)', [domain, baseURL, sns, accountId], function (
err
) {
if (err) {
reject(err)
}
resolve({
id: this.lastID,
baseURL,
domain,
sns,
accountId
})
})
})
})
}

55
src/main/db/setting.ts Normal file
View File

@ -0,0 +1,55 @@
import sqlite3 from 'sqlite3'
import { Setting } from '~/src/types/setting'
import { DefaultSetting } from '~/src/constants/initializer/setting'
export const getSetting = (db: sqlite3.Database, accountId: number): Promise<Setting> => {
return new Promise((resolve, reject) => {
db.get('SELECT * FROM settings WHERE account_id = ?', accountId, (err, row) => {
if (err) {
reject(err)
}
if (row) {
resolve({
accountId: row.account_id,
markerHome: Boolean(row.marker_home),
markerNotifications: Boolean(row.marker_notifications)
})
}
resolve(DefaultSetting)
})
})
}
export const createOrUpdateSetting = (db: sqlite3.Database, setting: Setting): Promise<Setting> => {
return new Promise((resolve, reject) => {
db.get('SELECT * FROM settings WHERE account_id = ?', setting.accountId, (err, row) => {
if (err) {
reject(err)
}
if (row) {
db.run(
'UPDATE settings SET marker_home = ?, marker_notifications = ? WHERE account_id = ?',
[setting.markerHome, setting.markerNotifications, setting.accountId],
err => {
if (err) {
reject(err)
}
resolve(setting)
}
)
resolve(setting)
} else {
db.run(
'INSERT INTO settings(account_id, marker_home, marker_notifications) VALUES (?, ?, ?)',
[setting.accountId, setting.markerHome, setting.markerNotifications],
function (err) {
if (err) {
reject(err)
}
resolve(setting)
}
)
}
})
})
}

View File

@ -1,44 +0,0 @@
import Datastore from 'nedb'
import { LocalTag } from '~/src/types/localTag'
export default class Hashtags {
private db: Datastore
constructor(db: Datastore) {
this.db = db
this.db.ensureIndex({ fieldName: 'tagName', unique: true }, _ => {})
}
listTags(): Promise<Array<LocalTag>> {
return new Promise((resolve, reject) => {
this.db.find<LocalTag>({}, (err, docs) => {
if (err) return reject(err)
resolve(docs)
})
})
}
insertTag(tag: string): Promise<LocalTag> {
return new Promise((resolve, reject) => {
this.db.insert({ tagName: tag }, (err, doc) => {
if (err) return reject(err)
resolve(doc)
})
})
}
removeTag(localTag: LocalTag): Promise<number> {
return new Promise((resolve, reject) => {
this.db.remove(
{
tagName: localTag.tagName
},
{ multi: true },
(err, numRemoved) => {
if (err) return reject(err)
resolve(numRemoved)
}
)
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,77 +0,0 @@
import { isEmpty } from 'lodash'
import Loki, { Collection } from 'lokijs'
import { LocalMarker } from '~/src/types/localMarker'
export default class Marker {
private markers: Collection<any>
constructor(db: Loki) {
this.markers = db.getCollection('markers')
}
private insert(marker: LocalMarker): Promise<LocalMarker> {
return new Promise((resolve, reject) => {
try {
const doc: LocalMarker = this.markers.insert(marker)
resolve(doc)
} catch (err) {
reject(err)
}
})
}
private update(marker: LocalMarker): Promise<LocalMarker> {
// @ts-ignore
return new Promise((resolve, reject) => {
// eslint-disable-line no-unused-vars
try {
this.markers.findAndUpdate(
{
owner_id: { $eq: marker.owner_id },
timeline: { $eq: marker.timeline }
},
(item: LocalMarker) => {
item.last_read_id = marker.last_read_id
}
)
return this.get(marker.owner_id, marker.timeline)
} catch (err) {
reject(err)
}
})
}
public async save(marker: LocalMarker): Promise<LocalMarker> {
return this.get(marker.owner_id, marker.timeline).then(l => {
if (isEmpty(l)) return this.insert(marker)
return this.update(marker)
})
}
public async get(owner_id: string, timeline: 'home' | 'notifications' | 'mentions'): Promise<LocalMarker | null> {
return new Promise((resolve, reject) => {
try {
const doc: LocalMarker | null = this.markers.findOne({
owner_id: { $eq: owner_id },
timeline: { $eq: timeline }
})
resolve(doc)
} catch (err) {
reject(err)
}
})
}
public async list(owner_id: string): Promise<Array<LocalMarker>> {
return new Promise((resolve, reject) => {
try {
const docs: Array<LocalMarker> = this.markers.find({
owner_id: { $eq: owner_id }
})
resolve(docs)
} catch (err) {
reject(err)
}
})
}
}

View File

@ -1,80 +0,0 @@
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 { DefaultSetting } 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 objectAssignDeep({}, DefaultSetting, find)
}
return objectAssignDeep({}, DefaultSetting, {
accountID: accountID
})
}
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,62 +0,0 @@
import generator, { detector, ProxyConfig } from 'megalodon'
import { LocalAccount } from '~/src/types/localAccount'
import { EnabledTimelines } from '~/src/types/enabledTimelines'
const confirm = async (account: LocalAccount, proxy: ProxyConfig | false) => {
const sns = await detector(account.baseURL, proxy)
const client = generator(sns, account.baseURL, account.accessToken, 'Whalebird', proxy)
let timelines: EnabledTimelines = {
home: true,
notification: true,
mention: true,
direct: true,
favourite: true,
bookmark: true,
local: true,
public: true,
tag: true,
list: true
}
const notification = async () => {
return client.getNotifications({ limit: 1 }).catch(() => {
timelines = { ...timelines, notification: false, mention: false }
})
}
const direct = async () => {
return client.getConversationTimeline({ limit: 1 }).catch(() => {
timelines = { ...timelines, direct: false }
})
}
const favourite = async () => {
return client.getFavourites({ limit: 1 }).catch(() => {
timelines = { ...timelines, favourite: false }
})
}
const bookmark = async () => {
return client.getBookmarks({ limit: 1 }).catch(() => {
timelines = { ...timelines, bookmark: false }
})
}
const local = async () => {
return client.getLocalTimeline({ limit: 1 }).catch(() => {
timelines = { ...timelines, local: false }
})
}
const pub = async () => {
return client.getPublicTimeline({ limit: 1 }).catch(() => {
timelines = { ...timelines, public: false }
})
}
const tag = async () => {
return client.getTagTimeline('whalebird', { limit: 1 }).catch(() => {
timelines = { ...timelines, tag: false }
})
}
await Promise.all([notification(), direct(), favourite(), bookmark(), local(), pub(), tag()])
return timelines
}
export default confirm

View File

@ -1,16 +1,18 @@
import generator, { MegalodonInterface, WebSocketInterface, Entity, ProxyConfig } from 'megalodon'
import log from 'electron-log'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~src/types/localServer'
const StreamingURL = async (
sns: 'mastodon' | 'pleroma' | 'misskey',
account: LocalAccount,
server: LocalServer,
proxy: ProxyConfig | false
): Promise<string> => {
if (!account.accessToken) {
throw new Error('access token is empty')
}
const client = generator(sns, account.baseURL, account.accessToken, 'Whalebird', proxy)
const client = generator(sns, server.baseURL, account.accessToken, 'Whalebird', proxy)
const res = await client.getInstance()
return res.data.urls.streaming_api
}

View File

@ -1,191 +0,0 @@
<template>
<div id="authorize">
<div>
<el-header>
<el-row>
<el-col :span="24" class="close">
<el-button class="close-button" link @click="close">
<font-awesome-icon icon="xmark"></font-awesome-icon>
</el-button>
</el-col>
</el-row>
</el-header>
<el-main>
<div class="authorization-url">
<p>{{ $t('authorize.manually_1') }}</p>
<p>{{ $t('authorize.manually_2') }}</p>
<p class="url">{{ $route.query.url }}</p>
</div>
<el-form
ref="form"
:model="authorizeForm"
label-width="120px"
label-position="top"
class="authorize-form"
v-on:submit.prevent="authorizeSubmit"
>
<p v-if="sns === 'misskey'">{{ $t('authorize.misskey_label') }}</p>
<el-form-item :label="$t('authorize.code_label')" v-else>
<el-input v-model="authorizeForm.code"></el-input>
</el-form-item>
<!-- Dummy form to guard submitting with enter -->
<el-form-item class="hidden">
<el-input></el-input>
</el-form-item>
<el-form-item class="submit">
<el-button
v-loading="submitting"
type="primary"
class="authorize"
element-loading-background="rgba(0, 0, 0, 0.8)"
@click="authorizeSubmit"
>
{{ $t('authorize.submit') }}
</el-button>
</el-form-item>
</el-form>
</el-main>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, toRefs, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18next } from 'vue3-i18next'
import { ElMessage } from 'element-plus'
import { useMagicKeys, whenever } from '@vueuse/core'
import { useStore } from '@/store'
import { ACTION_TYPES } from '@/store/Authorize'
export default defineComponent({
name: 'authorize',
props: {
url: {
type: String,
default: ''
},
sns: {
type: String,
default: 'mastodon'
}
},
setup(props) {
const space = 'Authorize'
const store = useStore()
const router = useRouter()
const i18n = useI18next()
const { escape } = useMagicKeys()
const { url, sns } = toRefs(props)
const authorizeForm = reactive({
code: null
})
const submitting = ref<boolean>(false)
onMounted(() => {
console.log(url.value)
})
whenever(escape, () => {
close()
})
const authorizeSubmit = () => {
submitting.value = true
store
.dispatch(`${space}/${ACTION_TYPES.SUBMIT}`, {
code: authorizeForm.code,
sns: sns.value
})
.finally(() => {
submitting.value = false
})
.then(id => {
router.push({ path: `/${id}/home` })
})
.catch(err => {
if (err.name === 'DuplicateRecordError') {
ElMessage({
message: i18n.t('message.authorize_duplicate_error'),
type: 'error'
})
} else {
ElMessage({
message: i18n.t('message.authorize_error'),
type: 'error'
})
}
})
}
const close = () => {
router.push({ path: '/', query: { redirect: 'home' } })
}
return {
authorizeForm,
submitting,
authorizeSubmit,
close
}
}
})
</script>
<style lang="scss" scoped>
#authorize {
background-color: #292f3f;
color: #fff;
text-align: center;
min-height: 100%;
.close {
text-align: right;
.close-button {
font-size: 24px;
}
}
.authorization-url {
margin: 0 auto 64px;
max-width: 80%;
.url {
color: #909399;
word-wrap: break-word;
}
}
.authorize-form {
width: 500px;
margin: 0 auto;
.authorize {
margin: 0 auto;
}
}
.authorize-form :deep() {
.el-form-item__label {
color: #f0f3f9;
}
.el-input__inner {
background-color: #373d48;
color: #fff;
border: 0;
}
.el-input__wrapper {
background-color: #373d48;
}
}
.hidden {
display: none;
}
}
</style>

View File

@ -12,16 +12,15 @@
role="menubar"
>
<el-menu-item
:index="`/${account._id}/`"
:route="{ path: `/${account._id}/home` }"
v-for="(account, _index) in accounts"
v-bind:key="account._id"
:index="`/${account.id}/`"
:route="{ path: `/${account.id}/home` }"
v-for="([account, server], _index) in accounts"
:key="account.id"
role="menuitem"
>
<font-awesome-icon icon="circle-user" v-if="account.avatar === undefined || account.avatar === null || account.avatar === ''" />
<FailoverImg v-else :src="account.avatar" class="avatar" :title="account.username + '@' + account.domain" />
<FailoverImg :src="`${account.baseURL}/favicon.ico`" :failoverSrc="`${account.baseURL}/favicon.png`" class="instance-icon" />
<span slot="title">{{ account.domain }}</span>
<FailoverImg :src="account.avatar" class="avatar" :title="account.username + '@' + server.domain" />
<FailoverImg :src="`${server.baseURL}/favicon.ico`" :failoverSrc="`${server.baseURL}/favicon.png`" class="instance-icon" />
<span slot="title">{{ server.domain }}</span>
</el-menu-item>
<el-menu-item index="/login" :title="$t('global_header.add_new_account')" role="menuitem" class="add-new-account">
<font-awesome-icon icon="plus" />
@ -37,11 +36,8 @@
<script lang="ts">
import { defineComponent, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useI18next } from 'vue3-i18next'
import { useStore } from '@/store'
import FailoverImg from '@/components/atoms/FailoverImg.vue'
import { StreamingError } from '~/src/errors/streamingError'
import { ACTION_TYPES } from '@/store/GlobalHeader'
export default defineComponent({
@ -54,7 +50,6 @@ export default defineComponent({
const store = useStore()
const route = useRoute()
const router = useRouter()
const i18n = useI18next()
const accounts = computed(() => store.state.GlobalHeader.accounts)
const hide = computed(() => store.state.GlobalHeader.hide)
@ -67,20 +62,10 @@ export default defineComponent({
const initialize = async () => {
await store
.dispatch(`${space}/initLoad`)
.dispatch(`${space}/${ACTION_TYPES.INIT_LOAD}`)
.then(accounts => {
store.dispatch(`${space}/${ACTION_TYPES.START_STREAMINGS}`).catch(err => {
if (err instanceof StreamingError) {
ElMessage({
message: i18n.t('message.start_all_streamings_error', {
domain: err.domain
}),
type: 'error'
})
}
})
if (route.params.id === undefined) {
router.push({ path: `/${accounts[0]._id}/home` })
router.push({ path: `/${accounts[0][0].id}/home` })
}
})
.catch(_ => {

View File

@ -10,29 +10,32 @@
</el-row>
</el-header>
<el-container>
<div></div>
<login-form></login-form>
<login-form v-if="appData === null" />
<authorize v-else />
</el-container>
</el-container>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { defineComponent, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from '@/store'
import { useMagicKeys, whenever } from '@vueuse/core'
import LoginForm from './Login/LoginForm.vue'
import Authorize from './Login/Authorize.vue'
import { ACTION_TYPES } from '@/store/Login'
export default defineComponent({
name: 'login',
components: { LoginForm },
components: { LoginForm, Authorize },
setup() {
const space = 'Login'
const store = useStore()
const router = useRouter()
const { escape } = useMagicKeys()
const appData = computed(() => store.state.Login.appData)
whenever(escape, () => {
close()
})
@ -46,7 +49,8 @@ export default defineComponent({
}
return {
close
close,
appData
}
}
})

View File

@ -0,0 +1,156 @@
<template>
<el-main id="authorize">
<div class="authorization-url">
<p>{{ $t('authorize.manually_1') }}</p>
<p>{{ $t('authorize.manually_2') }}</p>
<p class="url">{{ $route.query.url }}</p>
</div>
<el-form
ref="form"
:model="authorizeForm"
label-width="120px"
label-position="top"
class="authorize-form"
@submit.prevent="authorizeSubmit"
>
<p v-if="sns === 'misskey'">{{ $t('authorize.misskey_label') }}</p>
<el-form-item :label="$t('authorize.code_label')" v-else>
<el-input v-model="authorizeForm.code"></el-input>
</el-form-item>
<!-- Dummy form to guard submitting with enter -->
<el-form-item class="hidden">
<el-input></el-input>
</el-form-item>
<el-form-item class="submit">
<el-button
v-loading="submitting"
type="primary"
class="authorize"
element-loading-background="rgba(0, 0, 0, 0.8)"
@click="authorizeSubmit"
>
{{ $t('authorize.submit') }}
</el-button>
</el-form-item>
</el-form>
</el-main>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useI18next } from 'vue3-i18next'
import { ElMessage } from 'element-plus'
import { useMagicKeys, whenever } from '@vueuse/core'
import { useStore } from '@/store'
import { ACTION_TYPES } from '@/store/Login'
export default defineComponent({
name: 'Authorize',
setup() {
const space = 'Login'
const store = useStore()
const router = useRouter()
const i18n = useI18next()
const { escape } = useMagicKeys()
const sns = computed(() => store.state.Login.sns)
const authorizeForm = reactive({
code: null
})
const submitting = ref<boolean>(false)
whenever(escape, () => {
close()
})
const authorizeSubmit = () => {
submitting.value = true
store
.dispatch(`${space}/${ACTION_TYPES.AUTHORIZE}`, authorizeForm.code)
.finally(() => {
submitting.value = false
})
.then(id => {
router.push({ path: `/${id}/home` })
})
.catch(err => {
console.error(err)
ElMessage({
message: i18n.t('message.authorize_error'),
type: 'error'
})
})
}
const close = () => {
router.push({ path: '/', query: { redirect: 'home' } })
}
return {
authorizeForm,
submitting,
authorizeSubmit,
close,
sns
}
}
})
</script>
<style lang="scss" scoped>
#authorize {
background-color: #292f3f;
color: #fff;
text-align: center;
min-height: 100%;
.close {
text-align: right;
.close-button {
font-size: 24px;
}
}
.authorization-url {
margin: 0 auto 64px;
max-width: 80%;
.url {
color: #909399;
word-wrap: break-word;
}
}
.authorize-form {
width: 500px;
margin: 0 auto;
.authorize {
margin: 0 auto;
}
}
.authorize-form :deep() {
.el-form-item__label {
color: #f0f3f9;
}
.el-input__inner {
background-color: #373d48;
color: #fff;
border: 0;
}
.el-input__wrapper {
background-color: #373d48;
}
}
.hidden {
display: none;
}
}
</style>

View File

@ -40,7 +40,6 @@
import { defineComponent, computed, reactive, ref } from 'vue'
import { useI18next } from 'vue3-i18next'
import { ElLoading, ElMessage, FormInstance, FormRules } from 'element-plus'
import { useRouter } from 'vue-router'
import { useStore } from '@/store'
import { domainFormat } from '@/utils/validator'
import { ACTION_TYPES } from '@/store/Login'
@ -51,16 +50,14 @@ export default defineComponent({
const space = 'Login'
const store = useStore()
const i18n = useI18next()
const router = useRouter()
const form = reactive({
domainName: ''
})
const loginFormRef = ref<FormInstance>()
const selectedInstance = computed(() => store.state.Login.selectedInstance)
const selectedInstance = computed(() => store.state.Login.domain)
const searching = computed(() => store.state.Login.searching)
const sns = computed(() => store.state.Login.sns)
const allowLogin = computed(() => selectedInstance.value && form.domainName === selectedInstance.value)
const rules = reactive<FormRules>({
domainName: [
@ -77,31 +74,24 @@ export default defineComponent({
]
})
const login = () => {
const login = async () => {
const loading = ElLoading.service({
lock: true,
text: i18n.t('message.loading'),
background: 'rgba(0, 0, 0, 0.7)'
})
store
.dispatch(`${space}/${ACTION_TYPES.FETCH_LOGIN}`)
.then(url => {
store.dispatch(`${space}/${ACTION_TYPES.PAGE_BACK}`)
router.push({
path: '/authorize',
query: { url: url, sns: sns.value }
})
})
.catch(err => {
ElMessage({
message: i18n.t('message.authorize_url_error'),
type: 'error'
})
console.error(err)
})
.finally(() => {
loading.close()
try {
await store.dispatch(`${space}/${ACTION_TYPES.ADD_SERVER}`)
await store.dispatch(`${space}/${ACTION_TYPES.ADD_APP}`)
} catch (err) {
ElMessage({
message: i18n.t('message.authorize_url_error'),
type: 'error'
})
console.error(err)
} finally {
loading.close()
}
}
const confirm = async (formEl: FormInstance | undefined) => {

View File

@ -16,7 +16,16 @@
<el-table-column prop="domain" :label="$t('preferences.account.domain')"> </el-table-column>
<el-table-column :label="$t('preferences.account.association')">
<template #default="scope">
<el-button class="action" link @click.prevent="removeAccount(scope.$index, accounts)">
<el-button
class="action"
link
@click.prevent="
removeAccount(
scope.$index,
accounts.map(a => a.id)
)
"
>
<font-awesome-icon icon="xmark" />
{{ $t('preferences.account.remove_association') }}
</el-button>
@ -25,12 +34,30 @@
<el-table-column :label="$t('preferences.account.order')" width="60">
<template #default="scope">
<div class="allow-up">
<el-button class="arrow-up action" link @click.prevent="forward(scope.$index, accounts)">
<el-button
class="arrow-up action"
link
@click.prevent="
backward(
scope.$index,
accounts.map(a => a.id)
)
"
>
<font-awesome-icon icon="arrow-up" />
</el-button>
</div>
<div class="allow-down">
<el-button class="arrow-down action" link @click.prevent="backward(scope.$index, accounts)">
<el-button
class="arrow-down action"
link
@click.prevent="
forward(
scope.$index,
accounts.map(a => a.id)
)
"
>
<font-awesome-icon icon="arrow-down" />
</el-button>
</div>
@ -61,7 +88,6 @@ import { useRouter } from 'vue-router'
import { useStore } from '@/store'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/Preferences/Account'
import { ElMessage } from 'element-plus'
import { LocalAccount } from '~/src/types/localAccount'
export default defineComponent({
name: 'account',
@ -71,7 +97,13 @@ export default defineComponent({
const i18n = useI18next()
const router = useRouter()
const accounts = computed(() => store.state.Preferences.Account.accounts)
const accounts = computed(() =>
store.state.Preferences.Account.accounts.map(([a, s]) => ({
id: a.id,
username: a.username,
domain: s.domain
}))
)
const accountLoading = computed(() => store.state.Preferences.Account.accountLoading)
const backgroundColor = computed(() => store.state.App.theme.background_color)
@ -93,7 +125,7 @@ export default defineComponent({
}
}
const removeAccount = (index: number, accounts: Array<LocalAccount>) => {
const removeAccount = (index: number, accounts: Array<number>) => {
store
.dispatch(`${space}/${ACTION_TYPES.REMOVE_ACCOUNT}`, accounts[index])
.then(() => {
@ -107,13 +139,13 @@ export default defineComponent({
})
}
const forward = (index: number, accounts: Array<LocalAccount>) => {
const forward = (index: number, accounts: Array<number>) => {
store.dispatch(`${space}/${ACTION_TYPES.FORWARD_ACCOUNT}`, accounts[index]).then(() => {
loadAccounts()
})
}
const backward = (index: number, accounts: Array<LocalAccount>) => {
const backward = (index: number, accounts: Array<number>) => {
store.dispatch(`${space}/${ACTION_TYPES.BACKWARD_ACCOUNT}`, accounts[index]).then(() => {
loadAccounts()
})

View File

@ -64,7 +64,7 @@ export default defineComponent({
const activeRoute = computed(() => route.path)
onMounted(() => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_ACCOUNT_ID}`, id.value)
store.commit(`${space}/${MUTATION_TYPES.CHANGE_ACCOUNT_ID}`, parseInt(id.value as string))
router.push(`/${id.value}/settings/general`)
})

View File

@ -1,23 +1,6 @@
<template>
<div id="timeline">
<h2>{{ $t('settings.timeline.title') }}</h2>
<el-form class="unread-notification section" size="default" 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="directNotify" id="direct" />
</el-form-item>
<el-form-item for="local" :label="$t('settings.timeline.unread_notification.local')">
<el-switch v-model="localNotify" id="local" />
</el-form-item>
<el-form-item for="public" :label="$t('settings.timeline.unread_notification.public')">
<el-switch v-model="publicNotify" id="public" />
</el-form-item>
</el-form>
<el-form class="use-marker section" size="default" label-position="right" label-width="250px">
<h3>{{ $t('settings.timeline.use_marker.title') }}</h3>
<el-form-item for="marker_home" :label="$t('settings.timeline.use_marker.home')">
@ -41,39 +24,18 @@ export default defineComponent({
const space = 'Settings/Timeline'
const store = useStore()
const directNotify = computed({
get: () => store.state.Settings.Timeline.setting.unreadNotification.direct,
set: value =>
store.dispatch(`${space}/${ACTION_TYPES.CHANGE_UNREAD_NOTIFICATION}`, {
direct: value
})
})
const localNotify = computed({
get: () => store.state.Settings.Timeline.setting.unreadNotification.local,
set: value =>
store.dispatch(`${space}/${ACTION_TYPES.CHANGE_UNREAD_NOTIFICATION}`, {
local: value
})
})
const publicNotify = computed({
get: () => store.state.Settings.Timeline.setting.unreadNotification.public,
set: value =>
store.dispatch(`${space}/${ACTION_TYPES.CHANGE_UNREAD_NOTIFICATION}`, {
public: value
})
})
const marker_home = computed({
get: () => store.state.Settings.Timeline.setting.useMarker.home,
get: () => store.state.Settings.Timeline.setting.markerHome,
set: value =>
store.dispatch(`${space}/${ACTION_TYPES.CHANGE_USER_MARKER}`, {
home: value
markerHome: value
})
})
const marker_notifications = computed({
get: () => store.state.Settings.Timeline.setting.useMarker.notifications,
get: () => store.state.Settings.Timeline.setting.markerNotifications,
set: value =>
store.dispatch(`${space}/${ACTION_TYPES.CHANGE_USER_MARKER}`, {
notifications: value
markerNotifications: value
})
})
@ -82,9 +44,6 @@ export default defineComponent({
})
return {
directNotify,
localNotify,
publicNotify,
marker_home,
marker_notifications
}

View File

@ -75,14 +75,10 @@ export default defineComponent({
;(window as any).removeEventListener('dragleave', onDragLeave)
;(window as any).removeEventListener('dragover', onDragOver)
;(window as any).removeEventListener('drop', handleDrop)
store.dispatch(`${space}/${ACTION_TYPES.STOP_STREAMINGS}`)
store.dispatch(`${space}/${ACTION_TYPES.UNBIND_STREAMINGS}`)
})
const clear = async () => {
store.dispatch(`${space}/${ACTION_TYPES.UNBIND_STREAMINGS}`)
await store.dispatch(`${space}/${ACTION_TYPES.CLEAR_ACCOUNT}`)
store.dispatch(`${space}/${ACTION_TYPES.CLEAR_CONTENTS_TIMELINES}`)
await store.dispatch(`${space}/${ACTION_TYPES.REMOVE_SHORTCUT_EVENTS}`)
await store.dispatch(`${space}/${ACTION_TYPES.CLEAR_UNREAD}`)
return 'clear'

View File

@ -5,10 +5,13 @@
<template v-slot="{ item, index, active }">
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.uri]" :data-index="index" :watchData="true">
<toot
v-if="account.account && account.server"
:message="item"
:focused="item.uri === focusedId"
:overlaid="modalOpened"
:filters="[]"
:account="account.account"
:server="account.server"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusRight="focusSidebar"
@ -26,7 +29,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref, watch } from 'vue'
import { computed, defineComponent, onMounted, ref, watch, reactive } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useMagicKeys, whenever } from '@vueuse/core'
import { useStore } from '@/store'
@ -40,6 +43,9 @@ import { EventEmitter } from '@/components/event'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Bookmarks'
import { MUTATION_TYPES as TIMELINE_MUTATION } from '@/store/TimelineSpace'
import { MUTATION_TYPES as HEADER_MUTATION } from '@/store/TimelineSpace/HeaderMenu'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { MyWindow } from '~/src/types/global'
export default defineComponent({
name: 'bookmarks',
@ -54,22 +60,33 @@ export default defineComponent({
const focusedId = ref<string | null>(null)
const heading = ref<boolean>(true)
const scroller = ref<any>()
const lazyLoading = ref(false)
const { j, k, Ctrl_r } = useMagicKeys()
const win = (window as any) as MyWindow
const id = computed(() => parseInt(route.params.id as string))
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const bookmarks = computed(() => store.state.TimelineSpace.Contents.Bookmarks.bookmarks)
const lazyLoading = computed(() => store.state.TimelineSpace.Contents.Bookmarks.lazyLoading)
const account = computed(() => store.state.TimelineSpace.account)
const startReload = computed(() => store.state.TimelineSpace.HeaderMenu.reload)
const openSideBar = computed(() => store.state.TimelineSpace.Contents.SideBar.openSideBar)
const modalOpened = computed<boolean>(() => store.getters[`TimelineSpace/Modals/modalOpened`])
const currentFocusedIndex = computed(() => bookmarks.value.findIndex(toot => focusedId.value === toot.uri))
const shortcutEnabled = computed(() => !modalOpened.value)
onMounted(() => {
onMounted(async () => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
document.getElementById('scroller')?.addEventListener('scroll', onScroll)
store.commit(`TimelineSpace/Contents/${TIMELINE_MUTATION.CHANGE_LOADING}`, true)
store
.dispatch(`${space}/${ACTION_TYPES.FETCH_BOOKMARKS}`, account.value)
.dispatch(`${space}/${ACTION_TYPES.FETCH_BOOKMARKS}`, account)
.catch(() => {
ElMessage({
message: i18n.t('message.bookmark_fetch_error'),
@ -108,12 +125,18 @@ export default defineComponent({
document.getElementById('scroller')!.scrollHeight - 10 &&
!lazyLoading.value
) {
store.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_BOOKMARKS}`, bookmarks.value[bookmarks.value.length - 1]).catch(() => {
ElMessage({
message: i18n.t('message.bookmark_fetch_error'),
type: 'error'
lazyLoading.value = true
store
.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_BOOKMARKS}`, account)
.catch(() => {
ElMessage({
message: i18n.t('message.bookmark_fetch_error'),
type: 'error'
})
})
.finally(() => {
lazyLoading.value = false
})
})
}
// for upper
if ((event.target as HTMLElement)!.scrollTop > 10 && heading.value) {
@ -126,7 +149,7 @@ export default defineComponent({
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, true)
try {
await reloadable()
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_BOOKMARKS}`, account.value).catch(() => {
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_BOOKMARKS}`, account).catch(() => {
ElMessage({
message: i18n.t('message.bookmark_fetch_error'),
type: 'error'
@ -178,7 +201,8 @@ export default defineComponent({
focusToot,
openSideBar,
heading,
upper
upper,
account
}
}
})

View File

@ -1,14 +1,16 @@
<template>
<div id="directmessages">
<div></div>
<DynamicScroller :items="timeline" :min-item-size="86" id="scroller" class="scroller" ref="scroller">
<template v-slot="{ item, index, active }">
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.uri]" :data-index="index" :watchData="true">
<toot
v-if="account.account && account.server"
:message="item"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="[]"
:account="account.account"
:server="account.server"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusRight="focusSidebar"
@ -27,7 +29,7 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted, onBeforeUpdate, onBeforeUnmount, onUnmounted, watch } from 'vue'
import { defineComponent, ref, computed, onMounted, onBeforeUpdate, onBeforeUnmount, watch, reactive } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useMagicKeys, whenever } from '@vueuse/core'
import { useStore } from '@/store'
@ -35,14 +37,13 @@ import { useI18next } from 'vue3-i18next'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Entity } from 'megalodon'
import useReloadable from '@/components/utils/reloadable'
import Toot from '@/components/organisms/Toot.vue'
import { EventEmitter } from '@/components/event'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/DirectMessages'
import { MUTATION_TYPES as SIDE_MENU_MUTATION } from '@/store/TimelineSpace/SideMenu'
import { MUTATION_TYPES as TIMELINE_MUTATION, ACTION_TYPES as TIMELINE_ACTION } from '@/store/TimelineSpace'
import { MUTATION_TYPES as HEADER_MUTATION } from '@/store/TimelineSpace/HeaderMenu'
import { ACTION_TYPES as CONTENTS_ACTION } from '@/store/TimelineSpace/Contents'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { MyWindow } from '~/src/types/global'
export default defineComponent({
name: 'directmessages',
@ -52,31 +53,33 @@ export default defineComponent({
const store = useStore()
const route = useRoute()
const i18n = useI18next()
const { reloadable } = useReloadable(store, route, i18n)
const { j, k, Ctrl_r } = useMagicKeys()
const { j, k } = useMagicKeys()
const win = (window as any) as MyWindow
const id = computed(() => parseInt(route.params.id as string))
const focusedId = ref<string | null>(null)
const scroller = ref<any>()
const lazyLoading = ref(false)
const heading = ref(true)
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const timeline = computed(() => store.state.TimelineSpace.Contents.DirectMessages.timeline)
const lazyLoading = computed(() => store.state.TimelineSpace.Contents.DirectMessages.lazyLoading)
const heading = computed(() => store.state.TimelineSpace.Contents.DirectMessages.heading)
const timeline = computed(() => store.state.TimelineSpace.Contents.DirectMessages.timeline[id.value])
const openSideBar = computed(() => store.state.TimelineSpace.Contents.SideBar.openSideBar)
const startReload = computed(() => store.state.TimelineSpace.HeaderMenu.reload)
const unreadNotification = computed(() => store.state.TimelineSpace.timelineSetting.unreadNotification)
const modalOpened = computed<boolean>(() => store.getters[`TimelineSpace/Modals/modalOpened`])
const currentFocusedIndex = computed(() => timeline.value.findIndex(toot => focusedId.value === toot.uri + toot.id))
const shortcutEnabled = computed(() => !modalOpened.value)
onMounted(async () => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
store.commit(`TimelineSpace/SideMenu/${SIDE_MENU_MUTATION.CHANGE_UNREAD_HOME_TIMELINE}`, false)
document.getElementById('scroller')?.addEventListener('scroll', onScroll)
if (!unreadNotification.value.direct) {
store.commit(`TimelineSpace/Contents/${CONTENTS_ACTION.CHANGE_LOADING}`, true)
await initialize().finally(() => {
store.commit(`TimelineSpace/Contents/${CONTENTS_ACTION.CHANGE_LOADING}`, false)
})
}
})
onBeforeUpdate(() => {
if (store.state.TimelineSpace.SideMenu.unreadDirectMessagesTimeline && heading.value) {
@ -84,30 +87,13 @@ export default defineComponent({
}
})
onBeforeUnmount(() => {
if (!unreadNotification.value.direct) {
store.dispatch(`TimelineSpace/${TIMELINE_ACTION.STOP_DIRECT_MESSAGES_STREAMING}`)
store.dispatch(`TimelineSpace/${TIMELINE_ACTION.UNBIND_DIRECT_MESSAGES_STREAMING}`)
}
})
onUnmounted(() => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.ARCHIVE_TIMELINE}`)
if (!unreadNotification.value.direct) {
store.commit(`${space}/${MUTATION_TYPES.CLEAR_TIMELINE}`)
}
})
watch(startReload, (newVal, oldVal) => {
if (!oldVal && newVal) {
reload().finally(() => {
store.commit(`TimelineSpace/HeaderMenu/${HEADER_MUTATION.CHANGE_RELOAD}`, false)
})
}
EventEmitter.off('focus-timeline')
})
watch(focusedId, (newVal, _oldVal) => {
if (newVal && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if (newVal === null && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
heading.value = true
}
})
whenever(logicAnd(j, shortcutEnabled), () => {
@ -120,20 +106,7 @@ export default defineComponent({
whenever(logicAnd(k, shortcutEnabled), () => {
focusPrev()
})
whenever(logicAnd(Ctrl_r, shortcutEnabled), () => {
reload()
})
const initialize = async () => {
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_TIMELINE}`).catch(_ => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
})
await store.dispatch(`TimelineSpace/${TIMELINE_ACTION.BIND_DIRECT_MESSAGES_STREAMING}`)
store.dispatch(`TimelineSpace/${TIMELINE_ACTION.START_DIRECT_MESSAGES_STREAMING}`)
}
const onScroll = (event: Event) => {
// for lazyLoading
if (
@ -141,32 +114,38 @@ export default defineComponent({
document.getElementById('scroller')!.scrollHeight - 10 &&
!lazyLoading.value
) {
store.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_TIMELINE}`, timeline.value[timeline.value.length - 1]).catch(() => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
lazyLoading.value = true
store
.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_TIMELINE}`, {
statuses: timeline.value[timeline.value.length - 1],
account: account.account,
server: account.server
})
.catch(() => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
})
.finally(() => {
lazyLoading.value = false
})
})
}
if ((event.target as HTMLElement)!.scrollTop > 10 && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if ((event.target as HTMLElement)!.scrollTop <= 10 && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
heading.value = true
}
}
const updateToot = (message: Entity.Status) => {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, message)
if (account.account) {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, { status: message, accountId: account.account.id })
}
}
const deleteToot = (message: Entity.Status) => {
store.commit(`${space}/${MUTATION_TYPES.DELETE_TOOT}`, message.id)
}
const reload = async () => {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, true)
try {
await reloadable()
} finally {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, false)
if (account.account) {
store.commit(`${space}/${MUTATION_TYPES.DELETE_TOOT}`, { statusId: message.id, accountId: account.account.id })
}
}
const upper = () => {
@ -205,7 +184,8 @@ export default defineComponent({
focusToot,
openSideBar,
heading,
upper
upper,
account
}
}
})

View File

@ -5,10 +5,13 @@
<template v-slot="{ item, index, active }">
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.uri]" :data-index="index" :watchData="true">
<toot
v-if="account.account && account.server"
:message="item"
:focused="item.uri === focusedId"
:overlaid="modalOpened"
:filters="[]"
:account="account.account"
:server="account.server"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusRight="focusSidebar"
@ -27,7 +30,7 @@
</template>
<script lang="ts">
import { defineComponent, computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { defineComponent, computed, ref, onMounted, onUnmounted, watch, reactive } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useMagicKeys, whenever } from '@vueuse/core'
import { useStore } from '@/store'
@ -40,6 +43,10 @@ import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Fav
import { MUTATION_TYPES as CONTENTS_MUTATION } from '@/store/TimelineSpace/Contents'
import { MUTATION_TYPES as HEADER_MUTATION } from '@/store/TimelineSpace/HeaderMenu'
import { MUTATION_TYPES as TIMELINE_MUTATION } from '@/store/TimelineSpace'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { MyWindow } from '~/src/types/global'
import { useRoute } from 'vue-router'
export default defineComponent({
name: 'favourites',
@ -47,6 +54,7 @@ export default defineComponent({
setup() {
const space = 'TimelineSpace/Contents/Favourites'
const store = useStore()
const route = useRoute()
const i18n = useI18next()
const heading = ref<boolean>(false)
@ -54,21 +62,32 @@ export default defineComponent({
const scroller = ref<any>()
const { j, k, Ctrl_r } = useMagicKeys()
const win = (window as any) as MyWindow
const id = computed(() => parseInt(route.params.id as string))
const lazyLoading = ref(false)
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const openSideBar = computed(() => store.state.TimelineSpace.Contents.SideBar.openSideBar)
const startReload = computed(() => store.state.TimelineSpace.HeaderMenu.reload)
const account = computed(() => store.state.TimelineSpace.account)
const favourites = computed(() => store.state.TimelineSpace.Contents.Favourites.favourites)
const lazyLoading = computed(() => store.state.TimelineSpace.Contents.Favourites.lazyLoading)
const modalOpened = computed<boolean>(() => store.getters[`TimelineSpace/Modals/modalOpened`])
const currentFocusedIndex = computed(() => favourites.value.findIndex(status => focusedId.value === status.uri))
const shortcutEnabled = computed(() => !modalOpened.value)
onMounted(() => {
onMounted(async () => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
document.getElementById('scroller')?.addEventListener('scroll', onScroll)
store.commit(`TimelineSpace/Contents/${CONTENTS_MUTATION.CHANGE_LOADING}`, true)
store
.dispatch(`${space}/${ACTION_TYPES.FETCH_FAVOURITES}`, account.value)
.dispatch(`${space}/${ACTION_TYPES.FETCH_FAVOURITES}`, account)
.catch(() => {
ElMessage({
message: i18n.t('message.favourite_fetch_error'),
@ -116,12 +135,18 @@ export default defineComponent({
document.getElementById('scroller')!.scrollHeight - 10 &&
!lazyLoading.value
) {
store.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_FAVOURITES}`, favourites.value[favourites.value.length - 1]).catch(() => {
ElMessage({
message: i18n.t('message.favourite_fetch_error'),
type: 'error'
lazyLoading.value = true
store
.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_FAVOURITES}`, account)
.catch(() => {
ElMessage({
message: i18n.t('message.favourite_fetch_error'),
type: 'error'
})
})
.finally(() => {
lazyLoading.value = false
})
})
}
// for upper
if ((event.target as HTMLElement)!.scrollTop > 10 && heading.value) {
@ -139,7 +164,7 @@ export default defineComponent({
const reload = async () => {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, true)
try {
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_FAVOURITES}`, account.value).catch(() => {
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_FAVOURITES}`, account).catch(() => {
ElMessage({
message: i18n.t('message.favourite_fetch_error'),
type: 'error'
@ -185,7 +210,8 @@ export default defineComponent({
focusToot,
openSideBar,
heading,
upper
upper,
account
}
}
})

View File

@ -7,13 +7,17 @@
</template>
<script lang="ts">
import { defineComponent, computed, onMounted } from 'vue'
import { defineComponent, computed, onMounted, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { useI18next } from 'vue3-i18next'
import { Entity } from 'megalodon'
import { useStore } from '@/store'
import User from '@/components/molecules/User.vue'
import { ACTION_TYPES } from '@/store/TimelineSpace/Contents/FollowRequests'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { MyWindow } from '~/src/types/global'
import { useRoute } from 'vue-router'
export default defineComponent({
name: 'follow-requests',
@ -21,8 +25,17 @@ export default defineComponent({
setup() {
const space = 'TimelineSpace/Contents/FollowRequests'
const store = useStore()
const route = useRoute()
const i18n = useI18next()
const win = (window as any) as MyWindow
const id = computed(() => parseInt(route.params.id as string))
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const requests = computed(() => store.state.TimelineSpace.Contents.FollowRequests.requests)
onMounted(async () => {
@ -30,23 +43,27 @@ export default defineComponent({
})
const initialize = async () => {
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_REQUESTS}`).catch(_ => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_REQUESTS}`, account).catch(_ => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
})
}
const accept = (account: Entity.Account) => {
store.dispatch(`${space}/${ACTION_TYPES.ACCEPT_REQUEST}`, account).catch(_ => {
const accept = (user: Entity.Account) => {
store.dispatch(`${space}/${ACTION_TYPES.ACCEPT_REQUEST}`, { user, account: account.account, server: account.server }).catch(_ => {
ElMessage({
message: i18n.t('message.follow_request_accept_error'),
type: 'error'
})
})
}
const reject = (account: Entity.Account) => {
store.dispatch(`${space}/${ACTION_TYPES.REJECT_REQUEST}`, account).catch(_ => {
const reject = (user: Entity.Account) => {
store.dispatch(`${space}/${ACTION_TYPES.REJECT_REQUEST}`, { user, account: account.account, server: account.server }).catch(_ => {
ElMessage({
message: i18n.t('message.follow_request_reject_error'),
type: 'error'

View File

@ -11,11 +11,6 @@
<div class="form-item input">
<input v-model="tag" :placeholder="$t('hashtag.tag_name')" class="search-keyword" v-on:keyup.enter="search" autofocus />
</div>
<div class="form-item" v-show="tagPage">
<el-button link :title="$t('hashtag.save_tag')" @click="save">
<font-awesome-icon icon="thumbtack" />
</el-button>
</div>
</div>
</el-form>
</div>
@ -26,14 +21,12 @@
<script lang="ts">
import { computed, defineComponent, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useStore } from '@/store'
export default defineComponent({
name: 'hashtag',
setup() {
const route = useRoute()
const router = useRouter()
const store = useStore()
const tag = ref<string>('')
const id = computed(() => route.params.id)
@ -58,16 +51,12 @@ export default defineComponent({
const back = () => {
router.push({ path: `/${id.value}/hashtag` })
}
const save = () => {
store.dispatch('TimelineSpace/Contents/Hashtag/saveTag', tag.value)
}
return {
tagPage,
tag,
back,
search,
save
search
}
},
methods: {}

View File

@ -1,14 +1,16 @@
<template>
<div name="tag" class="tag-timeline">
<div class="unread">{{ unreads.length > 0 ? unreads.length : '' }}</div>
<DynamicScroller :items="timeline" :min-item-size="86" id="scroller" class="scroller" ref="scroller">
<template v-slot="{ item, index, active }">
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.uri]" :data-index="index" :watchData="true">
<toot
v-if="account.account && account.server"
:message="item"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="[]"
:account="account.account"
:server="account.server"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusRight="focusSidebar"
@ -27,7 +29,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue'
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, toRefs, watch, reactive } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useMagicKeys, whenever } from '@vueuse/core'
import { ElMessage } from 'element-plus'
@ -42,6 +44,9 @@ import { MUTATION_TYPES as TIMELINE_MUTATION } from '@/store/TimelineSpace'
import { MUTATION_TYPES as HEADER_MUTATION } from '@/store/TimelineSpace/HeaderMenu'
import { MUTATION_TYPES as CONTENTS_MUTATION } from '@/store/TimelineSpace/Contents'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Hashtag/Tag'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { MyWindow } from '~/src/types/global'
export default defineComponent({
name: 'tag',
@ -55,21 +60,31 @@ export default defineComponent({
const { reloadable } = useReloadable(store, route, i18n)
const { j, k, Ctrl_r } = useMagicKeys()
const win = (window as any) as MyWindow
const id = computed(() => parseInt(route.params.id as string))
const { tag } = toRefs(props)
const focusedId = ref<string | null>(null)
const scroller = ref<any>(null)
const lazyLoading = ref(false)
const heading = ref(true)
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const timeline = computed(() => store.state.TimelineSpace.Contents.Hashtag.Tag.timeline)
const unreads = computed(() => store.state.TimelineSpace.Contents.Hashtag.Tag.unreads)
const lazyLoading = computed(() => store.state.TimelineSpace.Contents.Hashtag.Tag.lazyLoading)
const heading = computed(() => store.state.TimelineSpace.Contents.Hashtag.Tag.heading)
const openSideBar = computed(() => store.state.TimelineSpace.Contents.SideBar.openSideBar)
const startReload = computed(() => store.state.TimelineSpace.HeaderMenu.reload)
const modalOpened = computed<boolean>(() => store.getters[`TimelineSpace/Modals/modalOpened`])
const currentFocusedIndex = computed(() => timeline.value.findIndex(toot => focusedId.value === toot.uri + toot.id))
const shortcutEnabled = computed(() => !modalOpened.value)
onMounted(() => {
onMounted(async () => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
store.commit(`TimelineSpace/Contents/${CONTENTS_MUTATION.CHANGE_LOADING}`, true)
load(tag.value).finally(() => {
store.commit(`TimelineSpace/Contents/${CONTENTS_MUTATION.CHANGE_LOADING}`, false)
@ -92,9 +107,9 @@ export default defineComponent({
})
watch(focusedId, (newVal, _oldVal) => {
if (newVal && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if (newVal === null && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
heading.value = true
}
})
whenever(logicAnd(j, shortcutEnabled), () => {
@ -118,13 +133,13 @@ export default defineComponent({
})
const load = async (tag: string) => {
await store.dispatch(`${space}/${ACTION_TYPES.FETCH}`, tag).catch(() => {
await store.dispatch(`${space}/${ACTION_TYPES.FETCH}`, { tag: tag, account: account.account, server: account.server }).catch(() => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
})
store.dispatch(`${space}/${ACTION_TYPES.START_STREAMING}`, tag).catch(() => {
store.dispatch(`${space}/${ACTION_TYPES.START_STREAMING}`, { tag: tag, account: account.account }).catch(() => {
ElMessage({
message: i18n.t('message.start_streaming_error'),
type: 'error'
@ -133,9 +148,7 @@ export default defineComponent({
return true
}
const reset = () => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.ARCHIVE_TIMELINE}`)
store.commit(`${space}/${MUTATION_TYPES.CLEAR_TIMELINE}`)
heading.value = true
const el = document.getElementById('scroller')
if (el !== undefined && el !== null) {
el.removeEventListener('scroll', onScroll)
@ -148,10 +161,13 @@ export default defineComponent({
document.getElementById('scroller')!.scrollHeight - 10 &&
!lazyLoading.value
) {
lazyLoading.value = true
store
.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_TIMELINE}`, {
tag: tag.value,
status: timeline.value[timeline.value.length - 1]
status: timeline.value[timeline.value.length - 1],
account: account.account,
server: account.server
})
.catch(() => {
ElMessage({
@ -159,13 +175,15 @@ export default defineComponent({
type: 'error'
})
})
.finally(() => {
lazyLoading.value = false
})
}
if ((event.target as HTMLElement)!.scrollTop > 10 && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if ((event.target as HTMLElement)!.scrollTop <= 10 && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.MERGE_UNREADS}`)
heading.value = true
}
}
const updateToot = (toot: Entity.Status) => {
@ -232,7 +250,7 @@ export default defineComponent({
openSideBar,
heading,
upper,
unreads
account
}
}
})

View File

@ -1,6 +1,5 @@
<template>
<div id="home">
<div class="unread">{{ unreads.length > 0 ? unreads.length : '' }}</div>
<DynamicScroller :items="filteredTimeline" :min-item-size="86" id="scroller" class="scroller" ref="scroller">
<template v-slot="{ item, index, active }">
<template v-if="item.id === 'loading-card'">
@ -11,10 +10,13 @@
<template v-else>
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.uri]" :data-index="index" :watchData="true">
<toot
v-if="account.account && account.server"
:message="item"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
:account="account.account"
:server="account.server"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusRight="focusSidebar"
@ -35,7 +37,7 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted, onBeforeUpdate, watch, onUnmounted } from 'vue'
import { defineComponent, ref, computed, onMounted, onBeforeUpdate, watch, onUnmounted, reactive } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useMagicKeys, whenever } from '@vueuse/core'
import { ElMessage } from 'element-plus'
@ -48,9 +50,9 @@ import StatusLoading from '@/components/organisms/StatusLoading.vue'
import { EventEmitter } from '@/components/event'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Home'
import { MUTATION_TYPES as SIDE_MENU_MUTATION } from '@/store/TimelineSpace/SideMenu'
import { MUTATION_TYPES as TIMELINE_MUTATION } from '@/store/TimelineSpace'
import { MUTATION_TYPES as HEADER_MUTATION } from '@/store/TimelineSpace/HeaderMenu'
import useReloadable from '@/components/utils/reloadable'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { MyWindow } from '~/src/types/global'
export default defineComponent({
name: 'home',
@ -60,26 +62,35 @@ export default defineComponent({
const store = useStore()
const route = useRoute()
const i18n = useI18next()
const { reloadable } = useReloadable(store, route, i18n)
const { j, k, Ctrl_r } = useMagicKeys()
const { j, k } = useMagicKeys()
const win = (window as any) as MyWindow
const id = computed(() => parseInt(route.params.id as string))
const focusedId = ref<string | null>(null)
const loadingMore = ref(false)
const scroller = ref<any>()
const lazyLoading = ref(false)
const heading = ref(true)
const showReblogs = ref(true)
const showReplies = ref(true)
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const timeline = computed(() => store.state.TimelineSpace.Contents.Home.timeline[id.value])
const timeline = computed(() => store.state.TimelineSpace.Contents.Home.timeline)
const unreads = computed(() => store.state.TimelineSpace.Contents.Home.unreads)
const lazyLoading = computed(() => store.state.TimelineSpace.Contents.Home.lazyLoading)
const heading = computed(() => store.state.TimelineSpace.Contents.Home.heading)
const showReblogs = computed(() => store.state.TimelineSpace.Contents.Home.showReblogs)
const showReplies = computed(() => store.state.TimelineSpace.Contents.Home.showReplies)
const openSideBar = computed(() => store.state.TimelineSpace.Contents.SideBar.openSideBar)
const startReload = computed(() => store.state.TimelineSpace.HeaderMenu.reload)
const modalOpened = computed<boolean>(() => store.getters[`TimelineSpace/Modals/modalOpened`])
const filters = computed(() => store.getters[`${space}/filters`])
const currentFocusedIndex = computed(() => timeline.value.findIndex(toot => focusedId.value === toot.uri + toot.id))
const shortcutEnabled = computed(() => !modalOpened.value)
const filteredTimeline = computed(() => {
if (!timeline.value) {
return []
}
return timeline.value.filter(toot => {
if ('url' in toot) {
if (toot.in_reply_to_id) {
@ -95,13 +106,13 @@ export default defineComponent({
})
})
onMounted(() => {
onMounted(async () => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
store.commit(`TimelineSpace/SideMenu/${SIDE_MENU_MUTATION.CHANGE_UNREAD_HOME_TIMELINE}`, false)
document.getElementById('scroller')?.addEventListener('scroll', onScroll)
if (heading.value && timeline.value.length > 0) {
store.dispatch(`${space}/${ACTION_TYPES.SAVE_MARKER}`)
}
})
onBeforeUpdate(() => {
if (store.state.TimelineSpace.SideMenu.unreadHomeTimeline && heading.value) {
@ -109,35 +120,26 @@ export default defineComponent({
}
})
onUnmounted(() => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.ARCHIVE_TIMELINE}`)
const el = document.getElementById('scroller')
if (el !== undefined && el !== null) {
el.removeEventListener('scroll', onScroll)
el.scrollTop = 0
}
})
watch(startReload, (newVal, oldVal) => {
if (!oldVal && newVal) {
reload().finally(() => {
store.commit(`TimelineSpace/HeaderMenu/${HEADER_MUTATION.CHANGE_RELOAD}`, false)
})
}
})
watch(
timeline,
(newState, _oldState) => {
if (heading.value && newState.length > 0) {
store.dispatch(`${space}/${ACTION_TYPES.SAVE_MARKER}`)
if (heading.value && newState.length > 0 && account.account && account.server) {
store.dispatch(`${space}/${ACTION_TYPES.SAVE_MARKER}`, account)
}
},
{ deep: true }
)
watch(focusedId, (newVal, _oldVal) => {
if (newVal && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if (newVal === null && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
heading.value = true
}
})
whenever(logicAnd(j, shortcutEnabled), () => {
@ -150,9 +152,6 @@ export default defineComponent({
whenever(logicAnd(k, shortcutEnabled), () => {
focusPrev()
})
whenever(logicAnd(Ctrl_r, shortcutEnabled), () => {
reload()
})
const onScroll = (event: Event) => {
// for lazyLoading
@ -161,42 +160,49 @@ export default defineComponent({
document.getElementById('scroller')!.scrollHeight - 10 &&
!lazyLoading.value
) {
store.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_TIMELINE}`, timeline.value[timeline.value.length - 1]).catch(() => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
lazyLoading.value = true
store
.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_TIMELINE}`, {
lastStatus: timeline.value[timeline.value.length - 1],
account: account.account,
server: account.server
})
.catch(() => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
})
.finally(() => {
lazyLoading.value = false
})
})
}
if ((event.target as HTMLElement)!.scrollTop > 10 && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if ((event.target as HTMLElement)!.scrollTop <= 5 && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.MERGE_UNREADS}`)
heading.value = true
}
}
const updateToot = (message: Entity.Status) => {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, message)
if (account.account) {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, { status: message, accountId: account.account.id })
}
}
const deleteToot = (message: Entity.Status) => {
store.commit(`${space}/${MUTATION_TYPES.DELETE_TOOT}`, message.id)
if (account.account) {
store.commit(`${space}/${MUTATION_TYPES.DELETE_TOOT}`, { statusId: message.id, accountId: account.account.id })
}
}
const fetchTimelineSince = (since_id: string) => {
loadingMore.value = true
store.dispatch(`${space}/${ACTION_TYPES.FETCH_TIMELINE_SINCE}`, since_id).finally(() => {
setTimeout(() => {
loadingMore.value = false
}, 500)
})
}
const reload = async () => {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, true)
try {
await reloadable()
} finally {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, false)
}
store
.dispatch(`${space}/${ACTION_TYPES.FETCH_TIMELINE_SINCE}`, { sinceId: since_id, account: account.account, server: account.server })
.finally(() => {
setTimeout(() => {
loadingMore.value = false
}, 500)
})
}
const upper = () => {
scroller.value.scrollToItem(0)
@ -240,7 +246,7 @@ export default defineComponent({
openSideBar,
heading,
upper,
unreads
account
}
}
})

View File

@ -1,14 +1,16 @@
<template>
<div name="list" class="list-timeline">
<div class="unread">{{ unreads.length > 0 ? unreads.length : '' }}</div>
<DynamicScroller :items="timeline" :min-item-size="86" id="scroller" class="scroller" ref="scroller">
<template v-slot="{ item, index, active }">
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.uri]" :data-index="index" :watchData="true">
<toot
v-if="account.account && account.server"
:message="item"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="[]"
:account="account.account"
:server="account.server"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusRight="focusSidebar"
@ -27,7 +29,7 @@
</template>
<script lang="ts">
import { defineComponent, toRefs, ref, computed, onMounted, watch, onBeforeUnmount, onUnmounted } from 'vue'
import { defineComponent, toRefs, ref, computed, onMounted, watch, onBeforeUnmount, onUnmounted, reactive } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useMagicKeys, whenever } from '@vueuse/core'
import { ElMessage } from 'element-plus'
@ -40,6 +42,10 @@ import { MUTATION_TYPES as CONTENTS_MUTATION } from '@/store/TimelineSpace/Conte
import { MUTATION_TYPES as HEADER_MUTATION } from '@/store/TimelineSpace/HeaderMenu'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Lists/Show'
import { MUTATION_TYPES as TIMELINE_MUTATION } from '@/store/TimelineSpace'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { MyWindow } from '~/src/types/global'
import { useRoute } from 'vue-router'
export default defineComponent({
name: 'list',
@ -48,24 +54,35 @@ export default defineComponent({
setup(props) {
const space = 'TimelineSpace/Contents/Lists/Show'
const store = useStore()
const route = useRoute()
const i18n = useI18next()
const { j, k, Ctrl_r } = useMagicKeys()
const win = (window as any) as MyWindow
const id = computed(() => parseInt(route.params.id as string))
const { list_id } = toRefs(props)
const focusedId = ref<string | null>(null)
const scroller = ref<any>(null)
const lazyLoading = ref<boolean>(false)
const heading = ref<boolean>(true)
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const timeline = computed(() => store.state.TimelineSpace.Contents.Lists.Show.timeline)
const unreads = computed(() => store.state.TimelineSpace.Contents.Lists.Show.unreads)
const lazyLoading = computed(() => store.state.TimelineSpace.Contents.Lists.Show.lazyLoading)
const heading = computed(() => store.state.TimelineSpace.Contents.Lists.Show.heading)
const openSideBar = computed(() => store.state.TimelineSpace.Contents.SideBar.openSideBar)
const startReload = computed(() => store.state.TimelineSpace.HeaderMenu.reload)
const modalOpened = computed<boolean>(() => store.getters[`TimelineSpace/Modals/modalOpened`])
const currentFocusedIndex = computed(() => timeline.value.findIndex(toot => focusedId.value === toot.uri + toot.id))
const shortcutEnabled = computed(() => !modalOpened.value)
onMounted(() => {
onMounted(async () => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
document.getElementById('scroller')?.addEventListener('scroll', onScroll)
store.commit(`TimelineSpace/Contents/${CONTENTS_MUTATION.CHANGE_LOADING}`, true)
load().finally(() => {
@ -87,9 +104,9 @@ export default defineComponent({
})
watch(focusedId, (newVal, _oldVal) => {
if (newVal && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if (newVal === null && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
heading.value = true
}
})
whenever(logicAnd(j, shortcutEnabled), () => {
@ -110,9 +127,7 @@ export default defineComponent({
store.dispatch(`${space}/${ACTION_TYPES.STOP_STREAMING}`)
})
onUnmounted(() => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.ARCHIVE_TIMELINE}`)
store.commit(`${space}/${MUTATION_TYPES.CLEAR_TIMELINE}`)
heading.value = true
const el = document.getElementById('scroller')
if (el !== undefined && el !== null) {
el.removeEventListener('scroll', onScroll)
@ -123,14 +138,18 @@ export default defineComponent({
const load = async () => {
await store.dispatch(`${space}/${ACTION_TYPES.STOP_STREAMING}`)
try {
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_TIMELINE}`, list_id.value)
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_TIMELINE}`, {
listID: list_id.value,
account: account.account,
server: account.server
})
} catch (err) {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
}
store.dispatch(`${space}/${ACTION_TYPES.START_STREAMING}`, list_id.value).catch(() => {
store.dispatch(`${space}/${ACTION_TYPES.START_STREAMING}`, { listID: list_id.value, account: account.account }).catch(() => {
ElMessage({
message: i18n.t('message.start_streaming_error'),
type: 'error'
@ -144,10 +163,13 @@ export default defineComponent({
document.getElementById('scroller')!.scrollHeight - 10 &&
!lazyLoading
) {
lazyLoading.value = true
store
.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_TIMELINE}`, {
list_id: list_id.value,
status: timeline.value[timeline.value.length - 1]
status: timeline.value[timeline.value.length - 1],
account: account.account,
server: account.server
})
.catch(() => {
ElMessage({
@ -155,13 +177,15 @@ export default defineComponent({
type: 'error'
})
})
.finally(() => {
lazyLoading.value = false
})
}
if ((event.target as HTMLElement)!.scrollTop > 10 && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if ((event.target as HTMLElement)!.scrollTop <= 10 && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.MERGE_UNREADS}`)
heading.value = true
}
}
const reload = async () => {
@ -227,7 +251,7 @@ export default defineComponent({
openSideBar,
heading,
upper,
unreads
account
}
}
})

View File

@ -1,14 +1,16 @@
<template>
<div id="local">
<div class="unread">{{ unreads.length > 0 ? unreads.length : '' }}</div>
<DynamicScroller :items="timeline" :min-item-size="86" id="scroller" class="scroller" ref="scroller">
<template v-slot="{ item, index, active }">
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.uri]" :data-index="index" :watchData="true">
<toot
v-if="account.account && account.server"
:message="item"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="[]"
:account="account.account"
:server="account.server"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusRight="focusSidebar"
@ -27,7 +29,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, defineComponent, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, ref, watch, reactive } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useMagicKeys, whenever } from '@vueuse/core'
import { ElMessage } from 'element-plus'
@ -37,12 +39,11 @@ import { useRoute } from 'vue-router'
import { useStore } from '@/store'
import Toot from '@/components/organisms/Toot.vue'
import { EventEmitter } from '@/components/event'
import useReloadable from '@/components/utils/reloadable'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Local'
import { MUTATION_TYPES as SIDE_MENU_MUTATION } from '@/store/TimelineSpace/SideMenu'
import { MUTATION_TYPES as TIMELINE_MUTATION, ACTION_TYPES as TIMELINE_ACTION } from '@/store/TimelineSpace'
import { MUTATION_TYPES as HEADER_MUTATION } from '@/store/TimelineSpace/HeaderMenu'
import { MUTATION_TYPES as CONTENTS_MUTATION } from '@/store/TimelineSpace/Contents'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { MyWindow } from '~/src/types/global'
export default defineComponent({
name: 'local',
@ -52,32 +53,34 @@ export default defineComponent({
const store = useStore()
const route = useRoute()
const i18n = useI18next()
const { reloadable } = useReloadable(store, route, i18n)
const { j, k, Ctrl_r } = useMagicKeys()
const { j, k } = useMagicKeys()
const win = (window as any) as MyWindow
const id = computed(() => parseInt(route.params.id as string))
const focusedId = ref<string | null>(null)
const scroller = ref<any>(null)
const lazyLoading = ref(false)
const heading = ref(true)
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const timeline = computed(() => store.state.TimelineSpace.Contents.Local.timeline[id.value])
const timeline = computed(() => store.state.TimelineSpace.Contents.Local.timeline)
const unreads = computed(() => store.state.TimelineSpace.Contents.Local.unreads)
const lazyLoading = computed(() => store.state.TimelineSpace.Contents.Local.lazyLoading)
const heading = computed(() => store.state.TimelineSpace.Contents.Local.heading)
const openSideBar = computed(() => store.state.TimelineSpace.Contents.SideBar.openSideBar)
const startReload = computed(() => store.state.TimelineSpace.HeaderMenu.reload)
const unreadNotification = computed(() => store.state.TimelineSpace.timelineSetting.unreadNotification)
const modalOpened = computed<boolean>(() => store.getters[`TimelineSpace/Modals/modalOpened`])
const currentFocusedIndex = computed(() => timeline.value.findIndex(toot => focusedId.value === toot.uri + toot.id))
const shortcutEnabled = computed(() => !modalOpened.value)
onMounted(async () => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
store.commit(`TimelineSpace/SideMenu/${SIDE_MENU_MUTATION.CHANGE_UNREAD_LOCAL_TIMELINE}`, false)
document.getElementById('scroller')?.addEventListener('scroll', onScroll)
if (!unreadNotification.value.local) {
store.commit(`TimelineSpace/Contents/${CONTENTS_MUTATION.CHANGE_LOADING}`, true)
await initialize().finally(() => {
store.commit(`TimelineSpace/Contents/${CONTENTS_MUTATION.CHANGE_LOADING}`, false)
})
}
})
onBeforeUpdate(() => {
if (store.state.TimelineSpace.SideMenu.unreadLocalTimeline && heading.value) {
@ -85,37 +88,21 @@ export default defineComponent({
}
})
onBeforeUnmount(() => {
if (!unreadNotification.value.local) {
store.dispatch(`TimelineSpace/${TIMELINE_ACTION.STOP_LOCAL_STREAMING}`)
store.dispatch(`TimelineSpace/${TIMELINE_ACTION.UNBIND_LOCAL_STREAMING}`)
}
EventEmitter.off('focus-timeline')
})
onUnmounted(() => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.ARCHIVE_TIMELINE}`)
if (!unreadNotification.value.local) {
store.commit(`${space}/${MUTATION_TYPES.CLEAR_TIMELINE}`)
}
const el = document.getElementById('scroller')
if (el) {
el.removeEventListener('scroll', onScroll)
el.scrollTop = 0
}
})
watch(startReload, (newVal, oldVal) => {
if (!oldVal && newVal) {
reload().finally(() => {
store.commit(`TimelineSpace/HeaderMenu/${HEADER_MUTATION.CHANGE_RELOAD}`, false)
})
}
})
watch(focusedId, (newVal, _oldVal) => {
if (newVal && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if (newVal === null && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
heading.value = true
}
})
whenever(logicAnd(j, shortcutEnabled), () => {
@ -128,53 +115,46 @@ export default defineComponent({
whenever(logicAnd(k, shortcutEnabled), () => {
focusPrev()
})
whenever(logicAnd(Ctrl_r, shortcutEnabled), () => {
reload()
})
const initialize = async () => {
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_LOCAL_TIMELINE}`).catch(_ => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
})
await store.dispatch(`TimelineSpace/${TIMELINE_ACTION.BIND_LOCAL_STREAMING}`)
store.dispatch(`TimelineSpace/${TIMELINE_ACTION.START_LOCAL_STREAMING}`)
}
const onScroll = (event: Event) => {
if (
(event.target as HTMLElement)!.clientHeight + (event.target as HTMLElement)!.scrollTop >=
document.getElementById('scroller')!.scrollHeight - 10 &&
!lazyLoading.value
) {
store.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_TIMELINE}`, timeline.value[timeline.value.length - 1]).catch(() => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
lazyLoading.value = true
store
.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_TIMELINE}`, {
lastStatus: timeline.value[timeline.value.length - 1],
account: account.account,
server: account.server
})
.catch(() => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
})
.finally(() => {
lazyLoading.value = false
})
})
}
if ((event.target as HTMLElement)!.scrollTop > 10 && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if ((event.target as HTMLElement)!.scrollTop <= 10 && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.MERGE_UNREADS}`)
}
}
const reload = async () => {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, true)
try {
await reloadable()
} finally {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, false)
heading.value = true
}
}
const updateToot = (message: Entity.Status) => {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, message)
if (account.account) {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, { status: message, accountId: account.account.id })
}
}
const deleteToot = (message: Entity.Status) => {
store.commit(`${space}/${MUTATION_TYPES.DELETE_TOOT}`, message.id)
if (account.account) {
store.commit(`${space}/${MUTATION_TYPES.DELETE_TOOT}`, { statusId: message.id, accountId: account.account.id })
}
}
const upper = () => {
scroller.value.scrollToItem(0)
@ -213,7 +193,7 @@ export default defineComponent({
openSideBar,
heading,
upper,
unreads
account
}
}
})

View File

@ -1,226 +0,0 @@
<template>
<div id="mentions">
<div></div>
<DynamicScroller :items="mentions" :min-item-size="86" id="scroller" class="scroller" ref="scroller">
<template v-slot="{ item, index, active }">
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.url]" :data-index="index" :watch-data="true">
<notification
:message="item"
:focused="item.id === focusedId"
:overlaid="modalOpened"
:filters="[]"
@update="updateToot"
@focus-right="focusSidebar"
@select-notification="focusNotification(item)"
>
</notification>
</DynamicScrollerItem>
</template>
</DynamicScroller>
<div :class="openSideBar ? 'upper-with-side-bar' : 'upper'" v-show="!heading">
<el-button type="primary" @click="upper" circle>
<font-awesome-icon icon="angle-up" class="upper-icon" />
</el-button>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeUpdate, onMounted, onUnmounted, ref, watch } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useMagicKeys, whenever } from '@vueuse/core'
import { useI18next } from 'vue3-i18next'
import { useRoute } from 'vue-router'
import { Entity } from 'megalodon'
import { ElMessage } from 'element-plus'
import { useStore } from '@/store'
import Notification from '@/components/organisms/Notification.vue'
import StatusLoading from '@/components/organisms/StatusLoading.vue'
import { EventEmitter } from '@/components/event'
import useReloadable from '@/components/utils/reloadable'
import { LoadingCard } from '@/types/loading-card'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Mentions'
import { MUTATION_TYPES as SIDE_MENU_MUTATION } from '@/store/TimelineSpace/SideMenu'
import { MUTATION_TYPES as TIMELINE_MUTATION } from '@/store/TimelineSpace'
import { MUTATION_TYPES as HEADER_MUTATION } from '@/store/TimelineSpace/HeaderMenu'
export default defineComponent({
name: 'mentions',
components: { Notification, StatusLoading },
setup() {
const space = 'TimelineSpace/Contents/Mentions'
const store = useStore()
const route = useRoute()
const i18n = useI18next()
const { reloadable } = useReloadable(store, route, i18n)
const { j, k, Ctrl_r } = useMagicKeys()
const focusedId = ref<string | null>(null)
const scroller = ref<any>()
const mentions = computed<Array<Entity.Notification | LoadingCard>>(() => store.getters[`${space}/mentions`])
const lazyLoading = computed(() => store.state.TimelineSpace.Contents.Mentions.lazyLoading)
const heading = computed(() => store.state.TimelineSpace.Contents.Mentions.heading)
const openSideBar = computed(() => store.state.TimelineSpace.Contents.SideBar.openSideBar)
const startReload = computed(() => store.state.TimelineSpace.HeaderMenu.reload)
const modalOpened = computed<boolean>(() => store.getters[`TimelineSpace/Modals/modalOpened`])
const currentFocusedIndex = computed(() => mentions.value.findIndex(notification => focusedId.value === notification.id))
const shortcutEnabled = computed(() => !modalOpened.value)
onMounted(() => {
store.commit(`TimelineSpace/SideMenu/${SIDE_MENU_MUTATION.CHANGE_UNREAD_MENTIONS}`, false)
document.getElementById('scroller')?.addEventListener('scroll', onScroll)
})
onBeforeUpdate(() => {
if (store.state.TimelineSpace.SideMenu.unreadMentions && heading.value) {
store.commit(`TimelineSpace/SideMenu/${SIDE_MENU_MUTATION.CHANGE_UNREAD_MENTIONS}`, false)
}
})
onUnmounted(() => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.ARCHIVE_MENTIONS}`)
const el = document.getElementById('scroller')
if (el !== undefined && el !== null) {
el.removeEventListener('scroll', onScroll)
el.scrollTop = 0
}
})
watch(startReload, (newVal, oldVal) => {
if (!oldVal && newVal) {
reload().finally(() => {
store.commit(`TimelineSpace/HeaderMenu/${HEADER_MUTATION.CHANGE_RELOAD}`, false)
})
}
})
watch(focusedId, (newVal, _oldVal) => {
if (newVal && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
} else if (newVal === null && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
}
})
whenever(logicAnd(j, shortcutEnabled), () => {
if (focusedId.value === null) {
focusedId.value = mentions.value[0].id
} else {
focusNext()
}
})
whenever(logicAnd(k, shortcutEnabled), () => {
focusPrev()
})
whenever(logicAnd(Ctrl_r, shortcutEnabled), () => {
reload()
})
const onScroll = (event: Event) => {
// for lazyLoading
if (
(event.target as HTMLElement)!.clientHeight + (event.target as HTMLElement)!.scrollTop >=
document.getElementById('scroller')!.scrollHeight - 10 &&
!lazyLoading.value
) {
store.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_MENTIONS}`, mentions.value[mentions.value.length - 1]).catch(() => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
})
}
if ((event.target as HTMLElement)!.scrollTop > 10 && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
} else if ((event.target as HTMLElement)!.scrollTop <= 10 && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
}
}
const updateToot = (message: Entity.Status) => {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, message)
}
const reload = async () => {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, true)
try {
await reloadable()
} finally {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, false)
}
}
const upper = () => {
scroller.value.scrollToItem(0)
focusedId.value = null
}
const focusNext = () => {
if (currentFocusedIndex.value === -1) {
focusedId.value = mentions.value[0].id
} else if (currentFocusedIndex.value < mentions.value.length) {
focusedId.value = mentions.value[currentFocusedIndex.value + 1].id
}
}
const focusPrev = () => {
if (currentFocusedIndex.value === 0) {
focusedId.value = null
} else if (currentFocusedIndex.value > 0) {
focusedId.value = mentions.value[currentFocusedIndex.value - 1].id
}
}
const focusNotification = (message: Entity.Notification) => {
focusedId.value = message.id
}
const focusSidebar = () => {
EventEmitter.emit('focus-sidebar')
}
return {
mentions,
scroller,
focusedId,
modalOpened,
updateToot,
focusSidebar,
focusNotification,
openSideBar,
heading,
upper
}
}
})
</script>
<style lang="scss" scoped>
#mentions {
height: 100%;
overflow: auto;
scroll-behavior: auto;
.scroller {
height: 100%;
}
.loading-card {
height: 60px;
}
.loading-card:empty {
height: 0;
}
.upper {
position: fixed;
bottom: 20px;
right: 20px;
}
.upper-with-side-bar {
position: fixed;
bottom: 20px;
right: calc(20px + var(--current-sidebar-width));
transition: all 0.5s;
}
.upper-icon {
padding: 3px;
}
}
</style>
<style lang="scss" src="@/assets/timeline-transition.scss"></style>

View File

@ -1,7 +1,6 @@
<template>
<div id="notifications">
<div></div>
<DynamicScroller :items="handledNotifications" :min-item-size="20" id="scroller" class="scroller" ref="scroller">
<DynamicScroller :items="notifications" :min-item-size="20" id="scroller" class="scroller" ref="scroller">
<template v-slot="{ item, index, active }">
<template v-if="item.id === 'loading-card'">
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.id]" :data-index="index" :watchData="true">
@ -11,10 +10,13 @@
<template v-else>
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.url]" :data-index="index" :watchData="true">
<notification
v-if="account.account && account.server"
:message="item"
:focused="item.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
:account="account.account"
:server="account.server"
v-on:update="updateToot"
@focusRight="focusSidebar"
@selectNotification="focusNotification(item)"
@ -33,7 +35,7 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted, onBeforeUpdate, onUnmounted, watch } from 'vue'
import { defineComponent, ref, computed, onMounted, onBeforeUpdate, onUnmounted, watch, reactive } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useMagicKeys, whenever } from '@vueuse/core'
import { ElMessage } from 'element-plus'
@ -44,11 +46,13 @@ import { EventEmitter } from '@/components/event'
import { useStore } from '@/store'
import { useRoute } from 'vue-router'
import { useI18next } from 'vue3-i18next'
import useReloadable from '@/components/utils/reloadable'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Notifications'
import { MUTATION_TYPES as SIDE_MENU_MUTATION } from '@/store/TimelineSpace/SideMenu'
import { MUTATION_TYPES as TIMELINE_MUTATION } from '@/store/TimelineSpace'
import { MUTATION_TYPES as HEADER_MUTATION } from '@/store/TimelineSpace/HeaderMenu'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { MyWindow } from '~/src/types/global'
export default defineComponent({
name: 'notifications',
@ -59,31 +63,40 @@ export default defineComponent({
const store = useStore()
const route = useRoute()
const i18n = useI18next()
const { reloadable } = useReloadable(store, route, i18n)
const { j, k, Ctrl_r } = useMagicKeys()
const { j, k } = useMagicKeys()
const win = (window as any) as MyWindow
const id = computed(() => parseInt(route.params.id as string))
const focusedId = ref<string | null>(null)
const loadingMore = ref(false)
const scroller = ref<any>()
const lazyLoading = ref(false)
const heading = ref(true)
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const notifications = computed(() => store.state.TimelineSpace.Contents.Notifications.notifications)
const lazyLoading = computed(() => store.state.TimelineSpace.Contents.Notifications.lazyLoading)
const heading = computed(() => store.state.TimelineSpace.Contents.Notifications.heading)
const notifications = computed(() => store.state.TimelineSpace.Contents.Notifications.notifications[id.value])
const openSideBar = computed(() => store.state.TimelineSpace.Contents.SideBar.openSideBar)
const startReload = computed(() => store.state.TimelineSpace.HeaderMenu.reload)
const modalOpened = computed<boolean>(() => store.getters[`TimelineSpace/Modals/modalOpened`])
const filters = computed(() => store.getters[`${space}/filters}`])
const handledNotifications = computed(() => store.getters[`${space}/handledNotifications`])
const currentFocusedIndex = computed(() => notifications.value.findIndex(notification => focusedId.value === notification.id))
const shortcutEnabled = computed(() => !modalOpened.value)
onMounted(() => {
onMounted(async () => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
store.commit(`TimelineSpace/SideMenu/${SIDE_MENU_MUTATION.CHANGE_UNREAD_NOTIFICATIONS}`, false)
store.dispatch(`${space}/${ACTION_TYPES.RESET_BADGE}`)
document.getElementById('scroller')?.addEventListener('scroll', onScroll)
if (heading.value && handledNotifications.value.length > 0) {
store.dispatch(`${space}/${ACTION_TYPES.SAVE_MARKER}`)
if (heading.value && notifications.value.length > 0) {
store.dispatch(`${space}/${ACTION_TYPES.SAVE_MARKER}`, account)
}
})
onBeforeUpdate(() => {
@ -92,41 +105,32 @@ export default defineComponent({
}
})
onUnmounted(() => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.ARCHIVE_NOTIFICATIONS}`)
const el = document.getElementById('scroller')
if (el !== undefined && el !== null) {
el.removeEventListener('scroll', onScroll)
el.scrollTop = 0
}
})
watch(startReload, (newVal, oldVal) => {
if (!oldVal && newVal) {
reload().finally(() => {
store.commit(`TimelineSpace/HeaderMenu/${HEADER_MUTATION.CHANGE_RELOAD}`, false)
})
}
})
watch(
notifications,
(newState, _oldState) => {
if (heading.value && newState.length > 0) {
store.dispatch(`${space}/${ACTION_TYPES.SAVE_MARKER}`)
if (heading.value && newState.length > 0 && account.account && account.server) {
store.dispatch(`${space}/${ACTION_TYPES.SAVE_MARKER}`, account)
}
},
{ deep: true }
)
watch(focusedId, (newVal, _oldVal) => {
if (newVal && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if (newVal === null && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
heading.value = true
store.commit(`${space}/${ACTION_TYPES.RESET_BADGE}`)
}
})
whenever(logicAnd(j, shortcutEnabled), () => {
if (focusedId.value === null) {
focusedId.value = handledNotifications.value[0].id
focusedId.value = notifications.value[0].id
} else {
focusNext()
}
@ -134,9 +138,6 @@ export default defineComponent({
whenever(logicAnd(k, shortcutEnabled), () => {
focusPrev()
})
whenever(logicAnd(Ctrl_r, shortcutEnabled), () => {
reload()
})
const onScroll = (event: Event) => {
if (
@ -144,43 +145,50 @@ export default defineComponent({
document.getElementById('scroller')!.scrollHeight - 10 &&
!lazyLoading.value
) {
lazyLoading.value = true
store
.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_NOTIFICATIONS}`, handledNotifications.value[handledNotifications.value.length - 1])
.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_NOTIFICATIONS}`, {
lastNotification: notifications.value[notifications.value.length - 1],
account: account.account,
server: account.server
})
.catch(() => {
ElMessage({
message: i18n.t('message.notification_fetch_error'),
type: 'error'
})
})
.finally(() => {
lazyLoading.value = false
})
}
if ((event.target as HTMLElement)!.scrollTop > 10 && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if ((event.target as HTMLElement)!.scrollTop <= 10 && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
heading.value = true
store.dispatch(`${space}/${ACTION_TYPES.RESET_BADGE}`)
store.dispatch(`${space}/${ACTION_TYPES.SAVE_MARKER}`)
store.dispatch(`${space}/${ACTION_TYPES.SAVE_MARKER}`, account)
}
}
const updateToot = (message: Entity.Status) => {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, message)
if (account.account) {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, { status: message, accountId: account.account.id })
}
}
const fetchNotificationsSince = (since_id: string) => {
loadingMore.value = true
store.dispatch(`${space}/${ACTION_TYPES.FETCH_NOTIFICATIONS_SINCE}`, since_id).finally(() => {
setTimeout(() => {
loadingMore.value = false
}, 500)
})
}
const reload = async () => {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, true)
try {
await reloadable()
store.dispatch(`${space}/${ACTION_TYPES.RESET_BADGE}`)
} finally {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, false)
}
store
.dispatch(`${space}/${ACTION_TYPES.FETCH_NOTIFICATIONS_SINCE}`, {
sinceId: since_id,
account: account.account,
server: account.server
})
.finally(() => {
setTimeout(() => {
loadingMore.value = false
}, 500)
})
}
const upper = () => {
scroller.value.scrollToItem(0)
@ -188,16 +196,16 @@ export default defineComponent({
}
const focusNext = () => {
if (currentFocusedIndex.value === -1) {
focusedId.value = handledNotifications.value[0].id
} else if (currentFocusedIndex.value < handledNotifications.value.length) {
focusedId.value = handledNotifications.value[currentFocusedIndex.value + 1].id
focusedId.value = notifications.value[0].id
} else if (currentFocusedIndex.value < notifications.value.length) {
focusedId.value = notifications.value[currentFocusedIndex.value + 1].id
}
}
const focusPrev = () => {
if (currentFocusedIndex.value === 0) {
focusedId.value = null
} else if (currentFocusedIndex.value > 0) {
focusedId.value = handledNotifications.value[currentFocusedIndex.value - 1].id
focusedId.value = notifications.value[currentFocusedIndex.value - 1].id
}
}
const focusNotification = (notification: Entity.Notification) => {
@ -208,7 +216,7 @@ export default defineComponent({
}
return {
handledNotifications,
notifications,
loadingMore,
fetchNotificationsSince,
focusedId,
@ -221,7 +229,8 @@ export default defineComponent({
focusNotification,
openSideBar,
heading,
upper
upper,
account
}
}
})

View File

@ -1,14 +1,16 @@
<template>
<div id="public">
<div class="unread">{{ unreads.length > 0 ? unreads.length : '' }}</div>
<DynamicScroller :items="timeline" :min-item-size="86" id="scroller" class="scroller" ref="scroller">
<template v-slot="{ item, index, active }">
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.uri]" :data-index="index" :watchData="true">
<toot
v-if="account.account && account.server"
:message="item"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
:filters="filters"
:account="account.account"
:server="account.server"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusRight="focusSidebar"
@ -27,7 +29,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, defineComponent, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, ref, watch, reactive } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useMagicKeys, whenever } from '@vueuse/core'
import { ElMessage } from 'element-plus'
@ -37,12 +39,11 @@ import { useRoute } from 'vue-router'
import { useStore } from '@/store'
import Toot from '@/components/organisms/Toot.vue'
import { EventEmitter } from '@/components/event'
import useReloadable from '@/components/utils/reloadable'
import { MUTATION_TYPES as SIDE_MENU_MUTATION } from '@/store/TimelineSpace/SideMenu'
import { MUTATION_TYPES as TIMELINE_MUTATION, ACTION_TYPES as TIMELINE_ACTION } from '@/store/TimelineSpace'
import { MUTATION_TYPES as HEADER_MUTATION } from '@/store/TimelineSpace/HeaderMenu'
import { MUTATION_TYPES as CONTENTS_MUTATION } from '@/store/TimelineSpace/Contents'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Public'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { MyWindow } from '~/src/types/global'
export default defineComponent({
name: 'public',
@ -52,33 +53,34 @@ export default defineComponent({
const store = useStore()
const route = useRoute()
const i18n = useI18next()
const { reloadable } = useReloadable(store, route, i18n)
const { j, k, Ctrl_r } = useMagicKeys()
const { j, k } = useMagicKeys()
const win = (window as any) as MyWindow
const id = computed(() => parseInt(route.params.id as string))
const focusedId = ref<string | null>(null)
const scroller = ref<any>(null)
const lazyLoading = ref(false)
const heading = ref(true)
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const timeline = computed(() => store.state.TimelineSpace.Contents.Public.timeline)
const unreads = computed(() => store.state.TimelineSpace.Contents.Public.unreads)
const lazyLoading = computed(() => store.state.TimelineSpace.Contents.Public.lazyLoading)
const heading = computed(() => store.state.TimelineSpace.Contents.Public.heading)
const timeline = computed(() => store.state.TimelineSpace.Contents.Public.timeline[id.value])
const openSideBar = computed(() => store.state.TimelineSpace.Contents.SideBar.openSideBar)
const startReload = computed(() => store.state.TimelineSpace.HeaderMenu.reload)
const unreadNotification = computed(() => store.state.TimelineSpace.timelineSetting.unreadNotification)
const modalOpened = computed<boolean>(() => store.getters[`TimelineSpace/Modals/modalOpened`])
const filters = computed(() => store.getters[`${space}/filters`])
const currentFocusedIndex = computed(() => timeline.value.findIndex(toot => focusedId.value === toot.uri + toot.id))
const shortcutEnabled = computed(() => !modalOpened.value)
onMounted(async () => {
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
store.commit(`TimelineSpace/SideMenu/${SIDE_MENU_MUTATION.CHANGE_UNREAD_PUBLIC_TIMELINE}`, false)
document.getElementById('scroller')?.addEventListener('scroll', onScroll)
if (!unreadNotification.value.public) {
store.commit(`TimelineSpace/Contents/${CONTENTS_MUTATION.CHANGE_LOADING}`, true)
await initialize().finally(() => {
store.commit(`TimelineSpace/Contents/${CONTENTS_MUTATION.CHANGE_LOADING}`, false)
})
}
})
onBeforeUpdate(() => {
if (store.state.TimelineSpace.SideMenu.unreadPublicTimeline && heading.value) {
@ -86,37 +88,21 @@ export default defineComponent({
}
})
onBeforeUnmount(() => {
if (!unreadNotification.value.public) {
store.dispatch(`TimelineSpace/${TIMELINE_ACTION.STOP_PUBLIC_STREAMING}`)
store.dispatch(`TimelineSpace/${TIMELINE_ACTION.UNBIND_PUBLIC_STREAMING}`)
}
EventEmitter.off('focus-timeline')
})
onUnmounted(() => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.ARCHIVE_TIMELINE}`)
if (!unreadNotification.value.public) {
store.commit(`${space}/${MUTATION_TYPES.CLEAR_TIMELINE}`)
}
const el = document.getElementById('scroller')
if (el !== undefined && el !== null) {
el.removeEventListener('scroll', onScroll)
el.scrollTop = 0
}
})
watch(startReload, (newVal, oldVal) => {
if (!oldVal && newVal) {
reload().finally(() => {
store.commit(`TimelineSpace/HeaderMenu/${HEADER_MUTATION.CHANGE_RELOAD}`, false)
})
}
})
watch(focusedId, (newVal, _oldVal) => {
if (newVal && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if (newVal === null && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
heading.value = true
}
})
whenever(logicAnd(j, shortcutEnabled), () => {
@ -129,54 +115,47 @@ export default defineComponent({
whenever(logicAnd(k, shortcutEnabled), () => {
focusPrev()
})
whenever(logicAnd(Ctrl_r, shortcutEnabled), () => {
reload()
})
const initialize = async () => {
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_PUBLIC_TIMELINE}`).catch(_ => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
})
await store.dispatch(`TimelineSpace/${TIMELINE_ACTION.BIND_PUBLIC_STREAMING}`)
store.dispatch(`TimelineSpace/${TIMELINE_ACTION.START_PUBLIC_STREAMING}`)
}
const onScroll = (event: Event) => {
if (
(event.target as HTMLElement)!.clientHeight + (event.target as HTMLElement)!.scrollTop >=
document.getElementById('scroller')!.scrollHeight - 10 &&
!lazyLoading.value
) {
store.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_TIMELINE}`, timeline.value[timeline.value.length - 1]).catch(() => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
lazyLoading.value = true
store
.dispatch(`${space}/${ACTION_TYPES.LAZY_FETCH_TIMELINE}`, {
statuses: timeline.value[timeline.value.length - 1],
account: account.account,
server: account.server
})
.catch(() => {
ElMessage({
message: i18n.t('message.timeline_fetch_error'),
type: 'error'
})
})
.finally(() => {
lazyLoading.value = false
})
})
}
if ((event.target as HTMLElement)!.scrollTop > 10 && heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, false)
heading.value = false
} else if ((event.target as HTMLElement)!.scrollTop <= 10 && !heading.value) {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_HEADING}`, true)
store.commit(`${space}/${MUTATION_TYPES.MERGE_UNREADS}`)
}
}
const reload = async () => {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, true)
try {
await reloadable()
} finally {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, false)
heading.value = true
}
}
const updateToot = (message: Entity.Status) => {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, message)
if (account.account) {
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TOOT}`, { status: message, accountId: account.account.id })
}
}
const deleteToot = (message: Entity.Status) => {
store.commit(`${space}/${MUTATION_TYPES.DELETE_TOOT}`, message.id)
if (account.account) {
store.commit(`${space}/${MUTATION_TYPES.DELETE_TOOT}`, { statusId: message.id, accountId: account.account.id })
}
}
const upper = () => {
scroller.value.scrollToItem(0)
@ -216,7 +195,7 @@ export default defineComponent({
openSideBar,
heading,
upper,
unreads
account
}
}
})

View File

@ -87,9 +87,6 @@ export default defineComponent({
case 'bookmarks':
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TITLE}`, i18n.t('header_menu.bookmark'))
break
case 'mentions':
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TITLE}`, i18n.t('header_menu.mention'))
break
case 'follow-requests':
store.commit(`${space}/${MUTATION_TYPES.UPDATE_TITLE}`, i18n.t('header_menu.follow_requests'))
break
@ -131,16 +128,10 @@ export default defineComponent({
}
const reload = () => {
switch (route.name) {
case 'home':
case 'notifications':
case 'mentions':
case 'favourites':
case 'bookmarks':
case 'local':
case 'public':
case 'tag':
case 'list':
case 'direct-messages':
store.commit(`${space}/${MUTATION_TYPES.CHANGE_RELOAD}`, true)
break
default:
@ -149,16 +140,10 @@ export default defineComponent({
}
const reloadable = () => {
switch (route.name) {
case 'home':
case 'notifications':
case 'mentions':
case 'favourites':
case 'bookmarks':
case 'local':
case 'public':
case 'tag':
case 'list':
case 'direct-messages':
return true
default:
return false

View File

@ -37,11 +37,7 @@ export default defineComponent({
const channelForm = ref<HTMLInputElement>()
const channelList = computed(() =>
store.state.TimelineSpace.Modals.Jump.defaultChannelList
.concat(store.state.TimelineSpace.Modals.Jump.tagChannelList)
.concat(store.state.TimelineSpace.Modals.Jump.listChannelList)
)
const channelList = computed(() => store.state.TimelineSpace.Modals.Jump.defaultChannelList)
const selectedChannel = computed(() => store.state.TimelineSpace.Modals.Jump.selectedChannel)
const inputtedChannel = computed({
get: () => store.state.TimelineSpace.Modals.Jump.channel,
@ -57,8 +53,6 @@ export default defineComponent({
)
onMounted(() => {
store.dispatch(`${space}/${ACTION_TYPES.SYNC_LIST_CHANNEL}`)
store.dispatch(`${space}/${ACTION_TYPES.SYNC_TAG_CHANNEL}`)
nextTick(() => {
setTimeout(() => {
channelForm.value?.focus()

View File

@ -4,11 +4,11 @@
<div :class="collapse ? 'profile-narrow' : 'profile-wide'">
<div class="account">
<div class="avatar" v-if="collapse">
<img :src="account.avatar" />
<img :src="account.account?.avatar" />
</div>
<div class="acct" v-else>
@{{ account.username }}
<span class="domain-name">{{ account.domain }}</span>
@{{ account.account?.username }}
<span class="domain-name">{{ account.server?.domain }}</span>
</div>
<el-dropdown trigger="click" @command="handleProfile" :title="$t('side_menu.profile')">
<span class="el-dropdown-link">
@ -71,23 +71,6 @@
</div>
</template>
</el-menu-item>
<el-menu-item
:index="`/${id}/mentions`"
role="menuitem"
:title="$t('side_menu.mention')"
class="menu-item"
v-if="enabledTimelines.mention"
>
<div class="menu-item-icon">
<font-awesome-icon icon="at" />
</div>
<template #title>
<div>
<span>{{ $t('side_menu.mention') }}</span>
<el-badge is-dot :hidden="!unreadMentions"> </el-badge>
</div>
</template>
</el-menu-item>
<el-menu-item
:index="`/${id}/direct-messages`"
role="menuitem"
@ -263,13 +246,18 @@
</template>
<script lang="ts">
import { defineComponent, computed, onMounted } from 'vue'
import { defineComponent, computed, onMounted, ref, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useStore } from '@/store'
import { ACTION_TYPES } from '@/store/TimelineSpace/SideMenu'
import { ACTION_TYPES as PROFILE_ACTION } from '@/store/TimelineSpace/Contents/SideBar/AccountProfile'
import { ACTION_TYPES as SIDEBAR_ACTION, MUTATION_TYPES as SIDEBAR_MUTATION } from '@/store/TimelineSpace/Contents/SideBar'
import { ACTION_TYPES as GLOBAL_ACTION } from '@/store/GlobalHeader'
import { MyWindow } from '~/src/types/global'
import generator, { Entity, MegalodonInterface } from 'megalodon'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
import { LocalTag } from '~/src/types/localTag'
export default defineComponent({
name: 'side-menu',
@ -279,33 +267,102 @@ export default defineComponent({
const route = useRoute()
const router = useRouter()
const win = (window as any) as MyWindow
const lists = ref<Array<Entity.List>>([])
const tags = ref<Array<LocalTag>>([])
const enabledTimelines = reactive({
home: true,
notification: true,
direct: true,
favourite: true,
bookmark: true,
local: true,
public: true,
tag: true,
list: true
})
const account = reactive<{ account: LocalAccount | null; server: LocalServer | null }>({
account: null,
server: null
})
const unreadHomeTimeline = computed(() => store.state.TimelineSpace.SideMenu.unreadHomeTimeline)
const unreadNotifications = computed(() => store.state.TimelineSpace.SideMenu.unreadNotifications)
const unreadMentions = computed(() => store.state.TimelineSpace.SideMenu.unreadMentions)
const unreadLocalTimeline = computed(() => store.state.TimelineSpace.SideMenu.unreadLocalTimeline)
const unreadDirectMessagesTimeline = computed(() => store.state.TimelineSpace.SideMenu.unreadDirectMessagesTimeline)
const unreadPublicTimeline = computed(() => store.state.TimelineSpace.SideMenu.unreadPublicTimeline)
const unreadFollowRequests = computed(() => store.state.TimelineSpace.SideMenu.unreadFollowRequests)
const lists = computed(() => store.state.TimelineSpace.SideMenu.lists)
const tags = computed(() => store.state.TimelineSpace.SideMenu.tags)
const collapse = computed(() => store.state.TimelineSpace.SideMenu.collapse)
const enabledTimelines = computed(() => store.state.TimelineSpace.SideMenu.enabledTimelines)
const account = computed(() => store.state.TimelineSpace.account)
const themeColor = computed(() => store.state.App.theme.side_menu_color)
const hideGlobalHeader = computed(() => store.state.GlobalHeader.hide)
const userAgent = computed(() => store.state.App.userAgent)
const activeRoute = computed(() => route.path)
const id = computed(() => route.params.id)
const id = computed(() => parseInt(route.params.id as string))
onMounted(() => {
onMounted(async () => {
store.dispatch(`${space}/${ACTION_TYPES.READ_COLLAPSE}`)
store.dispatch(`${space}/${ACTION_TYPES.LIST_TAGS}`)
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
account.account = a
account.server = s
const client = generator(s.sns, s.baseURL, a.accessToken, userAgent.value)
await fetchLists(client)
await fetchTags(a)
await confirmTimelines(client)
})
const fetchLists = async (client: MegalodonInterface) => {
const res = await client.getLists()
lists.value = res.data
}
const fetchTags = async (account: LocalAccount) => {
tags.value = await win.ipcRenderer.invoke('list-hashtags', account.id)
}
const confirmTimelines = async (client: MegalodonInterface) => {
const notification = async () => {
return client.getNotifications({ limit: 1 }).catch(() => {
enabledTimelines.notification = false
})
}
const direct = async () => {
return client.getConversationTimeline({ limit: 1 }).catch(() => {
enabledTimelines.direct = false
})
}
const favourite = async () => {
return client.getFavourites({ limit: 1 }).catch(() => {
enabledTimelines.favourite = false
})
}
const bookmark = async () => {
return client.getBookmarks({ limit: 1 }).catch(() => {
enabledTimelines.bookmark = false
})
}
const local = async () => {
return client.getLocalTimeline({ limit: 1 }).catch(() => {
enabledTimelines.local = false
})
}
const pub = async () => {
return client.getPublicTimeline({ limit: 1 }).catch(() => {
enabledTimelines.public = false
})
}
await Promise.all([notification(), direct(), favourite(), bookmark(), local(), pub()])
}
const handleProfile = (command: string) => {
switch (command) {
case 'show':
if (!account.account) {
return
}
store
.dispatch(`TimelineSpace/Contents/SideBar/AccountProfile/${PROFILE_ACTION.FETCH_ACCOUNT}`, account.value.accountId)
.dispatch(`TimelineSpace/Contents/SideBar/AccountProfile/${PROFILE_ACTION.FETCH_ACCOUNT}`, account.account.accountId)
.then(account => {
store.dispatch(`TimelineSpace/Contents/SideBar/AccountProfile/${PROFILE_ACTION.CHANGE_ACCOUNT}`, account)
store.commit(`TimelineSpace/Contents/SideBar/${SIDEBAR_MUTATION.CHANGE_OPEN_SIDEBAR}`, true)
@ -314,7 +371,9 @@ export default defineComponent({
store.dispatch(`TimelineSpace/Contents/SideBar/${SIDEBAR_ACTION.OPEN_ACCOUNT_COMPONENT}`)
break
case 'edit':
;(window as any).shell.openExternal(account.value.baseURL + '/settings/profile')
if (account.server) {
;(window as any).shell.openExternal(account.server.baseURL + '/settings/profile')
}
break
case 'settings': {
const url = `/${id.value}/settings`
@ -336,7 +395,6 @@ export default defineComponent({
return {
unreadHomeTimeline,
unreadNotifications,
unreadMentions,
unreadLocalTimeline,
unreadDirectMessagesTimeline,
unreadPublicTimeline,

View File

@ -35,6 +35,8 @@
:filters="filters"
:focused="focused"
:overlaid="overlaid"
:account="account"
:server="server"
@update="updateToot"
@delete="deleteToot"
@focus-right="$emit('focusRight')"
@ -69,6 +71,8 @@
:filters="filters"
:focused="focused"
:overlaid="overlaid"
:account="account"
:server="server"
@focus-right="$emit('focusRight')"
@select="$emit('selectNotification')"
>
@ -117,6 +121,8 @@ import Follow from './Notification/Follow.vue'
import FollowRequest from './Notification/FollowRequest.vue'
import Mention from './Notification/Mention.vue'
import Status from './Notification/Status.vue'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
export default defineComponent({
name: 'Notification',
@ -143,6 +149,14 @@ export default defineComponent({
overlaid: {
type: Boolean,
default: () => false
},
account: {
type: Object as PropType<LocalAccount>,
required: true
},
server: {
type: Object as PropType<LocalServer>,
required: true
}
},
emits: ['focusRight', 'selectNotification', 'update', 'delete'],

View File

@ -5,6 +5,8 @@
:filters="filters"
:focused="focused"
:overlaid="overlaid"
:account="account"
:server="server"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusRight="$emit('focusRight')"
@ -18,6 +20,8 @@
import { defineComponent, PropType } from 'vue'
import { Entity } from 'megalodon'
import Toot from '../Toot.vue'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
export default defineComponent({
name: 'mention',
@ -37,6 +41,14 @@ export default defineComponent({
overlaid: {
type: Boolean,
default: false
},
account: {
type: Object as PropType<LocalAccount>,
required: true
},
server: {
type: Object as PropType<LocalServer>,
required: true
}
},
components: { Toot },

View File

@ -26,6 +26,8 @@
:filters="filters"
:focused="focused"
:overlaid="overlaid"
:account="account"
:server="server"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusRight="$emit('focusRight')"
@ -45,6 +47,8 @@ import Toot from '../Toot.vue'
import { usernameWithStyle } from '@/utils/username'
import { MUTATION_TYPES as SIDEBAR_MUTATION, ACTION_TYPES as SIDEBAR_ACTION } from '@/store/TimelineSpace/Contents/SideBar'
import { ACTION_TYPES as PROFILE_ACTION } from '@/store/TimelineSpace/Contents/SideBar/AccountProfile'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
export default defineComponent({
name: 'mention',
@ -64,6 +68,14 @@ export default defineComponent({
overlaid: {
type: Boolean,
default: false
},
account: {
type: Object as PropType<LocalAccount>,
required: true
},
server: {
type: Object as PropType<LocalServer>,
required: true
}
},
components: { Toot, FailoverImg },

View File

@ -137,7 +137,6 @@
{{ favouritesCount }}
</div>
<el-button
v-if="bookmarkSupported"
:class="originalMessage.bookmarked ? 'bookmarked' : 'bookmark'"
link
:title="$t('cards.toot.bookmark')"
@ -149,7 +148,7 @@
<el-button v-if="quoteSupported" link class="quote-btn" @click="openQuote()">
<font-awesome-icon icon="quote-right" size="sm" />
</el-button>
<template v-if="sns !== 'mastodon'">
<template v-if="server!.sns !== 'mastodon'">
<el-popover
placement="bottom"
width="281"
@ -262,6 +261,8 @@ import { ACTION_TYPES as REPORT_ACTION } from '@/store/TimelineSpace/Modals/Repo
import { ACTION_TYPES as MUTE_ACTION } from '@/store/TimelineSpace/Modals/MuteConfirm'
import { ACTION_TYPES as VIEWER_ACTION } from '@/store/TimelineSpace/Modals/ImageViewer'
import { ACTION_TYPES } from '@/store/organisms/Toot'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalServer } from '~/src/types/localServer'
const defaultEmojiIndex = new EmojiIndex(data)
@ -298,16 +299,24 @@ export default defineComponent({
detailed: {
type: Boolean,
default: false
},
account: {
type: Object as PropType<LocalAccount>,
required: true
},
server: {
type: Object as PropType<LocalServer>,
required: true
}
},
emits: ['selectToot', 'focusRight', 'focusLeft'],
emits: ['selectToot', 'focusRight', 'focusLeft', 'update', 'delete', 'sizeChanged'],
setup(props, ctx) {
const space = 'organisms/Toot'
const store = useStore()
const route = useRoute()
const router = useRouter()
const i18n = useI18next()
const { focused, overlaid, message, filters } = toRefs(props)
const { focused, overlaid, message, filters, account, server } = toRefs(props)
const { l, h, r, b, f, o, p, i, x } = useMagicKeys()
const statusRef = ref<any>(null)
@ -320,9 +329,6 @@ export default defineComponent({
const displayNameStyle = computed(() => store.state.App.displayNameStyle)
const timeFormat = computed(() => store.state.App.timeFormat)
const language = computed(() => store.state.App.language)
const sns = computed(() => store.state.TimelineSpace.sns)
const account = computed(() => store.state.TimelineSpace.account)
const bookmarkSupported = computed(() => store.state.TimelineSpace.SideMenu.enabledTimelines.bookmark)
const shortcutEnabled = computed(() => focused.value && !overlaid.value)
const originalMessage = computed(() => {
if (message.value.reblog && !message.value.quote) {
@ -352,7 +358,7 @@ export default defineComponent({
return null
})
const isMyMessage = computed(() => {
return store.state.TimelineSpace.account.accountId === originalMessage.value.account.id
return account.value.accountId === originalMessage.value.account.id
})
const application = computed(() => {
const msg = originalMessage.value
@ -386,7 +392,7 @@ export default defineComponent({
return originalMessage.value.visibility === 'direct'
})
const quoteSupported = computed(() => {
return QuoteSupported(sns.value, account.value.domain)
return QuoteSupported(server.value.sns, server.value.domain)
})
whenever(logicAnd(l, shortcutEnabled), () => {
@ -677,9 +683,6 @@ export default defineComponent({
displayNameStyle,
timeFormat,
language,
sns,
account,
bookmarkSupported,
originalMessage,
timestamp,
readableTimestamp,

View File

@ -4,7 +4,6 @@ import { RouteLocationNormalizedLoaded } from 'vue-router'
import { i18n } from 'i18next'
import { ElMessage } from 'element-plus'
import { ACTION_TYPES } from '@/store/TimelineSpace'
import { ACTION_TYPES as GLOBAL_ACTION } from '@/store/GlobalHeader'
export default function useReloadable(store: Store<RootState>, route: RouteLocationNormalizedLoaded, i18next: i18n) {
async function reloadable() {
@ -15,11 +14,6 @@ export default function useReloadable(store: Store<RootState>, route: RouteLocat
})
throw err
})
await store.dispatch(`GlobalHeader/${GLOBAL_ACTION.STOP_USER_STREAMINGS}`)
await store.dispatch(`TimelineSpace/${ACTION_TYPES.STOP_STREAMINGS}`)
await store.dispatch(`TimelineSpace/${ACTION_TYPES.FETCH_CONTENTS_TIMELINES}`)
await store.dispatch(`TimelineSpace/${ACTION_TYPES.START_STREAMINGS}`)
store.dispatch(`GlobalHeader/${GLOBAL_ACTION.START_USER_STREAMINGS}`)
return account
}

View File

@ -1,7 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/components/Login.vue'
import Authorize from '@/components/Authorize.vue'
import Preferences from '@/components/Preferences.vue'
import PreferencesGeneral from '@/components/Preferences/General.vue'
import PreferencesAppearance from '@/components/Preferences/Appearance.vue'
@ -19,7 +18,6 @@ import SettingsFiltersNew from '@/components/Settings/Filters/New.vue'
import TimelineSpace from '@/components/TimelineSpace.vue'
import TimelineSpaceContentsHome from '@/components/TimelineSpace/Contents/Home.vue'
import TimelineSpaceContentsNotifications from '@/components/TimelineSpace/Contents/Notifications.vue'
import TimelineSpaceContentsMentions from '@/components/TimelineSpace/Contents/Mentions.vue'
import TimelineSpaceContentsFavourites from '@/components/TimelineSpace/Contents/Favourites.vue'
import TimelineSpaceContentsLocal from '@/components/TimelineSpace/Contents/Local.vue'
import TimelineSpaceContentsPublic from '@/components/TimelineSpace/Contents/Public.vue'
@ -40,12 +38,6 @@ const routes = [
name: 'login',
component: Login
},
{
path: '/authorize',
name: 'authorize',
component: Authorize,
props: route => ({ url: route.query.url, sns: route.query.sns })
},
{
path: '/preferences/',
name: 'preferences',
@ -130,11 +122,6 @@ const routes = [
name: 'notifications',
component: TimelineSpaceContentsNotifications
},
{
path: 'mentions',
name: 'mentions',
component: TimelineSpaceContentsMentions
},
{
path: 'follow-requests',
name: 'follow-requests',

View File

@ -1,37 +0,0 @@
import { Module, ActionTree } from 'vuex'
import { RootState } from '@/store'
import { MyWindow } from '~/src/types/global'
const win = window as any as MyWindow
export type AuthorizeState = {}
const state = (): AuthorizeState => ({})
export const ACTION_TYPES = {
SUBMIT: 'submit'
}
const actions: ActionTree<AuthorizeState, RootState> = {
[ACTION_TYPES.SUBMIT]: async (_, request: { code: string | null; sns: 'mastodon' | 'pleroma' | 'misskey' }): Promise<string> => {
let req = {
sns: request.sns
}
if (request.code) {
req = Object.assign(req, {
code: request.code.trim()
})
}
const id = await win.ipcRenderer.invoke('get-and-update-access-token', req)
return id
}
}
const Authorize: Module<AuthorizeState, RootState> = {
namespaced: true,
state: state,
mutations: {},
actions: actions
}
export default Authorize

View File

@ -2,13 +2,13 @@ import router from '@/router'
import { LocalAccount } from '~/src/types/localAccount'
import { Module, MutationTree, ActionTree } from 'vuex'
import { RootState } from '@/store'
import { StreamingError } from '~src/errors/streamingError'
import { MyWindow } from '~/src/types/global'
import { LocalServer } from '~src/types/localServer'
const win = window as any as MyWindow
const win = (window as any) as MyWindow
export type GlobalHeaderState = {
accounts: Array<LocalAccount>
accounts: Array<[LocalAccount, LocalServer]>
changing: boolean
hide: boolean
}
@ -26,7 +26,7 @@ export const MUTATION_TYPES = {
}
const mutations: MutationTree<GlobalHeaderState> = {
[MUTATION_TYPES.UPDATE_ACCOUNTS]: (state: GlobalHeaderState, accounts: Array<LocalAccount>) => {
[MUTATION_TYPES.UPDATE_ACCOUNTS]: (state: GlobalHeaderState, accounts: Array<[LocalAccount, LocalServer]>) => {
state.accounts = accounts
},
[MUTATION_TYPES.UPDATE_CHANGING]: (state: GlobalHeaderState, value: boolean) => {
@ -41,27 +41,28 @@ export const ACTION_TYPES = {
INIT_LOAD: 'initLoad',
START_STREAMINGS: 'startStreamings',
LIST_ACCOUNTS: 'listAccounts',
REFRESH_ACCOUNTS: 'refreshAccounts',
WATCH_SHORTCUT_EVENTS: 'watchShortcutEvents',
REMOVE_SHORTCUT_EVENTS: 'removeShortcutEvents',
LOAD_HIDE: 'loadHide',
SWITCH_HIDE: 'switchHide',
START_USER_STREAMINGS: 'startUserStreamings',
STOP_USER_STREAMINGS: 'stopUserStreamings',
LOAD_TIMELINES: 'loadTimelines',
BIND_STREAMINGS: 'bindStreamings',
BIND_NOTIFICATION: 'bindNotification'
}
const actions: ActionTree<GlobalHeaderState, RootState> = {
initLoad: async ({ dispatch }): Promise<Array<LocalAccount>> => {
[ACTION_TYPES.INIT_LOAD]: async ({ dispatch }): Promise<Array<LocalAccount>> => {
// Ignore error
try {
await dispatch('removeShortcutEvents')
await dispatch('loadHide')
dispatch('watchShortcutEvents')
await dispatch(ACTION_TYPES.REMOVE_SHORTCUT_EVENTS)
await dispatch(ACTION_TYPES.LOAD_HIDE)
dispatch(ACTION_TYPES.WATCH_SHORTCUT_EVENTS)
} catch (err) {
console.error(err)
}
const accounts = await dispatch('listAccounts')
const accounts = await dispatch(ACTION_TYPES.LIST_ACCOUNTS)
await dispatch(ACTION_TYPES.LOAD_TIMELINES, accounts)
await dispatch(ACTION_TYPES.BIND_STREAMINGS, accounts)
// Block to root path when user use browser-back, like mouse button.
// Because any contents are not rendered when browser back to / from home.
router.beforeEach((to, from, next) => {
@ -71,28 +72,17 @@ const actions: ActionTree<GlobalHeaderState, RootState> = {
})
return accounts
},
startStreamings: async ({ dispatch }) => {
dispatch('bindNotification')
dispatch('startUserStreamings')
},
listAccounts: async ({ dispatch, commit }): Promise<Array<LocalAccount>> => {
const accounts = await win.ipcRenderer.invoke('list-accounts')
commit(MUTATION_TYPES.UPDATE_ACCOUNTS, accounts)
dispatch('refreshAccounts')
return accounts
},
// Fetch account informations and save current state when GlobalHeader is displayed
refreshAccounts: async ({ commit }): Promise<Array<LocalAccount>> => {
const accounts: Array<LocalAccount> = await win.ipcRenderer.invoke('refresh-accounts')
[ACTION_TYPES.LIST_ACCOUNTS]: async ({ commit }): Promise<Array<[LocalAccount, LocalServer]>> => {
const accounts: Array<[LocalAccount, LocalServer]> = await win.ipcRenderer.invoke('list-accounts')
commit(MUTATION_TYPES.UPDATE_ACCOUNTS, accounts)
return accounts
},
watchShortcutEvents: ({ state, commit, rootState, rootGetters }) => {
[ACTION_TYPES.WATCH_SHORTCUT_EVENTS]: ({ state, commit, rootState, rootGetters }) => {
win.ipcRenderer.on('change-account', (_, account: LocalAccount) => {
if (state.changing) {
return null
}
if ((rootState.route.params.id as string) === account._id!) {
if ((rootState.route.params.id as string) === account[0].id) {
return null
}
// When the modal window is active, don't change account
@ -101,46 +91,81 @@ const actions: ActionTree<GlobalHeaderState, RootState> = {
}
// changing finish after loading
commit(MUTATION_TYPES.UPDATE_CHANGING, true)
router.push(`/${account._id}/home`)
router.push(`/${account[0].id}/home`)
return true
})
},
removeShortcutEvents: async () => {
[ACTION_TYPES.REMOVE_SHORTCUT_EVENTS]: async () => {
win.ipcRenderer.removeAllListeners('change-account')
return true
},
loadHide: async ({ commit }): Promise<boolean> => {
[ACTION_TYPES.LOAD_HIDE]: async ({ commit }): Promise<boolean> => {
const hide: boolean = await win.ipcRenderer.invoke('get-global-header')
commit(MUTATION_TYPES.CHANGE_HIDE, hide)
return hide
},
switchHide: async ({ dispatch }, hide: boolean): Promise<boolean> => {
[ACTION_TYPES.SWITCH_HIDE]: async ({ dispatch }, hide: boolean): Promise<boolean> => {
await win.ipcRenderer.invoke('change-global-header', hide)
dispatch('loadHide')
dispatch(ACTION_TYPES.LOAD_HIDE)
return true
},
startUserStreamings: ({ state }): Promise<{}> => {
// @ts-ignore
return new Promise((resolve, reject) => {
win.ipcRenderer.once('error-start-all-user-streamings', (_, err: StreamingError) => {
reject(err)
})
win.ipcRenderer.send(
'start-all-user-streamings',
state.accounts.map(a => a._id)
)
})
},
stopUserStreamings: () => {
win.ipcRenderer.send('stop-all-user-streamings')
},
bindNotification: () => {
[ACTION_TYPES.BIND_NOTIFICATION]: () => {
win.ipcRenderer.removeAllListeners('open-notification-tab')
win.ipcRenderer.on('open-notification-tab', (_, id: string) => {
router.push(`/${id}/home`)
// We have to wait until change el-menu-item
setTimeout(() => router.push(`/${id}/notifications`), 500)
})
},
[ACTION_TYPES.LOAD_TIMELINES]: async ({ dispatch }, req: Array<[LocalAccount, LocalServer]>) => {
req.forEach(async ([account, server]) => {
await dispatch('TimelineSpace/Contents/Home/fetchTimeline', { account, server }, { root: true })
await dispatch('TimelineSpace/Contents/Notifications/fetchNotifications', { account, server }, { root: true })
await dispatch('TimelineSpace/Contents/Local/fetchLocalTimeline', { account, server }, { root: true })
await dispatch('TimelineSpace/Contents/Public/fetchPublicTimeline', { account, server }, { root: true })
await dispatch('TimelineSpace/Contents/DirectMessages/fetchTimeline', { account, server }, { root: true })
})
},
[ACTION_TYPES.BIND_STREAMINGS]: async ({ commit }, req: Array<[LocalAccount, LocalServer]>) => {
req.forEach(async ([account, _server]) => {
win.ipcRenderer.removeAllListeners(`update-user-streamings-${account.id}`)
win.ipcRenderer.on(`update-user-streamings-${account.id}`, (_, update: Entity.Status) => {
commit('TimelineSpace/Contents/Home/appendTimeline', { status: update, accountId: account.id }, { root: true })
})
win.ipcRenderer.removeAllListeners(`notification-user-streamings-${account.id}`)
win.ipcRenderer.on(`notification-user-streamings-${account.id}`, (_, notification: Entity.Notification) => {
commit('TimelineSpace/Contents/Notifications/appendNotifications', { notification, accountId: account.id }, { root: true })
})
win.ipcRenderer.removeAllListeners(`delete-user-streamings-${account.id}`)
win.ipcRenderer.on(`delete-user-streamings-${account.id}`, (_, id: string) => {
commit('TimelineSpace/Contents/Home/deleteToot', { statusId: id, accountId: account.id }, { root: true })
commit('TimelineSpace/Contents/Notifications/deleteToot', { statusId: id, accountId: account.id }, { root: true })
})
win.ipcRenderer.removeAllListeners(`update-local-streamings-${account.id}`)
win.ipcRenderer.on(`update-local-streamings-${account.id}`, (_, update: Entity.Status) => {
commit('TimelineSpace/Contents/Local/appendTimeline', { status: update, accountId: account.id }, { root: true })
})
win.ipcRenderer.removeAllListeners(`delete-local-streamings-${account.id}`)
win.ipcRenderer.on(`delete-local-streamings-${account.id}`, (_, id: string) => {
commit('TimelineSpace/Contents/Local/deleteToot', { statusId: id, accountId: account.id }, { root: true })
})
win.ipcRenderer.removeAllListeners(`update-public-streamings-${account.id}`)
win.ipcRenderer.on(`update-public-streamings-${account.id}`, (_, update: Entity.Status) => {
commit('TimelineSpace/Contents/Public/appendTimeline', { status: update, accountId: account.id }, { root: true })
})
win.ipcRenderer.removeAllListeners(`delete-public-streamings-${account.id}`)
win.ipcRenderer.on(`delete-public-streamings-${account.id}`, (_, id: string) => {
commit('TimelineSpace/Contents/Public/deleteToot', { statusId: id, accountId: account.id }, { root: true })
})
win.ipcRenderer.removeAllListeners(`update-direct-streamings-${account.id}`)
win.ipcRenderer.on(`update-direct-streamings-${account.id}`, (_, update: Entity.Status) => {
commit('TimelineSpace/Contents/DirectMessages/appendTimeline', { status: update, accountId: account.id }, { root: true })
})
win.ipcRenderer.removeAllListeners(`delete-direct-streamings-${account.id}`)
win.ipcRenderer.on(`delete-direct-streamings-${account.id}`, (_, id: string) => {
commit('TimelineSpace/Contents/DirectMessages/deleteToot', { statusId: id, accountId: account.id }, { root: true })
})
})
}
}

View File

@ -1,63 +1,93 @@
import { detector } from 'megalodon'
import { Module, MutationTree, ActionTree } from 'vuex'
import { detector } from 'megalodon'
import { RootState } from '@/store'
import { MyWindow } from '~/src/types/global'
import { LocalServer } from '~src/types/localServer'
import { OAuth } from 'megalodon'
import { LocalAccount } from '~src/types/localAccount'
import { toRaw } from 'vue'
const win = window as any as MyWindow
const win = (window as any) as MyWindow
export type LoginState = {
selectedInstance: string | null
domain: string | null
searching: boolean
server: LocalServer | null
appData: OAuth.AppData | null
sns: 'mastodon' | 'pleroma' | 'misskey'
}
const state = (): LoginState => ({
selectedInstance: null,
domain: null,
searching: false,
server: null,
appData: null,
sns: 'mastodon'
})
export const MUTATION_TYPES = {
CHANGE_INSTANCE: 'changeInstance',
CHANGE_DOMAIN: 'changeDomain',
CHANGE_SEARCHING: 'changeSearching',
CHANGE_SERVER: 'changeServer',
CHANGE_APP_DATA: 'changeAppData',
CHANGE_SNS: 'changeSNS'
}
const mutations: MutationTree<LoginState> = {
[MUTATION_TYPES.CHANGE_INSTANCE]: (state: LoginState, instance: string) => {
state.selectedInstance = instance
[MUTATION_TYPES.CHANGE_DOMAIN]: (state: LoginState, instance: string | null) => {
state.domain = instance
},
[MUTATION_TYPES.CHANGE_SEARCHING]: (state: LoginState, searching: boolean) => {
state.searching = searching
},
[MUTATION_TYPES.CHANGE_SERVER]: (state: LoginState, server: LocalServer | null) => {
state.server = server
},
[MUTATION_TYPES.CHANGE_APP_DATA]: (state: LoginState, appData: OAuth.AppData | null) => {
state.appData = appData
},
[MUTATION_TYPES.CHANGE_SNS]: (state: LoginState, sns: 'mastodon' | 'pleroma' | 'misskey') => {
state.sns = sns
}
}
export const ACTION_TYPES = {
FETCH_LOGIN: 'fetchLogin',
ADD_SERVER: 'addServer',
ADD_APP: 'addApp',
AUTHORIZE: 'authorize',
PAGE_BACK: 'pageBack',
CONFIRM_INSTANCE: 'confirmInstance'
}
const actions: ActionTree<LoginState, RootState> = {
[ACTION_TYPES.FETCH_LOGIN]: async ({ state }): Promise<string> => {
const url = await win.ipcRenderer.invoke('get-auth-url', {
instance: state.selectedInstance,
sns: state.sns
[ACTION_TYPES.ADD_SERVER]: async ({ state, commit }): Promise<LocalServer> => {
const server = await win.ipcRenderer.invoke('add-server', state.domain)
commit(MUTATION_TYPES.CHANGE_SERVER, server)
return server
},
[ACTION_TYPES.ADD_APP]: async ({ state, commit }) => {
const appData = await win.ipcRenderer.invoke('add-app', `https://${state.domain}`)
commit(MUTATION_TYPES.CHANGE_APP_DATA, appData)
},
[ACTION_TYPES.AUTHORIZE]: async ({ state }, code: string): Promise<number> => {
const localAccount: LocalAccount = await win.ipcRenderer.invoke('authorize', {
server: toRaw(state.server),
appData: toRaw(state.appData),
code
})
return url
return localAccount.id
},
[ACTION_TYPES.PAGE_BACK]: ({ commit }) => {
commit(MUTATION_TYPES.CHANGE_INSTANCE, null)
commit(MUTATION_TYPES.CHANGE_DOMAIN, null)
commit(MUTATION_TYPES.CHANGE_SERVER, null)
commit(MUTATION_TYPES.CHANGE_APP_DATA, null)
},
[ACTION_TYPES.CONFIRM_INSTANCE]: async ({ commit }, domain: string): Promise<boolean> => {
commit(MUTATION_TYPES.CHANGE_SEARCHING, true)
const cleanDomain = domain.trim()
try {
const sns = await detector(`https://${cleanDomain}`)
commit(MUTATION_TYPES.CHANGE_INSTANCE, cleanDomain)
commit(MUTATION_TYPES.CHANGE_DOMAIN, cleanDomain)
commit(MUTATION_TYPES.CHANGE_SNS, sns)
} finally {
commit(MUTATION_TYPES.CHANGE_SEARCHING, false)

View File

@ -1,13 +1,13 @@
import { Module, MutationTree, ActionTree } from 'vuex'
import { toRaw } from 'vue'
import { LocalAccount } from '~/src/types/localAccount'
import { RootState } from '@/store'
import { MyWindow } from '~/src/types/global'
import { LocalServer } from '~src/types/localServer'
const win = window as any as MyWindow
const win = (window as any) as MyWindow
export type AccountState = {
accounts: Array<LocalAccount>
accounts: Array<[LocalAccount, LocalServer]>
accountLoading: boolean
}
@ -22,7 +22,7 @@ export const MUTATION_TYPES = {
}
const mutations: MutationTree<AccountState> = {
[MUTATION_TYPES.UPDATE_ACCOUNTS]: (state, accounts: Array<LocalAccount>) => {
[MUTATION_TYPES.UPDATE_ACCOUNTS]: (state, accounts: Array<[LocalAccount, LocalServer]>) => {
state.accounts = accounts
},
[MUTATION_TYPES.UPDATE_ACCOUNT_LOADING]: (state, value: boolean) => {
@ -39,19 +39,19 @@ export const ACTION_TYPES = {
}
const actions: ActionTree<AccountState, RootState> = {
[ACTION_TYPES.LOAD_ACCOUNTS]: async ({ commit }): Promise<Array<LocalAccount>> => {
const accounts = await win.ipcRenderer.invoke('list-accounts')
[ACTION_TYPES.LOAD_ACCOUNTS]: async ({ commit }): Promise<Array<[LocalAccount, LocalServer]>> => {
const accounts: Array<[LocalAccount, LocalServer]> = await win.ipcRenderer.invoke('list-accounts')
commit(MUTATION_TYPES.UPDATE_ACCOUNTS, accounts)
return accounts
},
[ACTION_TYPES.REMOVE_ACCOUNT]: async (_, account: LocalAccount) => {
await win.ipcRenderer.invoke('remove-account', account._id)
[ACTION_TYPES.REMOVE_ACCOUNT]: async (_, id: number) => {
await win.ipcRenderer.invoke('remove-account', id)
},
[ACTION_TYPES.FORWARD_ACCOUNT]: async (_, account: LocalAccount) => {
await win.ipcRenderer.invoke('forward-account', toRaw(account))
[ACTION_TYPES.FORWARD_ACCOUNT]: async (_, id: number) => {
await win.ipcRenderer.invoke('forward-account', id)
},
[ACTION_TYPES.BACKWARD_ACCOUNT]: async (_, account: LocalAccount) => {
await win.ipcRenderer.invoke('backward-account', toRaw(account))
[ACTION_TYPES.BACKWARD_ACCOUNT]: async (_, id: number) => {
await win.ipcRenderer.invoke('backward-account', id)
},
[ACTION_TYPES.REMOVE_ALL_ACCOUNTS]: async () => {
await win.ipcRenderer.invoke('remove-all-accounts')

View File

@ -5,20 +5,20 @@ import { Module, MutationTree } from 'vuex'
import { RootState } from '@/store'
export type SettingsState = {
accountID: string | null
accountId: number | null
}
const state = (): SettingsState => ({
accountID: null
accountId: null
})
export const MUTATION_TYPES = {
CHANGE_ACCOUNT_ID: 'changeAccountID'
CHANGE_ACCOUNT_ID: 'changeAccountId'
}
const mutations: MutationTree<SettingsState> = {
[MUTATION_TYPES.CHANGE_ACCOUNT_ID]: (state, id: string) => {
state.accountID = id
[MUTATION_TYPES.CHANGE_ACCOUNT_ID]: (state, id: number) => {
state.accountId = id
}
}

View File

@ -36,9 +36,9 @@ export const ACTION_TYPES = {
export const actions: ActionTree<FiltersState, RootState> = {
[ACTION_TYPES.FETCH_FILTERS]: async ({ commit, rootState }): Promise<Array<Entity.Filter>> => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
try {
@ -52,9 +52,9 @@ export const actions: ActionTree<FiltersState, RootState> = {
},
[ACTION_TYPES.DELETE_FILTER]: async ({ commit, dispatch, rootState }, id: string) => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
try {

View File

@ -42,9 +42,9 @@ export const ACTION_TYPES = {
export const actions: ActionTree<EditFiltersState, RootState> = {
fetchFilter: async ({ commit, rootState }, id: string): Promise<Entity.Filter> => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
try {
@ -65,9 +65,9 @@ export const actions: ActionTree<EditFiltersState, RootState> = {
throw new Error('filter is not set')
}
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
try {

View File

@ -54,9 +54,9 @@ export const actions: ActionTree<NewFiltersState, RootState> = {
throw new Error('filter is not set')
}
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
try {

View File

@ -36,9 +36,9 @@ export const ACTION_TYPES = {
const actions: ActionTree<GeneralState, RootState> = {
[ACTION_TYPES.FETCH_SETTINGS]: async ({ commit, rootState }): Promise<Entity.Account> => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
const res = await client.verifyAccountCredentials()
@ -51,9 +51,9 @@ const actions: ActionTree<GeneralState, RootState> = {
},
[ACTION_TYPES.SET_VISIBILITY]: async ({ commit, rootState }, value: number) => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
const visibility: VisibilityType | undefined = (Object.values(Visibility) as Array<VisibilityType>).find(v => {
@ -65,9 +65,9 @@ const actions: ActionTree<GeneralState, RootState> = {
},
[ACTION_TYPES.SET_SENSITIVE]: async ({ commit, rootState }, value: boolean) => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
const res = await client.updateCredentials({ source: { sensitive: value } })

View File

@ -1,18 +1,17 @@
import { Module, MutationTree, ActionTree } from 'vuex'
import { toRaw } from 'vue'
import { RootState } from '@/store'
import { MyWindow } from '~/src/types/global'
import { Setting, UnreadNotification, Timeline as TimelineSetting, UseMarker } from '~src/types/setting'
import { Setting } from '~src/types/setting'
import { DefaultSetting } from '~/src/constants/initializer/setting'
const win = window as any as MyWindow
const win = (window as any) as MyWindow
export type TimelineState = {
setting: TimelineSetting
setting: Setting
}
const state = (): TimelineState => ({
setting: DefaultSetting.timeline
setting: DefaultSetting
})
export const MUTATION_TYPES = {
@ -20,7 +19,7 @@ export const MUTATION_TYPES = {
}
const mutations: MutationTree<TimelineState> = {
[MUTATION_TYPES.UPDATE_TIMELINE_SETTING]: (state, setting: TimelineSetting) => {
[MUTATION_TYPES.UPDATE_TIMELINE_SETTING]: (state, setting: Setting) => {
state.setting = setting
}
}
@ -33,34 +32,16 @@ export const ACTION_TYPES = {
const actions: ActionTree<TimelineState, RootState> = {
[ACTION_TYPES.LOAD_TIMELINE_SETTING]: 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
},
[ACTION_TYPES.CHANGE_UNREAD_NOTIFICATION]: async ({ dispatch, state, rootState }, timeline: { key: boolean }): Promise<boolean> => {
const unread: UnreadNotification = Object.assign({}, state.setting.unreadNotification, timeline)
const tl: TimelineSetting = Object.assign({}, toRaw(state.setting), {
unreadNotification: unread
})
const setting: Setting = {
accountID: rootState.Settings.accountID!,
timeline: tl
}
await win.ipcRenderer.invoke('update-account-setting', setting)
dispatch('loadTimelineSetting')
const setting: Setting = await win.ipcRenderer.invoke('get-account-setting', rootState.Settings.accountId)
commit(MUTATION_TYPES.UPDATE_TIMELINE_SETTING, setting)
return true
},
[ACTION_TYPES.CHANGE_USER_MARKER]: async ({ dispatch, state, rootState }, timeline: { key: boolean }) => {
const marker: UseMarker = Object.assign({}, state.setting.useMarker, timeline)
const tl: TimelineSetting = Object.assign({}, toRaw(state.setting), {
useMarker: marker
})
const setting: Setting = {
accountID: rootState.Settings.accountID!,
timeline: tl
}
const setting: Setting = Object.assign({}, state.setting, timeline)
setting.accountId = rootState.Settings.accountId!
console.log(setting)
await win.ipcRenderer.invoke('update-account-setting', setting)
dispatch('loadTimelineSetting')
dispatch(ACTION_TYPES.LOAD_TIMELINE_SETTING)
return true
}
}

Some files were not shown because too many files have changed in this diff Show More