From 2c89d49a3d22ed2813a6a57fb6049341fa8624ba Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Thu, 4 Apr 2019 15:10:34 -0400
Subject: [PATCH] #468 - show pinned timeline and add pinned label to the
 status

---
 src/components/conversation/conversation.js   |  3 +-
 src/components/conversation/conversation.vue  |  1 +
 src/components/status/status.js               |  3 +-
 src/components/status/status.vue              | 17 +++++++++--
 src/components/timeline/timeline.js           |  3 +-
 src/components/timeline/timeline.vue          |  3 +-
 src/components/user_profile/user_profile.js   |  5 ++++
 src/components/user_profile/user_profile.vue  | 29 ++++++++++++-------
 src/modules/statuses.js                       |  3 +-
 src/services/api/api.service.js               | 14 +++++++++
 .../timeline_fetcher.service.js               |  6 ++++
 11 files changed, 69 insertions(+), 18 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index ffeb7244..fc239ee9 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -41,7 +41,8 @@ const conversation = {
   props: [
     'statusoid',
     'collapsable',
-    'isPage'
+    'isPage',
+    'pinned'
   ],
   created () {
     if (this.isPage) {
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index d04ff722..40011113 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -13,6 +13,7 @@
       :key="status.id"
       :inlineExpanded="collapsable && isExpanded"
       :statusoid="status"
+      :pinned="pinned"
       :expandable='!isExpanded'
       :focused="focused(status.id)"
       :inConversation="isExpanded"
diff --git a/src/components/status/status.js b/src/components/status/status.js
index a3596b69..f2881742 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -26,7 +26,8 @@ const Status = {
     'replies',
     'isPreview',
     'noHeading',
-    'inlineExpanded'
+    'inlineExpanded',
+    'pinned'
   ],
   data () {
     return {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 0443e758..a5614f59 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -12,6 +12,13 @@
       </div>
     </template>
     <template v-else>
+      <div v-if="pinned" class="status-pin">
+        <i class="fa icon-pin faint"></i>
+        <span class="faint">Pinned</span>
+        <div class="button-icon button-action-icon" v-if="status.pinned && ownStatus" @click.prevent="unpinStatus" title="Unpin">
+          <i class="fa icon-cancel"></i>
+        </div>
+      </div>
       <div v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
         <UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :user="statusoid.user"/>
         <div class="media-body faint">
@@ -55,9 +62,6 @@
                 <div class="button-icon button-action-icon" v-if="!status.pinned && ownStatus" @click.prevent="pinStatus" title="Pin">
                   <i class="fa icon-pin"></i>
                 </div>
-                <div class="button-icon button-action-icon" v-if="status.pinned && ownStatus" @click.prevent="unpinStatus" title="Unpin">
-                  <i class="fa icon-cancel"></i>
-                </div>
                 <div class="button-icon button-action-icon" v-if="expandable && !isPreview" @click.prevent="toggleExpanded" title="Expand">
                   <i class="button-icon icon-plus-squared"></i>
                 </div>
@@ -205,6 +209,13 @@ $status-margin: 0.75em;
   max-width: 100%;
 }
 
+.status-pin {
+  padding: 0.75em 0.75em 0;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+}
+
 .status-preview {
   position: absolute;
   max-width: 95%;
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 19d9a9ac..953b4a06 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -11,7 +11,8 @@ const Timeline = {
     'userId',
     'tag',
     'embedded',
-    'count'
+    'count',
+    'noLoadMore'
   ],
   data () {
     return {
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index e6a8d458..870fe4f5 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -21,11 +21,12 @@
           class="status-fadein"
           :key="status.id"
           :statusoid="status"
+          :pinned="timelineName === 'pinned'"
           :collapsable="true"
         />
       </div>
     </div>
-    <div :class="classes.footer">
+    <div :class="classes.footer" v-if="!noLoadMore">
       <div v-if="count===0" class="new-status-notification text-center panel-footer faint">
         {{$t('timeline.no_statuses')}}
       </div>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 4eddb8b1..71a88dcf 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -40,6 +40,9 @@ const UserProfile = {
     timeline () {
       return this.$store.state.statuses.timelines.user
     },
+    pinned () {
+      return this.$store.state.statuses.timelines.pinned
+    },
     favorites () {
       return this.$store.state.statuses.timelines.favorites
     },
@@ -91,6 +94,7 @@ const UserProfile = {
     fetchTimelines () {
       const userId = this.userId
       this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId })
+      this.$store.dispatch('startFetchingTimeline', { timeline: 'pinned', userId })
       this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId })
       if (this.isUs) {
         this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId })
@@ -98,6 +102,7 @@ const UserProfile = {
     },
     cleanUp () {
       this.$store.dispatch('stopFetching', 'user')
+      this.$store.dispatch('stopFetching', 'pinned')
       this.$store.dispatch('stopFetching', 'favorites')
       this.$store.dispatch('stopFetching', 'media')
       this.$store.commit('clearTimeline', { timeline: 'user' })
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 71c625b7..caf297c6 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -3,16 +3,25 @@
   <div v-if="user" class="user-profile panel panel-default">
     <UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
     <tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
-      <Timeline
-        :label="$t('user_card.statuses')"
-        :disabled="!user.statuses_count"
-        :count="user.statuses_count"
-        :embedded="true"
-        :title="$t('user_profile.timeline_title')"
-        :timeline="timeline"
-        :timeline-name="'user'"
-        :user-id="userId"
-      />
+      <div :label="$t('user_card.statuses')" :disabled="!user.statuses_count">
+        <Timeline
+          :count="user.statuses_count"
+          :embedded="true"
+          :title="$t('user_profile.timeline_title')"
+          :timeline="pinned"
+          :timeline-name="'pinned'"
+          :user-id="userId"
+          :no-load-more="true"
+        />
+        <Timeline
+          :count="user.statuses_count"
+          :embedded="true"
+          :title="$t('user_profile.timeline_title')"
+          :timeline="timeline"
+          :timeline-name="'user'"
+          :user-id="userId"
+        />
+      </div>
       <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
         <FriendList :userId="userId">
           <template slot="item" slot-scope="{item}">
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 176e5f42..94d7f30b 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -47,7 +47,8 @@ export const defaultState = () => ({
     publicAndExternal: emptyTl(),
     friends: emptyTl(),
     tag: emptyTl(),
-    dms: emptyTl()
+    dms: emptyTl(),
+    pinned: emptyTl()
   }
 })
 
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 65cebe78..03137d20 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -506,6 +506,19 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
     .then((data) => data.map(isNotifications ? parseNotification : parseStatus))
 }
 
+const fetchPinnedStatuses = ({ id, credentials }) => {
+  const url = MASTODON_USER_TIMELINE_URL(id) + '?pinned=true'
+  return fetch(url, { headers: authHeaders(credentials) })
+    .then((data) => {
+      if (data.ok) {
+        return data
+      }
+      throw new Error('Error fetching pinned timeline', data)
+    })
+    .then((data) => data.json())
+    .then((data) => data.map(parseStatus))
+}
+
 const verifyCredentials = (user) => {
   return fetch(LOGIN_URL, {
     method: 'POST',
@@ -726,6 +739,7 @@ const reportUser = ({credentials, userId, statusIds, comment, forward}) => {
 const apiService = {
   verifyCredentials,
   fetchTimeline,
+  fetchPinnedStatuses,
   fetchConversation,
   fetchStatus,
   fetchFriends,
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 8e954cdf..e8d9a2ed 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -16,6 +16,12 @@ const update = ({store, statuses, timeline, showImmediately, userId}) => {
 }
 
 const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false, until}) => {
+  if (timeline === 'pinned') {
+    return apiService.fetchPinnedStatuses({ id: userId, credentials })
+      .then(statuses => {
+        update({ store, statuses, timeline, showImmediately, userId })
+      })
+  }
   const args = { timeline, credentials }
   const rootState = store.rootState || store.state
   const timelineData = rootState.statuses.timelines[camelCase(timeline)]