From 44dea9f3646a5c27083dfe6cd6b1522e11c7dc69 Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Sun, 9 Feb 2020 17:25:24 -0500
Subject: [PATCH 01/32] Allow emoji suggestions based on a match anywhere in
 the emoji name, but improve sorting

---
 src/components/emoji_input/suggestor.js | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index aec5c39d..9e437ccc 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -29,17 +29,21 @@ export default data => input => {
 export const suggestEmoji = emojis => input => {
   const noPrefix = input.toLowerCase().substr(1)
   return emojis
-    .filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
+    .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
     .sort((a, b) => {
       let aScore = 0
       let bScore = 0
 
-      // Make custom emojis a priority
-      aScore += a.imageUrl ? 10 : 0
-      bScore += b.imageUrl ? 10 : 0
+      // Prioritize emoji that start with the input string
+      aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
+      bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
 
-      // Sort alphabetically
-      const alphabetically = a.displayText > b.displayText ? 1 : -1
+      // Sort by length
+      aScore -= a.displayText.length
+      bScore -= b.displayText.length
+
+      // Break ties alphabetically
+      const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
 
       return bScore - aScore + alphabetically
     })

From 02864bc07b2ab2f08232ba1c4c27079454dc87ef Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Mon, 10 Feb 2020 09:32:07 -0500
Subject: [PATCH 02/32] Prioritize custom emoji a lot and boost exact matches
 to the top

---
 src/components/emoji_input/suggestor.js | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index 9e437ccc..15a71eff 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -34,7 +34,15 @@ export const suggestEmoji = emojis => input => {
       let aScore = 0
       let bScore = 0
 
-      // Prioritize emoji that start with the input string
+      // An exact match always wins
+      aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
+      bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
+
+      // Prioritize custom emoji a lot
+      aScore += a.imageUrl ? 100 : 0
+      bScore += b.imageUrl ? 100 : 0
+
+      // Prioritize prefix matches somewhat
       aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
       bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
 

From 86561592d002b08d6b2cd9549e8057a4ffd091cb Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Mon, 24 Feb 2020 11:19:00 -0600
Subject: [PATCH 03/32] First attempt at not requiring email address for
 registration

---
 src/boot/after_store.js                     | 3 +++
 src/components/registration/registration.js | 9 +++++++--
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index d70e1058..9fb9a853 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -241,6 +241,9 @@ const getNodeInfo = async ({ store }) => {
           : federation.enabled
       })
 
+      const accountActivationRequired = metadata.accountActivationRequired
+      store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
+
       const accounts = metadata.staffAccounts
       resolveStaffAccounts({ store, accounts })
     } else {
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index ace8cc7c..fd2942a5 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -1,5 +1,5 @@
 import { validationMixin } from 'vuelidate'
-import { required, sameAs } from 'vuelidate/lib/validators'
+import { required, requiredIf, sameAs } from 'vuelidate/lib/validators'
 import { mapActions, mapState } from 'vuex'
 
 const registration = {
@@ -16,7 +16,7 @@ const registration = {
   }),
   validations: {
     user: {
-      email: { required },
+      email: requiredIf('accountActivationRequired'),
       username: { required },
       fullname: { required },
       password: { required },
@@ -24,6 +24,11 @@ const registration = {
         required,
         sameAsPassword: sameAs('password')
       }
+    },
+    nested: {
+      required: requiredIf(function (nestedModel) {
+        return this.accountActivationRequired
+      })
     }
   },
   created () {

From 39e3917118293912b2af09f509457d718f0207c9 Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Mon, 24 Feb 2020 11:23:16 -0600
Subject: [PATCH 04/32] Remove unneccessary nested

---
 src/components/registration/registration.js | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index fd2942a5..1d8109e4 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -24,11 +24,6 @@ const registration = {
         required,
         sameAsPassword: sameAs('password')
       }
-    },
-    nested: {
-      required: requiredIf(function (nestedModel) {
-        return this.accountActivationRequired
-      })
     }
   },
   created () {

From 7fa5eb07ddeb6d8c2b572e869d82a27bdd7a7fbf Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Mon, 24 Feb 2020 18:10:15 -0500
Subject: [PATCH 05/32] Refactor status showing/hiding code for better handling
 of edge cases and easier comprehension

---
 src/components/status/status.js | 35 ++++++++++++++-------------------
 1 file changed, 15 insertions(+), 20 deletions(-)

diff --git a/src/components/status/status.js b/src/components/status/status.js
index fc5956ec..61d66301 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -188,23 +188,22 @@ const Status = {
       }
       return this.status.attentions.length > 0
     },
+
+    // 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)
+    },
+    mightHideBecauseTall () {
+      return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
+    },
     hideSubjectStatus () {
-      if (this.tallStatus && !this.localCollapseSubjectDefault) {
-        return false
-      }
-      return !this.expandingSubject && this.status.summary
+      return this.mightHideBecauseSubject && !this.expandingSubject
     },
     hideTallStatus () {
-      if (this.status.summary && this.localCollapseSubjectDefault) {
-        return false
-      }
-      if (this.showingTall) {
-        return false
-      }
-      return this.tallStatus
+      return this.mightHideBecauseTall && !this.showingTall
     },
     showingMore () {
-      return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject)
+      return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
     },
     nsfwClickthrough () {
       if (!this.status.nsfw) {
@@ -408,14 +407,10 @@ const Status = {
       this.userExpanded = !this.userExpanded
     },
     toggleShowMore () {
-      if (this.showingTall) {
-        this.showingTall = false
-      } else if (this.expandingSubject && this.status.summary) {
-        this.expandingSubject = false
-      } else if (this.hideTallStatus) {
-        this.showingTall = true
-      } else if (this.hideSubjectStatus && this.status.summary) {
-        this.expandingSubject = true
+      if (this.mightHideBecauseTall) {
+        this.showingTall = !this.showingTall
+      } else if (this.mightHideBecauseSubject) {
+        this.expandingSubject = !this.expandingSubject
       }
     },
     generateUserProfileLink (id, name) {

From e4ded887964dc18513735d5d32f61a4e479314dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C4=99drzej=20Tomaszewski?= <jederow@hotmail.com>
Date: Sun, 1 Mar 2020 17:32:22 +0100
Subject: [PATCH 06/32] Update polish translation

---
 src/i18n/pl.json | 320 +++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 310 insertions(+), 10 deletions(-)

diff --git a/src/i18n/pl.json b/src/i18n/pl.json
index 51cadfb6..4a4b1e31 100644
--- a/src/i18n/pl.json
+++ b/src/i18n/pl.json
@@ -1,7 +1,47 @@
 {
+  "about": {
+    "mrf": {
+      "federation": "Federacja",
+      "keyword": {
+        "keyword_policies": "Zasady słów kluczowych",
+        "ftl_removal": "Usunięcie z \"Całej znanej sieci\"",
+        "reject": "Odrzucanie",
+        "replace": "Zastąpienie",
+        "is_replaced_by": "→"
+      },
+      "mrf_policies": "Włączone zasady MRF",
+      "mrf_policies_desc": "Zasady MRF zmieniają zachowanie federowania instancji. Następujące zasady są włączone:",
+      "simple": {
+        "simple_policies": "Zasady specyficzne dla instancji",
+        "accept": "Akceptowanie",
+        "accept_desc": "Ta instancja akceptuje tylko posty z wymienionych instancji:",
+        "reject": "Odrzucanie",
+        "reject_desc": "Ta instancja odrzuca posty z wymienionych instancji:",
+        "quarantine": "Kwarantanna",
+        "quarantine_desc": "Ta instancja wysyła tylko publiczne posty do wymienionych instancji:",
+        "ftl_removal": "Usunięcie z \"Całej znanej sieci\"",
+        "ftl_removal_desc": "Ta instancja usuwa te instancje z \"Całej znanej sieci\"",
+        "media_removal": "Usuwanie multimediów",
+        "media_removal_desc": "Ta instancja usuwa multimedia z postów od wymienionych instancji:",
+        "media_nsfw": "Multimedia ustawione jako wrażliwe",
+        "media_nsfw_desc": "Ta instancja wymusza, by multimedia z wymienionych instancji były ustawione jako wrażliwe:"
+      }
+    },
+    "staff": "Obsługa"
+  },
   "chat": {
     "title": "Czat"
   },
+  "domain_mute_card": {
+    "mute": "Wycisz",
+    "mute_progress": "Wyciszam...",
+    "unmute": "Odcisz",
+    "unmute_progress": "Odciszam..."
+  },
+  "exporter": {
+    "export": "Eksportuj",
+    "processing": "Przetwarzam, za chwilę zostaniesz zapytany o ściągnięcie pliku"
+  },
   "features_panel": {
     "chat": "Czat",
     "gopher": "Gopher",
@@ -20,7 +60,15 @@
     "submit": "Wyślij",
     "more": "Więcej",
     "generic_error": "Wystąpił błąd",
-    "optional": "nieobowiązkowe"
+    "optional": "nieobowiązkowe",
+    "show_more": "Pokaż więcej",
+    "show_less": "Pokaż mniej",
+    "dismiss": "Odrzuć",
+    "cancel": "Anuluj",
+    "disable": "Wyłącz",
+    "enable": "Włącz",
+    "confirm": "Potwierdź",
+    "verify": "Zweryfikuj"
   },
   "image_cropper": {
     "crop_picture": "Przytnij obrazek",
@@ -28,6 +76,11 @@
     "save_without_cropping": "Zapisz bez przycinania",
     "cancel": "Anuluj"
   },
+  "importer": {
+    "submit": "Wyślij",
+    "success": "Zaimportowano pomyślnie",
+    "error": "Wystąpił błąd podczas importowania pliku."
+  },
   "login": {
     "login": "Zaloguj",
     "description": "Zaloguj używając OAuth",
@@ -36,7 +89,15 @@
     "placeholder": "n.p. lain",
     "register": "Zarejestruj",
     "username": "Użytkownik",
-    "hint": "Zaloguj się, aby dołączyć do dyskusji"
+    "hint": "Zaloguj się, aby dołączyć do dyskusji",
+    "authentication_code": "Kod weryfikacyjny",
+    "enter_recovery_code": "Wprowadź kod zapasowy",
+    "enter_two_factor_code": "Wprowadź kod weryfikacyjny",
+    "recovery_code": "Kod zapasowy",
+    "heading" : {
+      "totp" : "Weryfikacja dwuetapowa",
+      "recovery" : "Zapasowa weryfikacja dwuetapowa"
+    }
   },
   "media_modal": {
     "previous": "Poprzednie",
@@ -44,15 +105,18 @@
   },
   "nav": {
     "about": "O nas",
+    "administration": "Administracja",
     "back": "Wróć",
     "chat": "Lokalny czat",
     "friend_requests": "Prośby o możliwość obserwacji",
     "mentions": "Wzmianki",
+    "interactions": "Interakcje",
     "dms": "Wiadomości prywatne",
     "public_tl": "Publiczna oś czasu",
     "timeline": "Oś czasu",
     "twkn": "Cała znana sieć",
     "user_search": "Wyszukiwanie użytkowników",
+    "search": "Wyszukiwanie",
     "who_to_follow": "Sugestie obserwacji",
     "preferences": "Preferencje"
   },
@@ -64,7 +128,40 @@
     "notifications": "Powiadomienia",
     "read": "Przeczytane!",
     "repeated_you": "powtórzył(-a) twój status",
-    "no_more_notifications": "Nie masz więcej powiadomień"
+    "no_more_notifications": "Nie masz więcej powiadomień",
+    "migrated_to": "wyemigrował do",
+    "reacted_with": "zareagował z {0}"
+  },
+  "polls": {
+    "add_poll": "Dodaj ankietę",
+    "add_option": "Dodaj opcję",
+    "option": "Opcja",
+    "votes": "głosów",
+    "vote": "Głosuj",
+    "type": "Typ ankiety",
+    "single_choice": "jednokrotnego wyboru",
+    "multiple_choices": "wielokrotnego wyboru",
+    "expiry": "Czas trwania ankiety",
+    "expires_in": "Ankieta kończy się za{0}",
+    "expired": "Ankieta skończyła się {0} temu",
+    "not_enough_options": "Zbyt mało unikalnych opcji w ankiecie"
+  },
+  "emoji": {
+    "stickers": "Naklejki",
+    "emoji": "Emoji",
+    "keep_open": "Zostaw selektor otwarty",
+    "search_emoji": "Wyszukaj emoji",
+    "add_emoji": "Wstaw emoji",
+    "custom": "Niestandardowe emoji",
+    "unicode": "Emoji unicode",
+    "load_all_hint": "Załadowano pierwsze {saneAmount} emoji, Załadowanie wszystkich emoji może spowodować problemy z wydajnością.",
+    "load_all": "Ładuję wszystkie {emojiAmount} emoji"
+  },
+  "interactions": {
+    "favs_repeats": "Powtórzenia i ulubione",
+    "follows": "Nowi obserwujący",
+    "moves": "Użytkownik migruje",
+    "load_older": "Załaduj starsze interakcje"
   },
   "post_status": {
     "new_status": "Dodaj nowy status",
@@ -79,8 +176,14 @@
     },
     "content_warning": "Temat (nieobowiązkowy)",
     "default": "Właśnie wróciłem z kościoła",
-    "direct_warning": "Ten wpis zobaczą tylko osoby, o których wspomniałeś(-aś).",
+    "direct_warning_to_all": "Ten wpis zobaczą wszystkie osoby, o których wspomniałeś(-aś).",
+    "direct_warning_to_first_only": "Ten wpis zobaczą tylko te osoby, o których wspomniałeś(-aś) na początku wiadomości.",
     "posting": "Wysyłanie",
+    "scope_notice": {
+      "public": "Ten post będzie widoczny dla każdego",
+      "private": "Ten post będzie widoczny tylko dla twoich obserwujących",
+      "unlisted": "Ten post nie będzie widoczny na publicznej osi czasu i całej znanej sieci"
+    },
     "scope": {
       "direct": "Bezpośredni – Tylko dla wspomnianych użytkowników",
       "private": "Tylko dla obserwujących – Umieść dla osób, które cię obserwują",
@@ -109,8 +212,40 @@
       "password_confirmation_match": "musi być takie jak hasło"
     }
   },
+  "remote_user_resolver": {
+    "remote_user_resolver": "Wyszukiwarka użytkowników nietutejszych",
+    "searching_for": "Szukam",
+    "error": "Nie znaleziono."
+  },
+  "selectable_list": {
+    "select_all": "Zaznacz wszystko"
+  },
   "settings": {
     "app_name": "Nazwa aplikacji",
+    "security": "Bezpieczeństwo",
+    "enter_current_password_to_confirm": "Wprowadź obecne hasło, by potwierdzić twoją tożsamość",
+    "mfa": {
+      "otp" : "OTP",
+      "setup_otp" : "Ustaw OTP",
+      "wait_pre_setup_otp" : "początkowe ustawianie OTP",
+      "confirm_and_enable" : "Potwierdź i włącz OTP",
+      "title": "Weryfikacja dwuetapowa",
+      "generate_new_recovery_codes" : "Wygeneruj nowe kody zapasowe",
+      "warning_of_generate_new_codes" : "Po tym gdy generujesz nowe kody zapasowe, stare przestaną działać.",
+      "recovery_codes" : "Kody zapasowe.",
+      "waiting_a_recovery_codes": "Otrzymuję kody zapasowe...",
+      "recovery_codes_warning" : "Spisz kody na kartce papieru, albo zapisz je w bezpiecznym miejscu - inaczej nie zobaczysz ich już nigdy. Jeśli stracisz dostęp do twojej aplikacji 2FA i kodów zapasowych, nie będziesz miał dostępu do swojego konta.",
+      "authentication_methods" : "Metody weryfikacji",
+      "scan": {
+        "title": "Skanuj",
+        "desc": "Zeskanuj ten kod QR używając twojej aplikacji 2FA albo wpisz ten klucz:",
+        "secret_code": "Klucz"
+      },
+      "verify": {
+        "desc": "By włączyć weryfikację dwuetapową, wpisz kod z twojej aplikacji 2FA:"
+      }
+    },
+    "allow_following_move": "Zezwalaj na automatyczną obserwację gdy obserwowane konto migruje",
     "attachmentRadius": "Załączniki",
     "attachments": "Załączniki",
     "autoload": "Włącz automatyczne ładowanie po przewinięciu do końca strony",
@@ -119,12 +254,20 @@
     "avatarRadius": "Awatary",
     "background": "Tło",
     "bio": "Bio",
+    "block_export": "Eksport blokad",
+    "block_export_button": "Eksportuj twoje blokady do pliku .csv",
+    "block_import": "Import blokad",
+    "block_import_error": "Wystąpił błąd podczas importowania blokad",
+    "blocks_imported": "Zaimportowano blokady, przetwarzanie może zająć trochę czasu.",
     "blocks_tab": "Bloki",
     "btnRadius": "Przyciski",
     "cBlue": "Niebieski (odpowiedz, obserwuj)",
     "cGreen": "Zielony (powtórzenia)",
     "cOrange": "Pomarańczowy (ulubione)",
     "cRed": "Czerwony (anuluj)",
+    "change_email": "Zmień email",
+    "change_email_error": "Wystąpił problem podczas zmiany emaila.",
+    "changed_email": "Pomyślnie zmieniono email!",
     "change_password": "Zmień hasło",
     "change_password_error": "Podczas zmiany hasła wystąpił problem.",
     "changed_password": "Pomyślnie zmieniono hasło!",
@@ -140,16 +283,20 @@
     "delete_account_description": "Trwale usuń konto i wszystkie posty.",
     "delete_account_error": "Wystąpił problem z usuwaniem twojego konta. Jeżeli problem powtarza się, poinformuj administratora swojej instancji.",
     "delete_account_instructions": "Wprowadź swoje hasło w poniższe pole aby potwierdzić usunięcie konta.",
+    "discoverable": "Zezwól na odkrywanie tego konta w wynikach wyszukiwania i innych usługa.",
+    "domain_mutes": "Domeny",
     "avatar_size_instruction": "Zalecany minimalny rozmiar awatarów to 150x150 pikseli.",
+    "pad_emoji": "Dodaj odstęp z obu stron emoji podczas dodawania selektorem",
+    "emoji_reactions_on_timeline": "Pokaż reakcje emoji na osi czasu",
     "export_theme": "Zapisz motyw",
     "filtering": "Filtrowanie",
     "filtering_explanation": "Wszystkie statusy zawierające te słowa będą wyciszone. Jedno słowo na linijkę.",
     "follow_export": "Eksport obserwowanych",
     "follow_export_button": "Eksportuj swoją listę obserwowanych do pliku CSV",
-    "follow_export_processing": "Przetwarzanie, wkrótce twój plik zacznie się ściągać.",
     "follow_import": "Import obserwowanych",
     "follow_import_error": "Błąd przy importowaniu obserwowanych",
     "follows_imported": "Obserwowani zaimportowani! Przetwarzanie może trochę potrwać.",
+    "accent": "Akcent",
     "foreground": "Pierwszy plan",
     "general": "Ogólne",
     "hide_attachments_in_convo": "Ukrywaj załączniki w rozmowach",
@@ -162,6 +309,7 @@
     "hide_post_stats": "Ukrywaj statysyki postów (np. liczbę polubień)",
     "hide_user_stats": "Ukrywaj statysyki użytkowników (np. liczbę obserwujących)",
     "hide_filtered_statuses": "Ukrywaj filtrowane statusy",
+    "import_blocks_from_a_csv_file": "Importuj blokady z pliku CSV",
     "import_followers_from_a_csv_file": "Importuj obserwowanych z pliku CSV",
     "import_theme": "Załaduj motyw",
     "inputRadius": "Pola tekstowe",
@@ -181,17 +329,22 @@
     "use_contain_fit": "Nie przycinaj załączników na miniaturach",
     "name": "Imię",
     "name_bio": "Imię i bio",
+    "new_email": "Nowy email",
     "new_password": "Nowe hasło",
     "notification_visibility": "Rodzaje powiadomień do wyświetlania",
     "notification_visibility_follows": "Obserwacje",
     "notification_visibility_likes": "Ulubione",
     "notification_visibility_mentions": "Wzmianki",
     "notification_visibility_repeats": "Powtórzenia",
+    "notification_visibility_moves": "Użytkownik migruje",
+    "notification_visibility_emoji_reactions": "Reakcje",
     "no_rich_text_description": "Usuwaj formatowanie ze wszystkich postów",
     "no_blocks": "Bez blokad",
     "no_mutes": "Bez wyciszeń",
     "hide_follows_description": "Nie pokazuj kogo obserwuję",
     "hide_followers_description": "Nie pokazuj kto mnie obserwuje",
+    "hide_follows_count_description": "Nie pokazuj licznika obserwowanych",
+    "hide_followers_count_description": "Nie pokazuj licznika obserwujących",
     "show_admin_badge": "Pokazuj odznakę Administrator na moim profilu",
     "show_moderator_badge": "Pokazuj odznakę Moderator na moim profilu",
     "nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
@@ -212,10 +365,14 @@
     "reply_visibility_all": "Pokazuj wszystkie odpowiedzi",
     "reply_visibility_following": "Pokazuj tylko odpowiedzi skierowane do mnie i osób które obserwuję",
     "reply_visibility_self": "Pokazuj tylko odpowiedzi skierowane do mnie",
+    "autohide_floating_post_button": "Ukryj automatycznie przycisk \"Nowy post\" (mobile)",
     "saving_err": "Nie udało się zapisać ustawień",
     "saving_ok": "Zapisano ustawienia",
+    "search_user_to_block": "Wyszukaj kogo chcesz zablokować",
+    "search_user_to_mute": "Wyszukaj kogo chcesz wyciszyć",
     "security_tab": "Bezpieczeństwo",
     "scope_copy": "Kopiuj zakres podczas odpowiadania (DM-y zawsze są kopiowane)",
+    "minimal_scopes_mode": "Zminimalizuj opcje wyboru zakresu postów",
     "set_new_avatar": "Ustaw nowy awatar",
     "set_new_profile_background": "Ustaw nowe tło profilu",
     "set_new_profile_banner": "Ustaw nowy banner profilu",
@@ -228,19 +385,32 @@
     "post_status_content_type": "Post status content type",
     "stop_gifs": "Odtwarzaj GIFy po najechaniu kursorem",
     "streaming": "Włącz automatycznie strumieniowanie nowych postów gdy jesteś na początku strony",
+    "user_mutes": "Users",
+    "useStreamingApi": "Otrzymuj posty i powiadomienia w czasie rzeczywistym",
+    "useStreamingApiWarning": "(Niezalecane, eksperymentalne, pomija posty)",
     "text": "Tekst",
     "theme": "Motyw",
     "theme_help": "Użyj kolorów w notacji szesnastkowej (#rrggbb), by stworzyć swój motyw.",
     "theme_help_v2_1": "Możesz też zastąpić kolory i widoczność poszczególnych komponentów przełączając pola wyboru, użyj „Wyczyść wszystko” aby usunąć wszystkie zastąpienia.",
     "theme_help_v2_2": "Ikony pod niektórych wpisami są wskaźnikami kontrastu pomiędzy tłem a tekstem, po najechaniu na nie otrzymasz szczegółowe informacje. Zapamiętaj, że jeżeli używasz przezroczystości, wskaźniki pokazują najgorszy możliwy przypadek.",
     "tooltipRadius": "Etykiety/alerty",
+    "type_domains_to_mute": "Wpisz domeny, które chcesz wyciszyć",
     "upload_a_photo": "Wyślij zdjęcie",
     "user_settings": "Ustawienia użytkownika",
     "values": {
       "false": "nie",
       "true": "tak"
     },
+    "fun": "Zabawa",
+    "greentext": "Memiczne strzałki",
     "notifications": "Powiadomienia",
+    "notification_setting": "Otrzymuj powiadomienia od:",
+    "notification_setting_follows": "Ludzi których obserwujesz",
+    "notification_setting_non_follows": "Ludzi których nie obserwujesz",
+    "notification_setting_followers": "Ludzi którzy obserwują ciebie",
+    "notification_setting_non_followers": "Ludzi którzy nie obserwują ciebie",
+    "notification_mutes": "By przestać otrzymywać powiadomienia od jednego użytkownika, wycisz go",
+    "notification_blocks": "Blokowanie uzytkownika zatrzymuje wszystkie powiadomienia i odsubskrybowuje go.",
     "enable_web_push_notifications": "Włącz powiadomienia push",
     "style": {
       "switcher": {
@@ -252,7 +422,24 @@
         "save_load_hint": "Opcje „zachowaj” pozwalają na pozostanie przy obecnych opcjach po wybraniu lub załadowaniu motywu, jak i przechowywanie ich podczas eksportowania motywu. Jeżeli wszystkie są odznaczone, eksportowanie motywu spowoduje zapisanie wszystkiego.",
         "reset": "Wyzeruj",
         "clear_all": "Wyczyść wszystko",
-        "clear_opacity": "Wyczyść widoczność"
+        "clear_opacity": "Wyczyść widoczność",
+        "load_theme": "Załaduj motyw",
+        "keep_as_is": "Zostaw po staremu",
+        "use_snapshot": "Stara wersja",
+        "use_source": "Nowa wersja",
+        "help": {
+          "upgraded_from_v2": "PleromaFE zostało zaaktualizowane, motyw może wyglądać nieco inaczej niż sobie zapamiętałeś.",
+          "v2_imported": "Plik który zaimportowałeś został stworzony dla starszego FE. Próbujemy zwiększyć kompatybiliność, lecz wciąż mogą występować rozbieżności.",
+          "future_version_imported": "Plik który zaimportowałeś został stworzony w nowszej wersji FE.",
+          "older_version_imported": "Plik który zaimportowałeś został stworzony w starszej wersji FE.",
+          "snapshot_present": "Migawka motywu jest załadowana, więc wszystkie wartości zostały nadpisane. Zamiast tego, możesz załadować właściwe dane motywu",
+          "snapshot_missing": "Nie znaleziono migawki motywu w pliku, więc motyw może wyglądać inaczej niż pierwotnie zaplanowano.",
+          "fe_upgraded": "Silnik motywów PleromaFE został zaaktualizowany.",
+          "fe_downgraded": "Wersja PleromaFE została cofnięta.",
+          "migration_snapshot_ok": "Żeby być bezpiecznym, migawka motywu została załadowana. Możesz spróbować załadować dane motywu.",
+          "migration_napshot_gone": "Z jakiegoś powodu migawka zniknęła, niektóre rzeczy mogą wyglądać inaczej niż sobie zapamiętałeś.",
+          "snapshot_source_mismatch": "Konflikt wersji: najprawdopodobniej FE zostało cofnięte do poprzedniej wersji i zaaktualizowane ponownie, jeśli zmieniłeś motyw używając starszej wersji FE, najprawdopodobniej chcesz używać starszej wersji, w przeciwnym razie użyj nowej wersji."
+        }
       },
       "common": {
         "color": "Kolor",
@@ -280,14 +467,28 @@
         "_tab_label": "Zaawansowane",
         "alert": "Tło alertu",
         "alert_error": "Błąd",
+        "alert_warning": "Ostrzeżenie",
+        "alert_neutral": "Neutralne",
+        "post": "Posty/Bio użytkowników",
         "badge": "Tło odznaki",
+        "popover": "Etykiety, menu, popovery",
         "badge_notification": "Powiadomienie",
         "panel_header": "Nagłówek panelu",
         "top_bar": "Górny pasek",
         "borders": "Granice",
         "buttons": "Przyciski",
         "inputs": "Pola wejścia",
-        "faint_text": "Zanikający tekst"
+        "faint_text": "Zanikający tekst",
+        "underlay": "Podkład",
+        "poll": "Wykres ankiety",
+        "icons": "Ikony",
+        "highlight": "Podświetlone elementy",
+        "pressed": "Naciśnięte",
+        "selectedPost": "Wybrany post",
+        "selectedMenu": "Wybrany element menu",
+        "disabled": "Wyłączone",
+        "toggled": "Przełączone",
+        "tabs": "Karty"
       },
       "radii": {
         "_tab_label": "Zaokrąglenie"
@@ -300,7 +501,7 @@
         "blur": "Rozmycie",
         "spread": "Szerokość",
         "inset": "Inset",
-        "hint": "Możesz też używać --zmiennych jako kolorów, aby wykorzystać zmienne CSS3. Pamiętaj, że ustawienie widoczności nie będzie wtedy działać.",
+        "hintV3": "Dla cieni możesz również użyć notacji {0} by użyć inny slot koloru.",
         "filter_hint": {
           "always_drop_shadow": "Ostrzeżenie, ten cień zawsze używa {0} jeżeli to obsługiwane przez przeglądarkę.",
           "drop_shadow_syntax": "{0} nie obsługuje parametru {1} i słowa kluczowego {2}.",
@@ -357,6 +558,40 @@
       "frontend_version": "Wersja front-endu"
     }
   },
+  "time": {
+    "day": "{0} dzień",
+    "days": "{0} dni",
+    "day_short": "{0}d",
+    "days_short": "{0}d",
+    "hour": "{0} godzina",
+    "hours": "{0} godzin",
+    "hour_short": "{0} godz.",
+    "hours_short": "{0} godz.",
+    "in_future": "za {0}",
+    "in_past": "{0} temu",
+    "minute": "{0} minuta",
+    "minutes": "{0} minut",
+    "minute_short": "{0}min",
+    "minutes_short": "{0}min",
+    "month": "{0} miesiąc",
+    "months": "{0} miesięcy",
+    "month_short": "{0} mies.",
+    "months_short": "{0} mies.",
+    "now": "teraz",
+    "now_short": "teraz",
+    "second": "{0} sekunda",
+    "seconds": "{0} sekund",
+    "second_short": "{0}s",
+    "seconds_short": "{0}s",
+    "week": "{0} tydzień",
+    "weeks": "{0} tygodni",
+    "week_short": "{0} tydz.",
+    "weeks_short": "{0} tyg.",
+    "year": "{0} rok",
+    "years": "{0} lata",
+    "year_short": "{0} r.",
+    "years_short": "{0} lata"
+  },
   "timeline": {
     "collapse": "Zwiń",
     "conversation": "Rozmowa",
@@ -370,8 +605,17 @@
     "no_statuses": "Brak statusów"
   },
   "status": {
+    "favorites": "Ulubione",
+    "repeats": "Powtórzenia",
+    "delete": "Usuń status",
+    "pin": "Przypnij na profilu",
+    "unpin": "Odepnij z profilu",
+    "pinned": "Przypnięte",
+    "delete_confirm": "Czy naprawdę chcesz usunąć ten status?",
     "reply_to": "Odpowiedź dla",
-    "replies_list": "Odpowiedzi:"
+    "replies_list": "Odpowiedzi:",
+    "mute_conversation": "Wycisz konwersację",
+    "unmute_conversation": "Odcisz konwersację"
   },
   "user_card": {
     "approve": "Przyjmij",
@@ -388,25 +632,60 @@
     "followers": "Obserwujący",
     "following": "Obserwowany!",
     "follows_you": "Obserwuje cię!",
+    "hidden": "Ukryte",
     "its_you": "To ty!",
     "media": "Media",
+    "mention": "Wspomnienie",
     "mute": "Wycisz",
     "muted": "Wyciszony(-a)",
     "per_day": "dziennie",
     "remote_follow": "Zdalna obserwacja",
+    "report": "Raportuj",
     "statuses": "Statusy",
+    "subscribe": "Subskrybuj",
+    "unsubscribe": "Odsubskrybuj",
     "unblock": "Odblokuj",
     "unblock_progress": "Odblokowuję…",
     "block_progress": "Blokuję…",
     "unmute": "Cofnij wyciszenie",
     "unmute_progress": "Cofam wyciszenie…",
-    "mute_progress": "Wyciszam…"
+    "mute_progress": "Wyciszam…",
+    "hide_repeats": "Ukryj powtórzenia",
+    "show_repeats": "Pokaż powtórzenia",
+    "admin_menu": {
+      "moderation": "Moderacja",
+      "grant_admin": "Przyznaj admina",
+      "revoke_admin": "Odwołaj admina",
+      "grant_moderator": "Przyznaj moderatora",
+      "revoke_moderator": "Odwołaj moderatora",
+      "activate_account": "Aktywuj konto",
+      "deactivate_account": "Dezaktywuj konto",
+      "delete_account": "Usuń konto",
+      "force_nsfw": "Oznacz wszystkie posty jako NSFW",
+      "strip_media": "Usuń multimedia z postów",
+      "force_unlisted": "Wymuś posty na niepubliczne",
+      "sandbox": "Wymuś by posty były tylko dla obserwujących",
+      "disable_remote_subscription": "Zakaż obserwowania użytkownika ze zdalnych instancji",
+      "disable_any_subscription": "Zakaż całkowicie obserwowania użytkownika",
+      "quarantine": "Zakaż federowania postów od tego użytkownika",
+      "delete_user": "Usuń użytkownika",
+      "delete_user_confirmation": "Czy jesteś absolutnie pewny? Ta operacja nie może być cofnięta."
+    }
   },
   "user_profile": {
     "timeline_title": "Oś czasu użytkownika",
     "profile_does_not_exist": "Przepraszamy, ten profil nie istnieje.",
     "profile_loading_error": "Przepraszamy, wystąpił błąd podczas ładowania tego profilu."
   },
+  "user_reporting": {
+    "title": "Raportowanie {0}",
+    "add_comment_description": "Raport zostanie wysłany do moderatorów instancji. Możesz dodać powód dlaczego raportujesz to konto poniżej:",
+    "additional_comments": "Dodatkowe komentarze",
+    "forward_description": "To konto jest z innego serwera. Wysłać również tam kopię raportu?",
+    "forward_to": "Przekaż do{0}",
+    "submit": "Wyślij",
+    "generic_error": "Wystąpił błąd podczas przetwarzania twojej prośby."
+  },
   "who_to_follow": {
     "more": "Więcej",
     "who_to_follow": "Propozycje obserwacji"
@@ -416,6 +695,7 @@
     "repeat": "Powtórz",
     "reply": "Odpowiedz",
     "favorite": "Dodaj do ulubionych",
+    "add_reaction": "Dodaj reakcję",
     "user_settings": "Ustawienia użytkownika"
   },
   "upload":{
@@ -431,5 +711,25 @@
       "GiB": "GiB",
       "TiB": "TiB"
     }
+  },
+  "search": {
+    "people": "Ludzie",
+    "hashtags": "Hasztagi",
+    "person_talking": "{count} osoba rozmawia o tym",
+    "people_talking": "{count} osób rozmawia o tym",
+    "no_results": "Brak wyników"
+  },
+  "password_reset": {
+    "forgot_password": "Zapomniałeś hasła?",
+    "password_reset": "Reset hasła",
+    "instruction": "Wprowadź swój adres email lub nazwę użytkownika. Wyślemy ci link z którym możesz zresetować hasło.",
+    "placeholder": "Twój email lub nazwa użytkownika",
+    "check_email": "Sprawdź pocztę, aby uzyskać link do zresetowania hasła.",
+    "return_home": "Wróć do strony głównej",
+    "not_found": "Nie mogliśmy znaleźć tego emaila lub nazwy użytkownika.",
+    "too_many_requests": "Przekroczyłeś limit prób, spróbuj ponownie później.",
+    "password_reset_disabled": "Resetowanie hasła jest wyłączone. Proszę skontaktuj się z administratorem tej instancji.",
+    "password_reset_required": "Musisz zresetować hasło, by się zalogować.",
+    "password_reset_required_but_mailer_is_disabled": "Musisz zresetować hasło, ale resetowanie hasła jest wyłączone. Proszę skontaktuj się z administratorem tej instancji."
   }
 }

From 40005240eb30ce59035299dc348a3962626e32c6 Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Tue, 31 Mar 2020 14:46:38 -0500
Subject: [PATCH 07/32] Send credentials for favourited_by and reblogged_by API
 endpoints

This ensures the data is fetchable on private instances
---
 src/services/api/api.service.js | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 03e88ae2..7db1d094 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -880,12 +880,20 @@ const fetchPoll = ({ pollId, credentials }) => {
   )
 }
 
-const fetchFavoritedByUsers = ({ id }) => {
-  return promisedRequest({ url: MASTODON_STATUS_FAVORITEDBY_URL(id) }).then((users) => users.map(parseUser))
+const fetchFavoritedByUsers = ({ id, credentials }) => {
+  return promisedRequest({
+    url: MASTODON_STATUS_FAVORITEDBY_URL(id),
+    method: 'GET',
+    credentials
+  }).then((users) => users.map(parseUser))
 }
 
-const fetchRebloggedByUsers = ({ id }) => {
-  return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
+const fetchRebloggedByUsers = ({ id, credentials }) => {
+  return promisedRequest({
+    url: MASTODON_STATUS_REBLOGGEDBY_URL(id),
+    method: 'GET',
+    credentials
+  }).then((users) => users.map(parseUser))
 }
 
 const fetchEmojiReactions = ({ id, credentials }) => {

From b4e8b4554a69fcdb86a12484a2c6116207c0500b Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Wed, 8 Apr 2020 00:04:53 +0300
Subject: [PATCH 08/32] CHANGELOG.md: Add entries for upcoming 2.0.2 release

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24c193b9..375e560f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ## [Unreleased]
 
+## [2.0.2] - 2020-04-08
+### Fixed
+- Favorite/Repeat avatars not showing up on private instances/non-public posts
+- Autocorrect getting triggered in the captcha field
+- Overflow on long domains in follow/move notifications
+
+### Changed
+- Polish translation updated
+
 ## [2.0.0] - 2020-02-28
 ### Added
 - Tons of color slots including ones for hover/pressed/toggled buttons

From 18fa338d43b6b7b61b484ae6106ef3b95e5adeee Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 13 Apr 2020 15:26:55 +0400
Subject: [PATCH 09/32] Fix pagination

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

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 7db1d094..ad2b2ad5 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -540,7 +540,7 @@ const fetchTimeline = ({
     params.push(['with_move', withMove])
   }
 
-  params.push(['count', 20])
+  params.push(['limit', 20])
   params.push(['with_muted', withMuted])
 
   const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')

From 2fbb94fe5c7f6743a699e6e1ccb2cd8de341ccca Mon Sep 17 00:00:00 2001
From: Karol Kosek <krkk@krkk.ct8.pl>
Date: Sat, 18 Apr 2020 18:48:45 +0200
Subject: [PATCH 10/32] Fix user names with the RTL char in notifications

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

diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 411c0271..51875747 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -47,7 +47,7 @@
         <span class="notification-details">
           <div class="name-and-action">
             <!-- eslint-disable vue/no-v-html -->
-            <span
+            <bdi
               v-if="!!notification.from_profile.name_html"
               class="username"
               :title="'@'+notification.from_profile.screen_name"

From dea7e2f6acd06839c89cd4a14a101ec162ca4e58 Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Mon, 27 Apr 2020 08:09:31 +0000
Subject: [PATCH 11/32] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 375e560f..f45561d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ## [Unreleased]
+### Fixed
+- Show more/less works correctly with auto-collapsed subjects and long posts
+- RTL characters won't look messed up in notifications
+
+### Changed
+- Emoji autocomplete will match any part of the word and not just start, for example :drool will now helpfully suggest :blobcatdrool: and :blobcatdroolreach:
 
 ## [2.0.2] - 2020-04-08
 ### Fixed

From 01b07f01e9340935faf51e5a3c8034cc90423989 Mon Sep 17 00:00:00 2001
From: eugenijm <eugenijm@protonmail.com>
Date: Sat, 25 Apr 2020 07:04:39 +0300
Subject: [PATCH 12/32] Add support for follow request notifications

---
 CHANGELOG.md                                  |  3 ++
 src/components/notification/notification.js   | 19 +++++++
 src/components/notification/notification.vue  | 50 +++++++++++++------
 .../notifications/notifications.scss          | 15 ++++++
 src/i18n/en.json                              |  5 +-
 src/modules/config.js                         |  3 +-
 src/modules/statuses.js                       | 22 +++++++-
 src/services/api/api.service.js               | 11 ++++
 .../entity_normalizer.service.js              |  5 +-
 .../notification_utils/notification_utils.js  |  7 ++-
 static/fontello.json                          | 14 +++++-
 11 files changed, 131 insertions(+), 23 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f45561d0..685fe629 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ### Changed
 - Emoji autocomplete will match any part of the word and not just start, for example :drool will now helpfully suggest :blobcatdrool: and :blobcatdroolreach:
 
+### Add
+- Follow request notification support
+
 ## [2.0.2] - 2020-04-08
 ### Fixed
 - Favorite/Repeat avatars not showing up on private instances/non-public posts
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index e7bd769e..6deee7d5 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -2,6 +2,7 @@ import Status from '../status/status.vue'
 import UserAvatar from '../user_avatar/user_avatar.vue'
 import UserCard from '../user_card/user_card.vue'
 import Timeago from '../timeago/timeago.vue'
+import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 
@@ -32,6 +33,21 @@ const Notification = {
     },
     toggleMute () {
       this.unmuted = !this.unmuted
+    },
+    approveUser () {
+      this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
+      this.$store.dispatch('removeFollowRequest', this.user)
+      this.$store.dispatch('updateNotification', {
+        id: this.notification.id,
+        updater: notification => {
+          notification.type = 'follow'
+        }
+      })
+    },
+    denyUser () {
+      this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
+      this.$store.dispatch('removeFollowRequest', this.user)
+      this.$store.dispatch('dismissNotification', { id: this.notification.id })
     }
   },
   computed: {
@@ -57,6 +73,9 @@ const Notification = {
     },
     needMute () {
       return this.user.muted
+    },
+    isStatusNotification () {
+      return isStatusNotification(this.notification.type)
     }
   }
 }
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 51875747..02802776 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -74,6 +74,10 @@
               <i class="fa icon-user-plus lit" />
               <small>{{ $t('notifications.followed_you') }}</small>
             </span>
+            <span v-if="notification.type === 'follow_request'">
+              <i class="fa icon-user lit" />
+              <small>{{ $t('notifications.follow_request') }}</small>
+            </span>
             <span v-if="notification.type === 'move'">
               <i class="fa icon-arrow-curved lit" />
               <small>{{ $t('notifications.migrated_to') }}</small>
@@ -87,18 +91,7 @@
             </span>
           </div>
           <div
-            v-if="notification.type === 'follow' || notification.type === 'move'"
-            class="timeago"
-          >
-            <span class="faint">
-              <Timeago
-                :time="notification.created_at"
-                :auto-update="240"
-              />
-            </span>
-          </div>
-          <div
-            v-else
+            v-if="isStatusNotification"
             class="timeago"
           >
             <router-link
@@ -112,6 +105,17 @@
               />
             </router-link>
           </div>
+          <div
+            v-else
+            class="timeago"
+          >
+            <span class="faint">
+              <Timeago
+                :time="notification.created_at"
+                :auto-update="240"
+              />
+            </span>
+          </div>
           <a
             v-if="needMute"
             href="#"
@@ -119,12 +123,30 @@
           ><i class="button-icon icon-eye-off" /></a>
         </span>
         <div
-          v-if="notification.type === 'follow'"
+          v-if="notification.type === 'follow' || notification.type === 'follow_request'"
           class="follow-text"
         >
-          <router-link :to="userProfileLink">
+          <router-link
+            :to="userProfileLink"
+            class="follow-name"
+          >
             @{{ notification.from_profile.screen_name }}
           </router-link>
+          <div
+            v-if="notification.type === 'follow_request'"
+            style="white-space: nowrap;"
+          >
+            <i
+              class="icon-ok button-icon add-reaction-button"
+              :title="$t('tool_tip.accept_follow_request')"
+              @click="approveUser()"
+            />
+            <i
+              class="icon-cancel button-icon add-reaction-button"
+              :title="$t('tool_tip.accept_follow_request')"
+              @click="denyUser()"
+            />
+          </div>
         </div>
         <div
           v-else-if="notification.type === 'move'"
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index a8f4430f..80dad28b 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -82,6 +82,16 @@
   .follow-text, .move-text {
     padding: 0.5em 0;
     overflow-wrap: break-word;
+    display: flex;
+    justify-content: space-between;
+
+    .follow-name {
+      display: block;
+      max-width: 100%;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
   }
 
   .status-el {
@@ -143,6 +153,11 @@
       color: var(--cGreen, $fallback--cGreen);
     }
 
+    .icon-user.lit {
+      color: $fallback--cBlue;
+      color: var(--cBlue, $fallback--cBlue);
+    }
+
     .icon-user-plus.lit {
       color: $fallback--cBlue;
       color: var(--cBlue, $fallback--cBlue);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 54d0608e..37d9591c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -124,6 +124,7 @@
     "broken_favorite": "Unknown status, searching for it...",
     "favorited_you": "favorited your status",
     "followed_you": "followed you",
+    "follow_request": "wants to follow you",
     "load_older": "Load older notifications",
     "notifications": "Notifications",
     "read": "Read!",
@@ -697,7 +698,9 @@
     "reply": "Reply",
     "favorite": "Favorite",
     "add_reaction": "Add Reaction",
-    "user_settings": "User Settings"
+    "user_settings": "User Settings",
+    "accept_follow_request": "Accept follow request",
+    "reject_follow_request": "Reject follow request"
   },
   "upload":{
     "error": {
diff --git a/src/modules/config.js b/src/modules/config.js
index 7997521d..8f4638f5 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -34,7 +34,8 @@ export const defaultState = {
     likes: true,
     repeats: true,
     moves: true,
-    emojiReactions: false
+    emojiReactions: false,
+    followRequest: true
   },
   webPushNotifications: false,
   muteWords: [],
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index f1b7dcbd..239f41eb 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -13,6 +13,7 @@ import {
   omitBy
 } from 'lodash'
 import { set } from 'vue'
+import { isStatusNotification } from '../services/notification_utils/notification_utils.js'
 import apiService from '../services/api/api.service.js'
 // import parse from '../services/status_parser/status_parser.js'
 
@@ -321,7 +322,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
 
 const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
   each(notifications, (notification) => {
-    if (notification.type !== 'follow' && notification.type !== 'move') {
+    if (isStatusNotification(notification.type)) {
       notification.action = addStatusToGlobalStorage(state, notification.action).item
       notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
     }
@@ -361,13 +362,16 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
           case 'move':
             i18nString = 'migrated_to'
             break
+          case 'follow_request':
+            i18nString = 'follow_request'
+            break
         }
 
         if (notification.type === 'pleroma:emoji_reaction') {
           notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji])
         } else if (i18nString) {
           notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
-        } else {
+        } else if (isStatusNotification(notification.type)) {
           notifObj.body = notification.status.text
         }
 
@@ -521,6 +525,13 @@ export const mutations = {
       notification.seen = true
     })
   },
+  dismissNotification (state, { id }) {
+    state.notifications.data = state.notifications.data.filter(n => n.id !== id)
+  },
+  updateNotification (state, { id, updater }) {
+    const notification = find(state.notifications.data, n => n.id === id)
+    notification && updater(notification)
+  },
   queueFlush (state, { timeline, id }) {
     state.timelines[timeline].flushMarker = id
   },
@@ -680,6 +691,13 @@ const statuses = {
         credentials: rootState.users.currentUser.credentials
       })
     },
+    dismissNotification ({ rootState, commit }, { id }) {
+      rootState.api.backendInteractor.dismissNotification({ id })
+        .then(() => commit('dismissNotification', { id }))
+    },
+    updateNotification ({ rootState, commit }, { id, updater }) {
+      commit('updateNotification', { id, updater })
+    },
     fetchFavsAndRepeats ({ rootState, commit }, id) {
       Promise.all([
         rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ad2b2ad5..cda61ee2 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -29,6 +29,7 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
 const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
 const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
 const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
+const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss`
 const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
 const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
 const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
@@ -1010,6 +1011,15 @@ const unmuteDomain = ({ domain, credentials }) => {
   })
 }
 
+const dismissNotification = ({ credentials, id }) => {
+  return promisedRequest({
+    url: MASTODON_DISMISS_NOTIFICATION_URL(id),
+    method: 'POST',
+    payload: { id },
+    credentials
+  })
+}
+
 export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
   return Object.entries({
     ...(credentials
@@ -1165,6 +1175,7 @@ const apiService = {
   denyUser,
   suggestions,
   markNotificationsAsSeen,
+  dismissNotification,
   vote,
   fetchPoll,
   fetchFavoritedByUsers,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 84169a7b..6cacd0b8 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 { isStatusNotification } from '../notification_utils/notification_utils.js'
 
 const qvitterStatusType = (status) => {
   if (status.is_post_verb) {
@@ -346,9 +347,7 @@ export const parseNotification = (data) => {
   if (masto) {
     output.type = mastoDict[data.type] || data.type
     output.seen = data.pleroma.is_seen
-    output.status = output.type === 'follow' || output.type === 'move'
-      ? null
-      : parseStatus(data.status)
+    output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
     output.action = output.status // TODO: Refactor, this is unneeded
     output.target = output.type !== 'move'
       ? null
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index b17bd7bf..eb479227 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -1,4 +1,4 @@
-import { filter, sortBy } from 'lodash'
+import { filter, sortBy, includes } from 'lodash'
 
 export const notificationsFromStore = store => store.state.statuses.notifications.data
 
@@ -7,10 +7,15 @@ export const visibleTypes = store => ([
   store.state.config.notificationVisibility.mentions && 'mention',
   store.state.config.notificationVisibility.repeats && 'repeat',
   store.state.config.notificationVisibility.follows && 'follow',
+  store.state.config.notificationVisibility.followRequest && 'follow_request',
   store.state.config.notificationVisibility.moves && 'move',
   store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
 ].filter(_ => _))
 
+const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
+
+export const isStatusNotification = (type) => includes(statusNotifications, type)
+
 const sortById = (a, b) => {
   const seqA = Number(a.id)
   const seqB = Number(b.id)
diff --git a/static/fontello.json b/static/fontello.json
index 5a7086a2..5963b68b 100755
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -345,6 +345,18 @@
       "css": "link",
       "code": 59427,
       "src": "fontawesome"
+    },
+    {
+      "uid": "8b80d36d4ef43889db10bc1f0dc9a862",
+      "css": "user",
+      "code": 59428,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "12f4ece88e46abd864e40b35e05b11cd",
+      "css": "ok",
+      "code": 59431,
+      "src": "fontawesome"
     }
   ]
-}
+}
\ No newline at end of file

From 02c8a9e3143f2b12f44d24f307e2718dec22987b Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Fri, 1 May 2020 17:26:07 +0300
Subject: [PATCH 13/32] remove with_move param

---
 src/services/api/api.service.js                             | 6 +-----
 .../notifications_fetcher/notifications_fetcher.service.js  | 3 ---
 2 files changed, 1 insertion(+), 8 deletions(-)

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ad2b2ad5..3c6b8f4e 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -495,8 +495,7 @@ const fetchTimeline = ({
   until = false,
   userId = false,
   tag = false,
-  withMuted = false,
-  withMove = false
+  withMuted = false
 }) => {
   const timelineUrls = {
     public: MASTODON_PUBLIC_TIMELINE,
@@ -536,9 +535,6 @@ const fetchTimeline = ({
   if (timeline === 'public' || timeline === 'publicAndExternal') {
     params.push(['only_media', false])
   }
-  if (timeline === 'notifications') {
-    params.push(['with_move', withMove])
-  }
 
   params.push(['limit', 20])
   params.push(['with_muted', withMuted])
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 864e32f8..64499a1b 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -11,12 +11,9 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
   const rootState = store.rootState || store.state
   const timelineData = rootState.statuses.notifications
   const hideMutedPosts = getters.mergedConfig.hideMutedPosts
-  const allowFollowingMove = rootState.users.currentUser.allow_following_move
 
   args['withMuted'] = !hideMutedPosts
 
-  args['withMove'] = !allowFollowingMove
-
   args['timeline'] = 'notifications'
   if (older) {
     if (timelineData.minId !== Number.POSITIVE_INFINITY) {

From af3e69743e3192898f185fbc867defa1d155a4d4 Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Fri, 1 May 2020 19:28:26 +0000
Subject: [PATCH 14/32] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f45561d0..86d981da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
-## [Unreleased]
+## [2.0.3] - 2020-05-02
 ### Fixed
 - Show more/less works correctly with auto-collapsed subjects and long posts
 - RTL characters won't look messed up in notifications

From 406fdd8edec210d14589e0ff684a166250236779 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Sat, 2 May 2020 10:19:47 +0300
Subject: [PATCH 15/32] follow request bugfixes, wrong text, notifs not being
 marked as read, approving from follow request view

---
 .../follow_request_card.js                    | 19 +++++++++++++++++++
 src/components/notification/notification.js   |  1 +
 src/components/notification/notification.vue  |  6 +++---
 .../notifications/notifications.scss          | 19 +++++++++++++++++++
 4 files changed, 42 insertions(+), 3 deletions(-)

diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js
index a8931787..2a9d3db5 100644
--- a/src/components/follow_request_card/follow_request_card.js
+++ b/src/components/follow_request_card/follow_request_card.js
@@ -1,4 +1,5 @@
 import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
 
 const FollowRequestCard = {
   props: ['user'],
@@ -6,13 +7,31 @@ const FollowRequestCard = {
     BasicUserCard
   },
   methods: {
+    findFollowRequestNotificationId () {
+      const notif = notificationsFromStore(this.$store).find(
+        (notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
+      )
+      return notif && notif.id
+    },
     approveUser () {
       this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
       this.$store.dispatch('removeFollowRequest', this.user)
+
+      const notifId = this.findFollowRequestNotificationId()
+      this.$store.dispatch('updateNotification', {
+        id: notifId,
+        updater: notification => {
+          notification.type = 'follow'
+          notification.seen = true
+        }
+      })
     },
     denyUser () {
       this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
       this.$store.dispatch('removeFollowRequest', this.user)
+
+      const notifId = this.findFollowRequestNotificationId()
+      this.$store.dispatch('dismissNotification', { id: notifId })
     }
   }
 }
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 6deee7d5..8c20ff09 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -41,6 +41,7 @@ const Notification = {
         id: this.notification.id,
         updater: notification => {
           notification.type = 'follow'
+          notification.seen = true
         }
       })
     },
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 02802776..f6da07dd 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -137,13 +137,13 @@
             style="white-space: nowrap;"
           >
             <i
-              class="icon-ok button-icon add-reaction-button"
+              class="icon-ok button-icon follow-request-accept"
               :title="$t('tool_tip.accept_follow_request')"
               @click="approveUser()"
             />
             <i
-              class="icon-cancel button-icon add-reaction-button"
-              :title="$t('tool_tip.accept_follow_request')"
+              class="icon-cancel button-icon follow-request-reject"
+              :title="$t('tool_tip.reject_follow_request')"
               @click="denyUser()"
             />
           </div>
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 80dad28b..9efcfcf8 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -79,6 +79,25 @@
     }
   }
 
+  .follow-request-accept {
+    cursor: pointer;
+
+    &:hover {
+      color: $fallback--text;
+      color: var(--text, $fallback--text);
+    }
+  }
+
+  .follow-request-reject {
+    cursor: pointer;
+
+    &:hover {
+      color: $fallback--cRed;
+      color: var(--cRed, $fallback--cRed);
+    }
+  }
+
+
   .follow-text, .move-text {
     padding: 0.5em 0;
     overflow-wrap: break-word;

From 75519223f9a715aacb99d3780ee681089a479292 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Sat, 2 May 2020 10:52:57 +0300
Subject: [PATCH 16/32] mark single notifs as seen properly on server

---
 .../follow_request_card/follow_request_card.js       |  2 +-
 src/components/notification/notification.js          |  2 +-
 src/modules/statuses.js                              | 12 ++++++++++++
 src/services/api/api.service.js                      | 12 ++++++++----
 4 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js
index 2a9d3db5..33e2699e 100644
--- a/src/components/follow_request_card/follow_request_card.js
+++ b/src/components/follow_request_card/follow_request_card.js
@@ -18,11 +18,11 @@ const FollowRequestCard = {
       this.$store.dispatch('removeFollowRequest', this.user)
 
       const notifId = this.findFollowRequestNotificationId()
+      this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
       this.$store.dispatch('updateNotification', {
         id: notifId,
         updater: notification => {
           notification.type = 'follow'
-          notification.seen = true
         }
       })
     },
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 8c20ff09..abe3bebe 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -37,11 +37,11 @@ const Notification = {
     approveUser () {
       this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
       this.$store.dispatch('removeFollowRequest', this.user)
+      this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
       this.$store.dispatch('updateNotification', {
         id: this.notification.id,
         updater: notification => {
           notification.type = 'follow'
-          notification.seen = true
         }
       })
     },
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 239f41eb..2a8b9581 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -525,6 +525,10 @@ export const mutations = {
       notification.seen = true
     })
   },
+  markSingleNotificationAsSeen (state, { id }) {
+    const notification = find(state.notifications.data, n => n.id === id)
+    if (notification) notification.seen = true
+  },
   dismissNotification (state, { id }) {
     state.notifications.data = state.notifications.data.filter(n => n.id !== id)
   },
@@ -691,6 +695,14 @@ const statuses = {
         credentials: rootState.users.currentUser.credentials
       })
     },
+    markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
+      commit('markSingleNotificationAsSeen', { id })
+      apiService.markNotificationsAsSeen({
+        single: true,
+        id,
+        credentials: rootState.users.currentUser.credentials
+      })
+    },
     dismissNotification ({ rootState, commit }, { id }) {
       rootState.api.backendInteractor.dismissNotification({ id })
         .then(() => commit('dismissNotification', { id }))
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 3a58c38d..72c8874f 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -4,7 +4,6 @@ import 'whatwg-fetch'
 import { RegistrationError, StatusCodeError } from '../errors/errors'
 
 /* eslint-env browser */
-const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
 const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
 const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
 const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
@@ -17,6 +16,7 @@ const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
 const ADMIN_USERS_URL = '/api/pleroma/admin/users'
 const SUGGESTIONS_URL = '/api/v1/suggestions'
 const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
+const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
 
 const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
 const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
@@ -841,12 +841,16 @@ const suggestions = ({ credentials }) => {
   }).then((data) => data.json())
 }
 
-const markNotificationsAsSeen = ({ id, credentials }) => {
+const markNotificationsAsSeen = ({ id, credentials, single = false }) => {
   const body = new FormData()
 
-  body.append('latest_id', id)
+  if (single) {
+    body.append('id', id)
+  } else {
+    body.append('max_id', id)
+  }
 
-  return fetch(QVITTER_USER_NOTIFICATIONS_READ_URL, {
+  return fetch(NOTIFICATION_READ_URL, {
     body,
     headers: authHeaders(credentials),
     method: 'POST'

From 92ccaa97bb0a2c15b96e2fbcf03823ba902dc516 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Sat, 2 May 2020 11:51:39 +0300
Subject: [PATCH 17/32] don't dismiss a rejected follow request on server

---
 .../follow_request_card/follow_request_card.js           | 9 +++++----
 src/components/notification/notification.js              | 6 ++++--
 src/modules/statuses.js                                  | 5 ++++-
 3 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js
index 33e2699e..cbd75311 100644
--- a/src/components/follow_request_card/follow_request_card.js
+++ b/src/components/follow_request_card/follow_request_card.js
@@ -27,11 +27,12 @@ const FollowRequestCard = {
       })
     },
     denyUser () {
-      this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
-      this.$store.dispatch('removeFollowRequest', this.user)
-
       const notifId = this.findFollowRequestNotificationId()
-      this.$store.dispatch('dismissNotification', { id: notifId })
+      this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
+        .then(() => {
+          this.$store.dispatch('dismissNotificationLocal', { id: notifId })
+          this.$store.dispatch('removeFollowRequest', this.user)
+        })
     }
   }
 }
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index abe3bebe..1ae81ce4 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -47,8 +47,10 @@ const Notification = {
     },
     denyUser () {
       this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
-      this.$store.dispatch('removeFollowRequest', this.user)
-      this.$store.dispatch('dismissNotification', { id: this.notification.id })
+        .then(() => {
+          this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
+          this.$store.dispatch('removeFollowRequest', this.user)
+        })
     }
   },
   computed: {
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 2a8b9581..cd8c1dba 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -703,9 +703,12 @@ const statuses = {
         credentials: rootState.users.currentUser.credentials
       })
     },
+    dismissNotificationLocal ({ rootState, commit }, { id }) {
+      commit('dismissNotification', { id })
+    },
     dismissNotification ({ rootState, commit }, { id }) {
+      commit('dismissNotification', { id })
       rootState.api.backendInteractor.dismissNotification({ id })
-        .then(() => commit('dismissNotification', { id }))
     },
     updateNotification ({ rootState, commit }, { id, updater }) {
       commit('updateNotification', { id, updater })

From 068abb4d268d20ab6265a0a1a2520ea74fc45aa2 Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Sat, 2 May 2020 13:05:45 +0000
Subject: [PATCH 18/32] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32f9a2eb..ebd0e613 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
 All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## [Unreleased]
+### Changed
+- Removed the use of with_move parameters when fetching notifications
 
 ## [2.0.3] - 2020-05-02
 ### Fixed

From 0ba34eeca58ae5caf13faf244bd3004800291051 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 13 Apr 2020 15:26:55 +0400
Subject: [PATCH 19/32] Fix pagination

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

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 7db1d094..ad2b2ad5 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -540,7 +540,7 @@ const fetchTimeline = ({
     params.push(['with_move', withMove])
   }
 
-  params.push(['count', 20])
+  params.push(['limit', 20])
   params.push(['with_muted', withMuted])
 
   const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')

From c4d1c2131ccd79bae84f668b64116d8fbf16840d Mon Sep 17 00:00:00 2001
From: Karol Kosek <krkk@krkk.ct8.pl>
Date: Sat, 18 Apr 2020 18:48:45 +0200
Subject: [PATCH 20/32] Fix user names with the RTL char in notifications

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

diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 411c0271..51875747 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -47,7 +47,7 @@
         <span class="notification-details">
           <div class="name-and-action">
             <!-- eslint-disable vue/no-v-html -->
-            <span
+            <bdi
               v-if="!!notification.from_profile.name_html"
               class="username"
               :title="'@'+notification.from_profile.screen_name"

From eae0bce3201ddca452320f2c00d4328bc808e548 Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Mon, 24 Feb 2020 18:10:15 -0500
Subject: [PATCH 21/32] Refactor status showing/hiding code for better handling
 of edge cases and easier comprehension

---
 src/components/status/status.js | 35 ++++++++++++++-------------------
 1 file changed, 15 insertions(+), 20 deletions(-)

diff --git a/src/components/status/status.js b/src/components/status/status.js
index fc5956ec..61d66301 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -188,23 +188,22 @@ const Status = {
       }
       return this.status.attentions.length > 0
     },
+
+    // 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)
+    },
+    mightHideBecauseTall () {
+      return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
+    },
     hideSubjectStatus () {
-      if (this.tallStatus && !this.localCollapseSubjectDefault) {
-        return false
-      }
-      return !this.expandingSubject && this.status.summary
+      return this.mightHideBecauseSubject && !this.expandingSubject
     },
     hideTallStatus () {
-      if (this.status.summary && this.localCollapseSubjectDefault) {
-        return false
-      }
-      if (this.showingTall) {
-        return false
-      }
-      return this.tallStatus
+      return this.mightHideBecauseTall && !this.showingTall
     },
     showingMore () {
-      return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject)
+      return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
     },
     nsfwClickthrough () {
       if (!this.status.nsfw) {
@@ -408,14 +407,10 @@ const Status = {
       this.userExpanded = !this.userExpanded
     },
     toggleShowMore () {
-      if (this.showingTall) {
-        this.showingTall = false
-      } else if (this.expandingSubject && this.status.summary) {
-        this.expandingSubject = false
-      } else if (this.hideTallStatus) {
-        this.showingTall = true
-      } else if (this.hideSubjectStatus && this.status.summary) {
-        this.expandingSubject = true
+      if (this.mightHideBecauseTall) {
+        this.showingTall = !this.showingTall
+      } else if (this.mightHideBecauseSubject) {
+        this.expandingSubject = !this.expandingSubject
       }
     },
     generateUserProfileLink (id, name) {

From aef03d53b2082f7a1198f63940a18dd112021982 Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Sun, 9 Feb 2020 17:25:24 -0500
Subject: [PATCH 22/32] Allow emoji suggestions based on a match anywhere in
 the emoji name, but improve sorting

---
 src/components/emoji_input/suggestor.js | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index aec5c39d..9e437ccc 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -29,17 +29,21 @@ export default data => input => {
 export const suggestEmoji = emojis => input => {
   const noPrefix = input.toLowerCase().substr(1)
   return emojis
-    .filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
+    .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
     .sort((a, b) => {
       let aScore = 0
       let bScore = 0
 
-      // Make custom emojis a priority
-      aScore += a.imageUrl ? 10 : 0
-      bScore += b.imageUrl ? 10 : 0
+      // Prioritize emoji that start with the input string
+      aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
+      bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
 
-      // Sort alphabetically
-      const alphabetically = a.displayText > b.displayText ? 1 : -1
+      // Sort by length
+      aScore -= a.displayText.length
+      bScore -= b.displayText.length
+
+      // Break ties alphabetically
+      const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
 
       return bScore - aScore + alphabetically
     })

From fe4282f44b4aff457c9b7473cb815b310fe6cb54 Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Mon, 10 Feb 2020 09:32:07 -0500
Subject: [PATCH 23/32] Prioritize custom emoji a lot and boost exact matches
 to the top

---
 src/components/emoji_input/suggestor.js | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index 9e437ccc..15a71eff 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -34,7 +34,15 @@ export const suggestEmoji = emojis => input => {
       let aScore = 0
       let bScore = 0
 
-      // Prioritize emoji that start with the input string
+      // An exact match always wins
+      aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
+      bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
+
+      // Prioritize custom emoji a lot
+      aScore += a.imageUrl ? 100 : 0
+      bScore += b.imageUrl ? 100 : 0
+
+      // Prioritize prefix matches somewhat
       aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
       bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
 

From 372eb723dba981550a4b258ad74c727e0d40ba60 Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Mon, 27 Apr 2020 08:09:31 +0000
Subject: [PATCH 24/32] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 375e560f..f45561d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ## [Unreleased]
+### Fixed
+- Show more/less works correctly with auto-collapsed subjects and long posts
+- RTL characters won't look messed up in notifications
+
+### Changed
+- Emoji autocomplete will match any part of the word and not just start, for example :drool will now helpfully suggest :blobcatdrool: and :blobcatdroolreach:
 
 ## [2.0.2] - 2020-04-08
 ### Fixed

From 4d1a67463436c963288d7c65a5856dee69fa4c93 Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Fri, 1 May 2020 19:28:26 +0000
Subject: [PATCH 25/32] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f45561d0..86d981da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
-## [Unreleased]
+## [2.0.3] - 2020-05-02
 ### Fixed
 - Show more/less works correctly with auto-collapsed subjects and long posts
 - RTL characters won't look messed up in notifications

From ab3c0e8512e231a5bfb9464379d5090b876034fe Mon Sep 17 00:00:00 2001
From: eugenijm <eugenijm@protonmail.com>
Date: Sat, 25 Apr 2020 07:04:39 +0300
Subject: [PATCH 26/32] Add support for follow request notifications

---
 CHANGELOG.md                                  |  3 ++
 src/components/notification/notification.js   | 19 +++++++
 src/components/notification/notification.vue  | 50 +++++++++++++------
 .../notifications/notifications.scss          | 15 ++++++
 src/i18n/en.json                              |  5 +-
 src/modules/config.js                         |  3 +-
 src/modules/statuses.js                       | 22 +++++++-
 src/services/api/api.service.js               | 11 ++++
 .../entity_normalizer.service.js              |  5 +-
 .../notification_utils/notification_utils.js  |  7 ++-
 static/fontello.json                          | 14 +++++-
 11 files changed, 131 insertions(+), 23 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86d981da..32f9a2eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ### Changed
 - Emoji autocomplete will match any part of the word and not just start, for example :drool will now helpfully suggest :blobcatdrool: and :blobcatdroolreach:
 
+### Add
+- Follow request notification support
+
 ## [2.0.2] - 2020-04-08
 ### Fixed
 - Favorite/Repeat avatars not showing up on private instances/non-public posts
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index e7bd769e..6deee7d5 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -2,6 +2,7 @@ import Status from '../status/status.vue'
 import UserAvatar from '../user_avatar/user_avatar.vue'
 import UserCard from '../user_card/user_card.vue'
 import Timeago from '../timeago/timeago.vue'
+import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 
@@ -32,6 +33,21 @@ const Notification = {
     },
     toggleMute () {
       this.unmuted = !this.unmuted
+    },
+    approveUser () {
+      this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
+      this.$store.dispatch('removeFollowRequest', this.user)
+      this.$store.dispatch('updateNotification', {
+        id: this.notification.id,
+        updater: notification => {
+          notification.type = 'follow'
+        }
+      })
+    },
+    denyUser () {
+      this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
+      this.$store.dispatch('removeFollowRequest', this.user)
+      this.$store.dispatch('dismissNotification', { id: this.notification.id })
     }
   },
   computed: {
@@ -57,6 +73,9 @@ const Notification = {
     },
     needMute () {
       return this.user.muted
+    },
+    isStatusNotification () {
+      return isStatusNotification(this.notification.type)
     }
   }
 }
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 51875747..02802776 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -74,6 +74,10 @@
               <i class="fa icon-user-plus lit" />
               <small>{{ $t('notifications.followed_you') }}</small>
             </span>
+            <span v-if="notification.type === 'follow_request'">
+              <i class="fa icon-user lit" />
+              <small>{{ $t('notifications.follow_request') }}</small>
+            </span>
             <span v-if="notification.type === 'move'">
               <i class="fa icon-arrow-curved lit" />
               <small>{{ $t('notifications.migrated_to') }}</small>
@@ -87,18 +91,7 @@
             </span>
           </div>
           <div
-            v-if="notification.type === 'follow' || notification.type === 'move'"
-            class="timeago"
-          >
-            <span class="faint">
-              <Timeago
-                :time="notification.created_at"
-                :auto-update="240"
-              />
-            </span>
-          </div>
-          <div
-            v-else
+            v-if="isStatusNotification"
             class="timeago"
           >
             <router-link
@@ -112,6 +105,17 @@
               />
             </router-link>
           </div>
+          <div
+            v-else
+            class="timeago"
+          >
+            <span class="faint">
+              <Timeago
+                :time="notification.created_at"
+                :auto-update="240"
+              />
+            </span>
+          </div>
           <a
             v-if="needMute"
             href="#"
@@ -119,12 +123,30 @@
           ><i class="button-icon icon-eye-off" /></a>
         </span>
         <div
-          v-if="notification.type === 'follow'"
+          v-if="notification.type === 'follow' || notification.type === 'follow_request'"
           class="follow-text"
         >
-          <router-link :to="userProfileLink">
+          <router-link
+            :to="userProfileLink"
+            class="follow-name"
+          >
             @{{ notification.from_profile.screen_name }}
           </router-link>
+          <div
+            v-if="notification.type === 'follow_request'"
+            style="white-space: nowrap;"
+          >
+            <i
+              class="icon-ok button-icon add-reaction-button"
+              :title="$t('tool_tip.accept_follow_request')"
+              @click="approveUser()"
+            />
+            <i
+              class="icon-cancel button-icon add-reaction-button"
+              :title="$t('tool_tip.accept_follow_request')"
+              @click="denyUser()"
+            />
+          </div>
         </div>
         <div
           v-else-if="notification.type === 'move'"
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index a8f4430f..80dad28b 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -82,6 +82,16 @@
   .follow-text, .move-text {
     padding: 0.5em 0;
     overflow-wrap: break-word;
+    display: flex;
+    justify-content: space-between;
+
+    .follow-name {
+      display: block;
+      max-width: 100%;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
   }
 
   .status-el {
@@ -143,6 +153,11 @@
       color: var(--cGreen, $fallback--cGreen);
     }
 
+    .icon-user.lit {
+      color: $fallback--cBlue;
+      color: var(--cBlue, $fallback--cBlue);
+    }
+
     .icon-user-plus.lit {
       color: $fallback--cBlue;
       color: var(--cBlue, $fallback--cBlue);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 54d0608e..37d9591c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -124,6 +124,7 @@
     "broken_favorite": "Unknown status, searching for it...",
     "favorited_you": "favorited your status",
     "followed_you": "followed you",
+    "follow_request": "wants to follow you",
     "load_older": "Load older notifications",
     "notifications": "Notifications",
     "read": "Read!",
@@ -697,7 +698,9 @@
     "reply": "Reply",
     "favorite": "Favorite",
     "add_reaction": "Add Reaction",
-    "user_settings": "User Settings"
+    "user_settings": "User Settings",
+    "accept_follow_request": "Accept follow request",
+    "reject_follow_request": "Reject follow request"
   },
   "upload":{
     "error": {
diff --git a/src/modules/config.js b/src/modules/config.js
index 7997521d..8f4638f5 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -34,7 +34,8 @@ export const defaultState = {
     likes: true,
     repeats: true,
     moves: true,
-    emojiReactions: false
+    emojiReactions: false,
+    followRequest: true
   },
   webPushNotifications: false,
   muteWords: [],
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index f1b7dcbd..239f41eb 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -13,6 +13,7 @@ import {
   omitBy
 } from 'lodash'
 import { set } from 'vue'
+import { isStatusNotification } from '../services/notification_utils/notification_utils.js'
 import apiService from '../services/api/api.service.js'
 // import parse from '../services/status_parser/status_parser.js'
 
@@ -321,7 +322,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
 
 const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
   each(notifications, (notification) => {
-    if (notification.type !== 'follow' && notification.type !== 'move') {
+    if (isStatusNotification(notification.type)) {
       notification.action = addStatusToGlobalStorage(state, notification.action).item
       notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
     }
@@ -361,13 +362,16 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
           case 'move':
             i18nString = 'migrated_to'
             break
+          case 'follow_request':
+            i18nString = 'follow_request'
+            break
         }
 
         if (notification.type === 'pleroma:emoji_reaction') {
           notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji])
         } else if (i18nString) {
           notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
-        } else {
+        } else if (isStatusNotification(notification.type)) {
           notifObj.body = notification.status.text
         }
 
@@ -521,6 +525,13 @@ export const mutations = {
       notification.seen = true
     })
   },
+  dismissNotification (state, { id }) {
+    state.notifications.data = state.notifications.data.filter(n => n.id !== id)
+  },
+  updateNotification (state, { id, updater }) {
+    const notification = find(state.notifications.data, n => n.id === id)
+    notification && updater(notification)
+  },
   queueFlush (state, { timeline, id }) {
     state.timelines[timeline].flushMarker = id
   },
@@ -680,6 +691,13 @@ const statuses = {
         credentials: rootState.users.currentUser.credentials
       })
     },
+    dismissNotification ({ rootState, commit }, { id }) {
+      rootState.api.backendInteractor.dismissNotification({ id })
+        .then(() => commit('dismissNotification', { id }))
+    },
+    updateNotification ({ rootState, commit }, { id, updater }) {
+      commit('updateNotification', { id, updater })
+    },
     fetchFavsAndRepeats ({ rootState, commit }, id) {
       Promise.all([
         rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ad2b2ad5..cda61ee2 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -29,6 +29,7 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
 const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
 const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
 const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
+const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss`
 const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
 const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
 const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
@@ -1010,6 +1011,15 @@ const unmuteDomain = ({ domain, credentials }) => {
   })
 }
 
+const dismissNotification = ({ credentials, id }) => {
+  return promisedRequest({
+    url: MASTODON_DISMISS_NOTIFICATION_URL(id),
+    method: 'POST',
+    payload: { id },
+    credentials
+  })
+}
+
 export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
   return Object.entries({
     ...(credentials
@@ -1165,6 +1175,7 @@ const apiService = {
   denyUser,
   suggestions,
   markNotificationsAsSeen,
+  dismissNotification,
   vote,
   fetchPoll,
   fetchFavoritedByUsers,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 84169a7b..6cacd0b8 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 { isStatusNotification } from '../notification_utils/notification_utils.js'
 
 const qvitterStatusType = (status) => {
   if (status.is_post_verb) {
@@ -346,9 +347,7 @@ export const parseNotification = (data) => {
   if (masto) {
     output.type = mastoDict[data.type] || data.type
     output.seen = data.pleroma.is_seen
-    output.status = output.type === 'follow' || output.type === 'move'
-      ? null
-      : parseStatus(data.status)
+    output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
     output.action = output.status // TODO: Refactor, this is unneeded
     output.target = output.type !== 'move'
       ? null
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index b17bd7bf..eb479227 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -1,4 +1,4 @@
-import { filter, sortBy } from 'lodash'
+import { filter, sortBy, includes } from 'lodash'
 
 export const notificationsFromStore = store => store.state.statuses.notifications.data
 
@@ -7,10 +7,15 @@ export const visibleTypes = store => ([
   store.state.config.notificationVisibility.mentions && 'mention',
   store.state.config.notificationVisibility.repeats && 'repeat',
   store.state.config.notificationVisibility.follows && 'follow',
+  store.state.config.notificationVisibility.followRequest && 'follow_request',
   store.state.config.notificationVisibility.moves && 'move',
   store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
 ].filter(_ => _))
 
+const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
+
+export const isStatusNotification = (type) => includes(statusNotifications, type)
+
 const sortById = (a, b) => {
   const seqA = Number(a.id)
   const seqB = Number(b.id)
diff --git a/static/fontello.json b/static/fontello.json
index 5a7086a2..5963b68b 100755
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -345,6 +345,18 @@
       "css": "link",
       "code": 59427,
       "src": "fontawesome"
+    },
+    {
+      "uid": "8b80d36d4ef43889db10bc1f0dc9a862",
+      "css": "user",
+      "code": 59428,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "12f4ece88e46abd864e40b35e05b11cd",
+      "css": "ok",
+      "code": 59431,
+      "src": "fontawesome"
     }
   ]
-}
+}
\ No newline at end of file

From 36dcfa8cc1315be65a47c041a8c926ff961b3ae4 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Sat, 2 May 2020 10:19:47 +0300
Subject: [PATCH 27/32] follow request bugfixes, wrong text, notifs not being
 marked as read, approving from follow request view

---
 .../follow_request_card.js                    | 19 +++++++++++++++++++
 src/components/notification/notification.js   |  1 +
 src/components/notification/notification.vue  |  6 +++---
 .../notifications/notifications.scss          | 19 +++++++++++++++++++
 4 files changed, 42 insertions(+), 3 deletions(-)

diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js
index a8931787..2a9d3db5 100644
--- a/src/components/follow_request_card/follow_request_card.js
+++ b/src/components/follow_request_card/follow_request_card.js
@@ -1,4 +1,5 @@
 import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
 
 const FollowRequestCard = {
   props: ['user'],
@@ -6,13 +7,31 @@ const FollowRequestCard = {
     BasicUserCard
   },
   methods: {
+    findFollowRequestNotificationId () {
+      const notif = notificationsFromStore(this.$store).find(
+        (notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
+      )
+      return notif && notif.id
+    },
     approveUser () {
       this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
       this.$store.dispatch('removeFollowRequest', this.user)
+
+      const notifId = this.findFollowRequestNotificationId()
+      this.$store.dispatch('updateNotification', {
+        id: notifId,
+        updater: notification => {
+          notification.type = 'follow'
+          notification.seen = true
+        }
+      })
     },
     denyUser () {
       this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
       this.$store.dispatch('removeFollowRequest', this.user)
+
+      const notifId = this.findFollowRequestNotificationId()
+      this.$store.dispatch('dismissNotification', { id: notifId })
     }
   }
 }
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 6deee7d5..8c20ff09 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -41,6 +41,7 @@ const Notification = {
         id: this.notification.id,
         updater: notification => {
           notification.type = 'follow'
+          notification.seen = true
         }
       })
     },
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 02802776..f6da07dd 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -137,13 +137,13 @@
             style="white-space: nowrap;"
           >
             <i
-              class="icon-ok button-icon add-reaction-button"
+              class="icon-ok button-icon follow-request-accept"
               :title="$t('tool_tip.accept_follow_request')"
               @click="approveUser()"
             />
             <i
-              class="icon-cancel button-icon add-reaction-button"
-              :title="$t('tool_tip.accept_follow_request')"
+              class="icon-cancel button-icon follow-request-reject"
+              :title="$t('tool_tip.reject_follow_request')"
               @click="denyUser()"
             />
           </div>
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 80dad28b..9efcfcf8 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -79,6 +79,25 @@
     }
   }
 
+  .follow-request-accept {
+    cursor: pointer;
+
+    &:hover {
+      color: $fallback--text;
+      color: var(--text, $fallback--text);
+    }
+  }
+
+  .follow-request-reject {
+    cursor: pointer;
+
+    &:hover {
+      color: $fallback--cRed;
+      color: var(--cRed, $fallback--cRed);
+    }
+  }
+
+
   .follow-text, .move-text {
     padding: 0.5em 0;
     overflow-wrap: break-word;

From 20b53d58b753075bb1bda49d0595eba02ed5f755 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Sat, 2 May 2020 10:52:57 +0300
Subject: [PATCH 28/32] mark single notifs as seen properly on server

---
 .../follow_request_card/follow_request_card.js       |  2 +-
 src/components/notification/notification.js          |  2 +-
 src/modules/statuses.js                              | 12 ++++++++++++
 src/services/api/api.service.js                      | 12 ++++++++----
 4 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js
index 2a9d3db5..33e2699e 100644
--- a/src/components/follow_request_card/follow_request_card.js
+++ b/src/components/follow_request_card/follow_request_card.js
@@ -18,11 +18,11 @@ const FollowRequestCard = {
       this.$store.dispatch('removeFollowRequest', this.user)
 
       const notifId = this.findFollowRequestNotificationId()
+      this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
       this.$store.dispatch('updateNotification', {
         id: notifId,
         updater: notification => {
           notification.type = 'follow'
-          notification.seen = true
         }
       })
     },
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 8c20ff09..abe3bebe 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -37,11 +37,11 @@ const Notification = {
     approveUser () {
       this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
       this.$store.dispatch('removeFollowRequest', this.user)
+      this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
       this.$store.dispatch('updateNotification', {
         id: this.notification.id,
         updater: notification => {
           notification.type = 'follow'
-          notification.seen = true
         }
       })
     },
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 239f41eb..2a8b9581 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -525,6 +525,10 @@ export const mutations = {
       notification.seen = true
     })
   },
+  markSingleNotificationAsSeen (state, { id }) {
+    const notification = find(state.notifications.data, n => n.id === id)
+    if (notification) notification.seen = true
+  },
   dismissNotification (state, { id }) {
     state.notifications.data = state.notifications.data.filter(n => n.id !== id)
   },
@@ -691,6 +695,14 @@ const statuses = {
         credentials: rootState.users.currentUser.credentials
       })
     },
+    markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
+      commit('markSingleNotificationAsSeen', { id })
+      apiService.markNotificationsAsSeen({
+        single: true,
+        id,
+        credentials: rootState.users.currentUser.credentials
+      })
+    },
     dismissNotification ({ rootState, commit }, { id }) {
       rootState.api.backendInteractor.dismissNotification({ id })
         .then(() => commit('dismissNotification', { id }))
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index cda61ee2..9d1ce393 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -4,7 +4,6 @@ import 'whatwg-fetch'
 import { RegistrationError, StatusCodeError } from '../errors/errors'
 
 /* eslint-env browser */
-const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
 const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
 const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
 const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
@@ -17,6 +16,7 @@ const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
 const ADMIN_USERS_URL = '/api/pleroma/admin/users'
 const SUGGESTIONS_URL = '/api/v1/suggestions'
 const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
+const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
 
 const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
 const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
@@ -845,12 +845,16 @@ const suggestions = ({ credentials }) => {
   }).then((data) => data.json())
 }
 
-const markNotificationsAsSeen = ({ id, credentials }) => {
+const markNotificationsAsSeen = ({ id, credentials, single = false }) => {
   const body = new FormData()
 
-  body.append('latest_id', id)
+  if (single) {
+    body.append('id', id)
+  } else {
+    body.append('max_id', id)
+  }
 
-  return fetch(QVITTER_USER_NOTIFICATIONS_READ_URL, {
+  return fetch(NOTIFICATION_READ_URL, {
     body,
     headers: authHeaders(credentials),
     method: 'POST'

From b095d2e17e03189adb62227da95ca5431bc64f5a Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Sat, 2 May 2020 11:51:39 +0300
Subject: [PATCH 29/32] don't dismiss a rejected follow request on server

---
 .../follow_request_card/follow_request_card.js           | 9 +++++----
 src/components/notification/notification.js              | 6 ++++--
 src/modules/statuses.js                                  | 5 ++++-
 3 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js
index 33e2699e..cbd75311 100644
--- a/src/components/follow_request_card/follow_request_card.js
+++ b/src/components/follow_request_card/follow_request_card.js
@@ -27,11 +27,12 @@ const FollowRequestCard = {
       })
     },
     denyUser () {
-      this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
-      this.$store.dispatch('removeFollowRequest', this.user)
-
       const notifId = this.findFollowRequestNotificationId()
-      this.$store.dispatch('dismissNotification', { id: notifId })
+      this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
+        .then(() => {
+          this.$store.dispatch('dismissNotificationLocal', { id: notifId })
+          this.$store.dispatch('removeFollowRequest', this.user)
+        })
     }
   }
 }
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index abe3bebe..1ae81ce4 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -47,8 +47,10 @@ const Notification = {
     },
     denyUser () {
       this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
-      this.$store.dispatch('removeFollowRequest', this.user)
-      this.$store.dispatch('dismissNotification', { id: this.notification.id })
+        .then(() => {
+          this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
+          this.$store.dispatch('removeFollowRequest', this.user)
+        })
     }
   },
   computed: {
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 2a8b9581..cd8c1dba 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -703,9 +703,12 @@ const statuses = {
         credentials: rootState.users.currentUser.credentials
       })
     },
+    dismissNotificationLocal ({ rootState, commit }, { id }) {
+      commit('dismissNotification', { id })
+    },
     dismissNotification ({ rootState, commit }, { id }) {
+      commit('dismissNotification', { id })
       rootState.api.backendInteractor.dismissNotification({ id })
-        .then(() => commit('dismissNotification', { id }))
     },
     updateNotification ({ rootState, commit }, { id, updater }) {
       commit('updateNotification', { id, updater })

From 2618c1b702d1881970cd1ee1109c421b24f2229e Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Sat, 2 May 2020 13:05:45 +0000
Subject: [PATCH 30/32] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32f9a2eb..ebd0e613 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
 All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## [Unreleased]
+### Changed
+- Removed the use of with_move parameters when fetching notifications
 
 ## [2.0.3] - 2020-05-02
 ### Fixed

From f7f8a579fa17102a994dc7bd7a4c7808e0964d55 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Mon, 4 May 2020 12:56:39 +0300
Subject: [PATCH 31/32] make email validation conditional work

---
 src/components/registration/registration.js | 23 ++++++++++++---------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index 1d8109e4..dab06e1e 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -14,15 +14,17 @@ const registration = {
     },
     captcha: {}
   }),
-  validations: {
-    user: {
-      email: requiredIf('accountActivationRequired'),
-      username: { required },
-      fullname: { required },
-      password: { required },
-      confirm: {
-        required,
-        sameAsPassword: sameAs('password')
+  validations () {
+    return {
+      user: {
+        email: { required: requiredIf(() => this.accountActivationRequired) },
+        username: { required },
+        fullname: { required },
+        password: { required },
+        confirm: {
+          required,
+          sameAsPassword: sameAs('password')
+        }
       }
     }
   },
@@ -43,7 +45,8 @@ const registration = {
       signedIn: (state) => !!state.users.currentUser,
       isPending: (state) => state.users.signUpPending,
       serverValidationErrors: (state) => state.users.signUpErrors,
-      termsOfService: (state) => state.instance.tos
+      termsOfService: (state) => state.instance.tos,
+      accountActivationRequired: (state) => state.instance.accountActivationRequired
     })
   },
   methods: {

From b3003d4e8de46ebf0ade12e6c6527bbfdb016e1d Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Wed, 6 May 2020 11:46:40 -0500
Subject: [PATCH 32/32] Add notification privacy option to user settings

---
 src/components/user_settings/user_settings.vue | 12 ++++++++++++
 src/i18n/en.json                               |  3 +++
 2 files changed, 15 insertions(+)

diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 8b2336b4..ad184520 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -379,6 +379,7 @@
           :label="$t('settings.notifications')"
         >
           <div class="setting-item">
+            <h2>{{ $t('settings.notification_setting_filters') }}</h2>
             <div class="select-multiple">
               <span class="label">{{ $t('settings.notification_setting') }}</span>
               <ul class="option-list">
@@ -404,6 +405,17 @@
                 </li>
               </ul>
             </div>
+          </div>
+
+          <div class="setting-item">
+            <h2>{{ $t('settings.notification_setting_privacy') }}</h2>
+            <p>
+              <Checkbox v-model="notificationSettings.privacy_option">
+                {{ $t('settings.notification_setting_privacy_option') }}
+              </Checkbox>
+            </p>
+          </div>
+          <div class="setting-item">
             <p>{{ $t('settings.notification_mutes') }}</p>
             <p>{{ $t('settings.notification_blocks') }}</p>
             <button
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 37d9591c..e42754ea 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -405,11 +405,14 @@
     "fun": "Fun",
     "greentext": "Meme arrows",
     "notifications": "Notifications",
+    "notification_setting_filters": "Filters",
     "notification_setting": "Receive notifications from:",
     "notification_setting_follows": "Users you follow",
     "notification_setting_non_follows": "Users you do not follow",
     "notification_setting_followers": "Users who follow you",
     "notification_setting_non_followers": "Users who do not follow you",
+    "notification_setting_privacy": "Privacy",
+    "notification_setting_privacy_option": "Hide the sender and contents of push notifications",
     "notification_mutes": "To stop receiving notifications from a specific user, use a mute.",
     "notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
     "enable_web_push_notifications": "Enable web push notifications",