diff --git a/spec/renderer/integration/store/TimelineSpace/Contents/Home.spec.ts b/spec/renderer/integration/store/TimelineSpace/Contents/Home.spec.ts index 4f08af43..113bc13e 100644 --- a/spec/renderer/integration/store/TimelineSpace/Contents/Home.spec.ts +++ b/spec/renderer/integration/store/TimelineSpace/Contents/Home.spec.ts @@ -2,7 +2,7 @@ import { Response, Status, Account, Application } from 'megalodon' import mockedMegalodon from '~/spec/mock/megalodon' import { createLocalVue } from '@vue/test-utils' import Vuex from 'vuex' -import Home from '~/src/renderer/store/TimelineSpace/Contents/Home' +import Home, { HomeState } from '@/store/TimelineSpace/Contents/Home' jest.mock('megalodon') @@ -88,7 +88,7 @@ const status2: Status = { pinned: null } -let state: any = () => { +let state = (): HomeState => { return { lazyLoading: false, heading: true, @@ -165,12 +165,6 @@ describe('Home', () => { }) describe('lazyFetchTimeline', () => { - describe('last is null', () => { - it('should not be updated', async () => { - const result = await store.dispatch('Home/lazyFetchTimeline', null) - expect(result).toEqual(null) - }) - }) describe('success', () => { beforeAll(() => { state = () => { diff --git a/spec/renderer/unit/store/TimelineSpace/Contents/Home.spec.ts b/spec/renderer/unit/store/TimelineSpace/Contents/Home.spec.ts index ee18eb7f..2a4b2dfd 100644 --- a/spec/renderer/unit/store/TimelineSpace/Contents/Home.spec.ts +++ b/spec/renderer/unit/store/TimelineSpace/Contents/Home.spec.ts @@ -1,8 +1,91 @@ -import Home from '@/store/TimelineSpace/Contents/Home' +import { Account, Status, Application } from 'megalodon' +import Home, { HomeState, MUTATION_TYPES } from '@/store/TimelineSpace/Contents/Home' + +const account: 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: 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', + 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, + application: { + name: 'Web' + } as Application, + language: null, + pinned: null +} +const status2: 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', + 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, + application: { + name: 'Web' + } as Application, + language: null, + pinned: null +} describe('TimelineSpace/Contents/Home', () => { describe('mutations', () => { - let state + let state: HomeState beforeEach(() => { state = { lazyLoading: false, @@ -17,14 +100,14 @@ describe('TimelineSpace/Contents/Home', () => { describe('changeLazyLoading', () => { it('should be change', () => { - Home.mutations.changeLazyLoading(state, true) + Home.mutations![MUTATION_TYPES.CHANGE_LAZY_LOADING](state, true) expect(state.lazyLoading).toEqual(true) }) }) describe('changeHeading', () => { it('should be change', () => { - Home.mutations.changeHeading(state, false) + Home.mutations![MUTATION_TYPES.CHANGE_HEADING](state, false) expect(state.heading).toEqual(false) }) }) @@ -35,7 +118,7 @@ describe('TimelineSpace/Contents/Home', () => { state = { lazyLoading: false, heading: true, - timeline: [5, 4, 3, 2, 1, 0], + timeline: [status1], unreadTimeline: [], filter: '', showReblogs: true, @@ -43,8 +126,8 @@ describe('TimelineSpace/Contents/Home', () => { } }) it('should update timeline', () => { - Home.mutations.appendTimeline(state, 6) - expect(state.timeline).toEqual([6, 5, 4, 3, 2, 1, 0]) + Home.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, status2) + expect(state.timeline).toEqual([status2, status1]) expect(state.unreadTimeline).toEqual([]) }) }) @@ -54,7 +137,7 @@ describe('TimelineSpace/Contents/Home', () => { state = { lazyLoading: false, heading: false, - timeline: [5, 4, 3, 2, 1, 0], + timeline: [status1], unreadTimeline: [], filter: '', showReblogs: true, @@ -62,9 +145,9 @@ describe('TimelineSpace/Contents/Home', () => { } }) it('should update unreadTimeline', () => { - Home.mutations.appendTimeline(state, 6) - expect(state.timeline).toEqual([5, 4, 3, 2, 1, 0]) - expect(state.unreadTimeline).toEqual([6]) + Home.mutations![MUTATION_TYPES.APPEND_TIMELINE](state, status2) + expect(state.timeline).toEqual([status1]) + expect(state.unreadTimeline).toEqual([status2]) }) }) }) @@ -74,16 +157,16 @@ describe('TimelineSpace/Contents/Home', () => { state = { lazyLoading: false, heading: true, - timeline: [5, 4, 3, 2, 1, 0], - unreadTimeline: [8, 7, 6], + timeline: [status1], + unreadTimeline: [status2], filter: '', showReblogs: true, showReplies: true } }) it('should be merged', () => { - Home.mutations.mergeTimeline(state) - expect(state.timeline).toEqual([8, 7, 6, 5, 4, 3, 2, 1, 0]) + Home.mutations![MUTATION_TYPES.MERGE_TIMELINE](state, null) + expect(state.timeline).toEqual([status2, status1]) expect(state.unreadTimeline).toEqual([]) }) }) @@ -93,7 +176,7 @@ describe('TimelineSpace/Contents/Home', () => { state = { lazyLoading: false, heading: true, - timeline: [5, 4, 3, 2, 1, 0], + timeline: [status1], unreadTimeline: [], filter: '', showReblogs: true, @@ -101,8 +184,8 @@ describe('TimelineSpace/Contents/Home', () => { } }) it('should be inserted', () => { - Home.mutations.insertTimeline(state, [-1, -2, -3, -4]) - expect(state.timeline).toEqual([5, 4, 3, 2, 1, 0, -1, -2, -3, -4]) + Home.mutations![MUTATION_TYPES.INSERT_TIMELINE](state, [status2]) + expect(state.timeline).toEqual([status1, status2]) }) }) @@ -112,78 +195,60 @@ describe('TimelineSpace/Contents/Home', () => { state = { lazyLoading: false, heading: true, - timeline: [ - { - id: 3, - reblog: null, - text: '3rd' - }, - { - id: 2, - reblog: null, - text: '2nd' - }, - { - id: 1, - reblog: null, - text: '1st' - }, - { - id: 0, - reblog: null, - text: 'zero' - } - ], + timeline: [status1, status2], unreadTimeline: [], filter: '', showReblogs: true, showReplies: true } }) + const favouritedStatus: Status = Object.assign(status1, { + favourited: true + }) it('should be updated', () => { - Home.mutations.updateToot(state, { - id: 2, - reblog: null, - text: 'second' - }) - expect(state.timeline[1]).toEqual({ - id: 2, - reblog: null, - text: 'second' - }) + Home.mutations![MUTATION_TYPES.UPDATE_TOOT](state, favouritedStatus) + expect(state.timeline).toEqual([favouritedStatus, status2]) }) }) describe('message is reblogged', () => { + const rebloggedStatus: 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: '', + 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, + application: { + name: 'Web' + } as Application, + language: null, + pinned: null + } + const favouritedStatus: Status = Object.assign(status1, { + favourited: true + }) beforeEach(() => { state = { lazyLoading: false, heading: true, - timeline: [ - { - id: 3, - reblog: null, - text: '3rd' - }, - { - id: 2, - reblog: null, - text: '2nd' - }, - { - id: 1, - reblog: { - id: -1, - reblog: null, - text: 'reblogged message' - }, - text: null - }, - { - id: 0, - reblog: null, - text: 'zero' - } - ], + timeline: [rebloggedStatus, status2], unreadTimeline: [], filter: '', showReblogs: true, @@ -191,20 +256,9 @@ describe('TimelineSpace/Contents/Home', () => { } }) it('should be updated', () => { - Home.mutations.updateToot(state, { - id: -1, - reblog: null, - text: 'negative id' - }) - expect(state.timeline[2]).toEqual({ - id: 1, - reblog: { - id: -1, - reblog: null, - text: 'negative id' - }, - text: null - }) + Home.mutations![MUTATION_TYPES.UPDATE_TOOT](state, favouritedStatus) + expect(state.timeline[0].reblog).not.toBeNull() + expect(state.timeline[0].reblog!.favourited).toEqual(true) }) }) }) @@ -215,28 +269,7 @@ describe('TimelineSpace/Contents/Home', () => { state = { lazyLoading: false, heading: true, - timeline: [ - { - id: 3, - reblog: null, - text: '3rd' - }, - { - id: 2, - reblog: null, - text: '2nd' - }, - { - id: 1, - reblog: null, - text: 'first' - }, - { - id: 0, - reblog: null, - text: 'zero' - } - ], + timeline: [status1, status2], unreadTimeline: [], filter: '', showReblogs: true, @@ -244,62 +277,47 @@ describe('TimelineSpace/Contents/Home', () => { } }) it('should be deleted', () => { - Home.mutations.deleteToot(state, { - id: 0, - reblog: null, - text: 'zero' - }) - expect(state.timeline).toEqual([ - { - id: 3, - reblog: null, - text: '3rd' - }, - { - id: 2, - reblog: null, - text: '2nd' - }, - { - id: 1, - reblog: null, - text: 'first' - } - ]) + Home.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1) + expect(state.timeline).toEqual([status2]) }) }) describe('message is reblogged', () => { beforeEach(() => { + const rebloggedStatus: 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: '', + 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, + application: { + name: 'Web' + } as Application, + language: null, + pinned: null + } state = { lazyLoading: false, heading: true, - timeline: [ - { - id: 3, - reblog: null, - text: '3rd' - }, - { - id: 2, - reblog: null, - text: '2nd' - }, - { - id: 1, - reblog: { - id: -1, - reblog: null, - text: 'reblogged toot' - }, - text: 'first' - }, - { - id: 0, - reblog: null, - text: 'zero' - } - ], + timeline: [rebloggedStatus, status2], unreadTimeline: [], filter: '', showReblogs: true, @@ -307,28 +325,8 @@ describe('TimelineSpace/Contents/Home', () => { } }) it('should be deleted', () => { - Home.mutations.deleteToot(state, { - id: -1, - reblog: null, - text: 'reblogged toot' - }) - expect(state.timeline).toEqual([ - { - id: 3, - reblog: null, - text: '3rd' - }, - { - id: 2, - reblog: null, - text: '2nd' - }, - { - id: 0, - reblog: null, - text: 'zero' - } - ]) + Home.mutations![MUTATION_TYPES.DELETE_TOOT](state, status1) + expect(state.timeline).toEqual([status2]) }) }) }) diff --git a/src/renderer/store/TimelineSpace/Contents.ts b/src/renderer/store/TimelineSpace/Contents.ts index d02807ca..7cd9b68a 100644 --- a/src/renderer/store/TimelineSpace/Contents.ts +++ b/src/renderer/store/TimelineSpace/Contents.ts @@ -1,5 +1,5 @@ import SideBar, { SideBarModuleState } from './Contents/SideBar' -import Home from './Contents/Home' +import Home, { HomeState } from './Contents/Home' import Notifications from './Contents/Notifications' import Favourites from './Contents/Favourites' import Local from './Contents/Local' @@ -15,7 +15,8 @@ import { RootState } from '@/store' export interface ContentsState {} export interface ContentsModuleState extends ContentsState { - SideBar: SideBarModuleState + SideBar: SideBarModuleState, + Home: HomeState } const state = (): ContentsState => ({}) diff --git a/src/renderer/store/TimelineSpace/Contents/Home.js b/src/renderer/store/TimelineSpace/Contents/Home.js deleted file mode 100644 index 5a1fd69b..00000000 --- a/src/renderer/store/TimelineSpace/Contents/Home.js +++ /dev/null @@ -1,117 +0,0 @@ -import Mastodon from 'megalodon' - -const Home = { - namespaced: true, - state: { - lazyLoading: false, - heading: true, - timeline: [], - unreadTimeline: [], - filter: '', - showReblogs: true, - showReplies: true - }, - mutations: { - changeLazyLoading (state, value) { - state.lazyLoading = value - }, - changeHeading (state, value) { - state.heading = value - }, - appendTimeline (state, update) { - if (state.heading) { - state.timeline = [update].concat(state.timeline) - } else { - state.unreadTimeline = [update].concat(state.unreadTimeline) - } - }, - updateTimeline (state, messages) { - state.timeline = messages - }, - mergeTimeline (state) { - state.timeline = state.unreadTimeline.slice(0, 80).concat(state.timeline) - state.unreadTimeline = [] - }, - insertTimeline (state, messages) { - state.timeline = state.timeline.concat(messages) - }, - archiveTimeline (state) { - state.timeline = state.timeline.slice(0, 40) - }, - clearTimeline (state) { - state.timeline = [] - state.unreadTimeline = [] - }, - updateToot (state, message) { - // Replace target message in homeTimeline and notifications - state.timeline = state.timeline.map((toot) => { - if (toot.id === message.id) { - return message - } else if (toot.reblog !== null && toot.reblog.id === message.id) { - // When user reblog/favourite a reblogged toot, target message is a original toot. - // So, a message which is received now is original toot. - const reblog = { - reblog: message - } - return Object.assign(toot, reblog) - } else { - return toot - } - }) - }, - deleteToot (state, message) { - state.timeline = state.timeline.filter((toot) => { - if (toot.reblog !== null && toot.reblog.id === message.id) { - return false - } else { - return toot.id !== message.id - } - }) - }, - changeFilter (state, filter) { - state.filter = filter - }, - showReblogs (state, visible) { - state.showReblogs = visible - }, - showReplies (state, visible) { - state.showReplies = visible - } - }, - actions: { - fetchTimeline ({ commit, rootState }) { - const client = new Mastodon( - rootState.TimelineSpace.account.accessToken, - rootState.TimelineSpace.account.baseURL + '/api/v1' - ) - return client.get('/timelines/home', { limit: 40 }) - .then(res => { - commit('updateTimeline', res.data) - return res.data - }) - }, - lazyFetchTimeline ({ state, commit, rootState }, last) { - if (last === undefined || last === null) { - return Promise.resolve(null) - } - if (state.lazyLoading) { - return Promise.resolve(null) - } - commit('changeLazyLoading', true) - const client = new Mastodon( - rootState.TimelineSpace.account.accessToken, - rootState.TimelineSpace.account.baseURL + '/api/v1' - ) - return client.get('/timelines/home', { max_id: last.id, limit: 40 }) - .then(res => { - commit('insertTimeline', res.data) - return res.data - }) - .finally(() => { - commit('changeLazyLoading', false) - }) - } - } -} - -export default Home diff --git a/src/renderer/store/TimelineSpace/Contents/Home.ts b/src/renderer/store/TimelineSpace/Contents/Home.ts new file mode 100644 index 00000000..8a7f5b2f --- /dev/null +++ b/src/renderer/store/TimelineSpace/Contents/Home.ts @@ -0,0 +1,146 @@ +import Mastodon, { Status, Response } from 'megalodon' +import { Module, MutationTree, ActionTree } from 'vuex' +import { RootState } from '@/store' + +export interface HomeState { + lazyLoading: boolean, + heading: boolean, + showReblogs: boolean, + showReplies: boolean, + timeline: Array, + unreadTimeline: Array, + filter: string +} + +const state = (): HomeState => ({ + lazyLoading: false, + heading: true, + timeline: [], + unreadTimeline: [], + filter: '', + showReblogs: true, + showReplies: true +}) + +export const MUTATION_TYPES = { + CHANGE_LAZY_LOADING: 'changeLazyLoading', + CHANGE_HEADING: 'changeHeading', + APPEND_TIMELINE: 'appendTimeline', + UPDATE_TIMELINE: 'updateTimeline', + MERGE_TIMELINE: 'mergeTimeline', + INSERT_TIMELINE: 'insertTimeline', + ARCHIVE_TIMELINE: 'archiveTimeline', + CLEAR_TIMELINE: 'clearTimeline', + UPDATE_TOOT: 'updateToot', + DELETE_TOOT: 'deleteToot', + CHANGE_FILTER: 'changeFilter', + SHOW_REBLOGS: 'showReblogs', + SHOW_REPLIES: 'showReplies' +} + +const mutations: MutationTree = { + [MUTATION_TYPES.CHANGE_LAZY_LOADING]: (state, value: boolean) => { + state.lazyLoading = value + }, + [MUTATION_TYPES.CHANGE_HEADING]: (state, value: boolean) => { + state.heading = value + }, + [MUTATION_TYPES.APPEND_TIMELINE]: (state, update: Status) => { + if (state.heading) { + state.timeline = [update].concat(state.timeline) + } else { + state.unreadTimeline = [update].concat(state.unreadTimeline) + } + }, + [MUTATION_TYPES.UPDATE_TIMELINE]: (state, messages: Array) => { + state.timeline = messages + }, + [MUTATION_TYPES.MERGE_TIMELINE]: (state) => { + state.timeline = state.unreadTimeline.slice(0, 80).concat(state.timeline) + state.unreadTimeline = [] + }, + [MUTATION_TYPES.INSERT_TIMELINE]: (state, messages: Array) => { + state.timeline = state.timeline.concat(messages) + }, + [MUTATION_TYPES.ARCHIVE_TIMELINE]: (state) => { + state.timeline = state.timeline.slice(0, 40) + }, + [MUTATION_TYPES.CLEAR_TIMELINE]: (state) => { + state.timeline = [] + state.unreadTimeline = [] + }, + [MUTATION_TYPES.UPDATE_TOOT]: (state, message: Status) => { + // Replace target message in homeTimeline and notifications + state.timeline = state.timeline.map((toot) => { + if (toot.id === message.id) { + return message + } else if (toot.reblog !== null && toot.reblog.id === message.id) { + // When user reblog/favourite a reblogged toot, target message is a original toot. + // So, a message which is received now is original toot. + const reblog = { + reblog: message + } + return Object.assign(toot, reblog) + } else { + return toot + } + }) + }, + [MUTATION_TYPES.DELETE_TOOT]: (state, message: Status) => { + state.timeline = state.timeline.filter((toot) => { + if (toot.reblog !== null && toot.reblog.id === message.id) { + return false + } else { + return toot.id !== message.id + } + }) + }, + [MUTATION_TYPES.CHANGE_FILTER]: (state, filter: string) => { + state.filter = filter + }, + [MUTATION_TYPES.SHOW_REBLOGS]: (state, visible: boolean) => { + state.showReblogs = visible + }, + [MUTATION_TYPES.SHOW_REPLIES]: (state, visible: boolean) => { + state.showReplies = visible + } +} + +const actions: ActionTree = { + fetchTimeline: async ({ commit, rootState }) => { + const client = new Mastodon( + rootState.TimelineSpace.account.accessToken!, + rootState.TimelineSpace.account.baseURL + '/api/v1' + ) + const res: Response> = await client.get>('/timelines/home', { limit: 40 }) + commit(MUTATION_TYPES.UPDATE_TIMELINE, res.data) + return res.data + }, + lazyFetchTimeline: async ({ state, commit, rootState }, lastStatus: Status): Promise | null> => { + if (state.lazyLoading) { + return Promise.resolve(null) + } + commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, true) + const client = new Mastodon( + rootState.TimelineSpace.account.accessToken!, + rootState.TimelineSpace.account.baseURL + '/api/v1' + ) + return client.get>('/timelines/home', { max_id: lastStatus.id, limit: 40 }) + .then(res => { + commit(MUTATION_TYPES.INSERT_TIMELINE, res.data) + return res.data + }) + .finally(() => { + commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, false) + }) + } +} + +const Home: Module = { + namespaced: true, + state: state, + mutations: mutations, + actions: actions +} + +export default Home diff --git a/src/renderer/store/TimelineSpace/Contents/SideBar/AccountProfile/Timeline.ts b/src/renderer/store/TimelineSpace/Contents/SideBar/AccountProfile/Timeline.ts index f520017b..05c31a5b 100644 --- a/src/renderer/store/TimelineSpace/Contents/SideBar/AccountProfile/Timeline.ts +++ b/src/renderer/store/TimelineSpace/Contents/SideBar/AccountProfile/Timeline.ts @@ -1,6 +1,7 @@ import Mastodon, { Status, Response } from 'megalodon' import { Module, MutationTree, ActionTree } from 'vuex' import { RootState } from '@/store' +import { LoadPosition } from '~src/types/load_position' export interface TimelineState { timeline: Array, @@ -95,11 +96,7 @@ const actions: ActionTree = { commit(MUTATION_TYPES.UPDATE_TIMELINE, res.data) return res.data }, - lazyFetchTimeline: async ({ state, commit, rootState }, info: any): Promise => { - const last = info.last - if (last === undefined || last === null) { - return Promise.resolve(null) - } + lazyFetchTimeline: async ({ state, commit, rootState }, loadPosition: LoadPosition): Promise => { if (state.lazyLoading) { return Promise.resolve(null) } @@ -109,7 +106,7 @@ const actions: ActionTree = { rootState.TimelineSpace.account.baseURL + '/api/v1' ) try { - const res: Response> = await client.get>(`/accounts/${info.account.id}/statuses`, { max_id: last.id, limit: 40 }) + const res: Response> = await client.get>(`/accounts/${loadPosition.account.id}/statuses`, { max_id: loadPosition.status.id, limit: 40 }) commit(MUTATION_TYPES.INSERT_TIMELINE, res.data) } finally { commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, false) diff --git a/src/types/load_position.ts b/src/types/load_position.ts new file mode 100644 index 00000000..32ded190 --- /dev/null +++ b/src/types/load_position.ts @@ -0,0 +1,6 @@ +import { Status, Account } from 'megalodon' + +export interface LoadPosition { + status: Status, + account: Account +}