From dbb16d56e29b8a32fb7c1a7af56a7953f571cdb4 Mon Sep 17 00:00:00 2001 From: shpuld <shp@cock.li> Date: Sat, 2 Feb 2019 22:29:10 +0200 Subject: [PATCH] follows/followers pagination ready for review --- .../followers_list/followers_list.js | 61 +++++++++++++++ .../followers_list/followers_list.vue | 12 +++ src/components/friends_list/friends_list.js | 61 +++++++++++++++ src/components/friends_list/friends_list.vue | 12 +++ src/components/user_card/user_card.vue | 4 +- src/components/user_profile/user_profile.js | 78 ++++++------------- src/components/user_profile/user_profile.vue | 9 +-- src/modules/users.js | 66 +++++++++++++--- .../user_profile_link_generator.js | 2 +- 9 files changed, 229 insertions(+), 76 deletions(-) create mode 100644 src/components/followers_list/followers_list.js create mode 100644 src/components/followers_list/followers_list.vue create mode 100644 src/components/friends_list/friends_list.js create mode 100644 src/components/friends_list/friends_list.vue diff --git a/src/components/followers_list/followers_list.js b/src/components/followers_list/followers_list.js new file mode 100644 index 00000000..13aace18 --- /dev/null +++ b/src/components/followers_list/followers_list.js @@ -0,0 +1,61 @@ +import UserCard from '../user_card/user_card.vue' + +const FollowersList = { + data () { + return { + loading: false, + bottomedOut: false, + error: false + } + }, + props: ['userId'], + created () { + window.addEventListener('scroll', this.scrollLoad) + if (this.user.followers.length === 0) { + this.fetchFollowers() + } + }, + destroyed () { + window.removeEventListener('scroll', this.scrollLoad) + this.$store.dispatch('clearFriendsAndFollowers', this.userId) + }, + computed: { + user () { + return this.$store.getters.userById(this.userId) + }, + followers () { + return this.user.followers + } + }, + methods: { + fetchFollowers () { + if (!this.loading) { + this.loading = true + this.$store.dispatch('addFollowers', this.userId).then(followers => { + this.error = false + this.loading = false + this.bottomedOut = followers.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.fetchFollowers() + } + } + }, + components: { + UserCard + } +} + +export default FollowersList diff --git a/src/components/followers_list/followers_list.vue b/src/components/followers_list/followers_list.vue new file mode 100644 index 00000000..b6bd35e1 --- /dev/null +++ b/src/components/followers_list/followers_list.vue @@ -0,0 +1,12 @@ +<template> + <div> + <user-card v-for="follower in followers" :key="follower.id" :user="follower" :showFollows="true"></user-card> + <div @click="fetchFollowers" class="new-status-notification text-center panel-footer"> + <span v-if="error" class="alert error">Error loading followers</span> + <i v-else-if="loading" class="icon-spin3 animate-spin"/> + <span v-else-if="bottomedOut"></span> + </div> + </div> +</template> + +<script src="./followers_list.js" /> diff --git a/src/components/friends_list/friends_list.js b/src/components/friends_list/friends_list.js new file mode 100644 index 00000000..d5c1837a --- /dev/null +++ b/src/components/friends_list/friends_list.js @@ -0,0 +1,61 @@ +import UserCard from '../user_card/user_card.vue' + +const FriendsList = { + data () { + return { + loading: false, + bottomedOut: false, + error: false + } + }, + props: ['userId'], + created () { + window.addEventListener('scroll', this.scrollLoad) + if (this.user.followers.length === 0) { + this.fetchFriends() + } + }, + destroyed () { + window.removeEventListener('scroll', this.scrollLoad) + this.$store.dispatch('clearFriendsAndFollowers', this.userId) + }, + computed: { + user () { + return this.$store.getters.userById(this.userId) + }, + friends () { + return this.user.friends + } + }, + methods: { + fetchFriends () { + if (!this.loading) { + this.loading = true + this.$store.dispatch('addFriends', this.userId).then(friends => { + this.error = false + this.loading = false + this.bottomedOut = friends.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.fetchFriends() + } + } + }, + components: { + UserCard + } +} + +export default FriendsList diff --git a/src/components/friends_list/friends_list.vue b/src/components/friends_list/friends_list.vue new file mode 100644 index 00000000..75657cc8 --- /dev/null +++ b/src/components/friends_list/friends_list.vue @@ -0,0 +1,12 @@ +<template> + <div> + <user-card v-for="friend in friends" :key="friend.id" :user="friend" :showFollows="true"></user-card> + <div @click="fetchFriends" class="new-status-notification text-center panel-footer"> + <span v-if="error" class="alert error">Error loading follows</span> + <i v-else-if="loading" class="icon-spin3 animate-spin"/> + <span v-else-if="bottomedOut"></span> + </div> + </div> +</template> + +<script src="./friends_list.js" /> diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index cf69606d..2a590889 100644 --- 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="#"> - <StillImage @click.prevent="toggleUserExpanded" class="avatar" :src="user.profile_image_url"/> + <StillImage @click.prevent.native="toggleUserExpanded" class="avatar" :src="user.profile_image_url"/> </a> <div class="usercard" v-if="userExpanded"> <user-card-content :user="user" :switcher="false"></user-card-content> @@ -79,7 +79,7 @@ .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 index 4f09c9e7..0361d253 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -1,6 +1,8 @@ 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 FriendsList from '../friends_list/friends_list.vue' +import FollowersList from '../followers_list/followers_list.vue' const UserProfile = { created () { @@ -15,15 +17,7 @@ const UserProfile = { } }, destroyed () { - this.$store.dispatch('stopFetching', 'user') - this.$store.dispatch('stopFetching', 'favorites') - this.$store.dispatch('stopFetching', 'media') - }, - data () { - return { - followsPage: 0, - followersPage: 0 - } + this.cleanUp(this.userId) }, computed: { timeline () { @@ -45,12 +39,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) @@ -74,66 +62,48 @@ 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]) } }, - nextFollowsPage () { - this.followsPage += 1 - this.$store.dispatch('addFriends', { id: this.userId, page: this.followsPage }) - console.log(this.user.friends) - } - }, - watch: { - // TODO get rid of this copypasta - userName () { - if (this.isExternal) { - return - } + startUp () { + this.$store.dispatch('startFetching', ['user', this.fetchBy]) + this.$store.dispatch('startFetching', ['media', this.fetchBy]) + + this.startFetchFavorites() + }, + 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() }, userId () { 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' }) - this.$store.dispatch('startFetching', ['user', this.fetchBy]) - this.$store.dispatch('startFetching', ['media', this.fetchBy]) - this.startFetchFavorites() - }, - user () { - if (this.user.id && !this.user.followers) { - this.fetchFollowers() - this.fetchFriends() - } + this.cleanUp() + this.startUp() } }, components: { UserCardContent, UserCard, - Timeline + Timeline, + FriendsList, + FollowersList } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index baf6aef6..4ba09dc3 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -5,18 +5,13 @@ <tab-switcher> <Timeline :label="$t('user_card.statuses')" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :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"></user-card> - <div @click="nextFollowsPage" class="panel-footer">MORE</div> - </div> + <FriendsList v-if="user.friends_count > 0" :userId="userId" /> <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"></user-card> - </div> + <FollowersList v-if="user.followers_count > 0" :userId="userId" /> <div class="userlist-placeholder" v-else> <i class="icon-spin3 animate-spin"></i> </div> diff --git a/src/modules/users.js b/src/modules/users.js index ca8e5606..ca2e0f31 100644 --- 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, concat } 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,14 +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] - console.log(user.friends) - user.friends = concat(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)) @@ -116,13 +137,34 @@ const users = { store.rootState.api.backendInteractor.fetchUser({ id }) .then((user) => store.commit('addNewUsers', [user])) }, - addFriends ({ rootState, commit }, { id, page }) { - rootState.api.backendInteractor.fetchFriends({ id, page }) - .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 }, { id, page }) { - rootState.api.backendInteractor.fetchFollowers({ id, page }) - .then((followers) => commit('addFollowers', { id, followers })) + 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() + }) + }) + }, + clearFriendsAndFollowers ({ commit }, userKey) { + commit('clearFriendsAndFollowers', userKey) }, registerPushNotifications (store) { const token = store.state.currentUser.credentials 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 index bca2c9cd..a214ca48 100644 --- 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