Merge pull request #2971 from h3poteto/feat/remember-feed-position
refs #574 Show loading card and load unread statuses in Home and Notifications
This commit is contained in:
commit
57a14f5f9d
|
@ -282,8 +282,8 @@ describe('TimelineSpace/Contents/Home', () => {
|
|||
})
|
||||
it('should be updated', () => {
|
||||
Home.mutations![MUTATION_TYPES.UPDATE_TOOT](state, favouritedStatus)
|
||||
expect(state.timeline[0].reblog).not.toBeNull()
|
||||
expect(state.timeline[0].reblog!.favourited).toEqual(true)
|
||||
expect((state.timeline[0] as Entity.Status).reblog).not.toBeNull()
|
||||
expect((state.timeline[0] as Entity.Status).reblog!.favourited).toEqual(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -378,6 +378,9 @@
|
|||
"left": "{{datetime}} left",
|
||||
"refresh": "Refresh"
|
||||
}
|
||||
},
|
||||
"status_loading": {
|
||||
"message": "Load more status"
|
||||
}
|
||||
},
|
||||
"side_bar": {
|
||||
|
|
|
@ -25,7 +25,7 @@ import path from 'path'
|
|||
import ContextMenu from 'electron-context-menu'
|
||||
import { initSplashScreen, Config } from '@trodi/electron-splashscreen'
|
||||
import openAboutWindow from 'about-window'
|
||||
import generator, { Entity, detector, NotificationType } from 'megalodon'
|
||||
import { Entity, detector, NotificationType } from 'megalodon'
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
import AutoLaunch from 'auto-launch'
|
||||
import minimist from 'minimist'
|
||||
|
@ -1169,49 +1169,16 @@ ipcMain.handle('get-notifications-marker', async (_: IpcMainInvokeEvent, ownerID
|
|||
return marker
|
||||
})
|
||||
|
||||
ipcMain.handle('save-marker', async (_: IpcMainInvokeEvent, marker: LocalMarker) => {
|
||||
if (marker.owner_id === null || marker.owner_id === undefined || marker.owner_id === '') {
|
||||
return
|
||||
ipcMain.on(
|
||||
'save-marker',
|
||||
async (_: IpcMainEvent, marker: LocalMarker): Promise<LocalMarker | null> => {
|
||||
if (marker.owner_id === null || marker.owner_id === undefined || marker.owner_id === '') {
|
||||
return null
|
||||
}
|
||||
const res = await markerRepo.save(marker)
|
||||
return res
|
||||
}
|
||||
await markerRepo.save(marker)
|
||||
})
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const accounts = await accountRepo.listAccounts()
|
||||
accounts.map(async acct => {
|
||||
const proxy = await proxyConfiguration.forMastodon()
|
||||
const sns = await detector(acct.baseURL, proxy)
|
||||
if (sns === 'misskey') {
|
||||
return
|
||||
}
|
||||
const client = generator(sns, acct.baseURL, acct.accessToken, 'Whalebird', proxy)
|
||||
const home = await markerRepo.get(acct._id!, 'home')
|
||||
const notifications = await markerRepo.get(acct._id!, 'notifications')
|
||||
let params = {}
|
||||
if (home !== null && home !== undefined) {
|
||||
params = Object.assign({}, params, {
|
||||
home: {
|
||||
last_read_id: home.last_read_id
|
||||
}
|
||||
})
|
||||
}
|
||||
if (notifications !== null && notifications !== undefined) {
|
||||
params = Object.assign({}, params, {
|
||||
notifications: {
|
||||
last_read_id: notifications.last_read_id
|
||||
}
|
||||
})
|
||||
}
|
||||
if (isEmpty(params)) {
|
||||
return
|
||||
}
|
||||
await client.saveMarkers(params)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}, 120000)
|
||||
)
|
||||
|
||||
// hashtag
|
||||
ipcMain.handle('save-hashtag', async (_: IpcMainInvokeEvent, tag: string) => {
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<el-switch id="hideAllAttachments" v-model="timeline_hide_attachments" active-color="#13ce66"> </el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item for="useMarker" :label="$t('preferences.general.timeline.useMarker')">
|
||||
<el-switch id="useMarker" v-model="timeline_use_marker" active-color="#13ce66" disabled> </el-switch>
|
||||
<el-switch id="useMarker" v-model="timeline_use_marker" active-color="#13ce66"> </el-switch>
|
||||
</el-form-item>
|
||||
<p class="notice">
|
||||
{{ $t('preferences.general.timeline.useMarkerNotice') }}
|
||||
|
|
|
@ -3,22 +3,29 @@
|
|||
<div v-shortkey="{ linux: ['ctrl', 'r'], mac: ['meta', 'r'] }" @shortkey="reload()"></div>
|
||||
<DynamicScroller :items="filteredTimeline" :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
|
||||
:message="item"
|
||||
:focused="item.uri + item.id === focusedId"
|
||||
:overlaid="modalOpened"
|
||||
:filters="filters"
|
||||
v-on:update="updateToot"
|
||||
v-on:delete="deleteToot"
|
||||
@focusNext="focusNext"
|
||||
@focusPrev="focusPrev"
|
||||
@focusRight="focusSidebar"
|
||||
@selectToot="focusToot(item)"
|
||||
@sizeChanged="sizeChanged"
|
||||
>
|
||||
</toot>
|
||||
</DynamicScrollerItem>
|
||||
<template v-if="item.id === 'loading-card'">
|
||||
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.id]" :data-index="index" :watchData="true">
|
||||
<StatusLoading :since_id="item.since_id" :max_id="item.max_id" :loading="loadingMore" @load_since="fetchTimelineSince" />
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.uri]" :data-index="index" :watchData="true">
|
||||
<toot
|
||||
:message="item"
|
||||
:focused="item.uri + item.id === focusedId"
|
||||
:overlaid="modalOpened"
|
||||
:filters="filters"
|
||||
v-on:update="updateToot"
|
||||
v-on:delete="deleteToot"
|
||||
@focusNext="focusNext"
|
||||
@focusPrev="focusPrev"
|
||||
@focusRight="focusSidebar"
|
||||
@selectToot="focusToot(item)"
|
||||
@sizeChanged="sizeChanged"
|
||||
>
|
||||
</toot>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
|
||||
|
@ -32,13 +39,14 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import Toot from '~/src/renderer/components/organisms/Toot'
|
||||
import StatusLoading from '~/src/renderer/components/organisms/StatusLoading'
|
||||
import reloadable from '~/src/renderer/components/mixins/reloadable'
|
||||
import { Event } from '~/src/renderer/components/event'
|
||||
import { ScrollPosition } from '~/src/renderer/components/utils/scroll'
|
||||
|
||||
export default {
|
||||
name: 'home',
|
||||
components: { Toot },
|
||||
components: { Toot, StatusLoading },
|
||||
mixins: [reloadable],
|
||||
data() {
|
||||
return {
|
||||
|
@ -46,7 +54,8 @@ export default {
|
|||
scrollPosition: null,
|
||||
observer: null,
|
||||
scrollTime: null,
|
||||
resizeTime: null
|
||||
resizeTime: null,
|
||||
loadingMore: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -101,14 +110,14 @@ export default {
|
|||
})
|
||||
|
||||
if (this.heading && this.timeline.length > 0) {
|
||||
this.$store.dispatch('TimelineSpace/Contents/Home/saveMarker', this.timeline[0].id)
|
||||
this.$store.dispatch('TimelineSpace/Contents/Home/saveMarker')
|
||||
}
|
||||
const el = document.getElementById('scroller')
|
||||
this.scrollPosition = new ScrollPosition(el)
|
||||
this.scrollPosition.prepare()
|
||||
|
||||
this.observer = new ResizeObserver(() => {
|
||||
if (this.scrollPosition && !this.heading && !this.lazyLoading && !this.scrolling) {
|
||||
if (this.loadingMore || (this.scrollPosition && !this.heading && !this.lazyLoading && !this.scrolling)) {
|
||||
this.resizeTime = moment()
|
||||
this.scrollPosition.restore()
|
||||
}
|
||||
|
@ -154,7 +163,7 @@ export default {
|
|||
},
|
||||
timeline: function (newState, _oldState) {
|
||||
if (this.heading && newState.length > 0) {
|
||||
this.$store.dispatch('TimelineSpace/Contents/Home/saveMarker', newState[0].id)
|
||||
this.$store.dispatch('TimelineSpace/Contents/Home/saveMarker')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -214,6 +223,14 @@ export default {
|
|||
deleteToot(message) {
|
||||
this.$store.commit('TimelineSpace/Contents/Home/deleteToot', message.id)
|
||||
},
|
||||
fetchTimelineSince(since_id) {
|
||||
this.loadingMore = true
|
||||
this.$store.dispatch('TimelineSpace/Contents/Home/fetchTimelineSince', since_id).finally(() => {
|
||||
setTimeout(() => {
|
||||
this.loadingMore = false
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
async reload() {
|
||||
this.$store.commit('TimelineSpace/changeLoading', true)
|
||||
try {
|
||||
|
|
|
@ -3,20 +3,27 @@
|
|||
<div v-shortkey="{ linux: ['ctrl', 'r'], mac: ['meta', 'r'] }" @shortkey="reload()"></div>
|
||||
<DynamicScroller :items="handledNotifications" :min-item-size="20" id="scroller" class="scroller" ref="scroller">
|
||||
<template v-slot="{ item, index, active }">
|
||||
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.url]" :data-index="index" :watchData="true">
|
||||
<notification
|
||||
:message="item"
|
||||
:focused="item.id === focusedId"
|
||||
:overlaid="modalOpened"
|
||||
:filters="filters"
|
||||
v-on:update="updateToot"
|
||||
@focusNext="focusNext"
|
||||
@focusPrev="focusPrev"
|
||||
@focusRight="focusSidebar"
|
||||
@selectNotification="focusNotification(item)"
|
||||
>
|
||||
</notification>
|
||||
</DynamicScrollerItem>
|
||||
<template v-if="item.id === 'loading-card'">
|
||||
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.id]" :data-index="index" :watchData="true">
|
||||
<StatusLoading :since_id="item.since_id" :max_id="item.max_id" :loading="loadingMore" @load_since="fetchNotificationsSince" />
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.url]" :data-index="index" :watchData="true">
|
||||
<notification
|
||||
:message="item"
|
||||
:focused="item.id === focusedId"
|
||||
:overlaid="modalOpened"
|
||||
:filters="filters"
|
||||
v-on:update="updateToot"
|
||||
@focusNext="focusNext"
|
||||
@focusPrev="focusPrev"
|
||||
@focusRight="focusSidebar"
|
||||
@selectNotification="focusNotification(item)"
|
||||
>
|
||||
</notification>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
<div :class="openSideBar ? 'upper-with-side-bar' : 'upper'" v-show="!heading">
|
||||
|
@ -29,13 +36,14 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import Notification from '~/src/renderer/components/organisms/Notification'
|
||||
import StatusLoading from '~/src/renderer/components/organisms/StatusLoading'
|
||||
import reloadable from '~/src/renderer/components/mixins/reloadable'
|
||||
import { Event } from '~/src/renderer/components/event'
|
||||
import { ScrollPosition } from '~/src/renderer/components/utils/scroll'
|
||||
|
||||
export default {
|
||||
name: 'notifications',
|
||||
components: { Notification },
|
||||
components: { Notification, StatusLoading },
|
||||
mixins: [reloadable],
|
||||
data() {
|
||||
return {
|
||||
|
@ -43,7 +51,8 @@ export default {
|
|||
scrollPosition: null,
|
||||
observer: null,
|
||||
scrollTime: null,
|
||||
resizeTime: null
|
||||
resizeTime: null,
|
||||
loadingMore: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -53,6 +62,7 @@ export default {
|
|||
backgroundColor: state => state.App.theme.background_color
|
||||
}),
|
||||
...mapState('TimelineSpace/Contents/Notifications', {
|
||||
notifications: state => state.notifications,
|
||||
lazyLoading: state => state.lazyLoading,
|
||||
heading: state => state.heading,
|
||||
scrolling: state => state.scrolling
|
||||
|
@ -86,14 +96,14 @@ export default {
|
|||
})
|
||||
|
||||
if (this.heading && this.handledNotifications.length > 0) {
|
||||
this.$store.dispatch('TimelineSpace/Contents/Notifications/saveMarker', this.handledNotifications[0].id)
|
||||
this.$store.dispatch('TimelineSpace/Contents/Notifications/saveMarker')
|
||||
}
|
||||
const el = document.getElementById('scroller')
|
||||
this.scrollPosition = new ScrollPosition(el)
|
||||
this.scrollPosition.prepare()
|
||||
|
||||
this.observer = new ResizeObserver(() => {
|
||||
if (this.scrollPosition && !this.heading && !this.lazyLoading && !this.scrolling) {
|
||||
if (this.loadingMore || (this.scrollPosition && !this.heading && !this.lazyLoading && !this.scrolling)) {
|
||||
this.resizeTime = moment()
|
||||
this.scrollPosition.restore()
|
||||
}
|
||||
|
@ -138,9 +148,9 @@ export default {
|
|||
this.$store.dispatch('TimelineSpace/Contents/Notifications/resetBadge')
|
||||
}
|
||||
},
|
||||
handledNotifications: function (newState, _oldState) {
|
||||
notifications: function (newState, _oldState) {
|
||||
if (this.heading && newState.length > 0) {
|
||||
this.$store.dispatch('TimelineSpace/Contents/Notifications/saveMarker', newState[0].id)
|
||||
this.$store.dispatch('TimelineSpace/Contents/Notifications/saveMarker')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -187,6 +197,7 @@ export default {
|
|||
} else if (event.target.scrollTop <= 10 && !this.heading) {
|
||||
this.$store.commit('TimelineSpace/Contents/Notifications/changeHeading', true)
|
||||
this.$store.dispatch('TimelineSpace/Contents/Notifications/resetBadge')
|
||||
this.$store.dispatch('TimelineSpace/Contents/Notifications/saveMarker')
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
|
@ -197,6 +208,14 @@ export default {
|
|||
}
|
||||
}, 150)
|
||||
},
|
||||
fetchNotificationsSince(since_id) {
|
||||
this.loadingMore = true
|
||||
this.$store.dispatch('TimelineSpace/Contents/Notifications/fetchNotificationsSince', since_id).finally(() => {
|
||||
setTimeout(() => {
|
||||
this.loadingMore = false
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
async reload() {
|
||||
this.$store.commit('TimelineSpace/changeLoading', true)
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="status-loading" tabIndex="0" @click="onClick">
|
||||
<img v-if="loading" src="../../assets/images/loading-spinner-wide.svg" class="load-icon" />
|
||||
<p v-else class="load-text">{{ $t('cards.status_loading.message') }}</p>
|
||||
<div class="fill-line"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'status-loading',
|
||||
props: {
|
||||
max_id: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
since_id: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
if (this.loading) {
|
||||
return
|
||||
}
|
||||
if (this.since_id !== '') {
|
||||
this.$emit('load_since', this.since_id)
|
||||
} else if (this.max_id !== '') {
|
||||
this.$emit('load_max', this.max_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.status-loading {
|
||||
background-color: var(--theme-background-color);
|
||||
text-align: center;
|
||||
padding: 12px 0;
|
||||
height: 36px;
|
||||
border-bottom: 1px solid var(--theme-border-color);
|
||||
|
||||
.load-icon {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.load-text {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -27,6 +27,7 @@ export type AppState = {
|
|||
hideAllAttachments: boolean
|
||||
tootPadding: number
|
||||
userAgent: string
|
||||
useMarker: boolean
|
||||
}
|
||||
|
||||
const state = (): AppState => ({
|
||||
|
@ -51,7 +52,8 @@ const state = (): AppState => ({
|
|||
ignoreCW: false,
|
||||
ignoreNSFW: false,
|
||||
hideAllAttachments: false,
|
||||
userAgent: 'Whalebird'
|
||||
userAgent: 'Whalebird',
|
||||
useMarker: false
|
||||
})
|
||||
|
||||
const MUTATION_TYPES = {
|
||||
|
@ -65,7 +67,8 @@ const MUTATION_TYPES = {
|
|||
ADD_FONT: 'addFont',
|
||||
UPDATE_IGNORE_CW: 'updateIgnoreCW',
|
||||
UPDATE_IGNORE_NSFW: 'updateIgnoreNSFW',
|
||||
UPDATE_HIDE_ALL_ATTACHMENTS: 'updateHideAllAttachments'
|
||||
UPDATE_HIDE_ALL_ATTACHMENTS: 'updateHideAllAttachments',
|
||||
UPDATE_USE_MARKER: 'updateUseMarker'
|
||||
}
|
||||
|
||||
const mutations: MutationTree<AppState> = {
|
||||
|
@ -102,6 +105,9 @@ const mutations: MutationTree<AppState> = {
|
|||
},
|
||||
[MUTATION_TYPES.UPDATE_HIDE_ALL_ATTACHMENTS]: (state: AppState, hideAllAttachments: boolean) => {
|
||||
state.hideAllAttachments = hideAllAttachments
|
||||
},
|
||||
[MUTATION_TYPES.UPDATE_USE_MARKER]: (state: AppState, useMarker: boolean) => {
|
||||
state.useMarker = useMarker
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,6 +133,7 @@ const actions: ActionTree<AppState, RootState> = {
|
|||
commit(MUTATION_TYPES.UPDATE_IGNORE_CW, conf.general.timeline.cw)
|
||||
commit(MUTATION_TYPES.UPDATE_IGNORE_NSFW, conf.general.timeline.nsfw)
|
||||
commit(MUTATION_TYPES.UPDATE_HIDE_ALL_ATTACHMENTS, conf.general.timeline.hideAllAttachments)
|
||||
commit(MUTATION_TYPES.UPDATE_USE_MARKER, conf.general.timeline.useMarker)
|
||||
return conf
|
||||
},
|
||||
updateTheme: async ({ commit }, appearance: Appearance) => {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
|
|||
import { RootState } from '@/store'
|
||||
import { LocalMarker } from '~/src/types/localMarker'
|
||||
import { MyWindow } from '~/src/types/global'
|
||||
import { LoadingCard } from '@/types/loading-card'
|
||||
|
||||
const win = (window as any) as MyWindow
|
||||
|
||||
|
@ -11,7 +12,7 @@ export type HomeState = {
|
|||
heading: boolean
|
||||
showReblogs: boolean
|
||||
showReplies: boolean
|
||||
timeline: Array<Entity.Status>
|
||||
timeline: Array<Entity.Status | LoadingCard>
|
||||
scrolling: boolean
|
||||
}
|
||||
|
||||
|
@ -36,7 +37,8 @@ export const MUTATION_TYPES = {
|
|||
DELETE_TOOT: 'deleteToot',
|
||||
SHOW_REBLOGS: 'showReblogs',
|
||||
SHOW_REPLIES: 'showReplies',
|
||||
CHANGE_SCROLLING: 'changeScrolling'
|
||||
CHANGE_SCROLLING: 'changeScrolling',
|
||||
APPEND_TIMELINE_AFTER_LOADING_CARD: 'appendTimelineAfterLoadingCard'
|
||||
}
|
||||
|
||||
const mutations: MutationTree<HomeState> = {
|
||||
|
@ -49,13 +51,13 @@ const mutations: MutationTree<HomeState> = {
|
|||
[MUTATION_TYPES.APPEND_TIMELINE]: (state, update: Entity.Status) => {
|
||||
// Reject duplicated status in timeline
|
||||
if (!state.timeline.find(item => item.id === update.id)) {
|
||||
state.timeline = [update].concat(state.timeline)
|
||||
state.timeline = ([update] as Array<Entity.Status | LoadingCard>).concat(state.timeline)
|
||||
}
|
||||
},
|
||||
[MUTATION_TYPES.UPDATE_TIMELINE]: (state, messages: Array<Entity.Status>) => {
|
||||
[MUTATION_TYPES.UPDATE_TIMELINE]: (state, messages: Array<Entity.Status | LoadingCard>) => {
|
||||
state.timeline = messages
|
||||
},
|
||||
[MUTATION_TYPES.INSERT_TIMELINE]: (state, messages: Array<Entity.Status>) => {
|
||||
[MUTATION_TYPES.INSERT_TIMELINE]: (state, messages: Array<Entity.Status | LoadingCard>) => {
|
||||
state.timeline = state.timeline.concat(messages)
|
||||
},
|
||||
[MUTATION_TYPES.ARCHIVE_TIMELINE]: state => {
|
||||
|
@ -66,7 +68,11 @@ const mutations: MutationTree<HomeState> = {
|
|||
},
|
||||
[MUTATION_TYPES.UPDATE_TOOT]: (state, message: Entity.Status) => {
|
||||
// Replace target message in homeTimeline and notifications
|
||||
state.timeline = state.timeline.map(toot => {
|
||||
state.timeline = state.timeline.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) {
|
||||
|
@ -82,7 +88,11 @@ const mutations: MutationTree<HomeState> = {
|
|||
})
|
||||
},
|
||||
[MUTATION_TYPES.DELETE_TOOT]: (state, messageId: string) => {
|
||||
state.timeline = state.timeline.filter(toot => {
|
||||
state.timeline = state.timeline.filter(status => {
|
||||
if (status.id === 'loading-card') {
|
||||
return true
|
||||
}
|
||||
const toot = status as Entity.Status
|
||||
if (toot.reblog !== null && toot.reblog.id === messageId) {
|
||||
return false
|
||||
} else {
|
||||
|
@ -98,20 +108,65 @@ const mutations: MutationTree<HomeState> = {
|
|||
},
|
||||
[MUTATION_TYPES.CHANGE_SCROLLING]: (state, value: boolean) => {
|
||||
state.scrolling = value
|
||||
},
|
||||
[MUTATION_TYPES.APPEND_TIMELINE_AFTER_LOADING_CARD]: (state, timeline: Array<Entity.Status | LoadingCard>) => {
|
||||
const tl = state.timeline.flatMap(status => {
|
||||
if (status.id !== 'loading-card') {
|
||||
return status
|
||||
} else {
|
||||
return timeline
|
||||
}
|
||||
})
|
||||
// Reject duplicated status in timeline
|
||||
state.timeline = Array.from(new Set(tl))
|
||||
}
|
||||
}
|
||||
|
||||
const actions: ActionTree<HomeState, RootState> = {
|
||||
fetchTimeline: async ({ commit, rootState }) => {
|
||||
fetchTimeline: async ({ dispatch, commit, rootState }) => {
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.sns,
|
||||
rootState.TimelineSpace.account.baseURL,
|
||||
rootState.TimelineSpace.account.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
const res = await client.getHomeTimeline({ limit: 40 })
|
||||
commit(MUTATION_TYPES.UPDATE_TIMELINE, res.data)
|
||||
return res.data
|
||||
const localMarker: LocalMarker | null = await dispatch('getMarker').catch(err => {
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
if (rootState.App.useMarker && localMarker !== null) {
|
||||
const last = await client.getStatus(localMarker.last_read_id)
|
||||
const lastReadStatus = last.data
|
||||
|
||||
let timeline: Array<Entity.Status | LoadingCard> = [lastReadStatus]
|
||||
const card: LoadingCard = {
|
||||
type: 'middle-load',
|
||||
since_id: lastReadStatus.id,
|
||||
// We don't need to fill this field in the first fetcing.
|
||||
// Because in most cases there is no new statuses at the first fetching.
|
||||
// After new statuses are received, if the number of unread statuses is more than 40, max_id is not necessary.
|
||||
// We can fill max_id when calling fetchTimelineSince.
|
||||
// If the number of unread statuses is less than 40, max_id is necessary, but it is enough to reject duplicated statuses.
|
||||
// So we do it in mutation.
|
||||
max_id: null,
|
||||
id: 'loading-card'
|
||||
}
|
||||
|
||||
const res = await client.getHomeTimeline({ limit: 40, max_id: lastReadStatus.id })
|
||||
// Make sure whether new statuses exist or not.
|
||||
const nextResponse = await client.getHomeTimeline({ limit: 1, min_id: lastReadStatus.id })
|
||||
if (nextResponse.data.length > 0) {
|
||||
timeline = ([card] as Array<Entity.Status | LoadingCard>).concat(timeline).concat(res.data)
|
||||
} else {
|
||||
timeline = timeline.concat(res.data)
|
||||
}
|
||||
commit(MUTATION_TYPES.UPDATE_TIMELINE, timeline)
|
||||
return res.data
|
||||
} else {
|
||||
const res = await client.getHomeTimeline({ limit: 40 })
|
||||
commit(MUTATION_TYPES.UPDATE_TIMELINE, res.data)
|
||||
return res.data
|
||||
}
|
||||
},
|
||||
lazyFetchTimeline: async ({ state, commit, rootState }, lastStatus: Entity.Status): Promise<Array<Entity.Status> | null> => {
|
||||
if (state.lazyLoading) {
|
||||
|
@ -134,12 +189,104 @@ const actions: ActionTree<HomeState, RootState> = {
|
|||
commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, false)
|
||||
})
|
||||
},
|
||||
saveMarker: async ({ rootState }, id: string) => {
|
||||
await win.ipcRenderer.invoke('save-marker', {
|
||||
fetchTimelineSince: async ({ state, rootState, commit }, since_id: string): Promise<Array<Entity.Status> | null> => {
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.sns,
|
||||
rootState.TimelineSpace.account.baseURL,
|
||||
rootState.TimelineSpace.account.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
const cardIndex = state.timeline.findIndex(s => {
|
||||
if (s.id === 'loading-card') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
let maxID: string | null = null
|
||||
if (cardIndex > 0) {
|
||||
maxID = state.timeline[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?
|
||||
// The max_id & since_id:
|
||||
// We can get statuses which are older than max_id and newer than since_id.
|
||||
// If the number of statuses exceeds the limit, it truncates older statuses.
|
||||
// That means, the status immediately after since_id is not included in the response.
|
||||
// The max_id & min_id:
|
||||
// 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: 40 }
|
||||
if (maxID !== null) {
|
||||
params = Object.assign({}, params, {
|
||||
max_id: maxID
|
||||
})
|
||||
}
|
||||
|
||||
const res = await client.getHomeTimeline(params)
|
||||
if (res.data.length >= 40) {
|
||||
const card: LoadingCard = {
|
||||
type: 'middle-load',
|
||||
since_id: res.data[0].id,
|
||||
max_id: maxID,
|
||||
id: 'loading-card'
|
||||
}
|
||||
let timeline: Array<Entity.Status | LoadingCard> = [card]
|
||||
timeline = timeline.concat(res.data)
|
||||
commit(MUTATION_TYPES.APPEND_TIMELINE_AFTER_LOADING_CARD, timeline)
|
||||
} else {
|
||||
commit(MUTATION_TYPES.APPEND_TIMELINE_AFTER_LOADING_CARD, res.data)
|
||||
}
|
||||
return res.data
|
||||
},
|
||||
getMarker: async ({ rootState }): Promise<LocalMarker | null> => {
|
||||
if (!rootState.App.useMarker) {
|
||||
return null
|
||||
}
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.sns,
|
||||
rootState.TimelineSpace.account.baseURL,
|
||||
rootState.TimelineSpace.account.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
let serverMarker: Entity.Marker | {} = {}
|
||||
try {
|
||||
const res = await client.getMarkers(['home'])
|
||||
serverMarker = res.data
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
if ((serverMarker as Entity.Marker).home !== undefined) {
|
||||
return {
|
||||
timeline: 'home',
|
||||
last_read_id: (serverMarker as Entity.Marker).home.last_read_id
|
||||
} as LocalMarker
|
||||
}
|
||||
const localMarker: LocalMarker | null = await win.ipcRenderer.invoke('get-home-marker', rootState.TimelineSpace.account._id)
|
||||
return localMarker
|
||||
},
|
||||
saveMarker: async ({ state, rootState }) => {
|
||||
const timeline = state.timeline
|
||||
if (timeline.length === 0 || timeline[0].id === 'loading-card') {
|
||||
return
|
||||
}
|
||||
win.ipcRenderer.send('save-marker', {
|
||||
owner_id: rootState.TimelineSpace.account._id,
|
||||
timeline: 'home',
|
||||
last_read_id: id
|
||||
last_read_id: timeline[0].id
|
||||
} as LocalMarker)
|
||||
|
||||
if (rootState.TimelineSpace.sns === 'misskey') {
|
||||
return
|
||||
}
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.sns,
|
||||
rootState.TimelineSpace.account.baseURL,
|
||||
rootState.TimelineSpace.account.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
const res = await client.saveMarkers({ home: { last_read_id: timeline[0].id } })
|
||||
return res.data
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,13 +3,14 @@ import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
|
|||
import { RootState } from '@/store'
|
||||
import { LocalMarker } from '~/src/types/localMarker'
|
||||
import { MyWindow } from '~/src/types/global'
|
||||
import { LoadingCard } from '@/types/loading-card'
|
||||
|
||||
const win = (window as any) as MyWindow
|
||||
|
||||
export type NotificationsState = {
|
||||
lazyLoading: boolean
|
||||
heading: boolean
|
||||
notifications: Array<Entity.Notification>
|
||||
notifications: Array<Entity.Notification | LoadingCard>
|
||||
scrolling: boolean
|
||||
}
|
||||
|
||||
|
@ -30,7 +31,8 @@ export const MUTATION_TYPES = {
|
|||
DELETE_TOOT: 'deleteToot',
|
||||
CLEAR_NOTIFICATIONS: 'clearNotifications',
|
||||
ARCHIVE_NOTIFICATIONS: 'archiveNotifications',
|
||||
CHANGE_SCROLLING: 'changeScrolling'
|
||||
CHANGE_SCROLLING: 'changeScrolling',
|
||||
APPEND_NOTIFICATIONS_AFTER_LOADING_CARD: 'appendNotificationsAfterLoadingCard'
|
||||
}
|
||||
|
||||
const mutations: MutationTree<NotificationsState> = {
|
||||
|
@ -43,13 +45,13 @@ const mutations: MutationTree<NotificationsState> = {
|
|||
[MUTATION_TYPES.APPEND_NOTIFICATIONS]: (state, notification: Entity.Notification) => {
|
||||
// Reject duplicated status in timeline
|
||||
if (!state.notifications.find(item => item.id === notification.id)) {
|
||||
state.notifications = [notification].concat(state.notifications)
|
||||
state.notifications = ([notification] as Array<Entity.Notification | LoadingCard>).concat(state.notifications)
|
||||
}
|
||||
},
|
||||
[MUTATION_TYPES.UPDATE_NOTIFICATIONS]: (state, notifications: Array<Entity.Notification>) => {
|
||||
[MUTATION_TYPES.UPDATE_NOTIFICATIONS]: (state, notifications: Array<Entity.Notification | LoadingCard>) => {
|
||||
state.notifications = notifications
|
||||
},
|
||||
[MUTATION_TYPES.INSERT_NOTIFICATIONS]: (state, notifications: Array<Entity.Notification>) => {
|
||||
[MUTATION_TYPES.INSERT_NOTIFICATIONS]: (state, notifications: Array<Entity.Notification | LoadingCard>) => {
|
||||
state.notifications = state.notifications.concat(notifications)
|
||||
},
|
||||
[MUTATION_TYPES.UPDATE_TOOT]: (state, message: Entity.Status) => {
|
||||
|
@ -67,7 +69,11 @@ const mutations: MutationTree<NotificationsState> = {
|
|||
})
|
||||
},
|
||||
[MUTATION_TYPES.DELETE_TOOT]: (state, id: string) => {
|
||||
state.notifications = state.notifications.filter(notification => {
|
||||
state.notifications = state.notifications.filter(notify => {
|
||||
if (notify.id === 'loading-card') {
|
||||
return true
|
||||
}
|
||||
const notification = notify as Entity.Notification
|
||||
if (notification.status) {
|
||||
if (notification.status.reblog && notification.status.reblog.id === id) {
|
||||
return false
|
||||
|
@ -87,20 +93,68 @@ const mutations: MutationTree<NotificationsState> = {
|
|||
},
|
||||
[MUTATION_TYPES.CHANGE_SCROLLING]: (state, value: boolean) => {
|
||||
state.scrolling = value
|
||||
},
|
||||
[MUTATION_TYPES.APPEND_NOTIFICATIONS_AFTER_LOADING_CARD]: (state, notifications: Array<Entity.Notification | LoadingCard>) => {
|
||||
const n = state.notifications.flatMap(notify => {
|
||||
if (notify.id !== 'loading-card') {
|
||||
return notify
|
||||
} else {
|
||||
return notifications
|
||||
}
|
||||
})
|
||||
// Reject duplicated status in timeline
|
||||
state.notifications = Array.from(new Set(n))
|
||||
}
|
||||
}
|
||||
|
||||
const actions: ActionTree<NotificationsState, RootState> = {
|
||||
fetchNotifications: async ({ commit, rootState }): Promise<Array<Entity.Notification>> => {
|
||||
fetchNotifications: async ({ dispatch, commit, rootState }): Promise<Array<Entity.Notification>> => {
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.sns,
|
||||
rootState.TimelineSpace.account.baseURL,
|
||||
rootState.TimelineSpace.account.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
const res = await client.getNotifications({ limit: 30 })
|
||||
commit(MUTATION_TYPES.UPDATE_NOTIFICATIONS, res.data)
|
||||
return res.data
|
||||
|
||||
const localMarker: LocalMarker | null = await dispatch('getMarker').catch(err => {
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
if (rootState.App.useMarker && localMarker !== null) {
|
||||
// The result does not contain max_id's notification, when we specify max_id parameter in get notifications.
|
||||
// So we need to get max_id's notification.
|
||||
const last = await client.getNotification(localMarker.last_read_id)
|
||||
const lastReadNotification = last.data
|
||||
|
||||
let notifications: Array<Entity.Notification | LoadingCard> = [lastReadNotification]
|
||||
const card: LoadingCard = {
|
||||
type: 'middle-load',
|
||||
since_id: lastReadNotification.id,
|
||||
// We don't need to fill this field in the first fetcing.
|
||||
// Because in most cases there is no new statuses at the first fetching.
|
||||
// After new statuses are received, if the number of unread statuses is more than 30, max_id is not necessary.
|
||||
// We can fill max_id when calling fetchTimelineSince.
|
||||
// If the number of unread statuses is less than 30, max_id is necessary, but it is enough to reject duplicated statuses.
|
||||
// So we do it in mutation.
|
||||
max_id: null,
|
||||
id: 'loading-card'
|
||||
}
|
||||
|
||||
const res = await client.getNotifications({ limit: 30, max_id: localMarker.last_read_id })
|
||||
// Make sure whether new notifications exist or not
|
||||
const nextResponse = await client.getNotifications({ limit: 1, min_id: lastReadNotification.id })
|
||||
if (nextResponse.data.length > 0) {
|
||||
notifications = ([card] as Array<Entity.Notification | LoadingCard>).concat(notifications).concat(res.data)
|
||||
} else {
|
||||
notifications = notifications.concat(res.data)
|
||||
}
|
||||
commit(MUTATION_TYPES.UPDATE_NOTIFICATIONS, notifications)
|
||||
return res.data
|
||||
} else {
|
||||
const res = await client.getNotifications({ limit: 30 })
|
||||
commit(MUTATION_TYPES.UPDATE_NOTIFICATIONS, res.data)
|
||||
return res.data
|
||||
}
|
||||
},
|
||||
lazyFetchNotifications: (
|
||||
{ state, commit, rootState },
|
||||
|
@ -126,15 +180,97 @@ const actions: ActionTree<NotificationsState, RootState> = {
|
|||
commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, false)
|
||||
})
|
||||
},
|
||||
fetchNotificationsSince: async ({ state, rootState, commit }, since_id: string): Promise<Array<Entity.Notification> | null> => {
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.sns,
|
||||
rootState.TimelineSpace.account.baseURL,
|
||||
rootState.TimelineSpace.account.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
const cardIndex = state.notifications.findIndex(s => {
|
||||
if (s.id === 'loading-card') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
let maxID: string | null = null
|
||||
if (cardIndex > 0) {
|
||||
maxID = state.notifications[cardIndex - 1].id
|
||||
}
|
||||
let params = { min_id: since_id, limit: 30 }
|
||||
if (maxID !== null) {
|
||||
params = Object.assign({}, params, {
|
||||
max_id: maxID
|
||||
})
|
||||
}
|
||||
|
||||
const res = await client.getNotifications(params)
|
||||
if (res.data.length >= 30) {
|
||||
const card: LoadingCard = {
|
||||
type: 'middle-load',
|
||||
since_id: res.data[0].id,
|
||||
max_id: maxID,
|
||||
id: 'loading-card'
|
||||
}
|
||||
let notifications: Array<Entity.Notification | LoadingCard> = [card]
|
||||
notifications = notifications.concat(res.data)
|
||||
commit(MUTATION_TYPES.APPEND_NOTIFICATIONS_AFTER_LOADING_CARD, notifications)
|
||||
} else {
|
||||
commit(MUTATION_TYPES.APPEND_NOTIFICATIONS_AFTER_LOADING_CARD, res.data)
|
||||
}
|
||||
return res.data
|
||||
},
|
||||
resetBadge: () => {
|
||||
win.ipcRenderer.send('reset-badge')
|
||||
},
|
||||
saveMarker: async ({ rootState }, id: string) => {
|
||||
await win.ipcRenderer.invoke('save-marker', {
|
||||
getMarker: async ({ rootState }): Promise<LocalMarker | null> => {
|
||||
if (!rootState.App.useMarker) {
|
||||
return null
|
||||
}
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.sns,
|
||||
rootState.TimelineSpace.account.baseURL,
|
||||
rootState.TimelineSpace.account.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
let serverMarker: Entity.Marker | {} = {}
|
||||
try {
|
||||
const res = await client.getMarkers(['notifications'])
|
||||
serverMarker = res.data
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
if ((serverMarker as Entity.Marker).notifications !== undefined) {
|
||||
return {
|
||||
timeline: 'notifications',
|
||||
last_read_id: (serverMarker as Entity.Marker).notifications.last_read_id
|
||||
} as LocalMarker
|
||||
}
|
||||
const localMarker: LocalMarker | null = await win.ipcRenderer.invoke('get-notifications-marker', rootState.TimelineSpace.account._id)
|
||||
return localMarker
|
||||
},
|
||||
saveMarker: async ({ state, rootState }) => {
|
||||
const notifications = state.notifications
|
||||
if (notifications.length === 0 || notifications[0].id === 'loading-card') {
|
||||
return
|
||||
}
|
||||
win.ipcRenderer.send('save-marker', {
|
||||
owner_id: rootState.TimelineSpace.account._id,
|
||||
timeline: 'notifications',
|
||||
last_read_id: id
|
||||
last_read_id: notifications[0].id
|
||||
} as LocalMarker)
|
||||
|
||||
if (rootState.TimelineSpace.sns === 'misskey') {
|
||||
return
|
||||
}
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.sns,
|
||||
rootState.TimelineSpace.account.baseURL,
|
||||
rootState.TimelineSpace.account.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
const res = await client.saveMarkers({ notifications: { last_read_id: notifications[0].id } })
|
||||
return res.data
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,6 +278,7 @@ const getters: GetterTree<NotificationsState, RootState> = {
|
|||
handledNotifications: state => {
|
||||
return state.notifications.filter(n => {
|
||||
switch (n.type) {
|
||||
case 'middle-load':
|
||||
case NotificationType.Follow:
|
||||
case NotificationType.Favourite:
|
||||
case NotificationType.Reblog:
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export type LoadingCard = {
|
||||
type: 'middle-load'
|
||||
max_id: string | null
|
||||
since_id: string | null
|
||||
id: 'loading-card'
|
||||
}
|
Loading…
Reference in New Issue