From dd4076f49cfbdda9c142c695288e9893bbe63874 Mon Sep 17 00:00:00 2001
From: TAKAHASHI Shuuji <shuuji3@gmail.com>
Date: Thu, 19 Dec 2024 13:19:20 +0900
Subject: [PATCH] feat: show pinned posts on individual account page (#2779)

---
 components/status/StatusCard.vue            | 14 ++++++++++++++
 locales/en.json                             |  1 +
 pages/[[server]]/@[account]/index/index.vue | 17 +++++++++++++++--
 3 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/components/status/StatusCard.vue b/components/status/StatusCard.vue
index 1ec162cf..84ebcd41 100644
--- a/components/status/StatusCard.vue
+++ b/components/status/StatusCard.vue
@@ -62,6 +62,7 @@ const timeago = useTimeAgo(() => status.value.createdAt, timeAgoOptions)
 const isSelfReply = computed(() => status.value.inReplyToAccountId === status.value.account.id)
 const collapseRebloggedBy = computed(() => rebloggedBy.value?.id === status.value.account.id)
 const isDM = computed(() => status.value.visibility === 'direct')
+const isPinned = computed(() => status.value.pinned)
 
 const showUpperBorder = computed(() => props.newer && !directReply.value)
 const showReplyTo = computed(() => !replyToMain.value && !directReply.value)
@@ -75,6 +76,19 @@ const forceShow = ref(false)
     <div :h="showUpperBorder ? '1px' : '0'" w-auto bg-border mb-1 z--1 />
 
     <slot name="meta">
+      <!-- Pinned status -->
+      <div flex="~ col" justify-between>
+        <div
+          v-if="isPinned"
+          flex="~ gap2" items-center h-auto text-sm text-orange
+          m="is-5" p="t-1 is-5"
+          relative text-secondary ws-nowrap
+        >
+          <div i-ri:pushpin-line />
+          <span>{{ $t('status.pinned') }}</span>
+        </div>
+      </div>
+
       <!-- Line connecting to previous status -->
       <template v-if="status.inReplyToAccountId">
         <StatusReplyingTo
diff --git a/locales/en.json b/locales/en.json
index f498f5a8..20520aa0 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -633,6 +633,7 @@
       "dismiss": "Dismiss",
       "read": "Read {0} description"
     },
+    "pinned": "Pinned post",
     "poll": {
       "count": "{0} votes|{0} vote|{0} votes",
       "ends": "ends {0}",
diff --git a/pages/[[server]]/@[account]/index/index.vue b/pages/[[server]]/@[account]/index/index.vue
index 506f722d..e8ed7a47 100644
--- a/pages/[[server]]/@[account]/index/index.vue
+++ b/pages/[[server]]/@[account]/index/index.vue
@@ -10,11 +10,21 @@ const { t } = useI18n()
 
 const account = await fetchAccountByHandle(handle.value)
 
+// we need to ensure `pinned === true` on status
+// because this prop is appeared only on current account's posts
+function applyPinned(statuses: mastodon.v1.Status[]) {
+  return statuses.map((status) => {
+    status.pinned = true
+    return status
+  })
+}
+
 function reorderAndFilter(items: mastodon.v1.Status[]) {
   return reorderedTimeline(items, 'account')
 }
 
-const paginator = useMastoClient().v1.accounts.$select(account.id).statuses.list({ limit: 30, excludeReplies: true })
+const pinnedPaginator = useMastoClient().v1.accounts.$select(account.id).statuses.list({ pinned: true })
+const postPaginator = useMastoClient().v1.accounts.$select(account.id).statuses.list({ limit: 30, excludeReplies: true })
 
 if (account) {
   useHydratedHead({
@@ -26,6 +36,9 @@ if (account) {
 <template>
   <div>
     <AccountTabs />
-    <TimelinePaginator :paginator="paginator" :preprocess="reorderAndFilter" context="account" :account="account" />
+    <TimelinePaginator :paginator="pinnedPaginator" :preprocess="applyPinned" context="account" :account="account" :end-message="false" />
+    <!-- Upper border -->
+    <div h="1px" w-auto bg-border mb-1 />
+    <TimelinePaginator :paginator="postPaginator" :preprocess="reorderAndFilter" context="account" :account="account" />
   </div>
 </template>