logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe
commit: 8c8eb99d73aacd38781da2e6b151c9b76c1639a2
parent: 3cd1deb133926281920f10c1ddd6eb694c09f44d
Author: Shpuld Shpludson <shp@cock.li>
Date:   Mon,  4 Feb 2019 17:54:48 +0000

Merge branch 'feat/follows-following-pagination' into 'develop'

Follows + followers pagination #285

See merge request pleroma/pleroma-fe!510

Diffstat:

Asrc/components/follow_list/follow_list.js62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/follow_list/follow_list.vue33+++++++++++++++++++++++++++++++++
Msrc/components/user_card/user_card.vue6+++---
Msrc/components/user_profile/user_profile.js65++++++++++++++++++++++-------------------------------------------
Msrc/components/user_profile/user_profile.vue18++----------------
Msrc/i18n/en.json10++++++----
Msrc/i18n/fi.json4+++-
Msrc/modules/users.js65++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/services/api/api.service.js10++++++++--
Msrc/services/backend_interactor_service/backend_interactor_service.js8++++----
Msrc/services/entity_normalizer/entity_normalizer.service.js2++
Msrc/services/user_profile_link_generator/user_profile_link_generator.js2+-
12 files changed, 200 insertions(+), 85 deletions(-)

diff --git a/src/components/follow_list/follow_list.js b/src/components/follow_list/follow_list.js @@ -0,0 +1,62 @@ +import UserCard from '../user_card/user_card.vue' + +const FollowList = { + data () { + return { + loading: false, + bottomedOut: false, + error: false + } + }, + props: ['userId', 'showFollowers'], + created () { + window.addEventListener('scroll', this.scrollLoad) + if (this.entries.length === 0) { + this.fetchEntries() + } + }, + destroyed () { + window.removeEventListener('scroll', this.scrollLoad) + this.$store.dispatch('clearFriendsAndFollowers', this.userId) + }, + computed: { + user () { + return this.$store.getters.userById(this.userId) + }, + entries () { + return this.showFollowers ? this.user.followers : this.user.friends + } + }, + methods: { + fetchEntries () { + if (!this.loading) { + const command = this.showFollowers ? 'addFollowers' : 'addFriends' + this.loading = true + this.$store.dispatch(command, this.userId).then(entries => { + this.error = false + this.loading = false + this.bottomedOut = entries.length === 0 + }).catch(() => { + this.error = true + this.loading = false + }) + } + }, + scrollLoad (e) { + const bodyBRect = document.body.getBoundingClientRect() + const height = Math.max(bodyBRect.height, -(bodyBRect.y)) + if (this.loading === false && + this.bottomedOut === false && + this.$el.offsetHeight > 0 && + (window.innerHeight + window.pageYOffset) >= (height - 750) + ) { + this.fetchEntries() + } + } + }, + components: { + UserCard + } +} + +export default FollowList diff --git a/src/components/follow_list/follow_list.vue b/src/components/follow_list/follow_list.vue @@ -0,0 +1,33 @@ +<template> + <div class="follow-list"> + <user-card + v-for="entry in entries" + :key="entry.id" :user="entry" + :showFollows="true" + /> + <div class="text-center panel-footer"> + <a v-if="error" @click="fetchEntries" class="alert error"> + {{$t('general.generic_error')}} + </a> + <i v-else-if="loading" class="icon-spin3 animate-spin"/> + <span v-else-if="bottomedOut"></span> + <a v-else @click="fetchEntries">{{$t('general.more')}}</a> + </div> + </div> +</template> + +<script src="./follow_list.js"></script> + +<style lang="scss"> + +.follow-list { + .panel-footer { + padding: 10px; + } + + .error { + font-size: 14px; + } +} + +</style> diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -1,7 +1,7 @@ <template> <div class="card"> <a href="#"> - <UserAvatar :compact="true" @click.prevent="toggleUserExpanded" :src="user.profile_image_url"/> + <UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/> </a> <div class="usercard" v-if="userExpanded"> <user-card-content :user="user" :switcher="false"></user-card-content> @@ -69,13 +69,13 @@ border-bottom-color: var(--border, $fallback--border); .avatar { - margin-top: 0.2em; + padding: 0; } } .usercard { width: fill-available; - margin: 0.2em 0 0.7em 0; + margin: 0.2em 0 0 0.7em; border-radius: $fallback--panelRadius; border-radius: var(--panelRadius, $fallback--panelRadius); border-style: solid; diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js @@ -1,6 +1,7 @@ import UserCardContent from '../user_card_content/user_card_content.vue' import UserCard from '../user_card/user_card.vue' import Timeline from '../timeline/timeline.vue' +import FollowList from '../follow_list/follow_list.vue' const UserProfile = { created () { @@ -15,9 +16,7 @@ const UserProfile = { } }, destroyed () { - this.$store.dispatch('stopFetching', 'user') - this.$store.dispatch('stopFetching', 'favorites') - this.$store.dispatch('stopFetching', 'media') + this.cleanUp(this.userId) }, computed: { timeline () { @@ -39,12 +38,6 @@ const UserProfile = { return this.userId && this.$store.state.users.currentUser.id && this.userId === this.$store.state.users.currentUser.id }, - friends () { - return this.user.friends - }, - followers () { - return this.user.followers - }, userInStore () { if (this.isExternal) { return this.$store.getters.userById(this.userId) @@ -68,61 +61,47 @@ const UserProfile = { } }, methods: { - fetchFollowers () { - const id = this.userId - this.$store.dispatch('addFollowers', { id }) - }, - fetchFriends () { - const id = this.userId - this.$store.dispatch('addFriends', { id }) - }, startFetchFavorites () { if (this.isUs) { this.$store.dispatch('startFetching', ['favorites', this.fetchBy]) } - } - }, - watch: { - // TODO get rid of this copypasta - userName () { - if (this.isExternal) { - return - } - this.$store.dispatch('stopFetching', 'user') - this.$store.dispatch('stopFetching', 'favorites') - this.$store.dispatch('stopFetching', 'media') - this.$store.commit('clearTimeline', { timeline: 'user' }) - this.$store.commit('clearTimeline', { timeline: 'favorites' }) - this.$store.commit('clearTimeline', { timeline: 'media' }) + }, + startUp () { this.$store.dispatch('startFetching', ['user', this.fetchBy]) this.$store.dispatch('startFetching', ['media', this.fetchBy]) + this.startFetchFavorites() }, - userId () { - if (!this.isExternal) { - return - } + cleanUp () { this.$store.dispatch('stopFetching', 'user') this.$store.dispatch('stopFetching', 'favorites') this.$store.dispatch('stopFetching', 'media') this.$store.commit('clearTimeline', { timeline: 'user' }) this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.commit('clearTimeline', { timeline: 'media' }) - this.$store.dispatch('startFetching', ['user', this.fetchBy]) - this.$store.dispatch('startFetching', ['media', this.fetchBy]) - this.startFetchFavorites() + } + }, + watch: { + userName () { + if (this.isExternal) { + return + } + this.cleanUp() + this.startUp() }, - user () { - if (this.user.id && !this.user.followers) { - this.fetchFollowers() - this.fetchFriends() + userId () { + if (!this.isExternal) { + return } + this.cleanUp() + this.startUp() } }, components: { UserCardContent, UserCard, - Timeline + Timeline, + FollowList } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue @@ -16,27 +16,13 @@ :user-id="fetchBy" /> <div :label="$t('user_card.followees')"> - <div v-if="friends"> - <user-card - v-for="friend in friends" - :key="friend.id" - :user="friend" - :showFollows="true" - /> - </div> + <FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" /> <div class="userlist-placeholder" v-else> <i class="icon-spin3 animate-spin"></i> </div> </div> <div :label="$t('user_card.followers')"> - <div v-if="followers"> - <user-card - v-for="follower in followers" - :key="follower.id" - :user="follower" - :showFollows="false" - /> - </div> + <FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" /> <div class="userlist-placeholder" v-else> <i class="icon-spin3 animate-spin"></i> </div> diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -17,7 +17,9 @@ }, "general": { "apply": "Apply", - "submit": "Submit" + "submit": "Submit", + "more": "More", + "generic_error": "An error occured" }, "login": { "login": "Log in", @@ -364,9 +366,9 @@ }, "upload":{ "error": { - "base": "Upload failed.", - "file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Try again later" + "base": "Upload failed.", + "file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Try again later" }, "file_size_units": { "B": "B", diff --git a/src/i18n/fi.json b/src/i18n/fi.json @@ -17,7 +17,9 @@ }, "general": { "apply": "Aseta", - "submit": "Lähetä" + "submit": "Lähetä", + "more": "Lisää", + "generic_error": "Virhe tapahtui" }, "login": { "login": "Kirjaudu sisään", diff --git a/src/modules/users.js b/src/modules/users.js @@ -1,5 +1,5 @@ import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' -import { compact, map, each, merge } from 'lodash' +import { compact, map, each, merge, find } from 'lodash' import { set } from 'vue' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' import oauthApi from '../services/new_api/oauth' @@ -52,13 +52,35 @@ export const mutations = { state.loggingIn = false }, // TODO Clean after ourselves? - addFriends (state, { id, friends }) { + addFriends (state, { id, friends, page }) { const user = state.usersObject[id] - user.friends = friends + each(friends, friend => { + if (!find(user.friends, { id: friend.id })) { + user.friends.push(friend) + } + }) + user.friendsPage = page + 1 }, - addFollowers (state, { id, followers }) { + addFollowers (state, { id, followers, page }) { const user = state.usersObject[id] - user.followers = followers + each(followers, follower => { + if (!find(user.followers, { id: follower.id })) { + user.followers.push(follower) + } + }) + user.followersPage = page + 1 + }, + // Because frontend doesn't have a reason to keep these stuff in memory + // outside of viewing someones user profile. + clearFriendsAndFollowers (state, userKey) { + const user = state.usersObject[userKey] + if (!user) { + return + } + user.friends = [] + user.followers = [] + user.friendsPage = 0 + user.followersPage = 0 }, addNewUsers (state, users) { each(users, (user) => mergeOrAdd(state.users, state.usersObject, user)) @@ -115,13 +137,34 @@ const users = { store.rootState.api.backendInteractor.fetchUser({ id }) .then((user) => store.commit('addNewUsers', [user])) }, - addFriends ({ rootState, commit }, { id }) { - rootState.api.backendInteractor.fetchFriends({ id }) - .then((friends) => commit('addFriends', { id, friends })) + addFriends ({ rootState, commit }, fetchBy) { + return new Promise((resolve, reject) => { + const user = rootState.users.usersObject[fetchBy] + const page = user.friendsPage || 1 + rootState.api.backendInteractor.fetchFriends({ id: user.id, page }) + .then((friends) => { + commit('addFriends', { id: user.id, friends, page }) + resolve(friends) + }).catch(() => { + reject() + }) + }) + }, + addFollowers ({ rootState, commit }, fetchBy) { + return new Promise((resolve, reject) => { + const user = rootState.users.usersObject[fetchBy] + const page = user.followersPage || 1 + rootState.api.backendInteractor.fetchFollowers({ id: user.id, page }) + .then((followers) => { + commit('addFollowers', { id: user.id, followers, page }) + resolve(followers) + }).catch(() => { + reject() + }) + }) }, - addFollowers ({ rootState, commit }, { id }) { - rootState.api.backendInteractor.fetchFollowers({ id }) - .then((followers) => commit('addFollowers', { id, followers })) + clearFriendsAndFollowers ({ commit }, userKey) { + commit('clearFriendsAndFollowers', userKey) }, registerPushNotifications (store) { const token = store.state.currentUser.credentials diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js @@ -247,15 +247,21 @@ const fetchUser = ({id, credentials}) => { .then((data) => parseUser(data)) } -const fetchFriends = ({id, credentials}) => { +const fetchFriends = ({id, page, credentials}) => { let url = `${FRIENDS_URL}?user_id=${id}` + if (page) { + url = url + `&page=${page}` + } return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) .then((data) => data.map(parseUser)) } -const fetchFollowers = ({id, credentials}) => { +const fetchFollowers = ({id, page, credentials}) => { let url = `${FOLLOWERS_URL}?user_id=${id}` + if (page) { + url = url + `&page=${page}` + } return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) .then((data) => data.map(parseUser)) diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js @@ -10,12 +10,12 @@ const backendInteractorService = (credentials) => { return apiService.fetchConversation({id, credentials}) } - const fetchFriends = ({id}) => { - return apiService.fetchFriends({id, credentials}) + const fetchFriends = ({id, page}) => { + return apiService.fetchFriends({id, page, credentials}) } - const fetchFollowers = ({id}) => { - return apiService.fetchFollowers({id, credentials}) + const fetchFollowers = ({id, page}) => { + return apiService.fetchFollowers({id, page, credentials}) } const fetchAllFollowing = ({username}) => { diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js @@ -112,6 +112,8 @@ export const parseUser = (data) => { output.locked = data.locked output.followers_count = data.followers_count output.statuses_count = data.statuses_count + output.friends = [] + output.followers = [] return output } diff --git a/src/services/user_profile_link_generator/user_profile_link_generator.js b/src/services/user_profile_link_generator/user_profile_link_generator.js @@ -8,6 +8,6 @@ const generateProfileLink = (id, screenName, restrictedNicknames) => { } } -const isExternal = screenName => screenName.includes('@') +const isExternal = screenName => screenName && screenName.includes('@') export default generateProfileLink