From a79bad5cdb896fd965e544df3ba8d0a87b3db458 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Fri, 19 Jun 2020 12:46:48 +0200
Subject: [PATCH 01/26] StatusContent: Better separate subject from status
 content.

---
 .../status_content/status_content.js          |  6 -----
 .../status_content/status_content.vue         | 22 +++++++++++--------
 2 files changed, 13 insertions(+), 15 deletions(-)

diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index c0a71e8f..cfee77a3 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -142,12 +142,6 @@ const StatusContent = {
         return html
       }
     },
-    contentHtml () {
-      if (!this.status.summary_html) {
-        return this.postBodyHtml
-      }
-      return this.status.summary_html + '<br />' + this.postBodyHtml
-    },
     ...mapGetters(['mergedConfig']),
     ...mapState({
       betterShadow: state => state.interface.browserSupport.cssFilter,
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index efc2485e..df980a71 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -31,7 +31,7 @@
       <div
         class="status-content media-body"
         @click.prevent="linkClicked"
-        v-html="contentHtml"
+        v-html="postBodyHtml"
       />
       <a
         v-if="showingLongSubject"
@@ -52,17 +52,17 @@
         href="#"
         @click.prevent="toggleShowMore"
       >{{ $t("general.show_more") }}</a>
+      <div
+        v-if="status.summary_html"
+        class="status-content media-body summary"
+        @click.prevent="linkClicked"
+        v-html="status.summary_html"
+      />
       <div
         v-if="!hideSubjectStatus"
         class="status-content media-body"
         @click.prevent="linkClicked"
-        v-html="contentHtml"
-      />
-      <div
-        v-else
-        class="status-content media-body"
-        @click.prevent="linkClicked"
-        v-html="status.summary_html"
+        v-html="postBodyHtml"
       />
       <a
         v-if="hideSubjectStatus"
@@ -181,6 +181,10 @@ $status-margin: 0.75em;
     line-height: 1.4em;
     white-space: pre-wrap;
 
+    &.summary {
+      font-weight: bold;
+    }
+
     blockquote {
       margin: 0.2em 0 0.2em 2em;
       font-style: italic;
@@ -226,7 +230,7 @@ $status-margin: 0.75em;
 
 .greentext {
   color: $fallback--cGreen;
-  color: var(--postGreentext, $fallback--cGreen);
+  color: var(--cGreen, $fallback--cGreen);
 }
 
 .timeline :not(.panel-disabled) > {

From 4da0a0c0bfcfaccd58def8d2170b952c1d15106a Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Fri, 19 Jun 2020 12:49:42 +0200
Subject: [PATCH 02/26] StatusContent: Fix greentext.

---
 src/components/status_content/status_content.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index df980a71..431661eb 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -230,7 +230,7 @@ $status-margin: 0.75em;
 
 .greentext {
   color: $fallback--cGreen;
-  color: var(--cGreen, $fallback--cGreen);
+  color: var(--postGreentext, $fallback--cGreen);
 }
 
 .timeline :not(.panel-disabled) > {

From 44edb730c1e2298a00be0c1a139f80a1335ad7cf Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Fri, 26 Jun 2020 14:07:39 +0300
Subject: [PATCH 03/26] rip restyle subject, fix some issues with long subject

---
 .../status_content/status_content.js          |   2 +-
 .../status_content/status_content.vue         | 133 +++++++++++-------
 2 files changed, 83 insertions(+), 52 deletions(-)

diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index cfee77a3..66501b3e 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -44,7 +44,7 @@ const StatusContent = {
       return lengthScore > 20
     },
     longSubject () {
-      return this.status.summary.length > 900
+      return this.status.summary.length > 240
     },
     // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
     mightHideBecauseSubject () {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 431661eb..5698b4c0 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -3,16 +3,55 @@
   <div class="status-body">
     <slot name="header" />
     <div
-      v-if="longSubject"
+      v-if="status.summary_html"
+      class="summary-wrapper"
+      :class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
+    >
+      <div
+        class="media-body summary"
+        @click.prevent="linkClicked"
+        v-html="status.summary_html"
+      />
+      <a
+        v-if="longSubject && showingLongSubject"
+        href="#"
+        class="tall-subject-hider"
+        @click.prevent="showingLongSubject=false"
+      >{{ $t("general.show_less") }}</a>
+      <a
+        v-else-if="longSubject"
+        class="tall-subject-hider"
+        :class="{ 'tall-subject-hider_focused': focused }"
+        href="#"
+        @click.prevent="showingLongSubject=true"
+      >
+        {{ $t("general.show_more") }}
+      </a>
+    </div>
+    <div
+      :class="{'tall-status': hideTallStatus}"
       class="status-content-wrapper"
-      :class="{ 'tall-status': !showingLongSubject }"
     >
       <a
-        v-if="!showingLongSubject"
+        v-if="hideTallStatus"
         class="tall-status-hider"
         :class="{ 'tall-status-hider_focused': focused }"
         href="#"
-        @click.prevent="showingLongSubject=true"
+        @click.prevent="toggleShowMore"
+      >
+        {{ $t("general.show_more") }}
+      </a>
+      <div
+        v-if="!hideSubjectStatus"
+        class="status-content media-body"
+        @click.prevent="linkClicked"
+        v-html="postBodyHtml"
+      />
+      <a
+        v-if="hideSubjectStatus"
+        href="#"
+        class="cw-status-hider"
+        @click.prevent="toggleShowMore"
       >
         {{ $t("general.show_more") }}
         <span
@@ -28,48 +67,6 @@
           class="icon-link"
         />
       </a>
-      <div
-        class="status-content media-body"
-        @click.prevent="linkClicked"
-        v-html="postBodyHtml"
-      />
-      <a
-        v-if="showingLongSubject"
-        href="#"
-        class="status-unhider"
-        @click.prevent="showingLongSubject=false"
-      >{{ $t("general.show_less") }}</a>
-    </div>
-    <div
-      v-else
-      :class="{'tall-status': hideTallStatus}"
-      class="status-content-wrapper"
-    >
-      <a
-        v-if="hideTallStatus"
-        class="tall-status-hider"
-        :class="{ 'tall-status-hider_focused': focused }"
-        href="#"
-        @click.prevent="toggleShowMore"
-      >{{ $t("general.show_more") }}</a>
-      <div
-        v-if="status.summary_html"
-        class="status-content media-body summary"
-        @click.prevent="linkClicked"
-        v-html="status.summary_html"
-      />
-      <div
-        v-if="!hideSubjectStatus"
-        class="status-content media-body"
-        @click.prevent="linkClicked"
-        v-html="postBodyHtml"
-      />
-      <a
-        v-if="hideSubjectStatus"
-        href="#"
-        class="cw-status-hider"
-        @click.prevent="toggleShowMore"
-      >{{ $t("general.show_more") }}</a>
       <a
         v-if="showingMore"
         href="#"
@@ -129,6 +126,12 @@ $status-margin: 0.75em;
   flex: 1;
   min-width: 0;
 
+  .status-content-wrapper {
+    display: flex;
+    flex-direction: column;
+    flex-wrap: nowrap;
+  }
+
   .tall-status {
     position: relative;
     height: 220px;
@@ -136,7 +139,7 @@ $status-margin: 0.75em;
     overflow-y: hidden;
     z-index: 1;
     .status-content {
-      height: 100%;
+      min-height: 0;
       mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
             linear-gradient(to top, white, white);
       /* Autoprefixed seem to ignore this one, and also syntax is different */
@@ -176,15 +179,43 @@ $status-margin: 0.75em;
     }
   }
 
+  .summary-wrapper {
+    margin-bottom: 0.5em;
+    border-style: solid;
+    border-width: 0 0 1px 0;
+    border-color: var(--border, $fallback--border);
+    flex-grow: 0;
+  }
+
+  .summary {
+    font-style: italic;
+    padding-bottom: 0.5em;
+  }
+
+  .tall-subject {
+    position: relative;
+    .summary {
+      max-height: 2em;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+  }
+
+  .tall-subject-hider {
+    display: inline-block;
+    word-break: break-all;
+    // position: absolute;
+    width: 100%;
+    text-align: center;
+    padding-bottom: 0.5em;
+  }
+
   .status-content {
     font-family: var(--postFont, sans-serif);
     line-height: 1.4em;
     white-space: pre-wrap;
 
-    &.summary {
-      font-weight: bold;
-    }
-
     blockquote {
       margin: 0.2em 0 0.2em 2em;
       font-style: italic;

From 8c3106c588f964a8e26c69784d5808ad1f72625c Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Fri, 26 Jun 2020 18:20:32 +0300
Subject: [PATCH 04/26] Change the show/hide strings about, remove subjected
 status toggle when 'collapse' option not used

---
 src/components/status_content/status_content.js  |  4 ++--
 src/components/status_content/status_content.vue | 10 ++++++----
 src/i18n/en.json                                 |  6 +++++-
 3 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 66501b3e..09ea3a20 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -48,10 +48,10 @@ const StatusContent = {
     },
     // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
     mightHideBecauseSubject () {
-      return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
+      return !!this.status.summary && this.localCollapseSubjectDefault
     },
     mightHideBecauseTall () {
-      return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
+      return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
     },
     hideSubjectStatus () {
       return this.mightHideBecauseSubject && !this.expandingSubject
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 5698b4c0..3460c2fa 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -17,7 +17,7 @@
         href="#"
         class="tall-subject-hider"
         @click.prevent="showingLongSubject=false"
-      >{{ $t("general.show_less") }}</a>
+      >{{ $t("status.hide_full_subject") }}</a>
       <a
         v-else-if="longSubject"
         class="tall-subject-hider"
@@ -25,7 +25,7 @@
         href="#"
         @click.prevent="showingLongSubject=true"
       >
-        {{ $t("general.show_more") }}
+        {{ $t("status.show_full_subject") }}
       </a>
     </div>
     <div
@@ -53,7 +53,7 @@
         class="cw-status-hider"
         @click.prevent="toggleShowMore"
       >
-        {{ $t("general.show_more") }}
+        {{ $t("status.show_content") }}
         <span
           v-if="hasImageAttachments"
           class="icon-picture"
@@ -72,7 +72,9 @@
         href="#"
         class="status-unhider"
         @click.prevent="toggleShowMore"
-      >{{ $t("general.show_less") }}</a>
+      >
+        {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
+      </a>
     </div>
 
     <div v-if="status.poll && status.poll.options">
diff --git a/src/i18n/en.json b/src/i18n/en.json
index eefe10e5..ede8c3d8 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -629,7 +629,11 @@
     "status_unavailable": "Status unavailable",
     "copy_link": "Copy link to status",
     "thread_muted": "Thread muted",
-    "thread_muted_and_words": ", has words:"
+    "thread_muted_and_words": ", has words:",
+    "show_full_subject": "Show full subject",
+    "hide_full_subject": "Hide full subject",
+    "show_content": "Show content",
+    "hide_content": "Hide content"
   },
   "user_card": {
     "approve": "Approve",

From 195e83d0c8ab660a1dd6b29137295c2a6a36dca9 Mon Sep 17 00:00:00 2001
From: Fristi <fristi@subcon.town>
Date: Sat, 27 Jun 2020 15:04:52 +0000
Subject: [PATCH 05/26] Translated using Weblate (Dutch)

Currently translated at 100.0% (626 of 626 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/nl/
---
 src/i18n/nl.json | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/i18n/nl.json b/src/i18n/nl.json
index 15ce5cbe..bf270f87 100644
--- a/src/i18n/nl.json
+++ b/src/i18n/nl.json
@@ -476,7 +476,14 @@
       "backend_version": "Backend Versie",
       "title": "Versie"
     },
-    "mutes_and_blocks": "Negeringen en Blokkades"
+    "mutes_and_blocks": "Negeringen en Blokkades",
+    "profile_fields": {
+      "value": "Inhoud",
+      "name": "Label",
+      "add_field": "Veld Toevoegen",
+      "label": "Profiel metadata"
+    },
+    "bot": "Dit is een bot account"
   },
   "timeline": {
     "collapse": "Inklappen",

From 6529f9fa34c5e4e4786f76f63068d70c771d868e Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Tue, 30 Jun 2020 15:04:16 +0300
Subject: [PATCH 06/26] add strikethrough when parent isn't visible

---
 src/components/status/status.vue                      | 11 ++++++++++-
 .../entity_normalizer/entity_normalizer.service.js    |  1 +
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 7ec29b28..c537358b 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -208,7 +208,12 @@
                     @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
                   >
                     <i class="button-icon icon-reply" />
-                    <span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span>
+                    <span
+                      class="faint-link reply-to-text"
+                      :class="{ 'strikethrough': !status.parent_visible }"
+                    >
+                      {{ $t('status.reply_to') }}
+                    </span>
                   </a>
                 </StatusPopover>
                 <span
@@ -526,6 +531,10 @@ $status-margin: 0.75em;
       margin: 0 0.4em 0 0.2em;
     }
 
+    .strikethrough {
+      text-decoration: line-through;
+    }
+
     .replies-separator {
       margin-left: 0.4em;
     }
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 3bdb92f3..10111045 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -248,6 +248,7 @@ export const parseStatus = (data) => {
       output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
       output.thread_muted = pleroma.thread_muted
       output.emoji_reactions = pleroma.emoji_reactions
+      output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible
     } else {
       output.text = data.content
       output.summary = data.spoiler_text

From ee1364a16770792e2a1040a0ea0b8f1693b5da52 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Tue, 30 Jun 2020 15:15:27 +0300
Subject: [PATCH 07/26] add no-statusId support for status popover

---
 src/components/status/status.vue                | 2 +-
 src/components/status_popover/status_popover.js | 4 ++++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index c537358b..8237be6c 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -197,7 +197,7 @@
               >
                 <StatusPopover
                   v-if="!isPreview"
-                  :status-id="status.in_reply_to_status_id"
+                  :status-id="status.parent_visible && status.in_reply_to_status_id"
                   class="reply-to-popover"
                   style="min-width: 0"
                 >
diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js
index 159132a9..51e7680c 100644
--- a/src/components/status_popover/status_popover.js
+++ b/src/components/status_popover/status_popover.js
@@ -22,6 +22,10 @@ const StatusPopover = {
   methods: {
     enter () {
       if (!this.status) {
+        if (!this.statusId) {
+          this.error = true
+          return
+        }
         this.$store.dispatch('fetchStatus', this.statusId)
           .then(data => (this.error = false))
           .catch(e => (this.error = true))

From 3a79918b89fa41f8676e7f19862e6e29abd4ea14 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Tue, 30 Jun 2020 15:23:47 +0300
Subject: [PATCH 08/26] update changelog for reply-to strikethrough

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 887588f3..e98b2728 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Autocomplete domains from list of known instances
 - 'Bot' settings option and badge
 - Added profile meta data fields that can be set in profile settings
+- When a post is a reply to an unavailable post, the 'Reply to'-text has a strike-through style
 
 ### Changed
 - Registration page no longer requires email if the server is configured not to require it

From ea09bbecf8b7715a1242a104b6233a7c3b5ac588 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Tue, 30 Jun 2020 17:02:38 +0300
Subject: [PATCH 09/26] Make use of backend reply filtering

---
 CHANGELOG.md                                  |  1 +
 .../settings_modal/tabs/filtering_tab.js      |  3 ++
 src/components/status/status.js               | 33 +------------------
 src/components/timeline/timeline.js           |  6 ++--
 src/components/timeline/timeline.vue          |  4 +--
 src/modules/statuses.js                       |  8 +++++
 src/services/api/api.service.js               |  6 +++-
 .../timeline_fetcher.service.js               |  4 ++-
 8 files changed, 27 insertions(+), 38 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 887588f3..d978d362 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Subject field now appears disabled when posting
 - Fix status ellipsis menu being cut off in notifications column
 - Fixed autocomplete sometimes not returning the right user when there's already some results
+- Reply filtering options in Settings -> Filtering now work again using filtering on server
 
 ## [2.0.3] - 2020-05-02
 ### Fixed
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
index 224a7f47..3b2df556 100644
--- a/src/components/settings_modal/tabs/filtering_tab.js
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -37,6 +37,9 @@ const FilteringTab = {
         })
       },
       deep: true
+    },
+    replyVisibility () {
+      this.$store.dispatch('queueFlushAll')
     }
   }
 }
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 73382521..ad0b72a9 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -141,7 +141,7 @@ const Status = {
       return this.mergedConfig.hideFilteredStatuses
     },
     hideStatus () {
-      return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)
+      return this.deleted || (this.muted && this.hideFilteredStatuses)
     },
     isFocused () {
       // retweet or root of an expanded conversation
@@ -164,37 +164,6 @@ const Status = {
         return user && user.screen_name
       }
     },
-    hideReply () {
-      if (this.mergedConfig.replyVisibility === 'all') {
-        return false
-      }
-      if (this.inConversation || !this.isReply) {
-        return false
-      }
-      if (this.status.user.id === this.currentUser.id) {
-        return false
-      }
-      if (this.status.type === 'retweet') {
-        return false
-      }
-      const checkFollowing = this.mergedConfig.replyVisibility === 'following'
-      for (var i = 0; i < this.status.attentions.length; ++i) {
-        if (this.status.user.id === this.status.attentions[i].id) {
-          continue
-        }
-        // There's zero guarantee of this working. If we happen to have that user and their
-        // relationship in store then it will work, but there's kinda little chance of having
-        // them for people you're not following.
-        const relationship = this.$store.state.users.relationships[this.status.attentions[i].id]
-        if (checkFollowing && relationship && relationship.following) {
-          return false
-        }
-        if (this.status.attentions[i].id === this.currentUser.id) {
-          return false
-        }
-      }
-      return this.status.attentions.length > 0
-    },
     replySubject () {
       if (!this.status.summary) return ''
       const decodedSummary = unescape(this.status.summary)
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 9a53acd6..3a244c83 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -45,6 +45,10 @@ const Timeline = {
     newStatusCount () {
       return this.timeline.newStatusCount
     },
+    showLoadButton () {
+      if (this.timelineError || this.errorData) return false
+      return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0
+    },
     newStatusCountStr () {
       if (this.timeline.flushMarker !== 0) {
         return ''
@@ -112,8 +116,6 @@ const Timeline = {
       if (e.key === '.') this.showNewStatuses()
     },
     showNewStatuses () {
-      if (this.newStatusCount === 0) return
-
       if (this.timeline.flushMarker !== 0) {
         this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true })
         this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 9777bd0c..bd8389b9 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -19,14 +19,14 @@
         {{ errorData.statusText }}
       </div>
       <button
-        v-if="timeline.newStatusCount > 0 && !timelineError && !errorData"
+        v-else-if="showLoadButton"
         class="loadmore-button"
         @click.prevent="showNewStatuses"
       >
         {{ $t('timeline.show_new') }}{{ newStatusCountStr }}
       </button>
       <div
-        v-if="!timeline.newStatusCount > 0 && !timelineError && !errorData"
+        v-else
         class="loadmore-text faint"
         @click.prevent
       >
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 073b15f1..4d3f8031 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -515,6 +515,11 @@ export const mutations = {
   queueFlush (state, { timeline, id }) {
     state.timelines[timeline].flushMarker = id
   },
+  queueFlushAll (state) {
+    Object.keys(state.timelines).forEach((timeline) => {
+      state.timelines[timeline].flushMarker = state.timelines[timeline].maxId
+    })
+  },
   addRepeats (state, { id, rebloggedByUsers, currentUser }) {
     const newStatus = state.allStatusesObject[id]
     newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
@@ -664,6 +669,9 @@ const statuses = {
     queueFlush ({ rootState, commit }, { timeline, id }) {
       commit('queueFlush', { timeline, id })
     },
+    queueFlushAll ({ rootState, commit }) {
+      commit('queueFlushAll')
+    },
     markNotificationsAsSeen ({ rootState, commit }) {
       commit('markNotificationsAsSeen')
       apiService.markNotificationsAsSeen({
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index dfffc291..7e5e9645 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -498,7 +498,8 @@ const fetchTimeline = ({
   until = false,
   userId = false,
   tag = false,
-  withMuted = false
+  withMuted = false,
+  replyVisibility = 'all'
 }) => {
   const timelineUrls = {
     public: MASTODON_PUBLIC_TIMELINE,
@@ -541,6 +542,9 @@ const fetchTimeline = ({
   if (timeline !== 'favorites') {
     params.push(['with_muted', withMuted])
   }
+  if (replyVisibility !== 'all') {
+    params.push(['reply_visibility', replyVisibility])
+  }
 
   params.push(['limit', 20])
 
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index c6b28ad5..30fb26bd 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -30,7 +30,8 @@ const fetchAndUpdate = ({
   const rootState = store.rootState || store.state
   const { getters } = store
   const timelineData = rootState.statuses.timelines[camelCase(timeline)]
-  const hideMutedPosts = getters.mergedConfig.hideMutedPosts
+  const { hideMutedPosts, replyVisibility } = getters.mergedConfig
+  const loggedIn = !!rootState.users.currentUser
 
   if (older) {
     args['until'] = until || timelineData.minId
@@ -41,6 +42,7 @@ const fetchAndUpdate = ({
   args['userId'] = userId
   args['tag'] = tag
   args['withMuted'] = !hideMutedPosts
+  if (loggedIn) args['replyVisibility'] = replyVisibility
 
   const numStatusesBeforeFetch = timelineData.statuses.length
 

From 38d8526660df4ca664c9ea50a660868be2cb5e49 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Tue, 30 Jun 2020 17:37:36 +0300
Subject: [PATCH 10/26] change Show New text to Reload when flushing

---
 src/components/timeline/timeline.js  | 6 +++---
 src/components/timeline/timeline.vue | 2 +-
 src/i18n/en.json                     | 1 +
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 3a244c83..d6519f4a 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -49,11 +49,11 @@ const Timeline = {
       if (this.timelineError || this.errorData) return false
       return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0
     },
-    newStatusCountStr () {
+    loadButtonString () {
       if (this.timeline.flushMarker !== 0) {
-        return ''
+        return this.$t('timeline.reload')
       } else {
-        return ` (${this.newStatusCount})`
+        return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
       }
     },
     classes () {
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index bd8389b9..111c0976 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -23,7 +23,7 @@
         class="loadmore-button"
         @click.prevent="showNewStatuses"
       >
-        {{ $t('timeline.show_new') }}{{ newStatusCountStr }}
+        {{ loadButtonString }}
       </button>
       <div
         v-else
diff --git a/src/i18n/en.json b/src/i18n/en.json
index e6af4266..59f69e57 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -617,6 +617,7 @@
     "no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
     "repeated": "repeated",
     "show_new": "Show new",
+    "reload": "Reload",
     "up_to_date": "Up-to-date",
     "no_more_statuses": "No more statuses",
     "no_statuses": "No statuses"

From 62d0bc47b333135f31abea90b4f5a3c28e608733 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Wed, 1 Jul 2020 14:15:04 +0300
Subject: [PATCH 11/26] remove unnecessary fetchAndUpdate, change notifications
 fetcher to not double fetch

---
 src/components/notifications/notifications.js              | 5 -----
 src/modules/api.js                                         | 3 ---
 .../backend_interactor_service.js                          | 4 ----
 .../notifications_fetcher/notifications_fetcher.service.js | 7 +++++--
 4 files changed, 5 insertions(+), 14 deletions(-)

diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index 26ffbab6..e999a18e 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -56,11 +56,6 @@ const Notifications = {
   components: {
     Notification
   },
-  created () {
-    const { dispatch } = this.$store
-
-    dispatch('fetchAndUpdateNotifications')
-  },
   watch: {
     unseenCount (count) {
       if (count > 0) {
diff --git a/src/modules/api.js b/src/modules/api.js
index 748570e5..04ef6ab4 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -138,9 +138,6 @@ const api = {
       if (!fetcher) return
       store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
     },
-    fetchAndUpdateNotifications (store) {
-      store.state.backendInteractor.fetchAndUpdateNotifications({ store })
-    },
 
     // Follow requests
     startFetchingFollowRequests (store) {
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index e1c32860..45e6bd0e 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -12,10 +12,6 @@ const backendInteractorService = credentials => ({
     return notificationsFetcher.startFetching({ store, credentials })
   },
 
-  fetchAndUpdateNotifications ({ store }) {
-    return notificationsFetcher.fetchAndUpdate({ store, credentials })
-  },
-
   startFetchingFollowRequests ({ store }) {
     return followRequestFetcher.startFetching({ store, credentials })
   },
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 64499a1b..581931f5 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -31,8 +31,11 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
     const notifications = timelineData.data
     const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
     if (readNotifsIds.length) {
-      args['since'] = Math.max(...readNotifsIds)
-      fetchNotifications({ store, args, older })
+      const possibleMax = Math.max(...readNotifsIds)
+      if (possibleMax !== timelineData.maxId) {
+        args['since'] = possibleMax
+        fetchNotifications({ store, args, older })
+      }
     }
 
     return result

From a3e370e9f816e3c98028bf82e8ac020b8e294466 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Wed, 1 Jul 2020 15:19:45 +0300
Subject: [PATCH 12/26] add initial fetching back in a more streamlined way

---
 src/components/notifications/notifications.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index e999a18e..30187072 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -27,6 +27,11 @@ const Notifications = {
       seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
     }
   },
+  created () {
+    const store = this.$store
+    const credentials = store.state.users.currentUser.credentials
+    notificationsFetcher.fetchAndUpdate({ store: this.$store, credentials })
+  },
   computed: {
     mainClass () {
       return this.minimalMode ? '' : 'panel panel-default'

From ca997f45e8b2e6c8df72833d774bad7266af76bf Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Wed, 1 Jul 2020 15:56:45 +0300
Subject: [PATCH 13/26] allow overscrolling enough to not have FAB block
 interactables

---
 src/App.scss | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/App.scss b/src/App.scss
index f2972eda..6597b6f4 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -858,6 +858,10 @@ nav {
     display: block;
     margin-right: 0.8em;
   }
+
+  .main {
+    margin-bottom: 7em;
+  }
 }
 
 .select-multiple {

From 3ebd4e4429a9680046daeac2ed8753471365be9e Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Wed, 1 Jul 2020 17:55:42 +0300
Subject: [PATCH 14/26] document the 'mark-as-read-detection' system

---
 .../notifications_fetcher.service.js            | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 581931f5..c2552480 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -27,17 +27,18 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
     }
     const result = fetchNotifications({ store, args, older })
 
-    // load unread notifications repeatedly to provide consistency between browser tabs
+    // If there's any unread notifications, try fetch notifications since
+    // the newest read notification to check if any of the unread notifs
+    // have changed their 'seen' state (marked as read in another session), so
+    // we can update the state in this session to mark them as read as well.
+    // The normal maxId-check does not tell if older notifications have changed
     const notifications = timelineData.data
     const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
-    if (readNotifsIds.length) {
-      const possibleMax = Math.max(...readNotifsIds)
-      if (possibleMax !== timelineData.maxId) {
-        args['since'] = possibleMax
-        fetchNotifications({ store, args, older })
-      }
+    const numUnseenNotifs = notifications.length - readNotifsIds.length
+    if (numUnseenNotifs > 0) {
+      args['since'] = Math.max(...readNotifsIds)
+      fetchNotifications({ store, args, older })
     }
-
     return result
   }
 }

From d30b0b28c9371e56ffe54b5a8b56087718221c1d Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Wed, 1 Jul 2020 19:15:28 +0300
Subject: [PATCH 15/26] catch localforage error and let the application work,
 add an alert for user to dismiss

---
 src/App.js               |  6 ++++++
 src/App.scss             |  9 +++++++++
 src/App.vue              | 10 ++++++++++
 src/i18n/en.json         |  3 +++
 src/main.js              | 13 +++++++++++--
 src/modules/instance.js  |  2 +-
 src/modules/interface.js |  7 +++++++
 7 files changed, 47 insertions(+), 3 deletions(-)

diff --git a/src/App.js b/src/App.js
index 040138c9..da66fe21 100644
--- a/src/App.js
+++ b/src/App.js
@@ -107,6 +107,9 @@ export default {
       return {
         'order': this.$store.state.instance.sidebarRight ? 99 : 0
       }
+    },
+    showStorageError () {
+      return this.$store.state.interface.storageError === 'show'
     }
   },
   methods: {
@@ -129,6 +132,9 @@ export default {
       if (changed) {
         this.$store.dispatch('setMobileLayout', mobileLayout)
       }
+    },
+    hideStorageError () {
+      this.$store.dispatch('setStorageError', 'hide')
     }
   }
 }
diff --git a/src/App.scss b/src/App.scss
index f2972eda..db447f1c 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -806,6 +806,15 @@ nav {
   }
 }
 
+.storage-error-notice {
+  text-align: center;
+  i {
+    cursor: pointer;
+    color: $fallback--text;
+    color: var(--alertErrorText, $fallback--text);
+  }
+}
+
 .button-icon {
   font-size: 1.2em;
 }
diff --git a/src/App.vue b/src/App.vue
index 7b9ad3dc..23991eac 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -101,6 +101,16 @@
         </div>
       </div>
       <div class="main">
+        <div
+          v-if="showStorageError"
+          class="alert error storage-error-notice"
+        >
+          {{ $t("errors.storage_unavailable") }}
+          <i
+            class="icon-cancel"
+            @click="hideStorageError"
+          />
+        </div>
         <div
           v-if="!currentUser"
           class="login-hint panel panel-default"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 59f69e57..4856008f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -163,6 +163,9 @@
     "load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
     "load_all": "Loading all {emojiAmount} emoji"
   },
+  "errors": {
+    "storage_unavailable": "Pleroma could not access browser storage. You may encounter issues and your login or your local settings won't be saved. Try enabling cookies."
+  },
   "interactions": {
     "favs_repeats": "Repeats and Favorites",
     "follows": "New follows",
diff --git a/src/main.js b/src/main.js
index 9a201e4f..bb2c8cd3 100644
--- a/src/main.js
+++ b/src/main.js
@@ -62,7 +62,16 @@ const persistedStateOptions = {
 };
 
 (async () => {
-  const persistedState = await createPersistedState(persistedStateOptions)
+  console.log('before perse state')
+  let persistedState
+  let storageError = 'none'
+  try {
+    persistedState = await createPersistedState(persistedStateOptions)
+  } catch (e) {
+    console.error(e)
+    storageError = 'show'
+    persistedState = _ => _
+  }
   const store = new Vuex.Store({
     modules: {
       i18n: {
@@ -89,7 +98,7 @@ const persistedStateOptions = {
     strict: false // Socket modifies itself, let's ignore this for now.
     // strict: process.env.NODE_ENV !== 'production'
   })
-
+  store.dispatch('setStorageError', storageError)
   afterStoreSetup({ store, i18n })
 })()
 
diff --git a/src/modules/instance.js b/src/modules/instance.js
index ec5f4e54..cc884317 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -8,7 +8,7 @@ const defaultState = {
   // Stuff from apiConfig
   name: 'Pleroma FE',
   registrationOpen: true,
-  server: 'http://localhost:4040/',
+  server: 'http://lain.com:4040',
   textlimit: 5000,
   themeData: undefined,
   vapidPublicKey: undefined,
diff --git a/src/modules/interface.js b/src/modules/interface.js
index eeebd65e..4b5b5b5d 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -8,6 +8,7 @@ const defaultState = {
     noticeClearTimeout: null,
     notificationPermission: null
   },
+  storageError: 'none',
   browserSupport: {
     cssFilter: window.CSS && window.CSS.supports && (
       window.CSS.supports('filter', 'drop-shadow(0 0)') ||
@@ -58,6 +59,9 @@ const interfaceMod = {
       if (!state.settingsModalLoaded) {
         state.settingsModalLoaded = true
       }
+    },
+    setStorageError (state, value) {
+      state.storageError = value
     }
   },
   actions: {
@@ -81,6 +85,9 @@ const interfaceMod = {
     },
     togglePeekSettingsModal ({ commit }) {
       commit('togglePeekSettingsModal')
+    },
+    setStorageError ({ commit }, value) {
+      commit('setStorageError', value)
     }
   }
 }

From 43b7a5d9b387e005a47debb5962c76c773cd862c Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Wed, 1 Jul 2020 19:22:39 +0300
Subject: [PATCH 16/26] update the message and changelog

---
 CHANGELOG.md     | 1 +
 src/i18n/en.json | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d978d362..b57e9afb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Fix status ellipsis menu being cut off in notifications column
 - Fixed autocomplete sometimes not returning the right user when there's already some results
 - Reply filtering options in Settings -> Filtering now work again using filtering on server
+- Don't show just blank-screen when cookies are disabled
 
 ## [2.0.3] - 2020-05-02
 ### Fixed
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4856008f..0b34ae07 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -164,7 +164,7 @@
     "load_all": "Loading all {emojiAmount} emoji"
   },
   "errors": {
-    "storage_unavailable": "Pleroma could not access browser storage. You may encounter issues and your login or your local settings won't be saved. Try enabling cookies."
+    "storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies."
   },
   "interactions": {
     "favs_repeats": "Repeats and Favorites",

From 15d492ace4db5243330741a007daa9929e221f64 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Wed, 1 Jul 2020 19:24:17 +0300
Subject: [PATCH 17/26] revert accidental change in instance.js

---
 src/modules/instance.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/modules/instance.js b/src/modules/instance.js
index cc884317..ec5f4e54 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -8,7 +8,7 @@ const defaultState = {
   // Stuff from apiConfig
   name: 'Pleroma FE',
   registrationOpen: true,
-  server: 'http://lain.com:4040',
+  server: 'http://localhost:4040/',
   textlimit: 5000,
   themeData: undefined,
   vapidPublicKey: undefined,

From 0997e5ff668a57d58002ec646f698c5503f66c35 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Wed, 1 Jul 2020 19:25:31 +0300
Subject: [PATCH 18/26] remove accidental log

---
 src/main.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main.js b/src/main.js
index bb2c8cd3..a7294ea0 100644
--- a/src/main.js
+++ b/src/main.js
@@ -62,7 +62,6 @@ const persistedStateOptions = {
 };
 
 (async () => {
-  console.log('before perse state')
   let persistedState
   let storageError = 'none'
   try {

From 1293bec77e5137acf64d3536c286f8ba3df284f4 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Thu, 2 Jul 2020 10:40:41 +0300
Subject: [PATCH 19/26] change storage error one-off into a global notice
 system

---
 src/App.js                                    | 10 +--
 src/App.scss                                  |  9 ---
 src/App.vue                                   | 11 +--
 .../global_notice_list/global_notice_list.js  | 15 ++++
 .../global_notice_list/global_notice_list.vue | 77 +++++++++++++++++++
 src/main.js                                   | 16 ++--
 src/modules/interface.js                      | 33 ++++++--
 src/services/theme_data/pleromafe.js          | 36 ++++++++-
 8 files changed, 167 insertions(+), 40 deletions(-)
 create mode 100644 src/components/global_notice_list/global_notice_list.js
 create mode 100644 src/components/global_notice_list/global_notice_list.vue

diff --git a/src/App.js b/src/App.js
index da66fe21..92c4e2f5 100644
--- a/src/App.js
+++ b/src/App.js
@@ -13,6 +13,7 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
 import MobileNav from './components/mobile_nav/mobile_nav.vue'
 import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
 import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
+import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
 import { windowWidth } from './services/window_utils/window_utils'
 
 export default {
@@ -32,7 +33,8 @@ export default {
     MobileNav,
     SettingsModal,
     UserReportingModal,
-    PostStatusModal
+    PostStatusModal,
+    GlobalNoticeList
   },
   data: () => ({
     mobileActivePanel: 'timeline',
@@ -107,9 +109,6 @@ export default {
       return {
         'order': this.$store.state.instance.sidebarRight ? 99 : 0
       }
-    },
-    showStorageError () {
-      return this.$store.state.interface.storageError === 'show'
     }
   },
   methods: {
@@ -132,9 +131,6 @@ export default {
       if (changed) {
         this.$store.dispatch('setMobileLayout', mobileLayout)
       }
-    },
-    hideStorageError () {
-      this.$store.dispatch('setStorageError', 'hide')
     }
   }
 }
diff --git a/src/App.scss b/src/App.scss
index db447f1c..f2972eda 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -806,15 +806,6 @@ nav {
   }
 }
 
-.storage-error-notice {
-  text-align: center;
-  i {
-    cursor: pointer;
-    color: $fallback--text;
-    color: var(--alertErrorText, $fallback--text);
-  }
-}
-
 .button-icon {
   font-size: 1.2em;
 }
diff --git a/src/App.vue b/src/App.vue
index 23991eac..03b632ec 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -101,16 +101,6 @@
         </div>
       </div>
       <div class="main">
-        <div
-          v-if="showStorageError"
-          class="alert error storage-error-notice"
-        >
-          {{ $t("errors.storage_unavailable") }}
-          <i
-            class="icon-cancel"
-            @click="hideStorageError"
-          />
-        </div>
         <div
           v-if="!currentUser"
           class="login-hint panel panel-default"
@@ -138,6 +128,7 @@
     <PostStatusModal />
     <SettingsModal />
     <portal-target name="modal" />
+    <GlobalNoticeList />
   </div>
 </template>
 
diff --git a/src/components/global_notice_list/global_notice_list.js b/src/components/global_notice_list/global_notice_list.js
new file mode 100644
index 00000000..3af29c23
--- /dev/null
+++ b/src/components/global_notice_list/global_notice_list.js
@@ -0,0 +1,15 @@
+
+const GlobalNoticeList = {
+  computed: {
+    notices () {
+      return this.$store.state.interface.globalNotices
+    }
+  },
+  methods: {
+    closeNotice (notice) {
+      this.$store.dispatch('removeGlobalNotice', notice)
+    }
+  }
+}
+
+export default GlobalNoticeList
diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue
new file mode 100644
index 00000000..0e4285cc
--- /dev/null
+++ b/src/components/global_notice_list/global_notice_list.vue
@@ -0,0 +1,77 @@
+<template>
+  <div class="global-notice-list">
+    <div
+      v-for="(notice, index) in notices"
+      :key="index"
+      class="alert global-notice"
+      :class="{ ['global-' + notice.level]: true }"
+    >
+      <div class="notice-message">
+        {{ $t(notice.messageKey, notice.messageArgs) }}
+      </div>
+      <i
+        class="button-icon icon-cancel"
+        @click="closeNotice(notice)"
+      />
+    </div>
+  </div>
+</template>
+
+<script src="./global_notice_list.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.global-notice-list {
+  position: fixed;
+  top: 50px;
+  width: 100%;
+  pointer-events: none;
+  z-index: 1001;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+
+  .global-notice {
+    pointer-events: auto;
+    text-align: center;
+    width: 40em;
+    max-width: calc(100% - 3em);
+    display: flex;
+    padding-left: 1.5em;
+    line-height: 2em;
+    .notice-message {
+      flex: 1 1 100%;
+    }
+    i {
+      flex: 0 0;
+      width: 1.5em;
+      cursor: pointer;
+    }
+  }
+
+  .global-error {
+    background-color: var(--alertPopupError, $fallback--cRed);
+    color: var(--alertPopupErrorText, $fallback--text);
+    i {
+      color: var(--alertPopupErrorText, $fallback--text);
+    }
+  }
+
+  .global-warning {
+    background-color: var(--alertPopupWarning, $fallback--cOrange);
+    color: var(--alertPopupWarningText, $fallback--text);
+    i {
+      color: var(--alertPopupWarningText, $fallback--text);
+    }
+  }
+
+  .global-info {
+    background-color: var(--alertPopupNeutral, $fallback--fg);
+    color: var(--alertPopupNeutralText, $fallback--text);
+    i {
+      color: var(--alertPopupNeutralText, $fallback--text);
+    }
+  }
+}
+</style>
diff --git a/src/main.js b/src/main.js
index a7294ea0..5bddc76e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -62,14 +62,14 @@ const persistedStateOptions = {
 };
 
 (async () => {
-  let persistedState
-  let storageError = 'none'
+  let storageError = false
+  const plugins = [pushNotifications]
   try {
-    persistedState = await createPersistedState(persistedStateOptions)
+    const persistedState = await createPersistedState(persistedStateOptions)
+    plugins.push(persistedState)
   } catch (e) {
     console.error(e)
-    storageError = 'show'
-    persistedState = _ => _
+    storageError = true
   }
   const store = new Vuex.Store({
     modules: {
@@ -93,11 +93,13 @@ const persistedStateOptions = {
       polls: pollsModule,
       postStatus: postStatusModule
     },
-    plugins: [persistedState, pushNotifications],
+    plugins,
     strict: false // Socket modifies itself, let's ignore this for now.
     // strict: process.env.NODE_ENV !== 'production'
   })
-  store.dispatch('setStorageError', storageError)
+  if (storageError) {
+    store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' })
+  }
   afterStoreSetup({ store, i18n })
 })()
 
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 4b5b5b5d..338ef651 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -8,14 +8,14 @@ const defaultState = {
     noticeClearTimeout: null,
     notificationPermission: null
   },
-  storageError: 'none',
   browserSupport: {
     cssFilter: window.CSS && window.CSS.supports && (
       window.CSS.supports('filter', 'drop-shadow(0 0)') ||
       window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
     )
   },
-  mobileLayout: false
+  mobileLayout: false,
+  globalNotices: []
 }
 
 const interfaceMod = {
@@ -60,8 +60,11 @@ const interfaceMod = {
         state.settingsModalLoaded = true
       }
     },
-    setStorageError (state, value) {
-      state.storageError = value
+    pushGlobalNotice (state, notice) {
+      state.globalNotices.push(notice)
+    },
+    removeGlobalNotice (state, notice) {
+      state.globalNotices = state.globalNotices.filter(n => n !== notice)
     }
   },
   actions: {
@@ -86,8 +89,26 @@ const interfaceMod = {
     togglePeekSettingsModal ({ commit }) {
       commit('togglePeekSettingsModal')
     },
-    setStorageError ({ commit }, value) {
-      commit('setStorageError', value)
+    pushGlobalNotice (
+      { commit, dispatch },
+      {
+        messageKey,
+        messageArgs = {},
+        level = 'error',
+        timeout = 0
+      }) {
+      const notice = {
+        messageKey,
+        messageArgs,
+        level
+      }
+      if (timeout) {
+        setTimeout(() => dispatch('removeGlobalNotice', notice), timeout)
+      }
+      commit('pushGlobalNotice', notice)
+    },
+    removeGlobalNotice ({ commit }, notice) {
+      commit('removeGlobalNotice', notice)
     }
   }
 }
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index b577cfab..83c878ed 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -34,7 +34,8 @@ export const DEFAULT_OPACITY = {
   alert: 0.5,
   input: 0.5,
   faint: 0.5,
-  underlay: 0.15
+  underlay: 0.15,
+  alertPopup: 0.85
 }
 
 /**  SUBJECT TO CHANGE IN THE FUTURE, this is all beta
@@ -627,6 +628,39 @@ export const SLOT_INHERITANCE = {
     textColor: true
   },
 
+  alertPopupError: {
+    depends: ['alertError'],
+    opacity: 'alertPopup'
+  },
+  alertPopupErrorText: {
+    depends: ['alertErrorText'],
+    layer: 'popover',
+    variant: 'alertPopupError',
+    textColor: true
+  },
+
+  alertPopupWarning: {
+    depends: ['alertWarning'],
+    opacity: 'alertPopup'
+  },
+  alertPopupWarningText: {
+    depends: ['alertWarningText'],
+    layer: 'popover',
+    variant: 'alertPopupWarning',
+    textColor: true
+  },
+
+  alertPopupNeutral: {
+    depends: ['alertNeutral'],
+    opacity: 'alertPopup'
+  },
+  alertPopupNeutralText: {
+    depends: ['alertNeutralText'],
+    layer: 'popover',
+    variant: 'alertPopupNeutral',
+    textColor: true
+  },
+
   badgeNotification: '--cRed',
   badgeNotificationText: {
     depends: ['text', 'badgeNotification'],

From 685ab4f33ee57e1dc4997b58314e5c0d38581e9d Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Thu, 2 Jul 2020 10:46:43 +0300
Subject: [PATCH 20/26] make the addNotice dispatch return the notice

---
 src/modules/interface.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/modules/interface.js b/src/modules/interface.js
index 338ef651..e31630fc 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -106,6 +106,7 @@ const interfaceMod = {
         setTimeout(() => dispatch('removeGlobalNotice', notice), timeout)
       }
       commit('pushGlobalNotice', notice)
+      return notice
     },
     removeGlobalNotice ({ commit }, notice) {
       commit('removeGlobalNotice', notice)

From f0668c9ff8b985cd5f537fbb0d1083480cbfb065 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Thu, 2 Jul 2020 12:19:33 +0300
Subject: [PATCH 21/26] add follow request users to store

---
 .../follow_request_fetcher/follow_request_fetcher.service.js     | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js
index 786740b7..93fac9bc 100644
--- a/src/services/follow_request_fetcher/follow_request_fetcher.service.js
+++ b/src/services/follow_request_fetcher/follow_request_fetcher.service.js
@@ -4,6 +4,7 @@ const fetchAndUpdate = ({ store, credentials }) => {
   return apiService.fetchFollowRequests({ credentials })
     .then((requests) => {
       store.commit('setFollowRequests', requests)
+      store.commit('addNewUsers', requests)
     }, () => {})
     .catch(() => {})
 }

From 9cac5d94dd78ea331254307871e876da7e266a6f Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Thu, 2 Jul 2020 15:17:58 +0300
Subject: [PATCH 22/26] change alert popup alpha

---
 src/services/theme_data/pleromafe.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 83c878ed..6b25cd6f 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -35,7 +35,7 @@ export const DEFAULT_OPACITY = {
   input: 0.5,
   faint: 0.5,
   underlay: 0.15,
-  alertPopup: 0.85
+  alertPopup: 0.95
 }
 
 /**  SUBJECT TO CHANGE IN THE FUTURE, this is all beta

From 08b593746faf54ac41df25bb2cc5a1f72ae1a9b5 Mon Sep 17 00:00:00 2001
From: Ilja <pleroma@spectraltheorem.be>
Date: Fri, 3 Jul 2020 10:17:42 +0000
Subject: [PATCH 23/26] FE part of BE issue 1586 provide index md

* I added an index.md which will be the landing page for the docs. It has an explanation of Pleroma-FE from the user point of view
* See also BE MR: https://git.pleroma.social/pleroma/pleroma/-/merge_requests/2669
* And issue: https://git.pleroma.social/pleroma/pleroma/-/issues/1586
---
 docs/USER_GUIDE.md | 2 --
 docs/index.md      | 8 ++++++++
 2 files changed, 8 insertions(+), 2 deletions(-)
 create mode 100644 docs/index.md

diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index f417f33d..241ad331 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -8,8 +8,6 @@
 >
 > --Catbag
 
-Pleroma-FE user interface is modeled after Qvitter which is modeled after older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
-
 ## Posting, reading, basic functions.
 
 After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..8764f9ab
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,8 @@
+# Introduction to Pleroma-FE
+## What is Pleroma-FE?
+
+Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
+
+## How can I use it?
+
+If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./USER_GUIDE.md). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md).

From de291e2e33f1d9e04b27ed30ba3b012d73178e63 Mon Sep 17 00:00:00 2001
From: Eugenij <eugenijm@protonmail.com>
Date: Fri, 3 Jul 2020 19:45:49 +0000
Subject: [PATCH 24/26] Add bookmarks

Co-authored-by: jared <jaredrmain@gmail.com>
---
 CHANGELOG.md                                  |  1 +
 package.json                                  |  1 +
 src/boot/routes.js                            |  2 +
 .../bookmark_timeline/bookmark_timeline.js    | 17 ++++++++
 .../bookmark_timeline/bookmark_timeline.vue   |  9 ++++
 src/components/extra_buttons/extra_buttons.js | 10 +++++
 .../extra_buttons/extra_buttons.vue           | 16 +++++++
 src/components/nav_panel/nav_panel.vue        |  5 +++
 src/components/side_drawer/side_drawer.vue    |  8 ++++
 src/components/timeline/timeline.js           |  2 +-
 src/i18n/en.json                              |  6 ++-
 src/i18n/ru.json                              |  7 ++-
 src/modules/statuses.js                       | 43 +++++++++++++++----
 src/services/api/api.service.js               | 34 +++++++++++++--
 .../entity_normalizer.service.js              | 16 +++++++
 .../notifications_fetcher.service.js          |  2 +-
 .../timeline_fetcher.service.js               | 17 +++++---
 static/fontello.json                          | 12 ++++++
 .../entity_normalizer.spec.js                 | 22 +++++++++-
 yarn.lock                                     |  7 +++
 20 files changed, 213 insertions(+), 24 deletions(-)
 create mode 100644 src/components/bookmark_timeline/bookmark_timeline.js
 create mode 100644 src/components/bookmark_timeline/bookmark_timeline.vue
 mode change 100755 => 100644 static/fontello.json

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d978d362..2595af1d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -99,6 +99,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Ability to change user's email
 - About page
 - Added remote user redirect
+- Bookmarks
 ### Changed
 - changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
 ### Fixed
diff --git a/package.json b/package.json
index c0665f6e..96231171 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
     "cropperjs": "^1.4.3",
     "diff": "^3.0.1",
     "escape-html": "^1.0.3",
+    "parse-link-header": "^1.0.1",
     "localforage": "^1.5.0",
     "phoenix": "^1.3.0",
     "portal-vue": "^2.1.4",
diff --git a/src/boot/routes.js b/src/boot/routes.js
index d98a3b50..f63d8adf 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -2,6 +2,7 @@ import PublicTimeline from 'components/public_timeline/public_timeline.vue'
 import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
 import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
 import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
+import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
 import ConversationPage from 'components/conversation-page/conversation-page.vue'
 import Interactions from 'components/interactions/interactions.vue'
 import DMs from 'components/dm_timeline/dm_timeline.vue'
@@ -40,6 +41,7 @@ export default (store) => {
     { name: 'public-timeline', path: '/main/public', component: PublicTimeline },
     { name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
     { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
+    { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
     { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
     { name: 'remote-user-profile-acct',
       path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
diff --git a/src/components/bookmark_timeline/bookmark_timeline.js b/src/components/bookmark_timeline/bookmark_timeline.js
new file mode 100644
index 00000000..64b69e5d
--- /dev/null
+++ b/src/components/bookmark_timeline/bookmark_timeline.js
@@ -0,0 +1,17 @@
+import Timeline from '../timeline/timeline.vue'
+
+const Bookmarks = {
+  computed: {
+    timeline () {
+      return this.$store.state.statuses.timelines.bookmarks
+    }
+  },
+  components: {
+    Timeline
+  },
+  destroyed () {
+    this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
+  }
+}
+
+export default Bookmarks
diff --git a/src/components/bookmark_timeline/bookmark_timeline.vue b/src/components/bookmark_timeline/bookmark_timeline.vue
new file mode 100644
index 00000000..8da6884b
--- /dev/null
+++ b/src/components/bookmark_timeline/bookmark_timeline.vue
@@ -0,0 +1,9 @@
+<template>
+  <Timeline
+    :title="$t('nav.bookmarks')"
+    :timeline="timeline"
+    :timeline-name="'bookmarks'"
+  />
+</template>
+
+<script src="./bookmark_timeline.js"></script>
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index e4b19d01..5e0c36bb 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -34,6 +34,16 @@ const ExtraButtons = {
       navigator.clipboard.writeText(this.statusLink)
         .then(() => this.$emit('onSuccess'))
         .catch(err => this.$emit('onError', err.error.error))
+    },
+    bookmarkStatus () {
+      this.$store.dispatch('bookmark', { id: this.status.id })
+        .then(() => this.$emit('onSuccess'))
+        .catch(err => this.$emit('onError', err.error.error))
+    },
+    unbookmarkStatus () {
+      this.$store.dispatch('unbookmark', { id: this.status.id })
+        .then(() => this.$emit('onSuccess'))
+        .catch(err => this.$emit('onError', err.error.error))
     }
   },
   computed: {
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 68db6fd8..7a4e8642 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -40,6 +40,22 @@
         >
           <i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
         </button>
+        <button
+          v-if="!status.bookmarked"
+          class="dropdown-item dropdown-item-icon"
+          @click.prevent="bookmarkStatus"
+          @click="close"
+        >
+          <i class="icon-bookmark-empty" /><span>{{ $t("status.bookmark") }}</span>
+        </button>
+        <button
+          v-if="status.bookmarked"
+          class="dropdown-item dropdown-item-icon"
+          @click.prevent="unbookmarkStatus"
+          @click="close"
+        >
+          <i class="icon-bookmark" /><span>{{ $t("status.unbookmark") }}</span>
+        </button>
         <button
           v-if="canDelete"
           class="dropdown-item dropdown-item-icon"
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 8cd04dc7..f164b2b0 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -17,6 +17,11 @@
             <i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
           </router-link>
         </li>
+        <li v-if="currentUser">
+          <router-link :to="{ name: 'bookmarks'}">
+            <i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }}
+          </router-link>
+        </li>
         <li v-if="currentUser && currentUser.locked">
           <router-link :to="{ name: 'friend-requests' }">
             <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index f253742d..0ac53b34 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -65,6 +65,14 @@
             <i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
           </router-link>
         </li>
+        <li
+          v-if="currentUser"
+          @click="toggleDrawer"
+        >
+          <router-link :to="{ name: 'bookmarks'}">
+            <i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }}
+          </router-link>
+        </li>
         <li
           v-if="currentUser && currentUser.locked"
           @click="toggleDrawer"
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index d6519f4a..bac73022 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -137,7 +137,7 @@ const Timeline = {
         showImmediately: true,
         userId: this.userId,
         tag: this.tag
-      }).then(statuses => {
+      }).then(({ statuses }) => {
         store.commit('setLoading', { timeline: this.timelineName, value: false })
         if (statuses && statuses.length === 0) {
           this.bottomedOut = true
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 59f69e57..2e48c473 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -120,6 +120,7 @@
     "public_tl": "Public Timeline",
     "timeline": "Timeline",
     "twkn": "The Whole Known Network",
+    "bookmarks": "Bookmarks",
     "user_search": "User Search",
     "search": "Search",
     "who_to_follow": "Who to follow",
@@ -629,6 +630,8 @@
     "pin": "Pin on profile",
     "unpin": "Unpin from profile",
     "pinned": "Pinned",
+    "bookmark": "Bookmark",
+    "unbookmark": "Unbookmark",
     "delete_confirm": "Do you really want to delete this status?",
     "reply_to": "Reply to",
     "replies_list": "Replies:",
@@ -724,7 +727,8 @@
     "add_reaction": "Add Reaction",
     "user_settings": "User Settings",
     "accept_follow_request": "Accept follow request",
-    "reject_follow_request": "Reject follow request"
+    "reject_follow_request": "Reject follow request",
+    "bookmark": "Bookmark"
   },
   "upload": {
     "error": {
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index aa78db26..08f05d18 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -45,7 +45,8 @@
     "timeline": "Лента",
     "twkn": "Федеративная лента",
     "search": "Поиск",
-    "friend_requests": "Запросы на чтение"
+    "friend_requests": "Запросы на чтение",
+    "bookmarks": "Закладки"
   },
   "notifications": {
     "broken_favorite": "Неизвестный статус, ищем...",
@@ -366,6 +367,10 @@
     "show_new": "Показать новые",
     "up_to_date": "Обновлено"
   },
+  "status": {
+    "bookmark": "В закладки",
+    "unbookmark": "Удалить из закладок"
+  },
   "user_card": {
     "block": "Заблокировать",
     "blocked": "Заблокирован",
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 4d3f8031..7fbf685c 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -62,7 +62,8 @@ export const defaultState = () => ({
     publicAndExternal: emptyTl(),
     friends: emptyTl(),
     tag: emptyTl(),
-    dms: emptyTl()
+    dms: emptyTl(),
+    bookmarks: emptyTl()
   }
 })
 
@@ -163,8 +164,7 @@ const removeStatusFromGlobalStorage = (state, status) => {
   }
 }
 
-const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {},
-  noIdUpdate = false, userId }) => {
+const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => {
   // Sanity check
   if (!isArray(statuses)) {
     return false
@@ -173,8 +173,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
   const allStatuses = state.allStatuses
   const timelineObject = state.timelines[timeline]
 
-  const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
-  const minNew = statuses.length > 0 ? minBy(statuses, 'id').id : 0
+  // Mismatch between API pagination and our internal minId/maxId tracking systems:
+  // pagination.maxId is the oldest of the returned statuses when fetching older,
+  // and pagination.minId is the newest when fetching newer. The names come directly
+  // from the arguments they're supposed to be passed as for the next fetch.
+  const minNew = pagination.maxId || (statuses.length > 0 ? minBy(statuses, 'id').id : 0)
+  const maxNew = pagination.minId || (statuses.length > 0 ? maxBy(statuses, 'id').id : 0)
+
   const newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0
   const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0
 
@@ -315,7 +320,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
   })
 
   // Keep the visible statuses sorted
-  if (timeline) {
+  if (timeline && !(timeline === 'bookmarks')) {
     sortTimeline(timelineObject)
   }
 }
@@ -463,6 +468,14 @@ export const mutations = {
       newStatus.rebloggedBy.push(user)
     }
   },
+  setBookmarked (state, { status, value }) {
+    const newStatus = state.allStatusesObject[status.id]
+    newStatus.bookmarked = value
+  },
+  setBookmarkedConfirm (state, { status }) {
+    const newStatus = state.allStatusesObject[status.id]
+    newStatus.bookmarked = status.bookmarked
+  },
   setDeleted (state, { status }) {
     const newStatus = state.allStatusesObject[status.id]
     newStatus.deleted = true
@@ -590,8 +603,8 @@ export const mutations = {
 const statuses = {
   state: defaultState(),
   actions: {
-    addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
-      commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
+    addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
+      commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
     },
     addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
       commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
@@ -666,6 +679,20 @@ const statuses = {
       rootState.api.backendInteractor.unretweet({ id: status.id })
         .then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
     },
+    bookmark ({ rootState, commit }, status) {
+      commit('setBookmarked', { status, value: true })
+      rootState.api.backendInteractor.bookmarkStatus({ id: status.id })
+        .then(status => {
+          commit('setBookmarkedConfirm', { status })
+        })
+    },
+    unbookmark ({ rootState, commit }, status) {
+      commit('setBookmarked', { status, value: false })
+      rootState.api.backendInteractor.unbookmarkStatus({ id: status.id })
+        .then(status => {
+          commit('setBookmarkedConfirm', { status })
+        })
+    },
     queueFlush ({ rootState, commit }, { timeline, id }) {
       commit('queueFlush', { timeline, id })
     },
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 7e5e9645..c9ec88b7 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,5 +1,5 @@
 import { each, map, concat, last, get } from 'lodash'
-import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
 import { RegistrationError, StatusCodeError } from '../errors/errors'
 
 /* eslint-env browser */
@@ -50,6 +50,7 @@ const MASTODON_USER_URL = '/api/v1/accounts'
 const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
 const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
 const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}`
+const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks'
 const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
 const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
 const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
@@ -58,6 +59,8 @@ const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
 const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
 const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe`
 const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe`
+const MASTODON_BOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/bookmark`
+const MASTODON_UNBOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/unbookmark`
 const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
 const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
 const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
@@ -510,7 +513,8 @@ const fetchTimeline = ({
     user: MASTODON_USER_TIMELINE_URL,
     media: MASTODON_USER_TIMELINE_URL,
     favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
-    tag: MASTODON_TAG_TIMELINE_URL
+    tag: MASTODON_TAG_TIMELINE_URL,
+    bookmarks: MASTODON_BOOKMARK_TIMELINE_URL
   }
   const isNotifications = timeline === 'notifications'
   const params = []
@@ -539,7 +543,7 @@ const fetchTimeline = ({
   if (timeline === 'public' || timeline === 'publicAndExternal') {
     params.push(['only_media', false])
   }
-  if (timeline !== 'favorites') {
+  if (timeline !== 'favorites' && timeline !== 'bookmarks') {
     params.push(['with_muted', withMuted])
   }
   if (replyVisibility !== 'all') {
@@ -552,16 +556,20 @@ const fetchTimeline = ({
   url += `?${queryString}`
   let status = ''
   let statusText = ''
+  let pagination = {}
   return fetch(url, { headers: authHeaders(credentials) })
     .then((data) => {
       status = data.status
       statusText = data.statusText
+      pagination = parseLinkHeaderPagination(data.headers.get('Link'), {
+        flakeId: timeline !== 'bookmarks' && timeline !== 'notifications'
+      })
       return data
     })
     .then((data) => data.json())
     .then((data) => {
       if (!data.error) {
-        return data.map(isNotifications ? parseNotification : parseStatus)
+        return { data: data.map(isNotifications ? parseNotification : parseStatus), pagination }
       } else {
         data.status = status
         data.statusText = statusText
@@ -612,6 +620,22 @@ const unretweet = ({ id, credentials }) => {
     .then((data) => parseStatus(data))
 }
 
+const bookmarkStatus = ({ id, credentials }) => {
+  return promisedRequest({
+    url: MASTODON_BOOKMARK_STATUS_URL(id),
+    headers: authHeaders(credentials),
+    method: 'POST'
+  })
+}
+
+const unbookmarkStatus = ({ id, credentials }) => {
+  return promisedRequest({
+    url: MASTODON_UNBOOKMARK_STATUS_URL(id),
+    headers: authHeaders(credentials),
+    method: 'POST'
+  })
+}
+
 const postStatus = ({
   credentials,
   status,
@@ -1150,6 +1174,8 @@ const apiService = {
   unfavorite,
   retweet,
   unretweet,
+  bookmarkStatus,
+  unbookmarkStatus,
   postStatus,
   deleteStatus,
   uploadMedia,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 3bdb92f3..0d27dc97 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,4 +1,5 @@
 import escape from 'escape-html'
+import parseLinkHeader from 'parse-link-header'
 import { isStatusNotification } from '../notification_utils/notification_utils.js'
 
 const qvitterStatusType = (status) => {
@@ -232,6 +233,8 @@ export const parseStatus = (data) => {
     output.repeated = data.reblogged
     output.repeat_num = data.reblogs_count
 
+    output.bookmarked = data.bookmarked
+
     output.type = data.reblog ? 'retweet' : 'status'
     output.nsfw = data.sensitive
 
@@ -381,3 +384,16 @@ const isNsfw = (status) => {
   const nsfwRegex = /#nsfw/i
   return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
 }
+
+export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
+  const flakeId = opts.flakeId
+  const parsedLinkHeader = parseLinkHeader(linkHeader)
+  if (!parsedLinkHeader) return
+  const maxId = parsedLinkHeader.next.max_id
+  const minId = parsedLinkHeader.prev.min_id
+
+  return {
+    maxId: flakeId ? maxId : parseInt(maxId, 10),
+    minId: flakeId ? minId : parseInt(minId, 10)
+  }
+}
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 64499a1b..4644e449 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -41,7 +41,7 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
 
 const fetchNotifications = ({ store, args, older }) => {
   return apiService.fetchTimeline(args)
-    .then((notifications) => {
+    .then(({ data: notifications }) => {
       update({ store, notifications, older })
       return notifications
     }, () => store.dispatch('setNotificationsError', { value: true }))
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 30fb26bd..214294eb 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -2,7 +2,7 @@ import { camelCase } from 'lodash'
 
 import apiService from '../api/api.service.js'
 
-const update = ({ store, statuses, timeline, showImmediately, userId }) => {
+const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => {
   const ccTimeline = camelCase(timeline)
 
   store.dispatch('setError', { value: false })
@@ -12,7 +12,8 @@ const update = ({ store, statuses, timeline, showImmediately, userId }) => {
     timeline: ccTimeline,
     userId,
     statuses,
-    showImmediately
+    showImmediately,
+    pagination
   })
 }
 
@@ -47,16 +48,18 @@ const fetchAndUpdate = ({
   const numStatusesBeforeFetch = timelineData.statuses.length
 
   return apiService.fetchTimeline(args)
-    .then((statuses) => {
-      if (statuses.error) {
-        store.dispatch('setErrorData', { value: statuses })
+    .then(response => {
+      if (response.error) {
+        store.dispatch('setErrorData', { value: response })
         return
       }
+
+      const { data: statuses, pagination } = response
       if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) {
         store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
       }
-      update({ store, statuses, timeline, showImmediately, userId })
-      return statuses
+      update({ store, statuses, timeline, showImmediately, userId, pagination })
+      return { statuses, pagination }
     }, () => store.dispatch('setError', { value: true }))
 }
 
diff --git a/static/fontello.json b/static/fontello.json
old mode 100755
new mode 100644
index ac3f0a18..6083c0bf
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -375,6 +375,18 @@
       "css": "download",
       "code": 59429,
       "src": "fontawesome"
+    },
+    {
+      "uid": "f04a5d24e9e659145b966739c4fde82a",
+      "css": "bookmark",
+      "code": 59430,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "2f5ef6f6b7aaebc56458ab4e865beff5",
+      "css": "bookmark-empty",
+      "code": 61591,
+      "src": "fontawesome"
     }
   ]
 }
\ No newline at end of file
diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
index ccb57942..e1f7a958 100644
--- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -1,4 +1,4 @@
-import { parseStatus, parseUser, parseNotification, addEmojis } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, addEmojis, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
 import mastoapidata from '../../../../fixtures/mastoapi.json'
 import qvitterapidata from '../../../../fixtures/statuses.json'
 
@@ -383,4 +383,24 @@ describe('API Entities normalizer', () => {
       expect(result).to.include('title=\':[a-z] {|}*:\'')
     })
   })
+
+  describe('Link header pagination', () => {
+    it('Parses min and max ids as integers', () => {
+      const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"'
+      const result = parseLinkHeaderPagination(linkHeader)
+      expect(result).to.eql({
+        'maxId': 861676,
+        'minId': 861741
+      })
+    })
+
+    it('Parses min and max ids as flakes', () => {
+      const linkHeader = '<http://example.com/api/v1/timelines/home?max_id=9waQx5IIS48qVue2Ai>; rel="next", <http://example.com/api/v1/timelines/home?min_id=9wi61nIPnfn674xgie>; rel="prev"'
+      const result = parseLinkHeaderPagination(linkHeader, { flakeId: true })
+      expect(result).to.eql({
+        'maxId': '9waQx5IIS48qVue2Ai',
+        'minId': '9wi61nIPnfn674xgie'
+      })
+    })
+  })
 })
diff --git a/yarn.lock b/yarn.lock
index f05b00b1..09316863 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5751,6 +5751,13 @@ parse-json@^4.0.0:
     error-ex "^1.3.1"
     json-parse-better-errors "^1.0.1"
 
+parse-link-header@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7"
+  integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc=
+  dependencies:
+    xtend "~4.0.1"
+
 parseqs@0.0.5:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"

From 9b40cf43d80f61d2afdaed757e4f873bf2dfd0a4 Mon Sep 17 00:00:00 2001
From: kPherox <admin@mail.kr-kp.com>
Date: Sat, 4 Jul 2020 18:42:15 +0900
Subject: [PATCH 25/26] fix height for emoji panel of settings modal

---
 src/components/settings_modal/settings_modal.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 833ff89a..0da4d9a8 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -30,7 +30,7 @@
       height: 100vh;
     }
 
-    .panel-body {
+    >.panel-body {
       height: 100%;
       overflow-y: hidden;
 

From 9178908c1ea8d0ad99b4a631abb9594edc3765bf Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Sun, 5 Jul 2020 06:54:12 +0000
Subject: [PATCH 26/26] Apply suggestion to
 src/components/notifications/notifications.js

---
 src/components/notifications/notifications.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index 30187072..d8a327b0 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -30,7 +30,7 @@ const Notifications = {
   created () {
     const store = this.$store
     const credentials = store.state.users.currentUser.credentials
-    notificationsFetcher.fetchAndUpdate({ store: this.$store, credentials })
+    notificationsFetcher.fetchAndUpdate({ store, credentials })
   },
   computed: {
     mainClass () {