[refactor] Home store

This commit is contained in:
AkiraFukushima 2023-01-02 16:30:56 +09:00
parent 797b00d309
commit 49f5be6230
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
6 changed files with 154 additions and 545 deletions

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,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'
@ -51,6 +53,9 @@ import { MUTATION_TYPES as SIDE_MENU_MUTATION } from '@/store/TimelineSpace/Side
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',
@ -63,16 +68,24 @@ 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 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`])
@ -80,6 +93,9 @@ export default defineComponent({
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 +111,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,8 +125,6 @@ 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)
@ -127,17 +141,17 @@ export default defineComponent({
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), () => {
@ -161,19 +175,28 @@ 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) => {
@ -184,11 +207,13 @@ export default defineComponent({
}
const fetchTimelineSince = (since_id: string) => {
loadingMore.value = true
store.dispatch(`${space}/${ACTION_TYPES.FETCH_TIMELINE_SINCE}`, since_id).finally(() => {
setTimeout(() => {
loadingMore.value = false
}, 500)
})
store
.dispatch(`${space}/${ACTION_TYPES.FETCH_TIMELINE_SINCE}`, { sinceId: since_id, account: account.account, server: account.server })
.finally(() => {
setTimeout(() => {
loadingMore.value = false
}, 500)
})
}
const reload = async () => {
store.commit(`TimelineSpace/${TIMELINE_MUTATION.CHANGE_LOADING}`, true)
@ -240,7 +265,7 @@ export default defineComponent({
openSideBar,
heading,
upper,
unreads
account
}
}
})

View File

@ -261,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)
@ -297,6 +299,14 @@ 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', 'update', 'delete', 'sizeChanged'],
@ -306,7 +316,7 @@ export default defineComponent({
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)
@ -319,8 +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 server = computed(() => store.state.TimelineSpace.server)
const account = computed(() => store.state.TimelineSpace.account)
const shortcutEnabled = computed(() => focused.value && !overlaid.value)
const originalMessage = computed(() => {
if (message.value.reblog && !message.value.quote) {
@ -350,7 +358,7 @@ export default defineComponent({
return null
})
const isMyMessage = computed(() => {
return account.value!.accountId === originalMessage.value.account.id
return account.value.accountId === originalMessage.value.account.id
})
const application = computed(() => {
const msg = originalMessage.value
@ -384,7 +392,7 @@ export default defineComponent({
return originalMessage.value.visibility === 'direct'
})
const quoteSupported = computed(() => {
return QuoteSupported(server.value!.sns, server.value!.domain)
return QuoteSupported(server.value.sns, server.value.domain)
})
whenever(logicAnd(l, shortcutEnabled), () => {

View File

@ -45,6 +45,8 @@ export const ACTION_TYPES = {
REMOVE_SHORTCUT_EVENTS: 'removeShortcutEvents',
LOAD_HIDE: 'loadHide',
SWITCH_HIDE: 'switchHide',
LOAD_TIMELINES: 'loadTimelines',
BIND_STREAMINGS: 'bindStreamings',
BIND_NOTIFICATION: 'bindNotification'
}
@ -59,6 +61,8 @@ const actions: ActionTree<GlobalHeaderState, RootState> = {
console.error(err)
}
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) => {
@ -112,6 +116,19 @@ const actions: ActionTree<GlobalHeaderState, RootState> = {
// 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 })
})
},
[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 })
})
})
}
}

View File

@ -9,7 +9,7 @@ import { RootState } from '@/store'
import { AccountLoadError } from '@/errors/load'
import { TimelineFetchError } from '@/errors/fetch'
import { MyWindow } from '~/src/types/global'
import { LocalServer } from '~src/types/localServer'
import { LocalServer } from '~/src/types/localServer'
import { Setting } from '~/src/types/setting'
import { DefaultSetting } from '~/src/constants/initializer/setting'
@ -199,11 +199,6 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
return true
},
[ACTION_TYPES.FETCH_CONTENTS_TIMELINES]: async ({ dispatch }) => {
dispatch('TimelineSpace/Contents/changeLoading', true, { root: true })
await dispatch('TimelineSpace/Contents/Home/fetchTimeline', {}, { root: true }).finally(() => {
dispatch('TimelineSpace/Contents/changeLoading', false, { root: true })
})
await dispatch('TimelineSpace/Contents/Notifications/fetchNotifications', {}, { root: true })
await dispatch('TimelineSpace/Contents/Mentions/fetchMentions', {}, { root: true })
await dispatch('TimelineSpace/Contents/DirectMessages/fetchTimeline', {}, { root: true })
@ -211,7 +206,6 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
await dispatch('TimelineSpace/Contents/Public/fetchPublicTimeline', {}, { root: true })
},
[ACTION_TYPES.CLEAR_CONTENTS_TIMELINES]: ({ commit }) => {
commit('TimelineSpace/Contents/Home/clearTimeline', {}, { root: true })
commit('TimelineSpace/Contents/Local/clearTimeline', {}, { root: true })
commit('TimelineSpace/Contents/DirectMessages/clearTimeline', {}, { root: true })
commit('TimelineSpace/Contents/Notifications/clearNotifications', {}, { root: true })
@ -232,14 +226,6 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
throw new Error('Account is not set')
}
win.ipcRenderer.on(`update-user-streamings-${state.account!.id}`, (_, update: Entity.Status) => {
commit('TimelineSpace/Contents/Home/appendTimeline', update, { root: true })
// Sometimes archive old statuses
if (rootState.TimelineSpace.Contents.Home.heading && Math.random() > 0.8) {
commit('TimelineSpace/Contents/Home/archiveTimeline', null, { root: true })
}
commit('TimelineSpace/SideMenu/changeUnreadHomeTimeline', true, { root: true })
})
win.ipcRenderer.on(`notification-user-streamings-${state.account!.id}`, (_, notification: Entity.Notification) => {
commit('TimelineSpace/Contents/Notifications/appendNotifications', notification, { root: true })
if (rootState.TimelineSpace.Contents.Notifications.heading && Math.random() > 0.8) {

View File

@ -2,85 +2,58 @@ import generator, { Entity, FilterContext } from 'megalodon'
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
import { RootState } from '@/store'
import { LoadingCard } from '@/types/loading-card'
import { LocalServer } from '~/src/types/localServer'
import { LocalAccount } from '~/src/types/localAccount'
export type HomeState = {
lazyLoading: boolean
heading: boolean
showReblogs: boolean
showReplies: boolean
timeline: Array<Entity.Status | LoadingCard>
unreads: Array<Entity.Status>
timeline: { [key: number]: Array<Entity.Status | LoadingCard> }
}
const state = (): HomeState => ({
lazyLoading: false,
heading: true,
timeline: [],
showReblogs: true,
showReplies: true,
unreads: []
timeline: {}
})
export const MUTATION_TYPES = {
CHANGE_LAZY_LOADING: 'changeLazyLoading',
CHANGE_HEADING: 'changeHeading',
APPEND_TIMELINE: 'appendTimeline',
UPDATE_TIMELINE: 'updateTimeline',
REPLACE_TIMELINE: 'replaceTimeline',
INSERT_TIMELINE: 'insertTimeline',
ARCHIVE_TIMELINE: 'archiveTimeline',
CLEAR_TIMELINE: 'clearTimeline',
UPDATE_TOOT: 'updateToot',
DELETE_TOOT: 'deleteToot',
SHOW_REBLOGS: 'showReblogs',
SHOW_REPLIES: 'showReplies',
APPEND_TIMELINE_AFTER_LOADING_CARD: 'appendTimelineAfterLoadingCard',
MERGE_UNREADS: 'mergeUnreads'
APPEND_TIMELINE_AFTER_LOADING_CARD: 'appendTimelineAfterLoadingCard'
}
const mutations: MutationTree<HomeState> = {
[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: Entity.Status) => {
// Reject duplicated status in timeline
if (!state.timeline.find(item => item.id === update.id) && !state.unreads.find(item => item.id === update.id)) {
if (state.heading) {
state.timeline = ([update] as Array<Entity.Status | LoadingCard>).concat(state.timeline)
} else {
state.unreads = [update].concat(state.unreads)
}
[MUTATION_TYPES.APPEND_TIMELINE]: (state, obj: { status: Entity.Status; accountId: number }) => {
if (state.timeline[obj.accountId]) {
state.timeline[obj.accountId] = [obj.status, ...state.timeline[obj.accountId]]
} else {
state.timeline[obj.accountId] = [obj.status]
}
},
[MUTATION_TYPES.UPDATE_TIMELINE]: (state, messages: Array<Entity.Status | LoadingCard>) => {
state.timeline = messages
[MUTATION_TYPES.REPLACE_TIMELINE]: (state, obj: { statuses: Array<Entity.Status | LoadingCard>; accountId: number }) => {
state.timeline[obj.accountId] = obj.statuses
},
[MUTATION_TYPES.INSERT_TIMELINE]: (state, messages: Array<Entity.Status | LoadingCard>) => {
state.timeline = state.timeline.concat(messages)
[MUTATION_TYPES.INSERT_TIMELINE]: (state, obj: { statuses: Array<Entity.Status | LoadingCard>; accountId: number }) => {
if (state.timeline[obj.accountId]) {
state.timeline[obj.accountId] = [...state.timeline[obj.accountId], ...obj.statuses]
} else {
state.timeline[obj.accountId] = obj.statuses
}
},
[MUTATION_TYPES.ARCHIVE_TIMELINE]: state => {
state.timeline = state.timeline.slice(0, 20)
},
[MUTATION_TYPES.CLEAR_TIMELINE]: state => {
state.timeline = []
state.unreads = []
},
[MUTATION_TYPES.UPDATE_TOOT]: (state, message: Entity.Status) => {
[MUTATION_TYPES.UPDATE_TOOT]: (state, obj: { status: Entity.Status; accountId: number }) => {
// Replace target message in homeTimeline and notifications
state.timeline = state.timeline.map(status => {
state.timeline[obj.accountId] = state.timeline[obj.accountId].map(status => {
if (status.id === 'loading-card') {
return status
}
const toot = status as Entity.Status
if (toot.id === message.id) {
return message
} else if (toot.reblog !== null && toot.reblog.id === message.id) {
if (toot.id === obj.status.id) {
return obj.status
} else if (toot.reblog !== null && toot.reblog.id === obj.status.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
reblog: obj.status
}
return Object.assign(toot, reblog)
} else {
@ -88,39 +61,32 @@ const mutations: MutationTree<HomeState> = {
}
})
},
[MUTATION_TYPES.DELETE_TOOT]: (state, messageId: string) => {
state.timeline = state.timeline.filter(status => {
[MUTATION_TYPES.DELETE_TOOT]: (state, obj: { statusId: string; accountId: number }) => {
state.timeline[obj.accountId] = state.timeline[obj.accountId].filter(status => {
if (status.id === 'loading-card') {
return true
}
const toot = status as Entity.Status
if (toot.reblog !== null && toot.reblog.id === messageId) {
if (toot.reblog !== null && toot.reblog.id === obj.statusId) {
return false
} else {
return toot.id !== messageId
return toot.id !== obj.statusId
}
})
},
[MUTATION_TYPES.SHOW_REBLOGS]: (state, visible: boolean) => {
state.showReblogs = visible
},
[MUTATION_TYPES.SHOW_REPLIES]: (state, visible: boolean) => {
state.showReplies = visible
},
[MUTATION_TYPES.APPEND_TIMELINE_AFTER_LOADING_CARD]: (state, timeline: Array<Entity.Status | LoadingCard>) => {
const tl = state.timeline.flatMap(status => {
[MUTATION_TYPES.APPEND_TIMELINE_AFTER_LOADING_CARD]: (
state,
obj: { statuses: Array<Entity.Status | LoadingCard>; accountId: number }
) => {
const tl = state.timeline[obj.accountId].flatMap(status => {
if (status.id !== 'loading-card') {
return status
} else {
return timeline
return obj.statuses
}
})
// Reject duplicated status in timeline
state.timeline = Array.from(new Set(tl))
},
[MUTATION_TYPES.MERGE_UNREADS]: state => {
state.timeline = (state.unreads.slice(0, 80) as Array<Entity.Status | LoadingCard>).concat(state.timeline)
state.unreads = []
state.timeline[obj.accountId] = Array.from(new Set(tl))
}
}
@ -133,14 +99,10 @@ export const ACTION_TYPES = {
}
const actions: ActionTree<HomeState, RootState> = {
[ACTION_TYPES.FETCH_TIMELINE]: async ({ dispatch, commit, rootState }) => {
const client = generator(
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
const marker: Entity.Marker | null = await dispatch('getMarker').catch(err => {
// vue
[ACTION_TYPES.FETCH_TIMELINE]: async ({ dispatch, commit, rootState }, req: { account: LocalAccount; server: LocalServer }) => {
const client = generator(req.server.sns, req.server.baseURL, req.account.accessToken, rootState.App.userAgent)
const marker: Entity.Marker | null = await dispatch(ACTION_TYPES.GET_MARKER, req).catch(err => {
console.error(err)
})
@ -171,46 +133,30 @@ const actions: ActionTree<HomeState, RootState> = {
} else {
timeline = timeline.concat(res.data)
}
commit(MUTATION_TYPES.UPDATE_TIMELINE, timeline)
commit(MUTATION_TYPES.REPLACE_TIMELINE, { statuses: timeline, accountId: req.account.id })
return res.data
} else {
const res = await client.getHomeTimeline({ limit: 20 })
commit(MUTATION_TYPES.UPDATE_TIMELINE, res.data)
commit(MUTATION_TYPES.REPLACE_TIMELINE, { statuses: res.data, accountId: req.account.id })
return res.data
}
},
[ACTION_TYPES.LAZY_FETCH_TIMELINE]: async (
{ state, commit, rootState },
lastStatus: Entity.Status
{ commit, rootState },
req: { lastStatus: Entity.Status; account: LocalAccount; server: LocalServer }
): Promise<Array<Entity.Status> | null> => {
if (state.lazyLoading) {
return Promise.resolve(null)
}
commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, true)
const client = generator(
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
return client
.getHomeTimeline({ max_id: lastStatus.id, limit: 20 })
.then(res => {
commit(MUTATION_TYPES.INSERT_TIMELINE, res.data)
return res.data
})
.finally(() => {
commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, false)
})
const client = generator(req.server.sns, req.server.baseURL, req.account.accessToken, rootState.App.userAgent)
return client.getHomeTimeline({ max_id: req.lastStatus.id, limit: 20 }).then(res => {
commit(MUTATION_TYPES.INSERT_TIMELINE, { statuses: res.data, accountId: req.account.id })
return res.data
})
},
[ACTION_TYPES.FETCH_TIMELINE_SINCE]: async ({ state, rootState, commit }, since_id: string): Promise<Array<Entity.Status> | null> => {
const client = generator(
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
const cardIndex = state.timeline.findIndex(s => {
[ACTION_TYPES.FETCH_TIMELINE_SINCE]: async (
{ state, rootState, commit },
req: { sinceId: string; account: LocalAccount; server: LocalServer }
): Promise<Array<Entity.Status> | null> => {
const client = generator(req.server.sns, req.server.baseURL, req.account.accessToken, rootState.App.userAgent)
const cardIndex = state.timeline[req.account.id].findIndex(s => {
if (s.id === 'loading-card') {
return true
}
@ -218,7 +164,7 @@ const actions: ActionTree<HomeState, RootState> = {
})
let maxID: string | null = null
if (cardIndex > 0) {
maxID = state.timeline[cardIndex - 1].id
maxID = state.timeline[req.account.id][cardIndex - 1].id
}
// Memo: What happens when we specify both of max_id and min_id?
// What is the difference between max_id & since_id and max_id & min_id?
@ -230,7 +176,7 @@ const actions: ActionTree<HomeState, RootState> = {
// Also, we can get statuses which are older than max_id and newer than min_id.
// If the number of statuses exceeds the limit, it truncates newer statuses.
// That means, the status immediately before max_id is not included in the response.
let params = { min_id: since_id, limit: 20 }
let params = { min_id: req.sinceId, limit: 20 }
if (maxID !== null) {
params = Object.assign({}, params, {
max_id: maxID
@ -248,22 +194,17 @@ const actions: ActionTree<HomeState, RootState> = {
}
let timeline: Array<Entity.Status | LoadingCard> = [card]
timeline = timeline.concat(res.data)
commit(MUTATION_TYPES.APPEND_TIMELINE_AFTER_LOADING_CARD, timeline)
commit(MUTATION_TYPES.APPEND_TIMELINE_AFTER_LOADING_CARD, { statuses: timeline, accountId: req.account.id })
} else {
commit(MUTATION_TYPES.APPEND_TIMELINE_AFTER_LOADING_CARD, res.data)
commit(MUTATION_TYPES.APPEND_TIMELINE_AFTER_LOADING_CARD, { statuses: res.data, accountId: req.account.id })
}
return res.data
},
[ACTION_TYPES.GET_MARKER]: async ({ rootState }): Promise<Entity.Marker | null> => {
[ACTION_TYPES.GET_MARKER]: async ({ rootState }, req: { account: LocalAccount; server: LocalServer }): Promise<Entity.Marker | null> => {
if (!rootState.TimelineSpace.setting.markerHome) {
return null
}
const client = generator(
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
const client = generator(req.server.sns, req.server.baseURL, req.account.accessToken, rootState.App.userAgent)
let serverMarker: Entity.Marker | {} = {}
try {
const res = await client.getMarkers(['home'])
@ -273,20 +214,15 @@ const actions: ActionTree<HomeState, RootState> = {
}
return serverMarker
},
[ACTION_TYPES.SAVE_MARKER]: async ({ state, rootState }) => {
const timeline = state.timeline
[ACTION_TYPES.SAVE_MARKER]: async ({ state, rootState }, req: { account: LocalAccount; server: LocalServer }) => {
const timeline = state.timeline[req.account.id]
if (timeline.length === 0 || timeline[0].id === 'loading-card') {
return
}
if (rootState.TimelineSpace.server!.sns === 'misskey') {
if (req.server.sns === 'misskey') {
return
}
const client = generator(
rootState.TimelineSpace.server!.sns,
rootState.TimelineSpace.server!.baseURL,
rootState.TimelineSpace.account!.accessToken,
rootState.App.userAgent
)
const client = generator(req.server.sns, req.server.baseURL, req.account.accessToken, rootState.App.userAgent)
const res = await client.saveMarkers({ home: { last_read_id: timeline[0].id } })
return res.data
}