From f49445a93e8ff9ea455a0c0a8fa602dd146e65ef Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Mon, 14 Aug 2017 16:10:25 -0500 Subject: [PATCH 01/13] initial code to fetch a followers / followings list on the user page --- js/collections/Followers.js | 41 ++-- js/languages/en-US.json | 13 +- js/start.js | 41 +--- js/templates/userPage/follow.html | 1 + js/templates/userPage/followLoading.html | 13 ++ js/views/userPage/Follow.js | 234 ++++++++++++++--------- js/views/userPage/FollowLoading.js | 48 +++++ js/views/userPage/UserPage.js | 20 +- styles/modules/_userPage.scss | 34 +++- 9 files changed, 275 insertions(+), 170 deletions(-) create mode 100644 js/templates/userPage/follow.html create mode 100644 js/templates/userPage/followLoading.html create mode 100644 js/views/userPage/FollowLoading.js diff --git a/js/collections/Followers.js b/js/collections/Followers.js index b99d36074..d3ea58db7 100644 --- a/js/collections/Followers.js +++ b/js/collections/Followers.js @@ -1,33 +1,24 @@ -import { Collection } from 'backbone'; -import UserShort from '../models/UserCard'; import app from '../app'; +import { Collection } from 'backbone'; +export default class extends Collection { + constructor(models = [], options = {}) { + super(models, options); -module.exports = Collection.extend({ - /* we have to use the older style for this collection, the ES6 style creates a bug where models - cannot be removed using their ids */ + const types = ['followers', 'following']; + if (types.indexOf(options.type) === -1) { + throw new Error(`Please provide a type as one of ${types.join(', ')}`); + } - initialize(models, options) { - if (!options.type) { - throw new Error('You must provide a type to the collection'); + if (!options.peerId) { + throw new Error('Please provide a peerId'); } - this.guid = options.guid; - this.type = options.type; - }, + this.options = options; + } url() { - return app.getServerUrl(this.guid === app.profile.id || !this.guid ? - `ob/${this.type}` : `ipns/${this.guid}/${this.type}`); - }, - - model: UserShort, - - parse(response) { - return response.map((guid) => { - // if a plain guid was passed in, convert it to an object - if (typeof guid === 'string') return { guid }; - return guid; - }); - }, -}); + return app.getServerUrl(`ob/${this.options.type === 'followers' ? 'followers' : 'following'}` + + `${app.profile.id === this.options.peerId ? '' : `/${this.options.peerId}`}`); + } +} diff --git a/js/languages/en-US.json b/js/languages/en-US.json index a182654a0..36edbbdc6 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -150,10 +150,15 @@ "website": "Website", "email": "Email", "noLocation": "No Location", - "noFollowers": "No One is Following %{name} Yet", - "noFollowing": "%{name} is Not Following Anyone Yet", - "noOwnFollowers": "You Don't Have Any Followers Yet", - "noOwnFollowing": "You Aren't Following Anyone Yet", + "followTab": { + "noFollowers": "No One is Following %{name} Yet", + "noFollowing": "%{name} is Not Following Anyone Yet", + "noOwnFollowers": "You Don't Have Any Followers Yet", + "noOwnFollowing": "You Aren't Following Anyone Yet", + "followersFetchError": "Unable to fetch the followers list.", + "followingFetchError": "Unable to fetch the following list.", + "btnRetry": "Retry" + }, "getFollowingError": "There was an error when determining if this user follows you.", "modAddError": "There was an error adding this moderator. \n %{errMsg}", "modRemoveError": "There was an error removing this moderator. \n %{errMsg}", diff --git a/js/start.js b/js/start.js index ea78e1cf7..4c49c6ba6 100644 --- a/js/start.js +++ b/js/start.js @@ -28,7 +28,6 @@ import { getLangByCode } from './data/languages'; import Profile from './models/profile/Profile'; import Settings from './models/Settings'; import WalletBalance from './models/wallet/WalletBalance'; -import Followers from './collections/Followers'; import { fetchExchangeRates } from './utils/currency'; import './utils/exchangeRateSyncer'; import './utils/listingData'; @@ -258,29 +257,23 @@ function onboard() { } const fetchStartupDataDeferred = $.Deferred(); -let ownFollowingFetch; -let ownFollowingFailed; let exchangeRatesFetch; let walletBalanceFetch; let walletBalanceFetchFailed; function fetchStartupData() { - ownFollowingFetch = !ownFollowingFetch || ownFollowingFetch ? - app.ownFollowing.fetch() : ownFollowingFetch; exchangeRatesFetch = exchangeRatesFetch || fetchExchangeRates(); walletBalanceFetch = !walletBalanceFetch || walletBalanceFetch ? app.walletBalance.fetch() : walletBalanceFetch; - $.whenAll(ownFollowingFetch, exchangeRatesFetch, walletBalanceFetch) + $.whenAll(exchangeRatesFetch, walletBalanceFetch) .progress((...args) => { const state = args[1]; if (state !== 'success') { const jqXhr = args[0]; - if (jqXhr === ownFollowingFetch) { - ownFollowingFailed = true; - } else if (jqXhr === walletBalanceFetch) { + if (jqXhr === walletBalanceFetch) { walletBalanceFetchFailed = true; } } @@ -289,17 +282,9 @@ function fetchStartupData() { fetchStartupDataDeferred.resolve(); }) .fail((jqXhr) => { - if (ownFollowingFailed || walletBalanceFetchFailed) { - let title = ''; - - if (ownFollowingFailed) { - title = app.polyglot.t('startUp.dialogs.unableToGetFollowData.title'); - } else { - title = app.polyglot.t('startUp.dialogs.unableToGetWalletBalance.title'); - } - + if (walletBalanceFetchFailed) { const retryFetchStarupDataDialog = new Dialog({ - title, + title: app.polyglot.t('startUp.dialogs.unableToGetWalletBalance.title'), message: jqXhr.responseJSON && jqXhr.responseJSON.reason || '', buttons: [ { @@ -382,9 +367,6 @@ function start() { app.localSettings.save('language', getValidLanguage(lang)); }); - app.ownFollowing = new Followers(null, { type: 'following' }); - app.ownFollowers = new Followers(null, { type: 'followers' }); - app.walletBalance = new WalletBalance(); onboardIfNeeded().done(() => { @@ -744,20 +726,5 @@ ipcRenderer.on('close-attempt', (e) => { } }); -// update ownFollowers based on follow socket communication -serverConnectEvents.on('connected', (connectedEvent) => { - connectedEvent.socket.on('message', (e) => { - if (e.jsonData) { - if (e.jsonData.notification) { - if (e.jsonData.notification.type === 'follow') { - app.ownFollowers.unshift({ guid: e.jsonData.notification.peerId }); - } else if (e.jsonData.notification.type === 'unfollow') { - app.ownFollowers.remove(e.jsonData.notification.peerId); // remove by id - } - } - } - }); -}); - // initialize our listing delete handler listingDeleteHandler(); diff --git a/js/templates/userPage/follow.html b/js/templates/userPage/follow.html new file mode 100644 index 000000000..ea7cf89c7 --- /dev/null +++ b/js/templates/userPage/follow.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/js/templates/userPage/followLoading.html b/js/templates/userPage/followLoading.html new file mode 100644 index 000000000..61d27ff57 --- /dev/null +++ b/js/templates/userPage/followLoading.html @@ -0,0 +1,13 @@ +<% if (ob.isFetching) { %> +
+ <% print(ob.spinner({ className: 'spinnerMd' })) %> +
+<% } else if (ob.fetchFailed) { %> +

<%= ob.fetchErrorTitle %>

+ <% if (ob.fetchErrorMsg) { %> +

<%= ob.fetchErrorMsg %>

+ <% } %> + +<% } else if (ob.noResults) { %> +

<%= ob.noResultsMsg %>

+<% } %> \ No newline at end of file diff --git a/js/views/userPage/Follow.js b/js/views/userPage/Follow.js index 0463722b4..f56266cb8 100644 --- a/js/views/userPage/Follow.js +++ b/js/views/userPage/Follow.js @@ -1,120 +1,168 @@ -import BaseVw from '../baseVw'; -import userShort from '../UserCard'; import app from '../../app'; -import Follows from '../../collections/Followers'; -import { followedByYou, followsYou } from '../../utils/follow'; -import { openSimpleMessage } from '../modals/SimpleMessage'; +import loadTemplate from '../../utils/loadTemplate'; +import BaseVw from '../baseVw'; +import FollowLoading from './FollowLoading'; +// import userShort from '../UserCard'; +// import { followedByYou, followsYou } from '../../utils/follow'; +// import { openSimpleMessage } from '../modals/SimpleMessage'; export default class extends BaseVw { constructor(options = {}) { - super(options); - this.options = options; - this.followType = options.followType; - this.userCache = []; + const opts = { + followType: 'follow', + ...options, + }; - if (!options.ownPage) { - this.followCol = new Follows(null, { - type: this.followType.toLowerCase(), - guid: this.model.id, - }); - this.followCol.fetch().done(() => { - this[`updateViewer${this.followType}`](true); - }); + super(opts); - this.listenTo(app.ownFollowing, 'update', () => { - this.updateViewerFollowers(); - }); + if (!opts.peerId) { + throw new Error('Please provide a peerId of the user who this list is for.'); + } - this.listenTo(app.ownFollowers, 'update', () => { - this.updateViewerFollowing(); - }); - } else { - this.followCol = app[`own${this.followType}`]; - if (this.followType === 'Followers') { - this.followCol.fetch(); // TODO: pagination - } + const types = ['followers', 'following']; + if (types.indexOf(opts.followType) === -1) { + throw new Error(`followType must be one of ${types.join(', ')}`); + } - this.listenTo(app[`own${this.followType}`], 'update', () => { - this.render(); - }); + if (!options.collection) { + throw new Error('Please provide a followers collection.'); } + + this.options = options; + + this.listenTo(this.collection, 'update', this.onCollectionUpdate); + + this.fetch(); } - updateViewerFollowers() { - /* if the viewer follows/unfollows this user, add them to the followers list without a fetch */ - if (this.followType === 'Followers' && !this.options.ownPage) { - // if the viewer has followed add them - if (followedByYou(this.model.id)) { - this.followCol.unshift({ guid: app.profile.id }); - // if the viewer has unfollowed remove them - } else if (!followedByYou(this.model.id)) { - this.followCol.remove(app.profile.id); // remove by id - } - } - this.render(); + className() { + return 'userPageFollow flexRow noResults'; } - updateViewerFollowing() { - /* if the viewer is unfollowed or followed, update the following list */ - if (this.followType === 'Following' && !this.options.ownPage) { - if (this.followsYou) this.followsYou.abort(); - - this.followsYou = followsYou(this.model.id) - .done(data => { - if (data.followsMe) { - // if this page has followed the viewer add them - this.followCol.unshift({ guid: app.profile.id }); - } else { - this.followCol.remove(app.profile.id); - } - }) - .fail(jqXhr => { - // this should normally never result in an error - if (jqXhr.statusText === 'abort') return; - - const failReason = jqXhr.responseJSON && jqXhr.responseJSON.reason || ''; - openSimpleMessage( - app.polyglot.t('userPage.getFollowingError'), - failReason - ); - }) - .always(() => { - this.render(); - }); - } + onCollectionUpdate(cl, opts) { + this.$el.toggleClass('noResults', !cl.length); + + // if (updateOpts.changes.added.length) { + // // Expecting either a single new notifcation on the bottom (will + // // be rendered on top) or a page of notifications on top (will be + // // rendered on the bottom). + // if (updateOpts.changes.added[updateOpts.changes.added.length - 1] === + // this.collection.at(0)) { + // // It's a page of notifcations at the bottom + // this.renderNotifications(updateOpts.changes.added, 'append'); + // } else { + // // New notification at top + // this.renderNotifications(updateOpts.changes.added, 'prepend'); + // } + + // updateOpts.changes.added.forEach(notif => { + // const innerNotif = notif.get('notification'); + // const types = ['follow', 'moderatorAdd', 'moderatorRemove']; + + // if (types.indexOf(innerNotif.type) > -1) { + // getCachedProfiles([innerNotif.peerId])[0] + // .done(profile => { + // notif.set('notification', { + // ...innerNotif, + // handle: profile.get('handle') || '', + // avatarHashes: profile.get('avatarHashes') && + // profile.get('avatarHashes').toJSON() || {}, + // }); + // }); + // } + // }); + + // this.listFetcher.setState({ noResults: false }); + // } } - className() { - return 'userPageFollow flexRow'; + get ownPage() { + return this.options.peerId === app.profile.id; + } + + fetch() { + if (this.fetchCall && this.fetchCall.state() === 'pending') { + return this.fetchCall; + } + + if (this.followLoading) { + this.followLoading.setState({ + isFetching: true, + fetchFailed: false, + fetchErrorMsg: '', + }); + } + + this.fetchCall = this.collection.fetch() + .done((list, txtStatus, xhr) => { + if (xhr.statusText === 'abort') return; + + const state = { + isFetching: false, + fetchFailed: false, + noResults: false, + fetchErrorMsg: '', + }; + + if (!list.length) { + state.noResults = true; + } + + if (this.followLoading) this.followLoading.setState(state); + }).fail(xhr => { + if (this.followLoading) { + this.followLoading.setState({ + isFetching: false, + fetchFailed: true, + fetchErrorMsg: xhr.responseJSON && xhr.responseJSON.reason || '', + }); + } + }); + + return this.fetchCall; } remove() { - if (this.followsYou) this.followsYou.abort(); - super.remove(); + if (this.fetchCall) this.fetchCall.abort(); } render() { - this.$el.empty(); - this.userCache.forEach((user) => { - user.remove(); + super.render(); + + loadTemplate('userPage/follow.html', (t) => { + this.$el.html(t({})); }); - this.userCache = []; - // TODO: add pagination if the collection is Followers - if (this.followCol.length) { - this.followCol.forEach((follow) => { - const user = this.createChild(userShort, { - model: follow, - }); - this.userCache.push(user); - this.$el.append(user.render().$el); - }); + + if (this.followLoading) this.followLoading.remove(); + let noResultsMsg; + let fetchErrorTitle; + + if (this.options.type === 'follow') { + fetchErrorTitle = app.polyglot.t('userPage.followTab.followersFetchError'); + noResultsMsg = this.ownPage ? + app.polyglot.t('userPage.followTab.noOwnFollowers') : + app.polyglot.t('userPage.followTab.noOwnFollowing'); } else { - const noneString = app.polyglot.t( - `userPage.no${this.options.ownPage ? 'Own' : ''}${this.followType}`, - { name: this.model.get('name') }); - const noneStringPartial1 = '
'; - this.$el.append(`${noneStringPartial1}

${noneString}

`); + fetchErrorTitle = app.polyglot.t('userPage.followTab.followingFetchError'); + noResultsMsg = this.ownPage ? + app.polyglot.t('userPage.followTab.noFollowers') : + app.polyglot.t('userPage.followTab.noFollowing'); } + + this.followLoading = this.createChild(FollowLoading, { + initialState: { + isFetching: this.fetchCall && this.fetchCall.state() === 'pending', + fetchFailed: this.fetchCall && this.fetchCall.state() === 'rejected', + fetchErrorTitle, + fetchErrorMsg: this.fetchCall && this.fetchCall.responseJSON && + this.fetchCall.responseJSON.reason || '', + noResultsMsg, + }, + }); + + this.listenTo(this.followLoading, 'retry-click', () => this.fetch()); + this.getCachedEl('.js-followLoadingContainer').html(this.followLoading.render().el); + return this; } } diff --git a/js/views/userPage/FollowLoading.js b/js/views/userPage/FollowLoading.js new file mode 100644 index 000000000..31ebcb953 --- /dev/null +++ b/js/views/userPage/FollowLoading.js @@ -0,0 +1,48 @@ +import loadTemplate from '../../utils/loadTemplate'; +import BaseVw from '../baseVw'; + +export default class extends BaseVw { + constructor(options = {}) { + const opts = { + initialState: { + isFetching: false, + noResults: false, + noResultsMsg: '', + fetchFailed: false, + fetchErrorTitle: '', + fetchErrorMsg: '', + ...options.initialState || {}, + }, + ...options, + }; + + super(opts); + this.options = opts; + } + + className() { + return 'followLoadingState txCtr tx5'; + } + + events() { + return { + 'click .js-retry': 'onClickRetry', + }; + } + + onClickRetry() { + this.trigger('retry-click'); + } + + render() { + super.render(); + + loadTemplate('userPage/followLoading.html', (t) => { + this.$el.html(t({ + ...this.getState(), + })); + }); + + return this; + } +} diff --git a/js/views/userPage/UserPage.js b/js/views/userPage/UserPage.js index efe7ce7ba..bb675718c 100644 --- a/js/views/userPage/UserPage.js +++ b/js/views/userPage/UserPage.js @@ -10,6 +10,7 @@ import { launchEditListingModal, launchSettingsModal } from '../../utils/modalMa import { getCurrentConnection } from '../../utils/serverConnect'; import Listing from '../../models/listing/Listing'; import Listings from '../../collections/Listings'; +import Followers from '../../collections/Followers'; import MiniProfile from '../MiniProfile'; import Home from './Home'; import Store from './Store'; @@ -120,14 +121,24 @@ export default class extends baseVw { createFollowersTabView(opts = {}) { return this.createChild(this.tabViews.Follow, { ...opts, - followType: 'Followers', + followType: 'followers', + peerId: this.model.id, + collection: new Followers([], { + peerId: this.model.id, + type: 'followers', + }), }); } createFollowingTabView(opts = {}) { return this.createChild(this.tabViews.Follow, { ...opts, - followType: 'Following', + followType: 'following', + peerId: this.model.id, + collection: new Followers([], { + peerId: this.model.id, + type: 'following', + }), }); } @@ -222,6 +233,11 @@ export default class extends baseVw { (this._$listingsCount = this.$('.js-listingsCount')); } + remove() { + if (this.followingFetch) this.followingFetch.abort(); + super.remove(); + } + render() { super.render(); loadTemplate('userPage/userPage.html', (t) => { diff --git a/styles/modules/_userPage.scss b/styles/modules/_userPage.scss index 1eeff6609..00f980907 100644 --- a/styles/modules/_userPage.scss +++ b/styles/modules/_userPage.scss @@ -65,15 +65,31 @@ } .userPageFollow { - flex-wrap: wrap; - width: auto; - margin: 0 #{-$padMd / 2}; - - .userCard { - width: 33.33333333333333%; - flex: 0 0 auto; - padding: 0 $padMd / 2 $padMd $padMd / 2; - box-sizing: border-box; + .userCards { + flex-wrap: wrap; + width: auto; + margin: 0 #{-$padMd / 2}; + + .userCard { + width: 33.33333333333333%; + flex: 0 0 auto; + padding: 0 $padMd / 2 $padMd $padMd / 2; + box-sizing: border-box; + } + } + + .followLoadingContainer { + padding: $padHg; + width: 100%; + } + + &.noResults { + min-height: 235px; + position: relative; + + .followLoadingContainer { + @include center(); + } } } From ab7ccd64b075468d4b2953ef77798bbe632e30ed Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Tue, 15 Aug 2017 11:00:16 -0500 Subject: [PATCH 02/13] resurrected the ownFollowing collection --- js/collections/Followers.js | 13 +++ js/models/Follower.js | 8 ++ js/start.js | 32 +++++-- js/templates/userPage/follow.html | 1 + js/utils/follow.js | 1 + js/views/userPage/Follow.js | 153 +++++++++++++++++++----------- js/views/userPage/UserPage.js | 16 +++- styles/modules/_userPage.scss | 2 +- 8 files changed, 161 insertions(+), 65 deletions(-) create mode 100644 js/models/Follower.js diff --git a/js/collections/Followers.js b/js/collections/Followers.js index d3ea58db7..1590c21dc 100644 --- a/js/collections/Followers.js +++ b/js/collections/Followers.js @@ -1,5 +1,6 @@ import app from '../app'; import { Collection } from 'backbone'; +import Follower from '../models/Follower'; export default class extends Collection { constructor(models = [], options = {}) { @@ -17,8 +18,20 @@ export default class extends Collection { this.options = options; } + model(attrs, options) { + return new Follower(attrs, options); + } + + modelId(attrs) { + return attrs.peerId; + } + url() { return app.getServerUrl(`ob/${this.options.type === 'followers' ? 'followers' : 'following'}` + `${app.profile.id === this.options.peerId ? '' : `/${this.options.peerId}`}`); } + + parse(response) { + return response.map(peerId => ({ peerId })); + } } diff --git a/js/models/Follower.js b/js/models/Follower.js new file mode 100644 index 000000000..7df25583b --- /dev/null +++ b/js/models/Follower.js @@ -0,0 +1,8 @@ +import BaseModel from './BaseModel'; + +export default class extends BaseModel { + get idAttribute() { + return 'peerId'; + } +} + diff --git a/js/start.js b/js/start.js index 4c49c6ba6..3d81d8bda 100644 --- a/js/start.js +++ b/js/start.js @@ -28,6 +28,7 @@ import { getLangByCode } from './data/languages'; import Profile from './models/profile/Profile'; import Settings from './models/Settings'; import WalletBalance from './models/wallet/WalletBalance'; +import Followers from './collections/Followers'; import { fetchExchangeRates } from './utils/currency'; import './utils/exchangeRateSyncer'; import './utils/listingData'; @@ -257,23 +258,29 @@ function onboard() { } const fetchStartupDataDeferred = $.Deferred(); +let ownFollowingFetch; +let ownFollowingFailed; let exchangeRatesFetch; let walletBalanceFetch; let walletBalanceFetchFailed; function fetchStartupData() { + ownFollowingFetch = !ownFollowingFetch || ownFollowingFailed ? + app.ownFollowing.fetch() : ownFollowingFetch; exchangeRatesFetch = exchangeRatesFetch || fetchExchangeRates(); - walletBalanceFetch = !walletBalanceFetch || walletBalanceFetch ? + walletBalanceFetch = !walletBalanceFetch || walletBalanceFetchFailed ? app.walletBalance.fetch() : walletBalanceFetch; - $.whenAll(exchangeRatesFetch, walletBalanceFetch) + $.whenAll(ownFollowingFetch, exchangeRatesFetch, walletBalanceFetch) .progress((...args) => { const state = args[1]; if (state !== 'success') { const jqXhr = args[0]; - if (jqXhr === walletBalanceFetch) { + if (jqXhr === ownFollowingFetch) { + ownFollowingFailed = true; + } else if (jqXhr === walletBalanceFetch) { walletBalanceFetchFailed = true; } } @@ -282,9 +289,17 @@ function fetchStartupData() { fetchStartupDataDeferred.resolve(); }) .fail((jqXhr) => { - if (walletBalanceFetchFailed) { + if (ownFollowingFailed || walletBalanceFetchFailed) { + let title = ''; + + if (ownFollowingFailed) { + title = app.polyglot.t('startUp.dialogs.unableToGetFollowData.title'); + } else { + title = app.polyglot.t('startUp.dialogs.unableToGetWalletBalance.title'); + } + const retryFetchStarupDataDialog = new Dialog({ - title: app.polyglot.t('startUp.dialogs.unableToGetWalletBalance.title'), + title, message: jqXhr.responseJSON && jqXhr.responseJSON.reason || '', buttons: [ { @@ -328,7 +343,7 @@ function onboardIfNeeded() { // let's go onboard onboard().done(() => onboardIfNeededDeferred.resolve()); } else { - fetchStartupData().done(() => onboardIfNeededDeferred.resolve()); + onboardIfNeededDeferred.resolve(); } }); @@ -367,6 +382,11 @@ function start() { app.localSettings.save('language', getValidLanguage(lang)); }); + app.ownFollowing = new Followers([], { + type: 'following', + peerId: app.profile.id, + }); + app.walletBalance = new WalletBalance(); onboardIfNeeded().done(() => { diff --git a/js/templates/userPage/follow.html b/js/templates/userPage/follow.html index ea7cf89c7..3ab9c78e0 100644 --- a/js/templates/userPage/follow.html +++ b/js/templates/userPage/follow.html @@ -1 +1,2 @@ +
\ No newline at end of file diff --git a/js/utils/follow.js b/js/utils/follow.js index a3b84dc07..8bbba7747 100644 --- a/js/utils/follow.js +++ b/js/utils/follow.js @@ -38,6 +38,7 @@ export function followUnfollow(guid, type = 'follow') { } }) .fail((data) => { + // todo: more specific error title. new Dialog({ title: app.polyglot.t('errors.badResult'), message: data.responseJSON.reason, diff --git a/js/views/userPage/Follow.js b/js/views/userPage/Follow.js index f56266cb8..387cecc09 100644 --- a/js/views/userPage/Follow.js +++ b/js/views/userPage/Follow.js @@ -1,8 +1,9 @@ import app from '../../app'; import loadTemplate from '../../utils/loadTemplate'; +import Followers from '../../collections/Followers'; import BaseVw from '../baseVw'; import FollowLoading from './FollowLoading'; -// import userShort from '../UserCard'; +import UserCard from '../UserCard'; // import { followedByYou, followsYou } from '../../utils/follow'; // import { openSimpleMessage } from '../modals/SimpleMessage'; @@ -29,55 +30,106 @@ export default class extends BaseVw { } this.options = options; + this.userCardViews = []; + this.renderedCl = new Followers([], { + peerId: opts.peerId, + type: opts.followType, + }); + this.listenTo(this.renderedCl, 'update', this.onCollectionUpdate); - this.listenTo(this.collection, 'update', this.onCollectionUpdate); - - this.fetch(); + if (this.collection === app.ownFollowing) { + this.onCollectionFetched.call(this); + } else { + this.fetch(); + } } className() { return 'userPageFollow flexRow noResults'; } + get userPerPage() { + return 12; + } + + get ownPage() { + return this.options.peerId === app.profile.id; + } + onCollectionUpdate(cl, opts) { this.$el.toggleClass('noResults', !cl.length); - // if (updateOpts.changes.added.length) { - // // Expecting either a single new notifcation on the bottom (will - // // be rendered on top) or a page of notifications on top (will be - // // rendered on the bottom). - // if (updateOpts.changes.added[updateOpts.changes.added.length - 1] === - // this.collection.at(0)) { - // // It's a page of notifcations at the bottom - // this.renderNotifications(updateOpts.changes.added, 'append'); - // } else { - // // New notification at top - // this.renderNotifications(updateOpts.changes.added, 'prepend'); - // } - - // updateOpts.changes.added.forEach(notif => { - // const innerNotif = notif.get('notification'); - // const types = ['follow', 'moderatorAdd', 'moderatorRemove']; - - // if (types.indexOf(innerNotif.type) > -1) { - // getCachedProfiles([innerNotif.peerId])[0] - // .done(profile => { - // notif.set('notification', { - // ...innerNotif, - // handle: profile.get('handle') || '', - // avatarHashes: profile.get('avatarHashes') && - // profile.get('avatarHashes').toJSON() || {}, - // }); - // }); - // } - // }); - - // this.listFetcher.setState({ noResults: false }); - // } + if (opts.changes.added.length) { + // Expecting either a single new user on the bottom (own node + // must have followed in the UI) or a page of users at the bottom. + if (opts.changes.added[opts.changes.added.length - 1] === + this.renderedCl.at(0)) { + // It's a page of users at the bottom + this.renderUsers(opts.changes.added, 'append'); + } else { + // New user at top + this.renderUsers(opts.changes.added, 'prepend'); + } + } else if (opts.changes.removed.length) { + console.log('removal yo'); + window.removal = opts; + } } - get ownPage() { - return this.options.peerId === app.profile.id; + onCollectionFetched() { + const state = { + isFetching: false, + fetchFailed: false, + noResults: false, + fetchErrorMsg: '', + }; + + if (!this.collection.length) { + state.noResults = true; + } + + if (this.followLoading) this.followLoading.setState(state); + this.renderedCl.add(this.collection.toJSON().slice(0, this.userPerPage)); + + // If our own node follows / unfollows the user of this page we'll add / remove + // ourselves from the main collection. Here we'll ensure the renderedCl is kept + // in sync with that. + this.listenTo(this.collection, 'add', md => { + this.renderedCl.add(md, { at: this.collection.models.indexOf(md) }); + }); + + this.listenTo(this.collection, 'remove', md => { + this.renderedCl.remove(md.id); + }); + } + + renderUsers(models = [], insertionType = 'append') { + if (!models) { + throw new Error('Please provide an array of Follower models.'); + } + + if (['append', 'prepend', 'replace'].indexOf(insertionType) === -1) { + throw new Error('Please provide a valid insertion type.'); + } + + if (insertionType === 'replace') { + this.userCardViews.forEach(user => user.remove()); + this.userCardViews = []; + } + + const usersFrag = document.createDocumentFragment(); + + models.forEach(user => { + const view = this.createChild(UserCard, { guid: user.id }); + this.userCardViews.push(view); + view.render().$el.appendTo(usersFrag); + }); + + if (insertionType === 'prepend') { + this.getCachedEl('.js-userCardsContainer').prepend(usersFrag); + } else { + this.getCachedEl('.js-userCardsContainer').append(usersFrag); + } } fetch() { @@ -96,19 +148,7 @@ export default class extends BaseVw { this.fetchCall = this.collection.fetch() .done((list, txtStatus, xhr) => { if (xhr.statusText === 'abort') return; - - const state = { - isFetching: false, - fetchFailed: false, - noResults: false, - fetchErrorMsg: '', - }; - - if (!list.length) { - state.noResults = true; - } - - if (this.followLoading) this.followLoading.setState(state); + this.onCollectionFetched.call(this); }).fail(xhr => { if (this.followLoading) { this.followLoading.setState({ @@ -141,12 +181,16 @@ export default class extends BaseVw { fetchErrorTitle = app.polyglot.t('userPage.followTab.followersFetchError'); noResultsMsg = this.ownPage ? app.polyglot.t('userPage.followTab.noOwnFollowers') : - app.polyglot.t('userPage.followTab.noOwnFollowing'); + app.polyglot.t('userPage.followTab.noFollowers', { + name: this.model.get('handle') || `${this.model.id.slice(0, 8)}…`, + }); } else { fetchErrorTitle = app.polyglot.t('userPage.followTab.followingFetchError'); noResultsMsg = this.ownPage ? - app.polyglot.t('userPage.followTab.noFollowers') : - app.polyglot.t('userPage.followTab.noFollowing'); + app.polyglot.t('userPage.followTab.noOwnFollowing') : + app.polyglot.t('userPage.followTab.noFollowing', { + name: this.model.get('handle') || `${this.model.id.slice(0, 8)}…`, + }); } this.followLoading = this.createChild(FollowLoading, { @@ -157,6 +201,7 @@ export default class extends BaseVw { fetchErrorMsg: this.fetchCall && this.fetchCall.responseJSON && this.fetchCall.responseJSON.reason || '', noResultsMsg, + noResults: !this.collection.length, }, }); diff --git a/js/views/userPage/UserPage.js b/js/views/userPage/UserPage.js index bb675718c..cec19d7e2 100644 --- a/js/views/userPage/UserPage.js +++ b/js/views/userPage/UserPage.js @@ -131,14 +131,22 @@ export default class extends baseVw { } createFollowingTabView(opts = {}) { + let collection; + + if (app.profile.id === this.model.id) { + collection = app.ownFollowing; + } else { + collection = new Followers([], { + peerId: this.model.id, + type: 'following', + }); + } + return this.createChild(this.tabViews.Follow, { ...opts, followType: 'following', peerId: this.model.id, - collection: new Followers([], { - peerId: this.model.id, - type: 'following', - }), + collection, }); } diff --git a/styles/modules/_userPage.scss b/styles/modules/_userPage.scss index 00f980907..f47e71020 100644 --- a/styles/modules/_userPage.scss +++ b/styles/modules/_userPage.scss @@ -65,7 +65,7 @@ } .userPageFollow { - .userCards { + .userCardsContainer { flex-wrap: wrap; width: auto; margin: 0 #{-$padMd / 2}; From 86df33a2aa71c3bf2f68cb9d7e2c32c8e2ece773 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Tue, 15 Aug 2017 17:14:03 -0500 Subject: [PATCH 03/13] wip - updating own following list as you follow / unfollow users --- js/templates/userPage/follow.html | 2 +- js/utils/follow.js | 2 +- js/views/UserCard.js | 12 +++++--- js/views/modals/BaseModal.js | 2 +- js/views/userPage/Follow.js | 46 +++++++++++++++++++++++++----- styles/components/_containers.scss | 6 ++-- styles/components/_modal.scss | 1 + 7 files changed, 54 insertions(+), 17 deletions(-) diff --git a/js/templates/userPage/follow.html b/js/templates/userPage/follow.html index 3ab9c78e0..8189b42a5 100644 --- a/js/templates/userPage/follow.html +++ b/js/templates/userPage/follow.html @@ -1,2 +1,2 @@ -
+
\ No newline at end of file diff --git a/js/utils/follow.js b/js/utils/follow.js index 8bbba7747..915adb683 100644 --- a/js/utils/follow.js +++ b/js/utils/follow.js @@ -32,7 +32,7 @@ export function followUnfollow(guid, type = 'follow') { .done(() => { // if the call succeeds, add or remove the guid from the collection if (type === 'follow') { - app.ownFollowing.unshift({ guid }); + app.ownFollowing.unshift({ peerId: guid }); } else { app.ownFollowing.remove(guid); // remove via id } diff --git a/js/views/UserCard.js b/js/views/UserCard.js index 9f9effd10..d133f5201 100644 --- a/js/views/UserCard.js +++ b/js/views/UserCard.js @@ -49,10 +49,14 @@ export default class extends BaseVw { this.$modBtn.attr('data-tip', this.getModTip()); }); - this.listenTo(app.ownFollowing, 'sync update', () => { - this.followedByYou = followedByYou(this.guid); - this.$followBtn.toggleClass('active', this.followedByYou); - this.$followBtn.attr('data-tip', this.getFollowTip()); + this.listenTo(app.ownFollowing, 'update', (cl, opts) => { + const updatedModels = opts.changes.added.concat(opts.changes.removed); + + if (updatedModels.filter(md => md.id === this.guid).length) { + this.followedByYou = followedByYou(this.guid); + this.$followBtn.toggleClass('active', this.followedByYou); + this.$followBtn.attr('data-tip', this.getFollowTip()); + } }); } diff --git a/js/views/modals/BaseModal.js b/js/views/modals/BaseModal.js index 0b2c4c081..4e3bcd624 100644 --- a/js/views/modals/BaseModal.js +++ b/js/views/modals/BaseModal.js @@ -13,7 +13,7 @@ export default class BaseModal extends baseVw { dismissOnOverlayClick: false, dismissOnEscPress: true, showCloseButton: true, - closeButtonClass: 'cornerTR iconBtn clrP clrBr clrSh3 toolTipNoWrap', + closeButtonClass: 'cornerTR iconBtn clrP clrBr clrSh3 toolTipNoWrap modalCloseBtn', innerButtonClass: 'ion-ios-close-empty', closeButtonTip: app.polyglot.t('pageNav.toolTip.close'), modelContentClass: 'modalContent', diff --git a/js/views/userPage/Follow.js b/js/views/userPage/Follow.js index 387cecc09..fa88d3413 100644 --- a/js/views/userPage/Follow.js +++ b/js/views/userPage/Follow.js @@ -38,14 +38,16 @@ export default class extends BaseVw { this.listenTo(this.renderedCl, 'update', this.onCollectionUpdate); if (this.collection === app.ownFollowing) { - this.onCollectionFetched.call(this); + setTimeout(() => this.onCollectionFetched.call(this)); } else { this.fetch(); } + + this.listenTo(app.ownFollowing, 'update', this.onOwnFollowingUpdate); } className() { - return 'userPageFollow flexRow noResults'; + return 'userPageFollow noResults'; } get userPerPage() { @@ -56,8 +58,34 @@ export default class extends BaseVw { return this.options.peerId === app.profile.id; } + onOwnFollowingUpdate(cl, opts) { + if (opts.changes.added.length) { + if (this.ownPage) { + if (this.options.type === 'following') this.collection.add(opts.changes.added); + console.log('own page follow'); + } else if (this.options.type === 'followers') { + const md = app.ownFollowing.get(this.model.id); + if (md && opts.changes.added.indexOf(md) > -1) { + this.collection.add(md); + } + } + } + + if (opts.changes.removed.length) { + if (this.ownPage) { + console.log('own page unfollow'); + if (this.options.type === 'following') this.collection.remove(opts.changes.removed); + } else if (this.options.type === 'followers') { + if (opts.changes.removed.filter(removedMd => (removedMd.id === this.model.id))) { + this.collection.remove(this.model.id); + } + } + } + } + onCollectionUpdate(cl, opts) { this.$el.toggleClass('noResults', !cl.length); + console.log('cl update nation'); if (opts.changes.added.length) { // Expecting either a single new user on the bottom (own node @@ -70,7 +98,9 @@ export default class extends BaseVw { // New user at top this.renderUsers(opts.changes.added, 'prepend'); } - } else if (opts.changes.removed.length) { + } + + if (opts.changes.removed.length) { console.log('removal yo'); window.removal = opts; } @@ -89,11 +119,12 @@ export default class extends BaseVw { } if (this.followLoading) this.followLoading.setState(state); - this.renderedCl.add(this.collection.toJSON().slice(0, this.userPerPage)); + this.renderedCl.add(this.collection.models.slice(0, this.userPerPage)); - // If our own node follows / unfollows the user of this page we'll add / remove - // ourselves from the main collection. Here we'll ensure the renderedCl is kept - // in sync with that. + // If any additions / removal occur on the main collection (e.g. this view + // is showing our own following list and we follow / unfollow someone; this view + // is showing anothers followers list and our own node has followed / unfollowed + // that user), we sync them over to the renderedCl. this.listenTo(this.collection, 'add', md => { this.renderedCl.add(md, { at: this.collection.models.indexOf(md) }); }); @@ -120,6 +151,7 @@ export default class extends BaseVw { const usersFrag = document.createDocumentFragment(); models.forEach(user => { + console.log(`the id is ${user.id}`); const view = this.createChild(UserCard, { guid: user.id }); this.userCardViews.push(view); view.render().$el.appendTo(usersFrag); diff --git a/styles/components/_containers.scss b/styles/components/_containers.scss index fac680967..fe5c097f9 100644 --- a/styles/components/_containers.scss +++ b/styles/components/_containers.scss @@ -297,7 +297,7 @@ } } -.toolTip { +.toolTip:not(.processing) { // Adds a tooltip that gets its text from the data-tip attribute. // You can't put this on a class that already had before and after rules, like an icon. // To add a tooltip to an icon, wrap it in another element, and put the tooltip on that. @@ -364,8 +364,8 @@ // Use for single line variable width tooltips. Factor in translations and // where you're tooltip will display. If it's possible it can get so wide that -// it wouldn't look right, you'll probably want it to wrap (i.e. dont use this class).' -&.toolTipNoWrap { +// it wouldn't look right, you'll probably want it to wrap (i.e. dont use this class). +.toolTipNoWrap:not(.processing) { @extend .toolTip; &:before { diff --git a/styles/components/_modal.scss b/styles/components/_modal.scss index e5283e910..973aaed10 100644 --- a/styles/components/_modal.scss +++ b/styles/components/_modal.scss @@ -23,6 +23,7 @@ & > .cornerTR { top: -40px; right: 0; + position: absolute; } } From 9cbeb6ebb1d666cf51f0c9013c5a85fb50bbe0a0 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Tue, 15 Aug 2017 22:14:29 -0500 Subject: [PATCH 04/13] wip - fun with counts --- js/templates/userPage/userPage.html | 5 +- js/views/userPage/Follow.js | 41 ++++++++++------ js/views/userPage/UserPage.js | 72 +++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 18 deletions(-) diff --git a/js/templates/userPage/userPage.html b/js/templates/userPage/userPage.html index b0e1fbff1..4b305922f 100644 --- a/js/templates/userPage/userPage.html +++ b/js/templates/userPage/userPage.html @@ -1,3 +1,4 @@ +<% console.log('sizzle');window.sizzle = ob %>