1
0
mirror of https://github.com/h3poteto/whalebird-desktop synced 2025-02-02 10:26:50 +01:00

refs #1714 Create bookmarks timeline

This commit is contained in:
AkiraFukushima 2020-08-24 19:33:55 +09:00
parent 375fb34873
commit bd4bb90870
6 changed files with 357 additions and 1 deletions

View File

@ -65,6 +65,7 @@
"notification": "Notification",
"mention": "Mention",
"favourite": "Favourite",
"bookmark": "Bookmark",
"follow_requests": "Follow Requests",
"direct_messages": "Direct Messages",
"local": "Local timeline",

View File

@ -0,0 +1,213 @@
<template>
<div id="bookmarks" v-shortkey="shortcutEnabled ? { next: ['j'] } : {}" @shortkey="handleKey">
<div v-shortkey="{ linux: ['ctrl', 'r'], mac: ['meta', 'r'] }" @shortkey="reload()"></div>
<div class="bookmark" v-for="message in bookmarks" v-bind:key="message.id">
<toot
:message="message"
:focused="message.uri === focusedId"
:overlaid="modalOpened"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusNext="focusNext"
@focusPrev="focusPrev"
@focusRight="focusSidebar"
@selectToot="focusToot(message)"
></toot>
</div>
<div class="loading-card" v-loading="lazyLoading" :element-loading-background="backgroundColor"></div>
<div :class="openSideBar ? 'upper-with-side-bar' : 'upper'" v-show="!heading">
<el-button type="primary" icon="el-icon-arrow-up" @click="upper" circle> </el-button>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import scrollTop from '@/components/utils/scroll'
import Toot from '@/components/organisms/Toot'
import reloadable from '~/src/renderer/components/mixins/reloadable'
import { Event } from '~/src/renderer/components/event'
export default {
name: 'bookmarks',
components: { Toot },
mixins: [reloadable],
data() {
return {
heading: true,
focusedId: null
}
},
computed: {
...mapState('TimelineSpace', {
account: state => state.account
}),
...mapState('App', {
backgroundColor: state => state.theme.background_color
}),
...mapState('TimelineSpace/HeaderMenu', {
startReload: state => state.reload
}),
...mapState('TimelineSpace/Contents/SideBar', {
openSideBar: state => state.openSideBar
}),
...mapState('TimelineSpace/Contents/Bookmarks', {
bookmarks: state => state.bookmarks,
lazyLoading: state => state.lazyLoading,
filter: state => state.filter
}),
...mapGetters('TimelineSpace/Modals', ['modalOpened']),
shortcutEnabled: function () {
return !this.focusedId && !this.modalOpened
}
},
created() {
this.$store.commit('TimelineSpace/Contents/changeLoading', true)
this.$store
.dispatch('TimelineSpace/Contents/Bookmarks/fetchBookmarks', this.account)
.catch(() => {
this.$message({
message: this.$t('message.bookmark_fetch_error'),
type: 'error'
})
})
.finally(() => {
this.$store.commit('TimelineSpace/Contents/changeLoading', false)
})
},
mounted() {
document.getElementById('scrollable').addEventListener('scroll', this.onScroll)
Event.$on('focus-timeline', () => {
// If focusedId does not change, we have to refresh focusedId because Toot component watch change events.
const previousFocusedId = this.focusedId
this.focusedId = 0
this.$nextTick(function () {
this.focusedId = previousFocusedId
})
})
},
beforeDestroy() {
Event.$off('focus-timeline')
},
destroyed() {
this.$store.commit('TimelineSpace/Contents/Bookmarks/updateBookmarks', [])
if (document.getElementById('scrollable') !== undefined && document.getElementById('scrollable') !== null) {
document.getElementById('scrollable').removeEventListener('scroll', this.onScroll)
document.getElementById('scrollable').scrollTop = 0
}
},
watch: {
startReload: function (newState, oldState) {
if (!oldState && newState) {
this.reload().finally(() => {
this.$store.commit('TimelineSpace/HeaderMenu/changeReload', false)
})
}
},
focusedId: function (newState, _oldState) {
if (newState && this.heading) {
this.heading = false
} else if (newState === null && !this.heading) {
this.heading = true
}
}
},
methods: {
updateToot(message) {
this.$store.commit('TimelineSpace/Contents/Bookmarks/updateToot', message)
},
deleteToot(message) {
this.$store.commit('TimelineSpace/Contents/Bookmarks/deleteToot', message)
},
onScroll(event) {
if (
event.target.clientHeight + event.target.scrollTop >= document.getElementById('bookmarks').clientHeight - 10 &&
!this.lazyloading
) {
this.$store.dispatch('TimelineSpace/Contents/Bookmarks/lazyFetchBookmarks', this.bookmarks[this.bookmarks.length - 1]).catch(() => {
this.$message({
message: this.$t('message.bookmark_fetch_error'),
type: 'error'
})
})
}
// for upper
if (event.target.scrollTop > 10 && this.heading) {
this.heading = false
} else if (event.target.scrollTop <= 10 && !this.heading) {
this.heading = true
}
},
async reload() {
this.$store.commit('TimelineSpace/changeLoading', true)
try {
const account = await this.reloadable()
await this.$store.dispatch('TimelineSpace/Contents/Bookmarks/fetchBookmarks', account).catch(() => {
this.$message({
message: this.$t('message.bookmark_fetch_error'),
type: 'error'
})
})
} finally {
this.$store.commit('TimelineSpace/changeLoading', false)
}
},
upper() {
scrollTop(document.getElementById('scrollable'), 0)
this.focusedId = null
},
focusNext() {
const currentIndex = this.bookmarks.findIndex(toot => this.focusedId === toot.uri)
if (currentIndex === -1) {
this.focusedId = this.bookmarks[0].uri
} else if (currentIndex < this.bookmarks.length) {
this.focusedId = this.bookmarks[currentIndex + 1].uri
}
},
focusPrev() {
const currentIndex = this.bookmarks.findIndex(toot => this.focusedId === toot.uri)
if (currentIndex === 0) {
this.focusedId = null
} else if (currentIndex > 0) {
this.focusedId = this.bookmarks[currentIndex - 1].uri
}
},
focusToot(message) {
this.focusedId = message.id
},
focusSidebar() {
Event.$emit('focus-sidebar')
},
handleKey(event) {
switch (event.srcKey) {
case 'next':
this.focusedId = this.bookmarks[0].uri
break
}
}
}
}
</script>
<style lang="scss" scoped>
.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;
}
</style>

View File

@ -74,7 +74,7 @@ export default {
this.$store.dispatch('TimelineSpace/HeaderMenu/setupLoading')
},
watch: {
$route: function() {
$route: function () {
this.channelName()
this.loadFilter()
}
@ -94,6 +94,9 @@ export default {
case 'favourites':
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.favourite'))
break
case 'bookmarks':
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.bookmark'))
break
case 'mentions':
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.mention'))
break
@ -142,6 +145,7 @@ export default {
case 'notifications':
case 'mentions':
case 'favourites':
case 'bookmarks':
case 'local':
case 'public':
case 'tag':
@ -159,6 +163,7 @@ export default {
case 'notifications':
case 'mentions':
case 'favourites':
case 'bookmarks':
case 'local':
case 'public':
case 'tag':

View File

@ -30,6 +30,7 @@ import TimelineSpaceContentsListsIndex from '@/components/TimelineSpace/Contents
import TimelineSpaceContentsListsEdit from '@/components/TimelineSpace/Contents/Lists/Edit.vue'
import TimelineSpaceContentsListsShow from '@/components/TimelineSpace/Contents/Lists/Show.vue'
import TimelineSpaceContentsFollowRequests from '@/components/TimelineSpace/Contents/FollowRequests.vue'
import TimelineSpaceContentsBookmarks from '@/components/TimelineSpace/Contents/Bookmarks.vue'
Vue.use(Router)
@ -132,6 +133,11 @@ const router = new Router({
name: 'favourites',
component: TimelineSpaceContentsFavourites
},
{
path: 'bookmarks',
name: 'bookmarks',
component: TimelineSpaceContentsBookmarks
},
{
path: 'local',
name: 'local',

View File

@ -2,6 +2,7 @@ import SideBar, { SideBarModuleState } from './Contents/SideBar'
import Home, { HomeState } from './Contents/Home'
import Notifications, { NotificationsState } from './Contents/Notifications'
import Favourites, { FavouritesState } from './Contents/Favourites'
import Bookmarks, { BookmarksState } from './Contents/Bookmarks'
import Local, { LocalState } from './Contents/Local'
import Public, { PublicState } from './Contents/Public'
import Search, { SearchModuleState } from './Contents/Search'
@ -24,6 +25,7 @@ type ContentsModule = {
Mentions: MentionsState
DirectMessages: DirectMessagesState
Favourites: FavouritesState
Bookmarks: BookmarksState
Local: LocalState
Public: PublicState
Search: SearchModuleState
@ -61,6 +63,7 @@ const Contents: Module<ContentsState, RootState> = {
Home,
Notifications,
Favourites,
Bookmarks,
Local,
DirectMessages,
Mentions,

View File

@ -0,0 +1,128 @@
import generator, { Entity } from 'megalodon'
import parse from 'parse-link-header'
import { Module, MutationTree, ActionTree } from 'vuex'
import { RootState } from '@/store'
import { LocalAccount } from '~/src/types/localAccount'
export type BookmarksState = {
bookmarks: Array<Entity.Status>
lazyLoading: boolean
maxId: string | null
}
const state = (): BookmarksState => ({
bookmarks: [],
lazyLoading: false,
maxId: null
})
export const MUTATION_TYPES = {
UPDATE_BOOKMARKS: 'updateBookmarks',
INSERT_BOOKMARKS: 'insertBookmarks',
UPDATE_TOOT: 'updateToot',
DELETE_TOOT: 'deleteToot',
CHANGE_LAZY_LOADING: 'changeLazyLoading',
CHANGE_MAX_ID: 'changeMaxId'
}
const mutations: MutationTree<BookmarksState> = {
[MUTATION_TYPES.UPDATE_BOOKMARKS]: (state, bookmarks: Array<Entity.Status>) => {
state.bookmarks = bookmarks
},
[MUTATION_TYPES.INSERT_BOOKMARKS]: (state, bookmarks: Array<Entity.Status>) => {
state.bookmarks = state.bookmarks.concat(bookmarks)
},
[MUTATION_TYPES.UPDATE_TOOT]: (state, message: Entity.Status) => {
state.bookmarks = state.bookmarks.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: Entity.Status) => {
state.bookmarks = state.bookmarks.filter(toot => {
if (toot.reblog !== null && toot.reblog.id === message.id) {
return false
} else {
return toot.id !== message.id
}
})
},
[MUTATION_TYPES.CHANGE_LAZY_LOADING]: (state, value: boolean) => {
state.lazyLoading = value
},
[MUTATION_TYPES.CHANGE_MAX_ID]: (state, id: string | null) => {
state.maxId = id
}
}
const actions: ActionTree<BookmarksState, RootState> = {
fetchBookmarks: async ({ commit, rootState }, account: LocalAccount): Promise<Array<Entity.Status>> => {
const client = generator(rootState.TimelineSpace.sns, account.baseURL, account.accessToken, rootState.App.userAgent)
const res = await client.getBookmarks({ limit: 40 })
commit(MUTATION_TYPES.UPDATE_BOOKMARKS, res.data)
// Parse link header
try {
const link = parse(res.headers.link)
if (link !== null) {
commit(MUTATION_TYPES.CHANGE_MAX_ID, link.next.max_id)
} else {
commit(MUTATION_TYPES.CHANGE_MAX_ID, null)
}
} catch (err) {
commit(MUTATION_TYPES.CHANGE_MAX_ID, null)
console.error(err)
}
return res.data
},
laxyFetchBookmarks: async ({ state, commit, rootState }): Promise<Array<Entity.Status> | null> => {
if (state.lazyLoading) {
return Promise.resolve(null)
}
if (!state.maxId) {
return Promise.resolve(null)
}
commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, true)
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
rootState.TimelineSpace.account.accessToken,
rootState.App.userAgent
)
const res = await client.getFavourites({ max_id: state.maxId, limit: 40 }).finally(() => {
commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, false)
})
commit(MUTATION_TYPES.INSERT_BOOKMARKS, res.data)
// Parse link header
try {
const link = parse(res.headers.link)
if (link !== null) {
commit(MUTATION_TYPES.CHANGE_MAX_ID, link.next.max_id)
} else {
commit(MUTATION_TYPES.CHANGE_MAX_ID, null)
}
} catch (err) {
commit(MUTATION_TYPES.CHANGE_MAX_ID, null)
console.error(err)
}
return res.data
}
}
const Bookmark: Module<BookmarksState, RootState> = {
namespaced: true,
state: state,
mutations: mutations,
actions: actions
}
export default Bookmark