refs #1804 Add media column under account timeline

This commit is contained in:
AkiraFukushima 2020-10-27 13:37:39 +09:00
parent d00031d9d3
commit dc6f3664cb
7 changed files with 280 additions and 9 deletions

View File

@ -181,7 +181,7 @@
"emojilib": "^2.4.0",
"i18next": "^19.6.3",
"lodash": "^4.17.20",
"megalodon": "3.3.1",
"megalodon": "3.3.2",
"moment": "^2.28.0",
"mousetrap": "^1.6.5",
"nedb": "^1.8.0",

View File

@ -3,7 +3,7 @@
<el-tabs v-model="activeName" @tab-click="handleClick" stretch>
<el-tab-pane label="Posts" name="posts"><Posts :account="account" /></el-tab-pane>
<el-tab-pane label="Posts and replies" name="posts_and_replies"><PostsAndReplies :account="account" /></el-tab-pane>
<el-tab-pane label="Media" name="media">Media</el-tab-pane>
<el-tab-pane label="Media" name="media"><Media :account="account" /></el-tab-pane>
</el-tabs>
</div>
</template>
@ -11,13 +11,15 @@
<script>
import Posts from './Timeline/Posts'
import PostsAndReplies from './Timeline/PostsAndReplies'
import Media from './Timeline/Media'
export default {
name: 'timeline',
props: ['account'],
components: {
Posts,
PostsAndReplies
PostsAndReplies,
Media
},
data() {
return {

View File

@ -0,0 +1,146 @@
<template>
<div id="timeline">
<DynamicScroller :items="timeline" :min-item-size="60" class="scroller" page-mode>
<template v-slot="{ item, index, active }">
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.uri]" :data-index="index">
<toot
:message="item"
:key="item.id"
:focused="item.uri + item.id === focusedId"
:overlaid="modalOpened"
v-on:update="updateToot"
v-on:delete="deleteToot"
@focusNext="focusNext"
@focusPrev="focusPrev"
@focusLeft="focusTimeline"
@selectToot="focusToot(item)"
>
</toot>
</DynamicScrollerItem>
</template>
</DynamicScroller>
<div class="loading-card" v-loading="lazyLoading" :element-loading-background="backgroundColor"></div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import Toot from '~/src/renderer/components/organisms/Toot'
import { Event } from '~/src/renderer/components/event'
export default {
name: 'media',
props: ['account'],
components: { Toot },
data() {
return {
focusedId: null
}
},
computed: {
...mapState('TimelineSpace/Contents/SideBar/AccountProfile/Timeline/Media', {
timeline: state => state.timeline,
lazyLoading: state => state.lazyLoading
}),
...mapState('App', {
backgroundColor: state => state.theme.background_color
}),
...mapGetters('TimelineSpace/Modals', ['modalOpened'])
},
created() {
this.load()
},
mounted() {
this.$store.dispatch('TimelineSpace/Contents/SideBar/AccountProfile/Timeline/Media/clearTimeline')
document.getElementById('sidebar_scrollable').addEventListener('scroll', this.onScroll)
Event.$on('focus-sidebar', () => {
this.focusedId = 0
this.$nextTick(function () {
this.focusedId = this.timeline[0].uri + this.timeline[0].id
})
})
},
beforeDestroy() {
Event.$emit('focus-timeline')
Event.$off('focus-sidebar')
},
destroyed() {
if (document.getElementById('sidebar_scrollable') !== undefined && document.getElementById('sidebar_scrollable') !== null) {
document.getElementById('sidebar_scrollable').removeEventListener('scroll', this.onScroll)
}
},
watch: {
account: function (_newAccount, _oldAccount) {
this.$store.dispatch('TimelineSpace/Contents/SideBar/AccountProfile/Timeline/Media/clearTimeline')
this.load()
}
},
methods: {
load() {
this.$store.dispatch('TimelineSpace/Contents/SideBar/AccountProfile/Timeline/Media/fetchTimeline', this.account).catch(() => {
this.$message({
message: this.$t('message.timeline_fetch_error'),
type: 'error'
})
})
},
updateToot(message) {
this.$store.commit('TimelineSpace/Contents/SideBar/AccountProfile/Timeline/Media/updateToot', message)
},
deleteToot(message) {
this.$store.commit('TimelineSpace/Contents/SideBar/AccountProfile/Timeline/Media/deleteToot', message)
},
onScroll(event) {
// for lazyLoading
if (
event.target.clientHeight + event.target.scrollTop >= document.getElementById('account_profile').clientHeight - 10 &&
!this.lazyloading
) {
this.$store
.dispatch('TimelineSpace/Contents/SideBar/AccountProfile/Timeline/Media/lazyFetchTimeline', {
account: this.account,
status: this.timeline[this.timeline.length - 1]
})
.catch(err => {
console.error(err)
this.$message({
message: this.$t('message.timeline_fetch_error'),
type: 'error'
})
})
}
},
focusNext() {
const currentIndex = this.timeline.findIndex(toot => this.focusedId === toot.uri + toot.id)
if (currentIndex === -1) {
this.focusedId = this.timeline[0].uri + this.timeline[0].id
} else if (currentIndex < this.timeline.length - 1) {
this.focusedId = this.timeline[currentIndex + 1].uri + this.timeline[currentIndex + 1].id
}
},
focusPrev() {
const currentIndex = this.timeline.findIndex(toot => this.focusedId === toot.uri + toot.id)
if (currentIndex > 0) {
this.focusedId = this.timeline[currentIndex - 1].uri + this.timeline[currentIndex - 1].id
}
},
focusToot(message) {
this.focusedId = message.uri + message.id
},
focusTimeline() {
this.focusedId = 0
Event.$emit('focus-timeline')
}
}
}
</script>
<style lang="scss" scoped>
.loading-card {
height: 60px;
}
.loading-card:empty {
height: 0;
}
</style>

View File

@ -129,6 +129,7 @@ const actions: ActionTree<AccountProfileState, RootState> = {
dispatch('fetchRelationship', state.account),
dispatch('TimelineSpace/Contents/SideBar/AccountProfile/Timeline/Posts/fetchTimeline', state.account, { root: true }),
dispatch('TimelineSpace/Contents/SideBar/AccountProfile/Timeline/PostsAndReplies/fetchTimeline', state.account, { root: true }),
dispatch('TimelineSpace/Contents/SideBar/AccountProfile/Timeline/Media/fetchTimeline', state.account, { root: true }),
dispatch('TimelineSpace/Contents/SideBar/AccountProfile/Followers/fetchFollowers', state.account, { root: true }),
dispatch('TimelineSpace/Contents/SideBar/AccountProfile/Follows/fetchFollows', state.account, { root: true })
]).finally(() => {

View File

@ -3,12 +3,14 @@ import { RootState } from '@/store'
import Posts, { PostsState } from './Timeline/Posts'
import PostsAndReplies, { PostsAndRepliesState } from './Timeline/PostsAndReplies'
import Media, { MediaState } from './Timeline/Media'
export type TimelineState = {}
type TimelineModule = {
Posts: PostsState
PostsAndReplies: PostsAndRepliesState
Media: MediaState
}
export type TimelineModuleState = TimelineModule & TimelineState
@ -19,7 +21,8 @@ const Timeline: Module<TimelineState, RootState> = {
namespaced: true,
modules: {
Posts,
PostsAndReplies
PostsAndReplies,
Media
},
state: state
}

View File

@ -0,0 +1,112 @@
import { RootState } from '@/store'
import generator, { Entity } from 'megalodon'
import { Module, MutationTree, ActionTree } from 'vuex'
import { LoadPositionWithAccount } from '@/types/loadPosition'
export type MediaState = {
timeline: Array<Entity.Status>
lazyLoading: boolean
}
const state = (): MediaState => ({
timeline: [],
lazyLoading: false
})
export const MUTATION_TYPES = {
UPDATE_TIMELINE: 'updateTimeline',
INSERT_TIMELINE: 'insertTimeline',
CHANGE_LAZY_LOADING: 'changeLazyLoading',
UPDATE_TOOT: 'updateToot',
DELETE_TOOT: 'deleteToot'
}
const mutations: MutationTree<MediaState> = {
[MUTATION_TYPES.UPDATE_TIMELINE]: (state, timeline: Array<Entity.Status>) => {
state.timeline = timeline
},
[MUTATION_TYPES.INSERT_TIMELINE]: (state, message: Array<Entity.Status>) => {
state.timeline = state.timeline.concat(message)
},
[MUTATION_TYPES.CHANGE_LAZY_LOADING]: (state, value: boolean) => {
state.lazyLoading = value
},
[MUTATION_TYPES.UPDATE_TOOT]: (state, message: Entity.Status) => {
// Replace target message in timeline
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: Entity.Status) => {
state.timeline = state.timeline.filter(toot => {
if (toot.reblog !== null && toot.reblog.id === message.id) {
return false
} else {
return toot.id !== message.id
}
})
}
}
const actions: ActionTree<MediaState, RootState> = {
fetchTimeline: async ({ commit, rootState }, account: Account) => {
commit('TimelineSpace/Contents/SideBar/AccountProfile/changeLoading', true, { root: true })
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
rootState.TimelineSpace.account.accessToken,
rootState.App.userAgent
)
const res = await client.getAccountStatuses(account.id, { limit: 40, pinned: false, only_media: true })
commit('TimelineSpace/Contents/SideBar/AccountProfile/changeLoading', false, { root: true })
commit(MUTATION_TYPES.UPDATE_TIMELINE, res.data)
return res.data
},
lazyFetchTimeline: async ({ state, commit, rootState }, loadPosition: LoadPositionWithAccount): Promise<null> => {
if (state.lazyLoading) {
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
)
try {
const res = await client.getAccountStatuses(loadPosition.account.id, {
max_id: loadPosition.status.id,
limit: 40,
pinned: false,
only_media: true
})
commit(MUTATION_TYPES.INSERT_TIMELINE, res.data)
} finally {
commit(MUTATION_TYPES.CHANGE_LAZY_LOADING, false)
}
return null
},
clearTimeline: ({ commit }) => {
commit(MUTATION_TYPES.UPDATE_TIMELINE, [])
}
}
const Media: Module<MediaState, RootState> = {
namespaced: true,
state: state,
mutations: mutations,
actions: actions
}
export default Media

View File

@ -2297,6 +2297,13 @@ axios@^0.20.0:
dependencies:
follow-redirects "^1.10.0"
axios@^0.21.0:
version "0.21.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca"
integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==
dependencies:
follow-redirects "^1.10.0"
babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@ -8530,14 +8537,14 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
megalodon@3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/megalodon/-/megalodon-3.3.1.tgz#0e62c006049790269b0d6ab6ea8f6ca919b4a447"
integrity sha512-vdCW6xCc0yp47eZ8SLrTzQKACoIzd3LnYEk62NSyHUzTzpMP6N5s0BuTfBAWnSn8m6Zx1q6865rNp/tBrKTpww==
megalodon@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/megalodon/-/megalodon-3.3.2.tgz#20737f47589feb8209f8129a921c3308be10d7b0"
integrity sha512-lmqkwEL4Yrb459gL2SUM7mFpdzZDxmXMG8xb7RWNeo93OuVY+jukqI8mZI/g4v4WoVHYfSaVlB7Tqm0DwuqXIw==
dependencies:
"@types/oauth" "^0.9.0"
"@types/ws" "^7.2.0"
axios "^0.20.0"
axios "^0.21.0"
https-proxy-agent "^5.0.0"
moment "^2.24.0"
oauth "^0.9.15"