From 8f63bbb64fa2860e73d8562de9d6c62629a8668f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 13 Jan 2020 00:33:04 +0200
Subject: poll slot renamed, lightBg customization implemented

---
 src/components/status/status.vue | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

(limited to 'src/components/status/status.vue')

diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index d291e762..72e9b25a 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -446,6 +446,11 @@ $status-margin: 0.75em;
   &_focused {
     background-color: $fallback--lightBg;
     background-color: var(--lightBg, $fallback--lightBg);
+    color: $fallback--text;
+    color: var(--lightBgText, $fallback--text);
+    --faint: var(--lightBgFaintText, $fallback--faint);
+    --faintLink: var(--lightBgFaintLink, $fallback--faint);
+    --icon: var(--lightBgIcon, $fallback--icon);
   }
 
   .timeline & {
@@ -573,8 +578,6 @@ $status-margin: 0.75em;
       overflow: hidden;
       text-overflow: ellipsis;
       margin: 0 0.4em 0 0.2em;
-      color: $fallback--faint;
-      color: var(--faint, $fallback--faint);
     }
 
     .replies-separator {
-- 
cgit v1.2.3-70-g09d2


From 8a7f3fc16a511e6c5917c0e25cba1f6163659264 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 13 Jan 2020 20:40:16 +0200
Subject: selectedPost and selectedMenu support

---
 src/components/emoji_input/emoji_input.vue         | 10 ++---
 src/components/nav_panel/nav_panel.vue             | 20 ++++-----
 src/components/selectable_list/selectable_list.vue | 10 ++---
 src/components/side_drawer/side_drawer.vue         | 10 ++---
 src/components/status/status.vue                   | 10 ++---
 src/components/style_switcher/style_switcher.vue   | 46 +++++++++++++++++++
 src/services/theme_data/theme_data.service.js      | 52 ++++++++++++++++++++++
 7 files changed, 128 insertions(+), 30 deletions(-)

(limited to 'src/components/status/status.vue')

diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index dcf51ff9..46ed6f25 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -157,11 +157,11 @@
 
       &.highlighted {
         background-color: $fallback--fg;
-        background-color: var(--lightBg, $fallback--fg);
-        color: var(--lightBgText, $fallback--text);
-        --faint: var(--lightBgFaintText, $fallback--faint);
-        --faintLink: var(--lightBgFaintLink, $fallback--faint);
-        --icon: var(--lightBgIcon, $fallback--icon);
+        background-color: var(--selectedMenu, $fallback--fg);
+        color: var(--selectedMenuText, $fallback--text);
+        --faint: var(--selectedMenuFaintText, $fallback--faint);
+        --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+        --icon: var(--selectedMenuIcon, $fallback--icon);
       }
     }
   }
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 0886bf8c..afc611ea 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -100,23 +100,23 @@
 
   &:hover {
     background-color: $fallback--lightBg;
-    background-color: var(--lightBg, $fallback--lightBg);
+    background-color: var(--selectedMenu, $fallback--lightBg);
     color: $fallback--link;
-    color: var(--lightBgText, $fallback--link);
-    --faint: var(--lightBgFaintText, $fallback--faint);
-    --faintLink: var(--lightBgFaintLink, $fallback--faint);
-    --icon: var(--lightBgIcon, $fallback--icon);
+    color: var(--selectedMenuText, $fallback--link);
+    --faint: var(--selectedMenuFaintText, $fallback--faint);
+    --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+    --icon: var(--selectedMenuIcon, $fallback--icon);
   }
 
   &.router-link-active {
     font-weight: bolder;
     background-color: $fallback--lightBg;
-    background-color: var(--lightBg, $fallback--lightBg);
+    background-color: var(--selectedMenu, $fallback--lightBg);
     color: $fallback--text;
-    color: var(--lightBgText, $fallback--text);
-    --faint: var(--lightBgFaintText, $fallback--faint);
-    --faintLink: var(--lightBgFaintLink, $fallback--faint);
-    --icon: var(--lightBgIcon, $fallback--icon);
+    color: var(--selectedMenuText, $fallback--text);
+    --faint: var(--selectedMenuFaintText, $fallback--faint);
+    --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+    --icon: var(--selectedMenuIcon, $fallback--icon);
 
     &:hover {
       text-decoration: underline;
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
index 416c9b6a..2d1e44a3 100644
--- a/src/components/selectable_list/selectable_list.vue
+++ b/src/components/selectable_list/selectable_list.vue
@@ -68,11 +68,11 @@
 
   &-item-selected-inner {
     background-color: $fallback--lightBg;
-    background-color: var(--lightBg, $fallback--lightBg);
-    color: var(--lightBgText, $fallback--text);
-    --faint: var(--lightBgFaintText, $fallback--faint);
-    --faintLink: var(--lightBgFaintLink, $fallback--faint);
-    --icon: var(--lightBgIcon, $fallback--icon);
+    background-color: var(--selectedMenu, $fallback--lightBg);
+    color: var(--selectedMenuText, $fallback--text);
+    --faint: var(--selectedMenuFaintText, $fallback--faint);
+    --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+    --icon: var(--selectedMenuIcon, $fallback--icon);
   }
 
   &-header {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 6d75221f..3188fd59 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -289,12 +289,12 @@
 
     &:hover {
       background-color: $fallback--lightBg;
-      background-color: var(--lightBg, $fallback--lightBg);
+      background-color: var(--selectedMenu, $fallback--lightBg);
       color: $fallback--text;
-      color: var(--lightBgText, $fallback--text);
-      --faint: var(--lightBgFaintText, $fallback--faint);
-      --faintLink: var(--lightBgFaintLink, $fallback--faint);
-      --icon: var(--lightBgIcon, $fallback--icon);
+      color: var(--selectedMenuText, $fallback--text);
+      --faint: var(--selectedMenuFaintText, $fallback--faint);
+      --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+      --icon: var(--selectedMenuIcon, $fallback--icon);
     }
   }
 }
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 72e9b25a..38d091ed 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -445,12 +445,12 @@ $status-margin: 0.75em;
 
   &_focused {
     background-color: $fallback--lightBg;
-    background-color: var(--lightBg, $fallback--lightBg);
+    background-color: var(--selectedPost, $fallback--lightBg);
     color: $fallback--text;
-    color: var(--lightBgText, $fallback--text);
-    --faint: var(--lightBgFaintText, $fallback--faint);
-    --faintLink: var(--lightBgFaintLink, $fallback--faint);
-    --icon: var(--lightBgIcon, $fallback--icon);
+    color: var(--selectedPostText, $fallback--text);
+    --faint: var(--selectedPostFaintText, $fallback--faint);
+    --faintLink: var(--selectedPostFaintLink, $fallback--faint);
+    --icon: var(--selectedPostIcon, $fallback--icon);
   }
 
   .timeline & {
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index f2fdfea2..613abd1f 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -465,6 +465,52 @@
             />
             <ContrastRatio :contrast="previewContrast.lightBgLink" />
           </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4>
+            <ColorInput
+              v-model="selectedPostColorLocal"
+              name="selectedPost"
+              :label="$t('settings.background')"
+              :fallback="previewTheme.colors.selectedPost"
+            />
+            <ColorInput
+              v-model="selectedPostTextColorLocal"
+              name="selectedPostText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.selectedPostText"
+            />
+            <ContrastRatio :contrast="previewContrast.selectedPostText" />
+            <ColorInput
+              v-model="selectedPostLinkColorLocal"
+              name="selectedPostLink"
+              :label="$t('settings.links')"
+              :fallback="previewTheme.colors.selectedPostLink"
+            />
+            <ContrastRatio :contrast="previewContrast.selectedPostLink" />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.selectedMenu') }}</h4>
+            <ColorInput
+              v-model="selectedMenuColorLocal"
+              name="selectedMenu"
+              :label="$t('settings.background')"
+              :fallback="previewTheme.colors.selectedMenu"
+            />
+            <ColorInput
+              v-model="selectedMenuTextColorLocal"
+              name="selectedMenuText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.selectedMenuText"
+            />
+            <ContrastRatio :contrast="previewContrast.selectedMenuText" />
+            <ColorInput
+              v-model="selectedMenuLinkColorLocal"
+              name="selectedMenuLink"
+              :label="$t('settings.links')"
+              :fallback="previewTheme.colors.selectedMenuLink"
+            />
+            <ContrastRatio :contrast="previewContrast.selectedMenuLink" />
+          </div>
         </div>
 
         <div
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index 221c3b48..dd5f8fd0 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -98,6 +98,58 @@ export const SLOT_INHERITANCE = {
     color: (mod, bg, text) => mixrgb(bg, text)
   },
 
+  selectedPost: '--lightBg',
+  selectedPostFaintText: {
+    depends: ['lightBgFaintText'],
+    layer: 'lightBg',
+    textColor: true
+  },
+  selectedPostFaintLink: {
+    depends: ['lightBgFaintLink'],
+    layer: 'lightBg',
+    textColor: 'preserve'
+  },
+  selectedPostText: {
+    depends: ['lightBgText'],
+    layer: 'lightBg',
+    textColor: true
+  },
+  selectedPostLink: {
+    depends: ['lightBgLink'],
+    layer: 'lightBg',
+    textColor: 'preserve'
+  },
+  selectedPostIcon: {
+    depends: ['selectedPost', 'selectedPostText'],
+    color: (mod, bg, text) => mixrgb(bg, text)
+  },
+
+  selectedMenu: '--lightBg',
+  selectedMenuFaintText: {
+    depends: ['lightBgFaintText'],
+    layer: 'lightBg',
+    textColor: true
+  },
+  selectedMenuFaintLink: {
+    depends: ['lightBgFaintLink'],
+    layer: 'lightBg',
+    textColor: 'preserve'
+  },
+  selectedMenuText: {
+    depends: ['lightBgText'],
+    layer: 'lightBg',
+    textColor: true
+  },
+  selectedMenuLink: {
+    depends: ['lightBgLink'],
+    layer: 'lightBg',
+    textColor: 'preserve'
+  },
+  selectedMenuIcon: {
+    depends: ['selectedMenu', 'selectedMenuText'],
+    color: (mod, bg, text) => mixrgb(bg, text)
+  },
+
   lightText: {
     depends: ['text'],
     color: (mod, text) => brightness(20 * mod, text).rgb
-- 
cgit v1.2.3-70-g09d2


From b96993e4dd6b2d9197b430af404e8fa652888b51 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 24 Jan 2020 00:36:32 +0200
Subject: improved selectedMenu again

---
 src/components/emoji_input/emoji_input.vue         | 12 +++--
 src/components/emoji_picker/emoji_picker.scss      |  9 ++++
 src/components/nav_panel/nav_panel.vue             |  2 +
 src/components/popper/popper.scss                  | 12 +++--
 src/components/selectable_list/selectable_list.vue |  1 +
 src/components/side_drawer/side_drawer.vue         | 12 +++--
 src/components/status/status.vue                   |  1 +
 src/services/color_convert/color_convert.js        |  2 +-
 src/services/theme_data/pleromafe.js               | 63 +++++++++++++++++++++-
 src/services/theme_data/theme_data.service.js      |  7 +--
 static/themes/breezy-dark.json                     |  4 +-
 static/themes/breezy-light.json                    |  4 +-
 12 files changed, 105 insertions(+), 24 deletions(-)

(limited to 'src/components/status/status.vue')

diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 9c2501a9..94eae560 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -115,6 +115,7 @@
         color: var(--popoverText, $fallback--link);
         --faint: var(--popoverFaintText, $fallback--faint);
         --faintLink: var(--popoverFaintLink, $fallback--faint);
+        --lightText: var(--popoverLightText, $fallback--lightText);
         --icon: var(--popoverIcon, $fallback--icon);
       }
     }
@@ -160,11 +161,12 @@
 
       &.highlighted {
         background-color: $fallback--fg;
-        background-color: var(--selectedMenu, $fallback--fg);
-        color: var(--selectedMenuText, $fallback--text);
-        --faint: var(--selectedMenuFaintText, $fallback--faint);
-        --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
-        --icon: var(--selectedMenuIcon, $fallback--icon);
+        background-color: var(--selectedMenuPopover, $fallback--fg);
+        color: var(--selectedMenuPopoverText, $fallback--text);
+        --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+        --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+        --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+        --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
       }
     }
   }
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 6608f393..8bd07e45 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -8,6 +8,15 @@
   left: 0;
   margin: 0 !important;
   z-index: 1;
+  background-color: $fallback--bg;
+  background-color: var(--popover, $fallback--bg);
+  color: $fallback--link;
+  color: var(--popoverText, $fallback--link);
+  --lightText: var(--popoverLightText, $fallback--faint);
+  --faint: var(--popoverFaintText, $fallback--faint);
+  --faintLink: var(--popoverFaintLink, $fallback--faint);
+  --lightText: var(--popoverLightText, $fallback--lightText);
+  --icon: var(--popoverIcon, $fallback--icon);
 
   .keep-open,
   .too-many-emoji {
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index afc611ea..a934a411 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -105,6 +105,7 @@
     color: var(--selectedMenuText, $fallback--link);
     --faint: var(--selectedMenuFaintText, $fallback--faint);
     --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+    --lightText: var(--selectedMenuLightText, $fallback--lightText);
     --icon: var(--selectedMenuIcon, $fallback--icon);
   }
 
@@ -116,6 +117,7 @@
     color: var(--selectedMenuText, $fallback--text);
     --faint: var(--selectedMenuFaintText, $fallback--faint);
     --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+    --lightText: var(--selectedMenuLightText, $fallback--lightText);
     --icon: var(--selectedMenuIcon, $fallback--icon);
 
     &:hover {
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
index e70b277d..591be37e 100644
--- a/src/components/popper/popper.scss
+++ b/src/components/popper/popper.scss
@@ -14,6 +14,7 @@
     color: var(--popoverText, $fallback--text);
     --faint: var(--popoverFaintText, $fallback--faint);
     --faintLink: var(--popoverFaintLink, $fallback--faint);
+    --lightText: var(--popoverLightText, $fallback--lightText);
     --icon: var(--popoverIcon, $fallback--icon);
   }
 
@@ -144,12 +145,13 @@
 
     &:active, &:hover {
       background-color: $fallback--lightBg;
-      background-color: var(--selectedMenu, $fallback--lightBg);
+      background-color: var(--selectedMenuPopover, $fallback--lightBg);
       color: $fallback--link;
-      color: var(--selectedMenuText, $fallback--link);
-      --faint: var(--selectedMenuFaintText, $fallback--faint);
-      --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
-      --icon: var(--selectedMenuIcon, $fallback--icon);
+      color: var(--selectedMenuPopoverText, $fallback--link);
+      --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+      --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+      --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+      --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
     }
   }
 }
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
index 2d1e44a3..a9bb12a1 100644
--- a/src/components/selectable_list/selectable_list.vue
+++ b/src/components/selectable_list/selectable_list.vue
@@ -72,6 +72,7 @@
     color: var(--selectedMenuText, $fallback--text);
     --faint: var(--selectedMenuFaintText, $fallback--faint);
     --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+    --lightText: var(--selectedMenuLightText, $fallback--lightText);
     --icon: var(--selectedMenuIcon, $fallback--icon);
   }
 
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index a5b754d7..25e4de01 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -228,6 +228,7 @@
   color: var(--popoverText, $fallback--link);
   --faint: var(--popoverFaintText, $fallback--faint);
   --faintLink: var(--popoverFaintLink, $fallback--faint);
+  --lightText: var(--popoverLightText, $fallback--lightText);
   --icon: var(--popoverIcon, $fallback--icon);
 
   .button-icon:before {
@@ -294,12 +295,13 @@
 
     &:hover {
       background-color: $fallback--lightBg;
-      background-color: var(--selectedMenu, $fallback--lightBg);
+      background-color: var(--selectedMenuPopover, $fallback--lightBg);
       color: $fallback--text;
-      color: var(--selectedMenuText, $fallback--text);
-      --faint: var(--selectedMenuFaintText, $fallback--faint);
-      --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
-      --icon: var(--selectedMenuIcon, $fallback--icon);
+      color: var(--selectedMenuPopoverText, $fallback--text);
+      --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+      --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+      --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+      --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
     }
   }
 }
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 38d091ed..1997e187 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -448,6 +448,7 @@ $status-margin: 0.75em;
     background-color: var(--selectedPost, $fallback--lightBg);
     color: $fallback--text;
     color: var(--selectedPostText, $fallback--text);
+    --lightText: var(--selectedPostLightText, $fallback--light);
     --faint: var(--selectedPostFaintText, $fallback--faint);
     --faintLink: var(--selectedPostFaintLink, $fallback--faint);
     --icon: var(--selectedPostIcon, $fallback--icon);
diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js
index 93cb1ba6..0bf8f646 100644
--- a/src/services/color_convert/color_convert.js
+++ b/src/services/color_convert/color_convert.js
@@ -69,7 +69,7 @@ const srgbToLinear = (srgb) => {
  * @param {Object} srgb - sRGB color
  * @returns {Number} relative luminance
  */
-const relativeLuminance = (srgb) => {
+export const relativeLuminance = (srgb) => {
   const { r, g, b } = srgbToLinear(srgb)
   return 0.2126 * r + 0.7152 * g + 0.0722 * b
 }
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 484874d7..a40d08a6 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -117,6 +117,11 @@ export const SLOT_INHERITANCE = {
     depends: ['bg'],
     color: (mod, bg) => brightness(5 * mod, bg).rgb
   },
+  highlightLightText: {
+    depends: ['lightText'],
+    layer: 'highlight',
+    textColor: true
+  },
   highlightFaintText: {
     depends: ['faint'],
     layer: 'highlight',
@@ -146,6 +151,11 @@ export const SLOT_INHERITANCE = {
     depends: ['bg'],
     opacity: 'popover'
   },
+  popoverLightText: {
+    depends: ['lightText'],
+    layer: 'popover',
+    textColor: true
+  },
   popoverFaintText: {
     depends: ['faint'],
     layer: 'popover',
@@ -178,6 +188,12 @@ export const SLOT_INHERITANCE = {
     variant: 'selectedPost',
     textColor: true
   },
+  selectedPostLightText: {
+    depends: ['highlightLightText'],
+    layer: 'highlight',
+    variant: 'selectedPost',
+    textColor: true
+  },
   selectedPostFaintLink: {
     depends: ['highlightFaintLink'],
     layer: 'highlight',
@@ -202,9 +218,15 @@ export const SLOT_INHERITANCE = {
   },
 
   selectedMenu: {
-    depends: ['popover'],
+    depends: ['bg'],
     color: (mod, bg) => brightness(5 * mod, bg).rgb
   },
+  selectedMenuLightText: {
+    depends: ['highlightLightText'],
+    layer: 'selectedMenu',
+    variant: 'selectedMenu',
+    textColor: true
+  },
   selectedMenuFaintText: {
     depends: ['highlightFaintText'],
     layer: 'selectedMenu',
@@ -234,6 +256,45 @@ export const SLOT_INHERITANCE = {
     color: (mod, bg, text) => mixrgb(bg, text)
   },
 
+  selectedMenuPopover: {
+    depends: ['popover'],
+    color: (mod, bg) => brightness(5 * mod, bg).rgb
+  },
+  selectedMenuPopoverLightText: {
+    depends: ['selectedMenuLightText'],
+    layer: 'selectedMenuPopover',
+    variant: 'selectedMenuPopover',
+    textColor: true
+  },
+  selectedMenuPopoverFaintText: {
+    depends: ['selectedMenuFaintText'],
+    layer: 'selectedMenuPopover',
+    variant: 'selectedMenuPopover',
+    textColor: true
+  },
+  selectedMenuPopoverFaintLink: {
+    depends: ['selectedMenuFaintLink'],
+    layer: 'selectedMenuPopover',
+    variant: 'selectedMenuPopover',
+    textColor: 'preserve'
+  },
+  selectedMenuPopoverText: {
+    depends: ['selectedMenuText'],
+    layer: 'selectedMenuPopover',
+    variant: 'selectedMenuPopover',
+    textColor: true
+  },
+  selectedMenuPopoverLink: {
+    depends: ['selectedMenuLink'],
+    layer: 'selectedMenuPopover',
+    variant: 'selectedMenuPopover',
+    textColor: 'preserve'
+  },
+  selectedMenuPopoverIcon: {
+    depends: ['selectedMenuPopover', 'selectedMenuText'],
+    color: (mod, bg, text) => mixrgb(bg, text)
+  },
+
   lightText: {
     depends: ['text'],
     color: (mod, text) => brightness(20 * mod, text).rgb
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index ea28481b..c0861a4a 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -1,5 +1,5 @@
 import { convert, brightness, contrastRatio } from 'chromatism'
-import { alphaBlendLayers, getTextColor } from '../color_convert/color_convert.js'
+import { alphaBlendLayers, getTextColor, relativeLuminance } from '../color_convert/color_convert.js'
 import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js'
 
 /*
@@ -318,13 +318,14 @@ export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.redu
           opacity
         )
       )
+      const isLightOnDark = relativeLuminance(bg) > 127
+      const mod = isLightOnDark ? 1 : -1
+
       if (value.textColor === 'bw') {
         outputColor = contrastRatio(bg).rgb
       } else {
         let color = { ...colors[deps[0]] }
         if (value.color) {
-          const isLightOnDark = convert(bg).hsl.l < convert(color).hsl.l
-          const mod = isLightOnDark ? 1 : -1
           color = value.color(mod, ...deps.map((dep) => ({ ...colors[dep] })))
         }
 
diff --git a/static/themes/breezy-dark.json b/static/themes/breezy-dark.json
index c3bdcf65..236b94ad 100644
--- a/static/themes/breezy-dark.json
+++ b/static/themes/breezy-dark.json
@@ -114,8 +114,8 @@
       "cGreen": "#27ae60",
       "cOrange": "#f67400",
       "btnPressed": "--accent",
-      "highlight": "--accent",
-      "selectedPost": "--bg,10"
+      "selectedMenu": "--accent",
+      "selectedMenuPopover": "--accent"
     },
     "radii": {
       "btn": "2",
diff --git a/static/themes/breezy-light.json b/static/themes/breezy-light.json
index 478407c9..d3f74cec 100644
--- a/static/themes/breezy-light.json
+++ b/static/themes/breezy-light.json
@@ -117,8 +117,8 @@
       "cGreen": "#27ae60",
       "cOrange": "#f67400",
       "btnPressed": "--accent",
-      "highlight": "--accent",
-      "selectedPost": "--bg,10"
+      "selectedMenu": "--accent",
+      "selectedMenuPopover": "--accent"
     },
     "radii": {
       "btn": "2",
-- 
cgit v1.2.3-70-g09d2


From 526b43eba66c597c78857da6ded3c7f672f96e49 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 28 Jan 2020 01:29:20 +0200
Subject: Kenomo (see: #624) theme. Ability to define link color for post
 contents. Fixes

---
 src/components/emoji_input/emoji_input.vue       |  2 +
 src/components/notifications/notifications.scss  |  3 +
 src/components/popper/popper.scss                |  2 +
 src/components/status/status.vue                 |  7 +++
 src/components/style_switcher/style_switcher.vue | 17 ++++++
 src/components/user_card/user_card.vue           |  5 ++
 src/services/theme_data/pleromafe.js             | 34 ++++++++++++
 static/themes/kenomo.json                        | 71 ++++++++++++++++++++++++
 8 files changed, 141 insertions(+)
 create mode 100644 static/themes/kenomo.json

(limited to 'src/components/status/status.vue')

diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 94eae560..e9ac09c3 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -116,6 +116,8 @@
         --faint: var(--popoverFaintText, $fallback--faint);
         --faintLink: var(--popoverFaintLink, $fallback--faint);
         --lightText: var(--popoverLightText, $fallback--lightText);
+        --postLink: var(--popoverPostLink, $fallback--link);
+        --postFaintLink: var(--popoverPostFaintLink, $fallback--link);
         --icon: var(--popoverIcon, $fallback--icon);
       }
     }
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 71876b14..ca762432 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -68,6 +68,9 @@
         a {
           color: var(--faintLink);
         }
+        .status-content a {
+          color: var(--postFaintLink);
+        }
       }
       padding: 0;
       .media-body {
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
index 341416c1..8e2dcd55 100644
--- a/src/components/popper/popper.scss
+++ b/src/components/popper/popper.scss
@@ -15,6 +15,8 @@
     --faint: var(--popoverFaintText, $fallback--faint);
     --faintLink: var(--popoverFaintLink, $fallback--faint);
     --lightText: var(--popoverLightText, $fallback--lightText);
+    --postLink: var(--popoverPostLink, $fallback--link);
+    --postFaintLink: var(--popoverPostFaintLink, $fallback--link);
     --icon: var(--popoverIcon, $fallback--icon);
   }
 
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 1997e187..b1048832 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -451,6 +451,8 @@ $status-margin: 0.75em;
     --lightText: var(--selectedPostLightText, $fallback--light);
     --faint: var(--selectedPostFaintText, $fallback--faint);
     --faintLink: var(--selectedPostFaintLink, $fallback--faint);
+    --postLink: var(--selectedPostPostLink, $fallback--faint);
+    --postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint);
     --icon: var(--selectedPostIcon, $fallback--icon);
   }
 
@@ -640,6 +642,11 @@ $status-margin: 0.75em;
     line-height: 1.4em;
     white-space: pre-wrap;
 
+    a {
+      color: $fallback--link;
+      color: var(--postLink, $fallback--link);
+    }
+
     img, video {
       max-width: 100%;
       max-height: 400px;
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 6e38bd8e..0b3b0fbf 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -248,6 +248,23 @@
             </button>
           </div>
           <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.post') }}</h4>
+            <ColorInput
+              v-model="postLinkColorLocal"
+              name="postLinkColor"
+              :fallback="previewTheme.colors.accent"
+              :label="$t('settings.links')"
+              :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
+            />
+            <ContrastRatio :contrast="previewContrast.bgPostLink" />
+            <ColorInput
+              v-model="iconColorLocal"
+              name="iconColor"
+              :fallback="previewTheme.colors.accent"
+              :label="$t('settings.links')"
+              :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
+            />
+            <ContrastRatio :contrast="previewContrast.bgIcon" />
             <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
             <ColorInput
               v-model="alertErrorColorLocal"
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index e286ceea..72656435 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -299,6 +299,11 @@
   &-bio {
     text-align: center;
 
+    a {
+      color: $fallback--link;
+      color: var(--postLink, $fallback--link);
+    }
+
     img {
       object-fit: contain;
       vertical-align: middle;
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index a40d08a6..74e6fc1c 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -107,6 +107,10 @@ export const SLOT_INHERITANCE = {
     depends: ['link'],
     opacity: 'faint'
   },
+  postFaintLink: {
+    depends: ['postLink'],
+    opacity: 'faint'
+  },
 
   cBlue: '#0000ff',
   cRed: '#FF0000',
@@ -122,6 +126,11 @@ export const SLOT_INHERITANCE = {
     layer: 'highlight',
     textColor: true
   },
+  highlightPostLink: {
+    depends: ['postLink'],
+    layer: 'highlight',
+    textColor: 'preserve'
+  },
   highlightFaintText: {
     depends: ['faint'],
     layer: 'highlight',
@@ -132,6 +141,11 @@ export const SLOT_INHERITANCE = {
     layer: 'highlight',
     textColor: 'preserve'
   },
+  highlightPostFaintLink: {
+    depends: ['postFaintLink'],
+    layer: 'highlight',
+    textColor: 'preserve'
+  },
   highlightText: {
     depends: ['text'],
     layer: 'highlight',
@@ -156,6 +170,11 @@ export const SLOT_INHERITANCE = {
     layer: 'popover',
     textColor: true
   },
+  popoverPostLink: {
+    depends: ['postLink'],
+    layer: 'popover',
+    textColor: 'preserve'
+  },
   popoverFaintText: {
     depends: ['faint'],
     layer: 'popover',
@@ -166,6 +185,11 @@ export const SLOT_INHERITANCE = {
     layer: 'popover',
     textColor: 'preserve'
   },
+  popoverPostFaintLink: {
+    depends: ['postFaintLink'],
+    layer: 'popover',
+    textColor: 'preserve'
+  },
   popoverText: {
     depends: ['text'],
     layer: 'popover',
@@ -194,6 +218,12 @@ export const SLOT_INHERITANCE = {
     variant: 'selectedPost',
     textColor: true
   },
+  selectedPostPostLink: {
+    depends: ['highlightPostLink'],
+    layer: 'highlight',
+    variant: 'selectedPost',
+    textColor: 'preserve'
+  },
   selectedPostFaintLink: {
     depends: ['highlightFaintLink'],
     layer: 'highlight',
@@ -300,6 +330,10 @@ export const SLOT_INHERITANCE = {
     color: (mod, text) => brightness(20 * mod, text).rgb
   },
 
+  postLink: {
+    depends: ['link']
+  },
+
   border: {
     depends: ['fg'],
     opacity: 'border',
diff --git a/static/themes/kenomo.json b/static/themes/kenomo.json
new file mode 100644
index 00000000..98ddf974
--- /dev/null
+++ b/static/themes/kenomo.json
@@ -0,0 +1,71 @@
+{
+  "_pleroma_theme_version": 2,
+  "name": "Kenomo",
+  "source": {
+    "themeEngineVersion": 3,
+    "fonts": {},
+    "shadows": {
+      "panel": [],
+      "topBar": [],
+      "button": [
+        {
+          "x": 0,
+          "y": 1,
+          "blur": 0,
+          "spread": 0,
+          "color": "#FFFFFF",
+          "alpha": 0.2,
+          "inset": true
+        },
+        {
+          "x": 0,
+          "y": -1,
+          "blur": 0,
+          "spread": 0,
+          "color": "#000000",
+          "alpha": 0.2,
+          "inset": true
+        }
+      ],
+      "input": [
+        {
+          "x": 0,
+          "y": "0",
+          "blur": 0,
+          "spread": "1",
+          "color": "#576574",
+          "alpha": "1",
+          "inset": true
+        }
+      ]
+    },
+    "opacity": {
+      "underlay": "1",
+      "border": "0"
+    },
+    "colors": {
+      "bg": "#ffffff",
+      "fg": "#f6f6f6",
+      "text": "#494949",
+      "underlay": "#ffffff",
+      "link": "#818181",
+      "accent": "#818181",
+      "cBlue": "#2e86de",
+      "cRed": "#c96248",
+      "cGreen": "#0fa00f",
+      "cOrange": "#aa7623",
+      "postLink": "#2e86de",
+      "border": "#ffffff",
+      "icon": "#8a8a8a",
+      "panel": "transparent",
+      "topBarText": "#4b4b4b",
+      "tab": "--btn,-30",
+      "btn": "#576574"
+    },
+    "radii": {
+      "panel": "0",
+      "avatar": "6",
+      "avatarAlt": "6"
+    }
+  }
+}
-- 
cgit v1.2.3-70-g09d2


From 31225f5d142b51d52bed305f25a37288c9188062 Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Fri, 28 Feb 2020 16:39:47 +0000
Subject: Fix/popover performance

---
 CHANGELOG.md                                       |   1 +
 package.json                                       |   1 -
 src/components/account_actions/account_actions.js  |   4 +-
 src/components/account_actions/account_actions.vue |  21 +--
 src/components/emoji_reactions/emoji_reactions.js  |  11 +-
 src/components/emoji_reactions/emoji_reactions.vue |  33 +++--
 src/components/extra_buttons/extra_buttons.js      |   3 +
 src/components/extra_buttons/extra_buttons.vue     |  14 +-
 .../moderation_tools/moderation_tools.js           |  11 +-
 .../moderation_tools/moderation_tools.vue          |  17 ++-
 src/components/popover/popover.js                  | 156 ++++++++++++++++++++
 src/components/popover/popover.vue                 | 118 +++++++++++++++
 src/components/popper/popper.scss                  | 164 ---------------------
 src/components/react_button/react_button.js        |  23 +--
 src/components/react_button/react_button.vue       |  32 ++--
 src/components/status/status.vue                   |  11 +-
 src/components/status_popover/status_popover.js    |  12 +-
 src/components/status_popover/status_popover.vue   |  64 +++-----
 src/main.js                                        |   8 -
 yarn.lock                                          |  19 ---
 20 files changed, 393 insertions(+), 330 deletions(-)
 create mode 100644 src/components/popover/popover.js
 create mode 100644 src/components/popover/popover.vue
 delete mode 100644 src/components/popper/popper.scss

(limited to 'src/components/status/status.vue')

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1cdd604b..e77334b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Registration fixed
 - Deactivation of remote accounts from frontend
 - Fixed NSFW unhiding not working with videos when using one-click unhiding/displaying
+- Improved performance of anything that uses popovers (most notably statuses)
 
 ## [1.1.7 and earlier] - 2019-12-14
 ### Added
diff --git a/package.json b/package.json
index 5c7fa31e..542086b4 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,6 @@
     "portal-vue": "^2.1.4",
     "sanitize-html": "^1.13.0",
     "v-click-outside": "^2.1.1",
-    "v-tooltip": "^2.0.2",
     "vue": "^2.5.13",
     "vue-chat-scroll": "^1.2.1",
     "vue-i18n": "^7.3.2",
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index d2153680..5d7ecf7e 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -1,4 +1,5 @@
 import ProgressButton from '../progress_button/progress_button.vue'
+import Popover from '../popover/popover.vue'
 
 const AccountActions = {
   props: [
@@ -8,7 +9,8 @@ const AccountActions = {
     return { }
   },
   components: {
-    ProgressButton
+    ProgressButton,
+    Popover
   },
   methods: {
     showRepeats () {
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index d3235be1..483783cf 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -1,13 +1,13 @@
 <template>
   <div class="account-actions">
-    <v-popover
+    <Popover
       trigger="click"
-      class="account-tools-popover"
-      :container="false"
-      placement="bottom-end"
-      :offset="5"
+      placement="bottom"
     >
-      <div slot="popover">
+      <div
+        slot="content"
+        class="account-tools-popover"
+      >
         <div class="dropdown-menu">
           <template v-if="user.following">
             <button
@@ -51,10 +51,13 @@
           </button>
         </div>
       </div>
-      <div class="btn btn-default ellipsis-button">
+      <div
+        slot="trigger"
+        class="btn btn-default ellipsis-button"
+      >
         <i class="icon-ellipsis trigger-button" />
       </div>
-    </v-popover>
+    </Popover>
   </div>
 </template>
 
@@ -62,7 +65,6 @@
 
 <style lang="scss">
 @import '../../_variables.scss';
-@import '../popper/popper.scss';
 .account-actions {
   margin: 0 .8em;
 }
@@ -70,6 +72,7 @@
 .account-actions button.dropdown-item {
   margin-left: 0;
 }
+
 .account-actions .trigger-button {
   color: $fallback--lightText;
   color: var(--lightText, $fallback--lightText);
diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js
index b799ac9a..ae7f53be 100644
--- a/src/components/emoji_reactions/emoji_reactions.js
+++ b/src/components/emoji_reactions/emoji_reactions.js
@@ -1,20 +1,17 @@
 import UserAvatar from '../user_avatar/user_avatar.vue'
+import Popover from '../popover/popover.vue'
 
 const EMOJI_REACTION_COUNT_CUTOFF = 12
 
 const EmojiReactions = {
   name: 'EmojiReactions',
   components: {
-    UserAvatar
+    UserAvatar,
+    Popover
   },
   props: ['status'],
   data: () => ({
-    showAll: false,
-    popperOptions: {
-      modifiers: {
-        preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
-      }
-    }
+    showAll: false
   }),
   computed: {
     tooManyReactions () {
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index b25c9716..bac4c605 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -1,15 +1,14 @@
 <template>
   <div class="emoji-reactions">
-    <v-popover
+    <Popover
       v-for="(reaction) in emojiReactions"
       :key="reaction.name"
-      :popper-options="popperOptions"
       trigger="hover"
       placement="top"
+      :offset="{ y: 5 }"
     >
-
       <div
-        slot="popover"
+        slot="content"
         class="reacted-users"
       >
         <div v-if="accountsForEmoji[reaction.name].length">
@@ -24,7 +23,12 @@
               :compact="true"
             />
             <div class="reacted-user-names">
-              <span class="reacted-user-name" v-html="account.name_html" />
+              <!-- eslint-disable vue/no-v-html -->
+              <span
+                class="reacted-user-name"
+                v-html="account.name_html"
+              />
+              <!-- eslint-enable vue/no-v-html -->
               <span class="reacted-user-screen-name">{{ account.screen_name }}</span>
             </div>
           </div>
@@ -34,6 +38,7 @@
         </div>
       </div>
       <button
+        slot="trigger"
         class="emoji-reaction btn btn-default"
         :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
         @click="emojiOnClick(reaction.name, $event)"
@@ -42,17 +47,16 @@
         <span class="reaction-emoji">{{ reaction.name }}</span>
         <span>{{ reaction.count }}</span>
       </button>
-    </v-popover>
+    </Popover>
     <a
-        v-if="tooManyReactions"
-        @click="toggleShowAll"
-        class="emoji-reaction-expand faint"
-        href='javascript:void(0)'
-      >
-        {{ showAll ? $t('general.show_less') : showMoreString }}
-      </a>
+      v-if="tooManyReactions"
+      class="emoji-reaction-expand faint"
+      href="javascript:void(0)"
+      @click="toggleShowAll"
+    >
+      {{ showAll ? $t('general.show_less') : showMoreString }}
+    </a>
   </div>
-
 </template>
 
 <script src="./emoji_reactions.js" ></script>
@@ -78,6 +82,7 @@
     display: flex;
     flex-direction: column;
     margin-left: 0.5em;
+    min-width: 5em;
 
     img {
       width: 1em;
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 5ac73e97..37485383 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -1,5 +1,8 @@
+import Popover from '../popover/popover.vue'
+
 const ExtraButtons = {
   props: [ 'status' ],
+  components: { Popover },
   methods: {
     deleteStatus () {
       const confirmed = window.confirm(this.$t('status.delete_confirm'))
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 746f1c91..3a7f1283 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -1,11 +1,11 @@
 <template>
-  <v-popover
+  <Popover
     v-if="canDelete || canMute || canPin"
     trigger="click"
     placement="top"
     class="extra-button-popover"
   >
-    <div slot="popover">
+    <div slot="content">
       <div class="dropdown-menu">
         <button
           v-if="canMute && !status.thread_muted"
@@ -47,17 +47,17 @@
         </button>
       </div>
     </div>
-    <div class="button-icon">
-      <i class="icon-ellipsis" />
-    </div>
-  </v-popover>
+    <i
+      slot="trigger"
+      class="icon-ellipsis button-icon"
+    />
+  </Popover>
 </template>
 
 <script src="./extra_buttons.js" ></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
-@import '../popper/popper.scss';
 
 .icon-ellipsis {
   cursor: pointer;
diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js
index 757166ed..d4fdc53e 100644
--- a/src/components/moderation_tools/moderation_tools.js
+++ b/src/components/moderation_tools/moderation_tools.js
@@ -1,4 +1,5 @@
 import DialogModal from '../dialog_modal/dialog_modal.vue'
+import Popover from '../popover/popover.vue'
 
 const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
 const STRIP_MEDIA = 'mrf_tag:media-strip'
@@ -14,7 +15,6 @@ const ModerationTools = {
   ],
   data () {
     return {
-      showDropDown: false,
       tags: {
         FORCE_NSFW,
         STRIP_MEDIA,
@@ -24,11 +24,13 @@ const ModerationTools = {
         SANDBOX,
         QUARANTINE
       },
-      showDeleteUserDialog: false
+      showDeleteUserDialog: false,
+      toggled: false
     }
   },
   components: {
-    DialogModal
+    DialogModal,
+    Popover
   },
   computed: {
     tagsSet () {
@@ -89,6 +91,9 @@ const ModerationTools = {
             window.history.back()
           }
         })
+    },
+    setToggled (value) {
+      this.toggled = value
     }
   }
 }
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index e78e05f1..b2d5acc5 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -1,13 +1,14 @@
 <template>
   <div>
-    <v-popover
+    <Popover
       trigger="click"
       class="moderation-tools-popover"
-      placement="bottom-end"
-      @show="showDropDown = true"
-      @hide="showDropDown = false"
+      placement="bottom"
+      :offset="{ y: 5 }"
+      @show="setToggled(true)"
+      @close="setToggled(false)"
     >
-      <div slot="popover">
+      <div slot="content">
         <div class="dropdown-menu">
           <span v-if="user.is_local">
             <button
@@ -122,12 +123,13 @@
         </div>
       </div>
       <button
+        slot="trigger"
         class="btn btn-default btn-block"
-        :class="{ toggled: showDropDown }"
+        :class="{ toggled }"
       >
         {{ $t('user_card.admin_menu.moderation') }}
       </button>
-    </v-popover>
+    </Popover>
     <portal to="modal">
       <DialogModal
         v-if="showDeleteUserDialog"
@@ -160,7 +162,6 @@
 
 <style lang="scss">
 @import '../../_variables.scss';
-@import '../popper/popper.scss';
 
 .menu-checkbox {
   float: right;
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
new file mode 100644
index 00000000..5881d266
--- /dev/null
+++ b/src/components/popover/popover.js
@@ -0,0 +1,156 @@
+
+const Popover = {
+  name: 'Popover',
+  props: {
+    // Action to trigger popover: either 'hover' or 'click'
+    trigger: String,
+    // Either 'top' or 'bottom'
+    placement: String,
+    // Takes object with properties 'x' and 'y', values of these can be
+    // 'container' for using offsetParent as boundaries for either axis
+    // or 'viewport'
+    boundTo: Object,
+    // Takes a top/bottom/left/right object, how much space to leave
+    // between boundary and popover element
+    margin: Object,
+    // Takes a x/y object and tells how many pixels to offset from
+    // anchor point on either axis
+    offset: Object,
+    // Additional styles you may want for the popover container
+    popoverClass: String
+  },
+  data () {
+    return {
+      hidden: true,
+      styles: { opacity: 0 },
+      oldSize: { width: 0, height: 0 }
+    }
+  },
+  methods: {
+    updateStyles () {
+      if (this.hidden) {
+        this.styles = {
+          opacity: 0
+        }
+        return
+      }
+
+      // Popover will be anchored around this element, trigger ref is the container, so
+      // its children are what are inside the slot. Expect only one slot="trigger".
+      const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
+      const screenBox = anchorEl.getBoundingClientRect()
+      // Screen position of the origin point for popover
+      const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
+      const content = this.$refs.content
+      // Minor optimization, don't call a slow reflow call if we don't have to
+      const parentBounds = this.boundTo &&
+        (this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
+        this.$el.offsetParent.getBoundingClientRect()
+      const margin = this.margin || {}
+
+      // What are the screen bounds for the popover? Viewport vs container
+      // when using viewport, using default margin values to dodge the navbar
+      const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
+        min: parentBounds.left + (margin.left || 0),
+        max: parentBounds.right - (margin.right || 0)
+      } : {
+        min: 0 + (margin.left || 10),
+        max: window.innerWidth - (margin.right || 10)
+      }
+
+      const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
+        min: parentBounds.top + (margin.top || 0),
+        max: parentBounds.bottom - (margin.bottom || 0)
+      } : {
+        min: 0 + (margin.top || 50),
+        max: window.innerHeight - (margin.bottom || 5)
+      }
+
+      let horizOffset = 0
+
+      // If overflowing from left, move it so that it doesn't
+      if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {
+        horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min
+      }
+
+      // If overflowing from right, move it so that it doesn't
+      if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {
+        horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max
+      }
+
+      // Default to whatever user wished with placement prop
+      let usingTop = this.placement !== 'bottom'
+
+      // Handle special cases, first force to displaying on top if there's not space on bottom,
+      // regardless of what placement value was. Then check if there's not space on top, and
+      // force to bottom, again regardless of what placement value was.
+      if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
+      if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
+
+      const yOffset = (this.offset && this.offset.y) || 0
+      const translateY = usingTop
+        ? -anchorEl.offsetHeight - yOffset - content.offsetHeight
+        : yOffset
+
+      const xOffset = (this.offset && this.offset.x) || 0
+      const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
+
+      // Note, separate translateX and translateY avoids blurry text on chromium,
+      // single translate or translate3d resulted in blurry text.
+      this.styles = {
+        opacity: 1,
+        transform: `translateX(${Math.floor(translateX)}px) translateY(${Math.floor(translateY)}px)`
+      }
+    },
+    showPopover () {
+      if (this.hidden) this.$emit('show')
+      this.hidden = false
+      this.$nextTick(this.updateStyles)
+    },
+    hidePopover () {
+      if (!this.hidden) this.$emit('close')
+      this.hidden = true
+      this.styles = { opacity: 0 }
+    },
+    onMouseenter (e) {
+      if (this.trigger === 'hover') this.showPopover()
+    },
+    onMouseleave (e) {
+      if (this.trigger === 'hover') this.hidePopover()
+    },
+    onClick (e) {
+      if (this.trigger === 'click') {
+        if (this.hidden) {
+          this.showPopover()
+        } else {
+          this.hidePopover()
+        }
+      }
+    },
+    onClickOutside (e) {
+      if (this.hidden) return
+      if (this.$el.contains(e.target)) return
+      this.hidePopover()
+    }
+  },
+  updated () {
+    // Monitor changes to content size, update styles only when content sizes have changed,
+    // that should be the only time we need to move the popover box if we don't care about scroll
+    // or resize
+    const content = this.$refs.content
+    if (!content) return
+    if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
+      this.updateStyles()
+      this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
+    }
+  },
+  created () {
+    document.addEventListener('click', this.onClickOutside)
+  },
+  destroyed () {
+    document.removeEventListener('click', this.onClickOutside)
+    this.hidePopover()
+  }
+}
+
+export default Popover
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
new file mode 100644
index 00000000..a271cb1b
--- /dev/null
+++ b/src/components/popover/popover.vue
@@ -0,0 +1,118 @@
+<template>
+  <div
+    @mouseenter="onMouseenter"
+    @mouseleave="onMouseleave"
+  >
+    <div
+      ref="trigger"
+      @click="onClick"
+    >
+      <slot name="trigger" />
+    </div>
+    <div
+      v-if="!hidden"
+      ref="content"
+      :style="styles"
+      class="popover"
+      :class="popoverClass"
+    >
+      <slot
+        name="content"
+        class="popover-inner"
+        :close="hidePopover"
+      />
+    </div>
+  </div>
+</template>
+
+<script src="./popover.js" />
+
+<style lang=scss>
+@import '../../_variables.scss';
+
+.popover {
+  z-index: 8;
+  position: absolute;
+  min-width: 0;
+  transition: opacity 0.3s;
+
+  box-shadow: 1px 1px 4px rgba(0,0,0,.6);
+  box-shadow: var(--panelShadow);
+  border-radius: $fallback--btnRadius;
+  border-radius: var(--btnRadius, $fallback--btnRadius);
+
+  background-color: $fallback--bg;
+  background-color: var(--popover, $fallback--bg);
+  color: $fallback--text;
+  color: var(--popoverText, $fallback--text);
+  --faint: var(--popoverFaintText, $fallback--faint);
+  --faintLink: var(--popoverFaintLink, $fallback--faint);
+  --lightText: var(--popoverLightText, $fallback--lightText);
+  --postLink: var(--popoverPostLink, $fallback--link);
+  --postFaintLink: var(--popoverPostFaintLink, $fallback--link);
+  --icon: var(--popoverIcon, $fallback--icon);
+}
+
+.dropdown-menu {
+  display: block;
+  padding: .5rem 0;
+  font-size: 1rem;
+  text-align: left;
+  list-style: none;
+  max-width: 100vw;
+  z-index: 10;
+  white-space: nowrap;
+
+  .dropdown-divider {
+    height: 0;
+    margin: .5rem 0;
+    overflow: hidden;
+    border-top: 1px solid $fallback--border;
+    border-top: 1px solid var(--border, $fallback--border);
+  }
+
+  .dropdown-item {
+    line-height: 21px;
+    margin-right: 5px;
+    overflow: auto;
+    display: block;
+    padding: .25rem 1.0rem .25rem 1.5rem;
+    clear: both;
+    font-weight: 400;
+    text-align: inherit;
+    white-space: nowrap;
+    border: none;
+    border-radius: 0px;
+    background-color: transparent;
+    box-shadow: none;
+    width: 100%;
+    height: 100%;
+
+    --btnText: var(--popoverText, $fallback--text);
+
+    &-icon {
+      padding-left: 0.5rem;
+
+      i {
+        margin-right: 0.25rem;
+        color: var(--menuPopoverIcon, $fallback--icon)
+      }
+    }
+
+    &:active, &:hover {
+      background-color: $fallback--lightBg;
+      background-color: var(--selectedMenuPopover, $fallback--lightBg);
+      color: $fallback--link;
+      color: var(--selectedMenuPopoverText, $fallback--link);
+      --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+      --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+      --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+      --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
+      i {
+        color: var(--selectedMenuPopoverIcon, $fallback--icon);
+      }
+    }
+
+  }
+}
+</style>
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
deleted file mode 100644
index 99b7e6fc..00000000
--- a/src/components/popper/popper.scss
+++ /dev/null
@@ -1,164 +0,0 @@
-@import '../../_variables.scss';
-
-.tooltip.popover {
-  z-index: 8;
-
-  .popover-inner {
-    box-shadow: 1px 1px 4px rgba(0,0,0,.6);
-    box-shadow: var(--panelShadow);
-    border-radius: $fallback--btnRadius;
-    border-radius: var(--btnRadius, $fallback--btnRadius);
-    background-color: $fallback--bg;
-    background-color: var(--popover, $fallback--bg);
-    color: $fallback--text;
-    color: var(--popoverText, $fallback--text);
-    --faint: var(--popoverFaintText, $fallback--faint);
-    --faintLink: var(--popoverFaintLink, $fallback--faint);
-    --lightText: var(--popoverLightText, $fallback--lightText);
-    --postLink: var(--popoverPostLink, $fallback--link);
-    --postFaintLink: var(--popoverPostFaintLink, $fallback--link);
-    --icon: var(--popoverIcon, $fallback--icon);
-  }
-
-  .popover-arrow {
-    width: 0;
-    height: 0;
-    border-style: solid;
-    position: absolute;
-    margin: 5px;
-    border-color: $fallback--bg;
-    border-color: var(--bg, $fallback--bg);
-  }
-
-  &[x-placement^="top"] {
-    margin-bottom: 5px;
-
-    .popover-arrow {
-      border-width: 5px 5px 0 5px;
-      border-left-color: transparent !important;
-      border-right-color: transparent !important;
-      border-bottom-color: transparent !important;
-      bottom: -4px;
-      left: calc(50% - 5px);
-      margin-top: 0;
-      margin-bottom: 0;
-    }
-  }
-
-  &[x-placement^="bottom"] {
-    margin-top: 5px;
-
-    .popover-arrow {
-      border-width: 0 5px 5px 5px;
-      border-left-color: transparent !important;
-      border-right-color: transparent !important;
-      border-top-color: transparent !important;
-      top: -4px;
-      left: calc(50% - 5px);
-      margin-top: 0;
-      margin-bottom: 0;
-    }
-  }
-
-  &[x-placement^="right"] {
-    margin-left: 5px;
-
-    .popover-arrow {
-      border-width: 5px 5px 5px 0;
-      border-left-color: transparent !important;
-      border-top-color: transparent !important;
-      border-bottom-color: transparent !important;
-      left: -4px;
-      top: calc(50% - 5px);
-      margin-left: 0;
-      margin-right: 0;
-    }
-  }
-
-  &[x-placement^="left"] {
-    margin-right: 5px;
-
-    .popover-arrow {
-      border-width: 5px 0 5px 5px;
-      border-top-color: transparent !important;
-      border-right-color: transparent !important;
-      border-bottom-color: transparent !important;
-      right: -4px;
-      top: calc(50% - 5px);
-      margin-left: 0;
-      margin-right: 0;
-    }
-  }
-
-  &[aria-hidden='true'] {
-    visibility: hidden;
-    opacity: 0;
-    transition: opacity .15s, visibility .15s;
-  }
-
-  &[aria-hidden='false'] {
-    visibility: visible;
-    opacity: 1;
-    transition: opacity .15s;
-  }
-}
-
-.dropdown-menu {
-  display: block;
-  padding: .5rem 0;
-  font-size: 1rem;
-  text-align: left;
-  list-style: none;
-  max-width: 100vw;
-  z-index: 10;
-
-  .dropdown-divider {
-    height: 0;
-    margin: .5rem 0;
-    overflow: hidden;
-    border-top: 1px solid $fallback--border;
-    border-top: 1px solid var(--border, $fallback--border);
-  }
-
-  .dropdown-item {
-    line-height: 21px;
-    margin-right: 5px;
-    overflow: auto;
-    display: block;
-    padding: .25rem 1.0rem .25rem 1.5rem;
-    clear: both;
-    font-weight: 400;
-    text-align: inherit;
-    white-space: normal;
-    border: none;
-    border-radius: 0px;
-    background-color: transparent;
-    box-shadow: none;
-    width: 100%;
-    height: 100%;
-
-    --btnText: var(--popoverText, $fallback--text);
-
-    &-icon {
-      padding-left: 0.5rem;
-
-      i {
-        margin-right: 0.25rem;
-      }
-    }
-
-    &:active, &:hover {
-      background-color: $fallback--lightBg;
-      background-color: var(--selectedMenuPopover, $fallback--lightBg);
-      color: $fallback--link;
-      color: var(--selectedMenuPopoverText, $fallback--link);
-      --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
-      --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
-      --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
-      --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
-      i {
-        color: var(--selectedMenuPopoverIcon, $fallback--icon);
-      }
-    }
-  }
-}
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index a6cf5b94..19949563 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -1,34 +1,25 @@
+import Popover from '../popover/popover.vue'
 import { mapGetters } from 'vuex'
 
 const ReactButton = {
   props: ['status', 'loggedIn'],
   data () {
     return {
-      showTooltip: false,
-      filterWord: '',
-      popperOptions: {
-        modifiers: {
-          preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
-        }
-      }
+      filterWord: ''
     }
   },
+  components: {
+    Popover
+  },
   methods: {
-    openReactionSelect () {
-      this.showTooltip = true
-      this.filterWord = ''
-    },
-    closeReactionSelect () {
-      this.showTooltip = false
-    },
-    addReaction (event, emoji) {
+    addReaction (event, emoji, close) {
       const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
       if (existingReaction && existingReaction.me) {
         this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
       } else {
         this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
       }
-      this.closeReactionSelect()
+      close()
     }
   },
   computed: {
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index fb43ebaf..ab4b4fcd 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -1,13 +1,14 @@
 <template>
-  <v-popover
-    :popper-options="popperOptions"
-    :open="showTooltip"
-    trigger="manual"
+  <Popover
+    trigger="click"
     placement="top"
+    :offset="{ y: 5 }"
     class="react-button-popover"
-    @hide="closeReactionSelect"
   >
-    <div slot="popover">
+    <div
+      slot="content"
+      slot-scope="{close}"
+    >
       <div class="reaction-picker-filter">
         <input
           v-model="filterWord"
@@ -19,7 +20,7 @@
           v-for="emoji in commonEmojis"
           :key="emoji"
           class="emoji-button"
-          @click="addReaction($event, emoji)"
+          @click="addReaction($event, emoji, close)"
         >
           {{ emoji }}
         </span>
@@ -28,23 +29,20 @@
           v-for="(emoji, key) in emojis"
           :key="key"
           class="emoji-button"
-          @click="addReaction($event, emoji.replacement)"
+          @click="addReaction($event, emoji.replacement, close)"
         >
           {{ emoji.replacement }}
         </span>
         <div class="reaction-bottom-fader" />
       </div>
     </div>
-    <div
+    <i
       v-if="loggedIn"
-      @click.prevent="openReactionSelect"
-    >
-      <i
-        class="icon-smile button-icon add-reaction-button"
-        :title="$t('tool_tip.add_reaction')"
-      />
-    </div>
-  </v-popover>
+      slot="trigger"
+      class="icon-smile button-icon add-reaction-button"
+      :title="$t('tool_tip.add_reaction')"
+    />
+  </Popover>
 </template>
 
 <script src="./react_button.js" ></script>
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 76b038d9..ca295640 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -177,6 +177,8 @@
                 <StatusPopover
                   v-if="!isPreview"
                   :status-id="status.in_reply_to_status_id"
+                  class="reply-to-popover"
+                  style="min-width: 0"
                 >
                   <a
                     class="reply-to"
@@ -572,11 +574,10 @@ $status-margin: 0.75em;
       align-items: stretch;
 
       > .reply-to-and-accountname > a {
+        overflow: hidden;
         max-width: 100%;
         text-overflow: ellipsis;
-        overflow: hidden;
         white-space: nowrap;
-        display: inline-block;
         word-break: break-all;
       }
     }
@@ -585,7 +586,6 @@ $status-margin: 0.75em;
       display: flex;
       height: 18px;
       margin-right: 0.5em;
-      overflow: hidden;
       max-width: 100%;
       .icon-reply {
         transform: scaleX(-1);
@@ -596,6 +596,10 @@ $status-margin: 0.75em;
       display: flex;
     }
 
+    .reply-to-popover {
+      min-width: 0;
+    }
+
     .reply-to {
       display: flex;
     }
@@ -603,6 +607,7 @@ $status-margin: 0.75em;
     .reply-to-text {
       overflow: hidden;
       text-overflow: ellipsis;
+      white-space: nowrap;
       margin: 0 0.4em 0 0.2em;
     }
 
diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js
index 19f16bd9..cb55f67e 100644
--- a/src/components/status_popover/status_popover.js
+++ b/src/components/status_popover/status_popover.js
@@ -5,22 +5,14 @@ const StatusPopover = {
   props: [
     'statusId'
   ],
-  data () {
-    return {
-      popperOptions: {
-        modifiers: {
-          preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
-        }
-      }
-    }
-  },
   computed: {
     status () {
       return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
     }
   },
   components: {
-    Status: () => import('../status/status.vue')
+    Status: () => import('../status/status.vue'),
+    Popover: () => import('../popover/popover.vue')
   },
   methods: {
     enter () {
diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue
index eacf4c06..11f6cb5a 100644
--- a/src/components/status_popover/status_popover.vue
+++ b/src/components/status_popover/status_popover.vue
@@ -1,11 +1,16 @@
 <template>
-  <v-popover
+  <Popover
+    trigger="hover"
     popover-class="status-popover"
-    placement="top-start"
-    :popper-options="popperOptions"
-    @show="enter()"
+    :bound-to="{ x: 'container' }"
+    @show="enter"
   >
-    <template slot="popover">
+    <template slot="trigger">
+      <slot />
+    </template>
+    <div
+      slot="content"
+    >
       <Status
         v-if="status"
         :is-preview="true"
@@ -18,10 +23,8 @@
       >
         <i class="icon-spin4 animate-spin" />
       </div>
-    </template>
-
-    <slot />
-  </v-popover>
+    </div>
+  </Popover>
 </template>
 
 <script src="./status_popover.js" ></script>
@@ -29,44 +32,19 @@
 <style lang="scss">
 @import '../../_variables.scss';
 
-.tooltip.popover.status-popover {
+.status-popover {
   font-size: 1rem;
   min-width: 15em;
   max-width: 95%;
-  margin-left: 0.5em;
-
-  .popover-inner {
-    border-color: $fallback--border;
-    border-color: var(--border, $fallback--border);
-    border-style: solid;
-    border-width: 1px;
-    border-radius: $fallback--tooltipRadius;
-    border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
-    box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
-    box-shadow: var(--popupShadow);
-  }
 
-  .popover-arrow::before {
-    position: absolute;
-    content: '';
-    left: -7px;
-    border: solid 7px transparent;
-    z-index: -1;
-  }
-
-  &[x-placement^="bottom-start"] .popover-arrow::before {
-    top: -2px;
-    border-top-width: 0;
-    border-bottom-color: $fallback--border;
-    border-bottom-color: var(--border, $fallback--border);
-  }
-
-  &[x-placement^="top-start"] .popover-arrow::before {
-    bottom: -2px;
-    border-bottom-width: 0;
-    border-top-color: $fallback--border;
-    border-top-color: var(--border, $fallback--border);
-  }
+  border-color: $fallback--border;
+  border-color: var(--border, $fallback--border);
+  border-style: solid;
+  border-width: 1px;
+  border-radius: $fallback--tooltipRadius;
+  border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+  box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
+  box-shadow: var(--popupShadow);
 
   .status-el.status-el {
     border: none;
diff --git a/src/main.js b/src/main.js
index baf73ac8..be4213fa 100644
--- a/src/main.js
+++ b/src/main.js
@@ -31,7 +31,6 @@ import VueChatScroll from 'vue-chat-scroll'
 import VueClickOutside from 'v-click-outside'
 import PortalVue from 'portal-vue'
 import VBodyScrollLock from './directives/body_scroll_lock'
-import VTooltip from 'v-tooltip'
 
 import afterStoreSetup from './boot/after_store.js'
 
@@ -44,13 +43,6 @@ Vue.use(VueChatScroll)
 Vue.use(VueClickOutside)
 Vue.use(PortalVue)
 Vue.use(VBodyScrollLock)
-Vue.use(VTooltip, {
-  popover: {
-    defaultTrigger: 'hover click',
-    defaultContainer: false,
-    defaultOffset: 5
-  }
-})
 
 const i18n = new VueI18n({
   // By default, use the browser locale, we will update it if neccessary
diff --git a/yarn.lock b/yarn.lock
index b794042f..0defefcb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5941,11 +5941,6 @@ pngjs@^3.3.0:
   version "3.3.3"
   resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b"
 
-popper.js@^1.15.0:
-  version "1.15.0"
-  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
-  integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
-
 portal-vue@^2.1.4:
   version "2.1.4"
   resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.4.tgz#1fc679d77e294dc8d026f1eb84aa467de11b392e"
@@ -7823,15 +7818,6 @@ v-click-outside@^2.1.1:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7"
 
-v-tooltip@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.2.tgz#8610d9eece2cc44fd66c12ef2f12eec6435cab9b"
-  integrity sha512-xQ+qzOFfywkLdjHknRPgMMupQNS8yJtf9Utd5Dxiu/0n4HtrxqsgDtN2MLZ0LKbburtSAQgyypuE/snM8bBZhw==
-  dependencies:
-    lodash "^4.17.11"
-    popper.js "^1.15.0"
-    vue-resize "^0.4.5"
-
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -7906,11 +7892,6 @@ vue-loader@^14.0.0:
     vue-style-loader "^4.0.1"
     vue-template-es2015-compiler "^1.6.0"
 
-vue-resize@^0.4.5:
-  version "0.4.5"
-  resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea"
-  integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==
-
 vue-router@^3.0.1:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be"
-- 
cgit v1.2.3-70-g09d2


From 6bb75a3a6d8452a3e1b88085fe87cf27386f222c Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Tue, 21 Apr 2020 23:27:51 +0300
Subject: make relationships separate from users

---
 src/boot/after_store.js                            |  3 ++
 src/components/account_actions/account_actions.js  |  2 +-
 src/components/account_actions/account_actions.vue |  8 ++---
 src/components/basic_user_card/basic_user_card.vue |  2 +-
 src/components/block_card/block_card.js            |  5 ++-
 src/components/follow_button/follow_button.js      | 14 ++++----
 src/components/follow_card/follow_card.js          |  3 ++
 src/components/follow_card/follow_card.vue         |  5 +--
 src/components/mute_card/mute_card.js              |  9 ++++--
 src/components/notification/notification.js        |  2 +-
 src/components/notification/notification.vue       |  2 +-
 src/components/side_drawer/side_drawer.vue         |  2 +-
 src/components/status/status.js                    | 15 +++++++--
 src/components/status/status.vue                   |  2 +-
 src/components/user_card/user_card.js              |  8 ++++-
 src/components/user_card/user_card.vue             | 15 +++++----
 src/components/user_panel/user_panel.vue           |  2 +-
 src/components/user_profile/user_profile.vue       |  2 +-
 src/components/user_settings/user_settings.js      |  8 ++---
 src/modules/users.js                               | 37 ++++++++--------------
 src/services/api/api.service.js                    |  4 ++-
 .../entity_normalizer/entity_normalizer.service.js | 25 +++++++++------
 .../follow_manipulate/follow_manipulate.js         | 15 +++++----
 .../notifications_fetcher.service.js               |  1 -
 .../timeline_fetcher/timeline_fetcher.service.js   |  2 ++
 25 files changed, 112 insertions(+), 81 deletions(-)

(limited to 'src/components/status/status.vue')

diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index d70e1058..1522d4f0 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -304,6 +304,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
     getNodeInfo({ store })
   ])
 
+  // Start fetching things that don't need to block the UI
+  store.dispatch('fetchMutes')
+
   const router = new VueRouter({
     mode: 'history',
     routes: routes(store),
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index 5d7ecf7e..0826c275 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -3,7 +3,7 @@ import Popover from '../popover/popover.vue'
 
 const AccountActions = {
   props: [
-    'user'
+    'user', 'relationship'
   ],
   data () {
     return { }
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 483783cf..744b77d5 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -9,16 +9,16 @@
         class="account-tools-popover"
       >
         <div class="dropdown-menu">
-          <template v-if="user.following">
+          <template v-if="relationship.following">
             <button
-              v-if="user.showing_reblogs"
+              v-if="relationship.showing_reblogs"
               class="btn btn-default dropdown-item"
               @click="hideRepeats"
             >
               {{ $t('user_card.hide_repeats') }}
             </button>
             <button
-              v-if="!user.showing_reblogs"
+              v-if="!relationship.showing_reblogs"
               class="btn btn-default dropdown-item"
               @click="showRepeats"
             >
@@ -30,7 +30,7 @@
             />
           </template>
           <button
-            v-if="user.statusnet_blocking"
+            v-if="relationship.blocking"
             class="btn btn-default btn-block dropdown-item"
             @click="unblockUser"
           >
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 8a02174e..b5a11e2b 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -12,7 +12,7 @@
       class="basic-user-card-expanded-content"
     >
       <UserCard
-        :user="user"
+        :userId="user.id"
         :rounded="true"
         :bordered="true"
       />
diff --git a/src/components/block_card/block_card.js b/src/components/block_card/block_card.js
index c459ff1b..659c75d8 100644
--- a/src/components/block_card/block_card.js
+++ b/src/components/block_card/block_card.js
@@ -11,8 +11,11 @@ const BlockCard = {
     user () {
       return this.$store.getters.findUser(this.userId)
     },
+    relationship () {
+      return this.$store.state.users.relationships[this.userId] || {}
+    },
     blocked () {
-      return this.user.statusnet_blocking
+      return this.relationship.blocking
     }
   },
   components: {
diff --git a/src/components/follow_button/follow_button.js b/src/components/follow_button/follow_button.js
index 12da2645..af7b1fef 100644
--- a/src/components/follow_button/follow_button.js
+++ b/src/components/follow_button/follow_button.js
@@ -1,6 +1,6 @@
 import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
 export default {
-  props: ['user', 'labelFollowing', 'buttonClass'],
+  props: ['relationship', 'labelFollowing', 'buttonClass'],
   data () {
     return {
       inProgress: false
@@ -8,12 +8,12 @@ export default {
   },
   computed: {
     isPressed () {
-      return this.inProgress || this.user.following
+      return this.inProgress || this.relationship.following
     },
     title () {
-      if (this.inProgress || this.user.following) {
+      if (this.inProgress || this.relationship.following) {
         return this.$t('user_card.follow_unfollow')
-      } else if (this.user.requested) {
+      } else if (this.relationship.requested) {
         return this.$t('user_card.follow_again')
       } else {
         return this.$t('user_card.follow')
@@ -22,9 +22,9 @@ export default {
     label () {
       if (this.inProgress) {
         return this.$t('user_card.follow_progress')
-      } else if (this.user.following) {
+      } else if (this.relationship.following) {
         return this.labelFollowing || this.$t('user_card.following')
-      } else if (this.user.requested) {
+      } else if (this.relationship.requested) {
         return this.$t('user_card.follow_sent')
       } else {
         return this.$t('user_card.follow')
@@ -33,7 +33,7 @@ export default {
   },
   methods: {
     onClick () {
-      this.user.following ? this.unfollow() : this.follow()
+      this.relationship.following ? this.unfollow() : this.follow()
     },
     follow () {
       this.inProgress = true
diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js
index aefd609e..620ae7fd 100644
--- a/src/components/follow_card/follow_card.js
+++ b/src/components/follow_card/follow_card.js
@@ -18,6 +18,9 @@ const FollowCard = {
     },
     loggedIn () {
       return this.$store.state.users.currentUser
+    },
+    relationship () {
+      return this.$store.state.users.relationships[this.user.id]
     }
   }
 }
diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
index 81e6e6dc..d789a325 100644
--- a/src/components/follow_card/follow_card.vue
+++ b/src/components/follow_card/follow_card.vue
@@ -2,14 +2,14 @@
   <basic-user-card :user="user">
     <div class="follow-card-content-container">
       <span
-        v-if="!noFollowsYou && user.follows_you"
+        v-if="!noFollowsYou && relationship.followed_by"
         class="faint"
       >
         {{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
       </span>
       <template v-if="!loggedIn">
         <div
-          v-if="!user.following"
+          v-if="!relationship.following"
           class="follow-card-follow-button"
         >
           <RemoteFollow :user="user" />
@@ -18,6 +18,7 @@
       <template v-else>
         <FollowButton
           :user="user"
+          :relationship="relationship"
           class="follow-card-follow-button"
           :label-following="$t('user_card.follow_unfollow')"
         />
diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js
index 65c9cfb5..be528d37 100644
--- a/src/components/mute_card/mute_card.js
+++ b/src/components/mute_card/mute_card.js
@@ -11,8 +11,11 @@ const MuteCard = {
     user () {
       return this.$store.getters.findUser(this.userId)
     },
+    relationship () {
+      return this.$store.state.users.relationships[this.userId]
+    },
     muted () {
-      return this.user.muted
+      return this.relationship.muting
     }
   },
   components: {
@@ -21,13 +24,13 @@ const MuteCard = {
   methods: {
     unmuteUser () {
       this.progress = true
-      this.$store.dispatch('unmuteUser', this.user.id).then(() => {
+      this.$store.dispatch('unmuteUser', this.userId).then(() => {
         this.progress = false
       })
     },
     muteUser () {
       this.progress = true
-      this.$store.dispatch('muteUser', this.user.id).then(() => {
+      this.$store.dispatch('muteUser', this.userId).then(() => {
         this.progress = false
       })
     }
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index e7bd769e..09554f54 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -56,7 +56,7 @@ const Notification = {
       return this.generateUserProfileLink(this.targetUser)
     },
     needMute () {
-      return this.user.muted
+      return (this.$store.state.users.relationships[this.user.id] || {}).muting
     }
   }
 }
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 411c0271..24d9fe90 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -40,7 +40,7 @@
       <div class="notification-right">
         <UserCard
           v-if="userExpanded"
-          :user="getUser(notification)"
+          :user-id="getUser(notification).id"
           :rounded="true"
           :bordered="true"
         />
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index df1d22a4..a9dbbeec 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -19,7 +19,7 @@
       >
         <UserCard
           v-if="currentUser"
-          :user="currentUser"
+          :userId="currentUser.id"
           :hide-bio="true"
         />
         <div
diff --git a/src/components/status/status.js b/src/components/status/status.js
index fc5956ec..a73e3ae2 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -118,7 +118,13 @@ const Status = {
 
       return hits
     },
-    muted () { return !this.unmuted && ((!(this.inProfile && this.status.user.id === this.profileUserId) && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
+    muted () {
+      const relationship = this.$store.state.users.relationships[this.status.user.id] || {}
+      return !this.unmuted && (
+        (!(this.inProfile && this.status.user.id === this.profileUserId) && relationship.muting) ||
+        (!this.inConversation && this.status.thread_muted) ||
+        this.muteWordHits.length > 0)
+    },
     hideFilteredStatuses () {
       return this.mergedConfig.hideFilteredStatuses
     },
@@ -178,8 +184,11 @@ const Status = {
         if (this.status.user.id === this.status.attentions[i].id) {
           continue
         }
-        const taggedUser = this.$store.getters.findUser(this.status.attentions[i].id)
-        if (checkFollowing && taggedUser && taggedUser.following) {
+        // There's zero guarantee of this working. If we happen to have that user and their
+        // relationship in store then it will work, but there's kinda little chance of having
+        // them for people you're not following.
+        const relationship = this.$store.state.users.relationships[this.status.attentions[i].id]
+        if (checkFollowing && relationship && relationship.following) {
           return false
         }
         if (this.status.attentions[i].id === this.currentUser.id) {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index ca295640..a04b501a 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -94,7 +94,7 @@
         <div class="status-body">
           <UserCard
             v-if="userExpanded"
-            :user="status.user"
+            :userId="status.user.id"
             :rounded="true"
             :bordered="true"
             class="status-usercard"
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 1cdbd3fa..fb3cfebc 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -9,7 +9,7 @@ import { mapGetters } from 'vuex'
 
 export default {
   props: [
-    'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
+    'userId', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
   ],
   data () {
     return {
@@ -21,6 +21,12 @@ export default {
     this.$store.dispatch('fetchUserRelationship', this.user.id)
   },
   computed: {
+    user () {
+      return this.$store.getters.findUser(this.userId)
+    },
+    relationship () {
+      return this.$store.state.users.relationships[this.userId] || {}
+    },
     classes () {
       return [{
         'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 4ee040e8..25fdd70e 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -8,7 +8,9 @@
       :style="style"
       class="background-image"
     />
-    <div class="panel-heading">
+    <div
+      class="panel-heading"
+    >
       <div class="user-info">
         <div class="container">
           <a
@@ -69,6 +71,7 @@
               <AccountActions
                 v-if="isOtherUser && loggedIn"
                 :user="user"
+                :relationship="relationship"
               />
             </div>
             <div class="bottom-line">
@@ -92,7 +95,7 @@
         </div>
         <div class="user-meta">
           <div
-            v-if="user.follows_you && loggedIn && isOtherUser"
+            v-if="relationship.followed_by && loggedIn && isOtherUser"
             class="following"
           >
             {{ $t('user_card.follows_you') }}
@@ -139,10 +142,10 @@
           class="user-interactions"
         >
           <div class="btn-group">
-            <FollowButton :user="user" />
-            <template v-if="user.following">
+            <FollowButton :relationship="relationship" />
+            <template v-if="relationship.following">
               <ProgressButton
-                v-if="!user.subscribed"
+                v-if="!relationship.subscribing"
                 class="btn btn-default"
                 :click="subscribeUser"
                 :title="$t('user_card.subscribe')"
@@ -161,7 +164,7 @@
           </div>
           <div>
             <button
-              v-if="user.muted"
+              v-if="relationship.muting"
               class="btn btn-default btn-block toggled"
               @click="unmuteUser"
             >
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index e9f08015..ea4e2e9f 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -6,7 +6,7 @@
       class="panel panel-default signed-in"
     >
       <UserCard
-        :user="user"
+        :userId="user.id"
         :hide-bio="true"
         rounded="top"
       />
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 14082e83..7855c839 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -5,7 +5,7 @@
       class="user-profile panel panel-default"
     >
       <UserCard
-        :user="user"
+        :userId="user.id"
         :switcher="true"
         :selected="timeline.viewing"
         :allow-zooming-avatar="true"
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index eca6f9b1..adfab8fa 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -351,14 +351,14 @@ const UserSettings = {
     },
     filterUnblockedUsers (userIds) {
       return reject(userIds, (userId) => {
-        const user = this.$store.getters.findUser(userId)
-        return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
+        const relationship = this.$store.state.users.relationships[userId] || {}
+        return relationship.blocking || userId === this.$store.state.users.currentUser.id
       })
     },
     filterUnMutedUsers (userIds) {
       return reject(userIds, (userId) => {
-        const user = this.$store.getters.findUser(userId)
-        return !user || user.muted || user.id === this.$store.state.users.currentUser.id
+        const relationship = this.$store.state.users.relationships[userId] || {}
+        return relationship.muting || userId === this.$store.state.users.currentUser.id
       })
     },
     queryUserIds (query) {
diff --git a/src/modules/users.js b/src/modules/users.js
index df133be0..7e73ab18 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -146,26 +146,19 @@ export const mutations = {
     }
   },
   addNewUsers (state, users) {
-    each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
+    each(users, (user) => {
+      // console.log(user)
+      if (user.relationship) {
+        set(state.relationships, user.relationship.id, user.relationship)
+      }
+      mergeOrAdd(state.users, state.usersObject, user)
+    })
   },
   updateUserRelationship (state, relationships) {
     relationships.forEach((relationship) => {
-      const user = state.usersObject[relationship.id]
-      if (user) {
-        user.follows_you = relationship.followed_by
-        user.following = relationship.following
-        user.muted = relationship.muting
-        user.statusnet_blocking = relationship.blocking
-        user.subscribed = relationship.subscribing
-        user.showing_reblogs = relationship.showing_reblogs
-      }
+      set(state.relationships, relationship.id, relationship)
     })
   },
-  updateBlocks (state, blockedUsers) {
-    // Reset statusnet_blocking of all fetched users
-    each(state.users, (user) => { user.statusnet_blocking = false })
-    each(blockedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
-  },
   saveBlockIds (state, blockIds) {
     state.currentUser.blockIds = blockIds
   },
@@ -174,11 +167,6 @@ export const mutations = {
       state.currentUser.blockIds.push(blockId)
     }
   },
-  updateMutes (state, mutedUsers) {
-    // Reset muted of all fetched users
-    each(state.users, (user) => { user.muted = false })
-    each(mutedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
-  },
   saveMuteIds (state, muteIds) {
     state.currentUser.muteIds = muteIds
   },
@@ -254,7 +242,8 @@ export const defaultState = {
   users: [],
   usersObject: {},
   signUpPending: false,
-  signUpErrors: []
+  signUpErrors: [],
+  relationships: {}
 }
 
 const users = {
@@ -279,7 +268,7 @@ const users = {
       return store.rootState.api.backendInteractor.fetchBlocks()
         .then((blocks) => {
           store.commit('saveBlockIds', map(blocks, 'id'))
-          store.commit('updateBlocks', blocks)
+          store.commit('addNewUsers', blocks)
           return blocks
         })
     },
@@ -298,8 +287,8 @@ const users = {
     fetchMutes (store) {
       return store.rootState.api.backendInteractor.fetchMutes()
         .then((mutes) => {
-          store.commit('updateMutes', mutes)
           store.commit('saveMuteIds', map(mutes, 'id'))
+          store.commit('addNewUsers', mutes)
           return mutes
         })
     },
@@ -416,7 +405,7 @@ const users = {
     },
     addNewNotifications (store, { notifications }) {
       const users = map(notifications, 'from_profile')
-      const targetUsers = map(notifications, 'target')
+      const targetUsers = map(notifications, 'target').filter(_ => _)
       const notificationIds = notifications.map(_ => _.id)
       store.commit('addNewUsers', users)
       store.commit('addNewUsers', targetUsers)
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 7db1d094..ee6bf151 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -496,7 +496,8 @@ const fetchTimeline = ({
   userId = false,
   tag = false,
   withMuted = false,
-  withMove = false
+  withMove = false,
+  withRelationships = false
 }) => {
   const timelineUrls = {
     public: MASTODON_PUBLIC_TIMELINE,
@@ -542,6 +543,7 @@ const fetchTimeline = ({
 
   params.push(['count', 20])
   params.push(['with_muted', withMuted])
+  params.push(['with_relationships', withRelationships])
 
   const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
   url += `?${queryString}`
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 84169a7b..97f9f2ae 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -73,7 +73,7 @@ export const parseUser = (data) => {
       output.background_image = data.pleroma.background_image
       output.token = data.pleroma.chat_token
 
-      if (relationship) {
+      if (relationship && !relationship) {
         output.follows_you = relationship.followed_by
         output.requested = relationship.requested
         output.following = relationship.following
@@ -82,6 +82,9 @@ export const parseUser = (data) => {
         output.showing_reblogs = relationship.showing_reblogs
         output.subscribed = relationship.subscribing
       }
+      if (relationship) {
+        output.relationship = relationship
+      }
 
       output.allow_following_move = data.pleroma.allow_following_move
 
@@ -137,16 +140,10 @@ export const parseUser = (data) => {
 
     output.statusnet_profile_url = data.statusnet_profile_url
 
-    output.statusnet_blocking = data.statusnet_blocking
-
     output.is_local = data.is_local
     output.role = data.role
     output.show_role = data.show_role
 
-    output.follows_you = data.follows_you
-
-    output.muted = data.muted
-
     if (data.rights) {
       output.rights = {
         moderator: data.rights.delete_others_notice,
@@ -160,10 +157,16 @@ export const parseUser = (data) => {
     output.hide_follows_count = data.hide_follows_count
     output.hide_followers_count = data.hide_followers_count
     output.background_image = data.background_image
-    // on mastoapi this info is contained in a "relationship"
-    output.following = data.following
     // Websocket token
     output.token = data.token
+
+    // Convert relationsip data to expected format
+    output.relationship = {
+      muting: data.muted,
+      blocking: data.statusnet_blocking,
+      followed_by: data.follows_you,
+      following: data.following
+    }
   }
 
   output.created_at = new Date(data.created_at)
@@ -317,6 +320,9 @@ export const parseStatus = (data) => {
     ? String(output.in_reply_to_user_id)
     : null
 
+  if (data.account.pleroma.relationship) {
+    data.account.pleroma.relationship = undefined
+  }
   output.user = parseUser(masto ? data.account : data.user)
 
   output.attentions = ((masto ? data.mentions : data.attentions) || []).map(parseUser)
@@ -342,7 +348,6 @@ export const parseNotification = (data) => {
   }
   const masto = !data.hasOwnProperty('ntype')
   const output = {}
-
   if (masto) {
     output.type = mastoDict[data.type] || data.type
     output.seen = data.pleroma.is_seen
diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js
index 29b38a0f..c5d86afa 100644
--- a/src/services/follow_manipulate/follow_manipulate.js
+++ b/src/services/follow_manipulate/follow_manipulate.js
@@ -1,15 +1,18 @@
-const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
+const fetchRelationship = (attempt, user, store) => new Promise((resolve, reject) => {
   setTimeout(() => {
-    store.state.api.backendInteractor.fetchUser({ id: user.id })
-      .then((user) => store.commit('addNewUsers', [user]))
-      .then(() => resolve([user.following, user.requested, user.locked, attempt]))
+    store.state.api.backendInteractor.fetchUserRelationship({ id: user.id })
+      .then((relationship) => {
+        store.commit('updateUserRelationship', [relationship])
+        return relationship
+      })
+      .then((relationship) => resolve([relationship.following, relationship.requested, user.locked, attempt]))
       .catch((e) => reject(e))
   }, 500)
 }).then(([following, sent, locked, attempt]) => {
   if (!following && !(locked && sent) && attempt <= 3) {
     // If we BE reports that we still not following that user - retry,
     // increment attempts by one
-    fetchUser(++attempt, user, store)
+    fetchRelationship(++attempt, user, store)
   }
 })
 
@@ -31,7 +34,7 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
       // don't know that yet.
       // Recursive Promise, it will call itself up to 3 times.
 
-      return fetchUser(1, user, store)
+      return fetchRelationship(1, user, store)
         .then(() => {
           resolve()
         })
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 864e32f8..eb6d84c4 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -48,7 +48,6 @@ const fetchNotifications = ({ store, args, older }) => {
       update({ store, notifications, older })
       return notifications
     }, () => store.dispatch('setNotificationsError', { value: true }))
-    .catch(() => store.dispatch('setNotificationsError', { value: true }))
 }
 
 const startFetching = ({ credentials, store }) => {
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index c6b28ad5..96fafff9 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -31,6 +31,7 @@ const fetchAndUpdate = ({
   const { getters } = store
   const timelineData = rootState.statuses.timelines[camelCase(timeline)]
   const hideMutedPosts = getters.mergedConfig.hideMutedPosts
+  const replyVisibility = getters.mergedConfig.replyVisibility
 
   if (older) {
     args['until'] = until || timelineData.minId
@@ -41,6 +42,7 @@ const fetchAndUpdate = ({
   args['userId'] = userId
   args['tag'] = tag
   args['withMuted'] = !hideMutedPosts
+  args['withRelationships'] = replyVisibility === 'following'
 
   const numStatusesBeforeFetch = timelineData.statuses.length
 
-- 
cgit v1.2.3-70-g09d2


From ca00e93b6092c855b0109e1e6dab93c29a28716a Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Thu, 23 Apr 2020 14:27:27 +0300
Subject: minor fixes

---
 src/components/follow_card/follow_card.vue                  | 3 +--
 src/components/side_drawer/side_drawer.vue                  | 2 +-
 src/components/status/status.vue                            | 2 +-
 src/services/entity_normalizer/entity_normalizer.service.js | 1 +
 4 files changed, 4 insertions(+), 4 deletions(-)

(limited to 'src/components/status/status.vue')

diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
index d789a325..76a70730 100644
--- a/src/components/follow_card/follow_card.vue
+++ b/src/components/follow_card/follow_card.vue
@@ -17,10 +17,9 @@
       </template>
       <template v-else>
         <FollowButton
-          :user="user"
           :relationship="relationship"
-          class="follow-card-follow-button"
           :label-following="$t('user_card.follow_unfollow')"
+          class="follow-card-follow-button"
         />
       </template>
     </div>
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index a9dbbeec..2958a386 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -19,7 +19,7 @@
       >
         <UserCard
           v-if="currentUser"
-          :userId="currentUser.id"
+          :user-id="currentUser.id"
           :hide-bio="true"
         />
         <div
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index a04b501a..dd7cd579 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -94,7 +94,7 @@
         <div class="status-body">
           <UserCard
             v-if="userExpanded"
-            :userId="status.user.id"
+            :user-id="status.user.id"
             :rounded="true"
             :bordered="true"
             class="status-usercard"
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 7237fdfc..724dcc4b 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -336,6 +336,7 @@ export const parseNotification = (data) => {
   }
   const masto = !data.hasOwnProperty('ntype')
   const output = {}
+
   if (masto) {
     output.type = mastoDict[data.type] || data.type
     output.seen = data.pleroma.is_seen
-- 
cgit v1.2.3-70-g09d2


From 7a25160ddc15a97a02366dff60ec12801348f229 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Mon, 27 Apr 2020 12:53:04 +0300
Subject: Separate status content from status like in direct conversations mr

---
 src/components/status/status.js                  | 188 +-----------------
 src/components/status/status.vue                 | 230 +---------------------
 src/components/status_content/status_content.js  | 210 ++++++++++++++++++++
 src/components/status_content/status_content.vue | 240 +++++++++++++++++++++++
 4 files changed, 460 insertions(+), 408 deletions(-)
 create mode 100644 src/components/status_content/status_content.js
 create mode 100644 src/components/status_content/status_content.vue

(limited to 'src/components/status/status.vue')

diff --git a/src/components/status/status.js b/src/components/status/status.js
index 61d66301..ab3de5fc 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,23 +1,17 @@
-import Attachment from '../attachment/attachment.vue'
 import FavoriteButton from '../favorite_button/favorite_button.vue'
 import ReactButton from '../react_button/react_button.vue'
 import RetweetButton from '../retweet_button/retweet_button.vue'
-import Poll from '../poll/poll.vue'
 import ExtraButtons from '../extra_buttons/extra_buttons.vue'
 import PostStatusForm from '../post_status_form/post_status_form.vue'
 import UserCard from '../user_card/user_card.vue'
 import UserAvatar from '../user_avatar/user_avatar.vue'
-import Gallery from '../gallery/gallery.vue'
-import LinkPreview from '../link-preview/link-preview.vue'
 import AvatarList from '../avatar_list/avatar_list.vue'
 import Timeago from '../timeago/timeago.vue'
+import StatusContent from '../status_content/status_content.vue'
 import StatusPopover from '../status_popover/status_popover.vue'
 import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
-import fileType from 'src/services/file_type/file_type.service'
-import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
-import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
 import { filter, unescape, uniqBy } from 'lodash'
 import { mapGetters, mapState } from 'vuex'
 
@@ -43,17 +37,10 @@ const Status = {
       replying: false,
       unmuted: false,
       userExpanded: false,
-      showingTall: this.inConversation && this.focused,
-      showingLongSubject: false,
-      error: null,
-      // not as computed because it sets the initial state which will be changed later
-      expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
+      error: null
     }
   },
   computed: {
-    localCollapseSubjectDefault () {
-      return this.mergedConfig.collapseMessageWithSubject
-    },
     muteWords () {
       return this.mergedConfig.muteWords
     },
@@ -79,10 +66,6 @@ const Status = {
       const highlight = this.mergedConfig.highlight
       return highlightStyle(highlight[user.screen_name])
     },
-    hideAttachments () {
-      return (this.mergedConfig.hideAttachments && !this.inConversation) ||
-        (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
-    },
     userProfileLink () {
       return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
     },
@@ -135,20 +118,6 @@ const Status = {
       // use conversation highlight only when in conversation
       return this.status.id === this.highlight
     },
-    // This is a bit hacky, but we want to approximate post height before rendering
-    // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
-    // as well as approximate line count by counting characters and approximating ~80
-    // per line.
-    //
-    // Using max-height + overflow: auto for status components resulted in false positives
-    // very often with japanese characters, and it was very annoying.
-    tallStatus () {
-      const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
-      return lengthScore > 20
-    },
-    longSubject () {
-      return this.status.summary.length > 900
-    },
     isReply () {
       return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
     },
@@ -188,32 +157,6 @@ 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 () {
-      return this.mightHideBecauseSubject && !this.expandingSubject
-    },
-    hideTallStatus () {
-      return this.mightHideBecauseTall && !this.showingTall
-    },
-    showingMore () {
-      return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
-    },
-    nsfwClickthrough () {
-      if (!this.status.nsfw) {
-        return false
-      }
-      if (this.status.summary && this.localCollapseSubjectDefault) {
-        return false
-      }
-      return true
-    },
     replySubject () {
       if (!this.status.summary) return ''
       const decodedSummary = unescape(this.status.summary)
@@ -227,83 +170,6 @@ const Status = {
         return ''
       }
     },
-    attachmentSize () {
-      if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
-        (this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
-        (this.status.attachments.length > this.maxThumbnails)) {
-        return 'hide'
-      } else if (this.compact) {
-        return 'small'
-      }
-      return 'normal'
-    },
-    galleryTypes () {
-      if (this.attachmentSize === 'hide') {
-        return []
-      }
-      return this.mergedConfig.playVideosInModal
-        ? ['image', 'video']
-        : ['image']
-    },
-    galleryAttachments () {
-      return this.status.attachments.filter(
-        file => fileType.fileMatchesSomeType(this.galleryTypes, file)
-      )
-    },
-    nonGalleryAttachments () {
-      return this.status.attachments.filter(
-        file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
-      )
-    },
-    hasImageAttachments () {
-      return this.status.attachments.some(
-        file => fileType.fileType(file.mimetype) === 'image'
-      )
-    },
-    hasVideoAttachments () {
-      return this.status.attachments.some(
-        file => fileType.fileType(file.mimetype) === 'video'
-      )
-    },
-    maxThumbnails () {
-      return this.mergedConfig.maxThumbnails
-    },
-    postBodyHtml () {
-      const html = this.status.statusnet_html
-
-      if (this.mergedConfig.greentext) {
-        try {
-          if (html.includes('&gt;')) {
-            // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
-            return processHtml(html, (string) => {
-              if (string.includes('&gt;') &&
-                  string
-                    .replace(/<[^>]+?>/gi, '') // remove all tags
-                    .replace(/@\w+/gi, '') // remove mentions (even failed ones)
-                    .trim()
-                    .startsWith('&gt;')) {
-                return `<span class='greentext'>${string}</span>`
-              } else {
-                return string
-              }
-            })
-          } else {
-            return html
-          }
-        } catch (e) {
-          console.err('Failed to process status html', e)
-          return html
-        }
-      } else {
-        return html
-      }
-    },
-    contentHtml () {
-      if (!this.status.summary_html) {
-        return this.postBodyHtml
-      }
-      return this.status.summary_html + '<br />' + this.postBodyHtml
-    },
     combinedFavsAndRepeatsUsers () {
       // Use the status from the global status repository since favs and repeats are saved in it
       const combinedUsers = [].concat(
@@ -312,9 +178,6 @@ const Status = {
       )
       return uniqBy(combinedUsers, 'id')
     },
-    ownStatus () {
-      return this.status.user.id === this.currentUser.id
-    },
     tags () {
       return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
     },
@@ -328,21 +191,18 @@ const Status = {
     })
   },
   components: {
-    Attachment,
     FavoriteButton,
     ReactButton,
     RetweetButton,
     ExtraButtons,
     PostStatusForm,
-    Poll,
     UserCard,
     UserAvatar,
-    Gallery,
-    LinkPreview,
     AvatarList,
     Timeago,
     StatusPopover,
-    EmojiReactions
+    EmojiReactions,
+    StatusContent
   },
   methods: {
     visibilityIcon (visibility) {
@@ -363,32 +223,6 @@ const Status = {
     clearError () {
       this.error = undefined
     },
-    linkClicked (event) {
-      const target = event.target.closest('.status-content a')
-      if (target) {
-        if (target.className.match(/mention/)) {
-          const href = target.href
-          const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
-          if (attn) {
-            event.stopPropagation()
-            event.preventDefault()
-            const link = this.generateUserProfileLink(attn.id, attn.screen_name)
-            this.$router.push(link)
-            return
-          }
-        }
-        if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
-          // Extract tag name from link url
-          const tag = extractTagFromUrl(target.href)
-          if (tag) {
-            const link = this.generateTagLink(tag)
-            this.$router.push(link)
-            return
-          }
-        }
-        window.open(target.href, '_blank')
-      }
-    },
     toggleReplying () {
       this.replying = !this.replying
     },
@@ -406,22 +240,8 @@ const Status = {
     toggleUserExpanded () {
       this.userExpanded = !this.userExpanded
     },
-    toggleShowMore () {
-      if (this.mightHideBecauseTall) {
-        this.showingTall = !this.showingTall
-      } else if (this.mightHideBecauseSubject) {
-        this.expandingSubject = !this.expandingSubject
-      }
-    },
     generateUserProfileLink (id, name) {
       return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
-    },
-    generateTagLink (tag) {
-      return `/tag/${tag}`
-    },
-    setMedia () {
-      const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
-      return () => this.$store.dispatch('setMedia', attachments)
     }
   },
   watch: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index ca295640..1ee4f27c 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -226,118 +226,12 @@
             </div>
           </div>
 
-          <div
-            v-if="longSubject"
-            class="status-content-wrapper"
-            :class="{ 'tall-status': !showingLongSubject }"
-          >
-            <a
-              v-if="!showingLongSubject"
-              class="tall-status-hider"
-              :class="{ 'tall-status-hider_focused': isFocused }"
-              href="#"
-              @click.prevent="showingLongSubject=true"
-            >{{ $t("general.show_more") }}</a>
-            <div
-              class="status-content media-body"
-              @click.prevent="linkClicked"
-              v-html="contentHtml"
-            />
-            <a
-              v-if="showingLongSubject"
-              href="#"
-              class="status-unhider"
-              @click.prevent="showingLongSubject=false"
-            >{{ $t("general.show_less") }}</a>
-          </div>
-          <div
-            v-else
-            :class="{'tall-status': hideTallStatus}"
-            class="status-content-wrapper"
-          >
-            <a
-              v-if="hideTallStatus"
-              class="tall-status-hider"
-              :class="{ 'tall-status-hider_focused': isFocused }"
-              href="#"
-              @click.prevent="toggleShowMore"
-            >{{ $t("general.show_more") }}</a>
-            <div
-              v-if="!hideSubjectStatus"
-              class="status-content media-body"
-              @click.prevent="linkClicked"
-              v-html="contentHtml"
-            />
-            <div
-              v-else
-              class="status-content media-body"
-              @click.prevent="linkClicked"
-              v-html="status.summary_html"
-            />
-            <a
-              v-if="hideSubjectStatus"
-              href="#"
-              class="cw-status-hider"
-              @click.prevent="toggleShowMore"
-            >
-              {{ $t("general.show_more") }}
-              <span
-                v-if="hasImageAttachments"
-                class="icon-picture"
-              />
-              <span
-                v-if="hasVideoAttachments"
-                class="icon-video"
-              />
-              <span
-                v-if="status.card"
-                class="icon-link"
-              />
-            </a>
-            <a
-              v-if="showingMore"
-              href="#"
-              class="status-unhider"
-              @click.prevent="toggleShowMore"
-            >{{ $t("general.show_less") }}</a>
-          </div>
-
-          <div v-if="status.poll && status.poll.options">
-            <poll :base-poll="status.poll" />
-          </div>
-
-          <div
-            v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)"
-            class="attachments media-body"
-          >
-            <attachment
-              v-for="attachment in nonGalleryAttachments"
-              :key="attachment.id"
-              class="non-gallery"
-              :size="attachmentSize"
-              :nsfw="nsfwClickthrough"
-              :attachment="attachment"
-              :allow-play="true"
-              :set-media="setMedia()"
-            />
-            <gallery
-              v-if="galleryAttachments.length > 0"
-              :nsfw="nsfwClickthrough"
-              :attachments="galleryAttachments"
-              :set-media="setMedia()"
-            />
-          </div>
-
-          <div
-            v-if="status.card && !hideSubjectStatus && !noHeading"
-            class="link-preview media-body"
-          >
-            <link-preview
-              :card="status.card"
-              :size="attachmentSize"
-              :nsfw="nsfwClickthrough"
-            />
-          </div>
+          <StatusContent
+            :status="status"
+            :no-heading="noHeading"
+            :highlight="highlight"
+            :focused="isFocused"
+          />
 
           <transition name="fade">
             <div
@@ -630,105 +524,6 @@ $status-margin: 0.75em;
     }
   }
 
-  .tall-status {
-    position: relative;
-    height: 220px;
-    overflow-x: hidden;
-    overflow-y: hidden;
-    z-index: 1;
-    .status-content {
-      height: 100%;
-      mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
-            linear-gradient(to top, white, white);
-      /* Autoprefixed seem to ignore this one, and also syntax is different */
-      -webkit-mask-composite: xor;
-      mask-composite: exclude;
-    }
-  }
-
-  .tall-status-hider {
-    display: inline-block;
-    word-break: break-all;
-    position: absolute;
-    height: 70px;
-    margin-top: 150px;
-    width: 100%;
-    text-align: center;
-    line-height: 110px;
-    z-index: 2;
-  }
-
-  .status-unhider, .cw-status-hider {
-    width: 100%;
-    text-align: center;
-    display: inline-block;
-    word-break: break-all;
-  }
-
-  .status-content {
-    font-family: var(--postFont, sans-serif);
-    line-height: 1.4em;
-    white-space: pre-wrap;
-
-    a {
-      color: $fallback--link;
-      color: var(--postLink, $fallback--link);
-    }
-
-    img, video {
-      max-width: 100%;
-      max-height: 400px;
-      vertical-align: middle;
-      object-fit: contain;
-
-      &.emoji {
-        width: 32px;
-        height: 32px;
-      }
-    }
-
-    blockquote {
-      margin: 0.2em 0 0.2em 2em;
-      font-style: italic;
-    }
-
-    pre {
-      overflow: auto;
-    }
-
-    code, samp, kbd, var, pre {
-      font-family: var(--postCodeFont, monospace);
-    }
-
-    p {
-      margin: 0 0 1em 0;
-    }
-
-    p:last-child {
-      margin: 0 0 0 0;
-    }
-
-    h1 {
-      font-size: 1.1em;
-      line-height: 1.2em;
-      margin: 1.4em 0;
-    }
-
-    h2 {
-      font-size: 1.1em;
-      margin: 1.0em 0;
-    }
-
-    h3 {
-      font-size: 1em;
-      margin: 1.2em 0;
-    }
-
-    h4 {
-      margin: 1.1em 0;
-    }
-  }
-
   .retweet-info {
     padding: 0.4em $status-margin;
     margin: 0;
@@ -790,11 +585,6 @@ $status-margin: 0.75em;
   }
 }
 
-.greentext {
-  color: $fallback--cGreen;
-  color: var(--cGreen, $fallback--cGreen);
-}
-
 .status-conversation {
   border-left-style: solid;
 }
@@ -866,14 +656,6 @@ a.unmute {
   flex: 1;
 }
 
-.timeline :not(.panel-disabled) > {
-  .status-el:last-child {
-    border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
-    border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
-    border-bottom: none;
-  }
-}
-
 .favs-repeated-users {
   margin-top: $status-margin;
 
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
new file mode 100644
index 00000000..ccc01b6f
--- /dev/null
+++ b/src/components/status_content/status_content.js
@@ -0,0 +1,210 @@
+import Attachment from '../attachment/attachment.vue'
+import Poll from '../poll/poll.vue'
+import Gallery from '../gallery/gallery.vue'
+import LinkPreview from '../link-preview/link-preview.vue'
+import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import fileType from 'src/services/file_type/file_type.service'
+import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
+import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
+import { mapGetters, mapState } from 'vuex'
+
+const StatusContent = {
+  name: 'StatusContent',
+  props: [
+    'status',
+    'focused',
+    'noHeading',
+    'fullContent'
+  ],
+  data () {
+    return {
+      showingTall: this.inConversation && this.focused,
+      showingLongSubject: false,
+      // not as computed because it sets the initial state which will be changed later
+      expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
+    }
+  },
+  computed: {
+    localCollapseSubjectDefault () {
+      return this.mergedConfig.collapseMessageWithSubject
+    },
+    hideAttachments () {
+      return (this.mergedConfig.hideAttachments && !this.inConversation) ||
+        (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
+    },
+    // This is a bit hacky, but we want to approximate post height before rendering
+    // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
+    // as well as approximate line count by counting characters and approximating ~80
+    // per line.
+    //
+    // Using max-height + overflow: auto for status components resulted in false positives
+    // very often with japanese characters, and it was very annoying.
+    tallStatus () {
+      const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
+      return lengthScore > 20
+    },
+    longSubject () {
+      return this.status.summary.length > 900
+    },
+    // 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 () {
+      return this.mightHideBecauseSubject && !this.expandingSubject
+    },
+    hideTallStatus () {
+      return this.mightHideBecauseTall && !this.showingTall
+    },
+    showingMore () {
+      return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
+    },
+    nsfwClickthrough () {
+      if (!this.status.nsfw) {
+        return false
+      }
+      if (this.status.summary && this.localCollapseSubjectDefault) {
+        return false
+      }
+      return true
+    },
+    attachmentSize () {
+      if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
+        (this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
+        (this.status.attachments.length > this.maxThumbnails)) {
+        return 'hide'
+      } else if (this.compact) {
+        return 'small'
+      }
+      return 'normal'
+    },
+    galleryTypes () {
+      if (this.attachmentSize === 'hide') {
+        return []
+      }
+      return this.mergedConfig.playVideosInModal
+        ? ['image', 'video']
+        : ['image']
+    },
+    galleryAttachments () {
+      return this.status.attachments.filter(
+        file => fileType.fileMatchesSomeType(this.galleryTypes, file)
+      )
+    },
+    nonGalleryAttachments () {
+      return this.status.attachments.filter(
+        file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
+      )
+    },
+    hasImageAttachments () {
+      return this.status.attachments.some(
+        file => fileType.fileType(file.mimetype) === 'image'
+      )
+    },
+    hasVideoAttachments () {
+      return this.status.attachments.some(
+        file => fileType.fileType(file.mimetype) === 'video'
+      )
+    },
+    maxThumbnails () {
+      return this.mergedConfig.maxThumbnails
+    },
+    postBodyHtml () {
+      const html = this.status.statusnet_html
+
+      if (this.mergedConfig.greentext) {
+        try {
+          if (html.includes('&gt;')) {
+            // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
+            return processHtml(html, (string) => {
+              if (string.includes('&gt;') &&
+                  string
+                    .replace(/<[^>]+?>/gi, '') // remove all tags
+                    .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+                    .trim()
+                    .startsWith('&gt;')) {
+                return `<span class='greentext'>${string}</span>`
+              } else {
+                return string
+              }
+            })
+          } else {
+            return html
+          }
+        } catch (e) {
+          console.err('Failed to process status html', e)
+          return html
+        }
+      } else {
+        return html
+      }
+    },
+    contentHtml () {
+      if (!this.status.summary_html) {
+        return this.postBodyHtml
+      }
+      return this.status.summary_html + '<br />' + this.postBodyHtml
+    },
+    ...mapGetters(['mergedConfig']),
+    ...mapState({
+      betterShadow: state => state.interface.browserSupport.cssFilter,
+      currentUser: state => state.users.currentUser
+    })
+  },
+  components: {
+    Attachment,
+    Poll,
+    Gallery,
+    LinkPreview
+  },
+  methods: {
+    linkClicked (event) {
+      const target = event.target.closest('.status-content a')
+      if (target) {
+        if (target.className.match(/mention/)) {
+          const href = target.href
+          const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
+          if (attn) {
+            event.stopPropagation()
+            event.preventDefault()
+            const link = this.generateUserProfileLink(attn.id, attn.screen_name)
+            this.$router.push(link)
+            return
+          }
+        }
+        if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
+          // Extract tag name from link url
+          const tag = extractTagFromUrl(target.href)
+          if (tag) {
+            const link = this.generateTagLink(tag)
+            this.$router.push(link)
+            return
+          }
+        }
+        window.open(target.href, '_blank')
+      }
+    },
+    toggleShowMore () {
+      if (this.mightHideBecauseTall) {
+        this.showingTall = !this.showingTall
+      } else if (this.mightHideBecauseSubject) {
+        this.expandingSubject = !this.expandingSubject
+      }
+    },
+    generateUserProfileLink (id, name) {
+      return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
+    },
+    generateTagLink (tag) {
+      return `/tag/${tag}`
+    },
+    setMedia () {
+      const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
+      return () => this.$store.dispatch('setMedia', attachments)
+    }
+  }
+}
+
+export default StatusContent
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
new file mode 100644
index 00000000..8c2e8749
--- /dev/null
+++ b/src/components/status_content/status_content.vue
@@ -0,0 +1,240 @@
+<template>
+  <!-- eslint-disable vue/no-v-html -->
+  <div class="status-body">
+    <slot name="header" />
+    <div
+      v-if="longSubject"
+      class="status-content-wrapper"
+      :class="{ 'tall-status': !showingLongSubject }"
+    >
+      <a
+        v-if="!showingLongSubject"
+        class="tall-status-hider"
+        :class="{ 'tall-status-hider_focused': focused }"
+        href="#"
+        @click.prevent="showingLongSubject=true"
+      >
+        {{ $t("general.show_more") }}
+        <span
+          v-if="hasImageAttachments"
+          class="icon-picture"
+        />
+        <span
+          v-if="hasVideoAttachments"
+          class="icon-video"
+        />
+        <span
+          v-if="status.card"
+          class="icon-link"
+        />
+      </a>
+      <div
+        class="status-content media-body"
+        @click.prevent="linkClicked"
+        v-html="contentHtml"
+      />
+      <a
+        v-if="showingLongSubject"
+        href="#"
+        class="status-unhider"
+        @click.prevent="showingLongSubject=false"
+      >{{ $t("general.show_less") }}</a>
+    </div>
+    <div
+      v-else
+      :class="{'tall-status': hideTallStatus}"
+      class="status-content-wrapper"
+    >
+      <a
+        v-if="hideTallStatus"
+        class="tall-status-hider"
+        :class="{ 'tall-status-hider_focused': focused }"
+        href="#"
+        @click.prevent="toggleShowMore"
+      >{{ $t("general.show_more") }}</a>
+      <div
+        v-if="!hideSubjectStatus"
+        class="status-content media-body"
+        @click.prevent="linkClicked"
+        v-html="contentHtml"
+      />
+      <div
+        v-else
+        class="status-content media-body"
+        @click.prevent="linkClicked"
+        v-html="status.summary_html"
+      />
+      <a
+        v-if="hideSubjectStatus"
+        href="#"
+        class="cw-status-hider"
+        @click.prevent="toggleShowMore"
+      >{{ $t("general.show_more") }}</a>
+      <a
+        v-if="showingMore"
+        href="#"
+        class="status-unhider"
+        @click.prevent="toggleShowMore"
+      >{{ $t("general.show_less") }}</a>
+    </div>
+
+    <div v-if="status.poll && status.poll.options">
+      <poll :base-poll="status.poll" />
+    </div>
+
+    <div
+      v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
+      class="attachments media-body"
+    >
+      <attachment
+        v-for="attachment in nonGalleryAttachments"
+        :key="attachment.id"
+        class="non-gallery"
+        :size="attachmentSize"
+        :nsfw="nsfwClickthrough"
+        :attachment="attachment"
+        :allow-play="true"
+        :set-media="setMedia()"
+      />
+      <gallery
+        v-if="galleryAttachments.length > 0"
+        :nsfw="nsfwClickthrough"
+        :attachments="galleryAttachments"
+        :set-media="setMedia()"
+      />
+    </div>
+
+    <div
+      v-if="status.card && !hideSubjectStatus && !noHeading"
+      class="link-preview media-body"
+    >
+      <link-preview
+        :card="status.card"
+        :size="attachmentSize"
+        :nsfw="nsfwClickthrough"
+      />
+    </div>
+    <slot name="footer" />
+  </div>
+  <!-- eslint-enable vue/no-v-html -->
+</template>
+
+<script src="./status_content.js" ></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+$status-margin: 0.75em;
+
+.status-body {
+  flex: 1;
+  min-width: 0;
+
+  .tall-status {
+    position: relative;
+    height: 220px;
+    overflow-x: hidden;
+    overflow-y: hidden;
+    z-index: 1;
+    .status-content {
+      height: 100%;
+      mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
+            linear-gradient(to top, white, white);
+      /* Autoprefixed seem to ignore this one, and also syntax is different */
+      -webkit-mask-composite: xor;
+      mask-composite: exclude;
+    }
+  }
+
+  .tall-status-hider {
+    display: inline-block;
+    word-break: break-all;
+    position: absolute;
+    height: 70px;
+    margin-top: 150px;
+    width: 100%;
+    text-align: center;
+    line-height: 110px;
+    z-index: 2;
+  }
+
+  .status-unhider, .cw-status-hider {
+    width: 100%;
+    text-align: center;
+    display: inline-block;
+    word-break: break-all;
+  }
+
+  .status-content {
+    font-family: var(--postFont, sans-serif);
+    line-height: 1.4em;
+    white-space: pre-wrap;
+
+    img, video {
+      max-width: 100%;
+      max-height: 400px;
+      vertical-align: middle;
+      object-fit: contain;
+
+      &.emoji {
+        width: 32px;
+        height: 32px;
+      }
+    }
+
+    blockquote {
+      margin: 0.2em 0 0.2em 2em;
+      font-style: italic;
+    }
+
+    pre {
+      overflow: auto;
+    }
+
+    code, samp, kbd, var, pre {
+      font-family: var(--postCodeFont, monospace);
+    }
+
+    p {
+      margin: 0 0 1em 0;
+    }
+
+    p:last-child {
+      margin: 0 0 0 0;
+    }
+
+    h1 {
+      font-size: 1.1em;
+      line-height: 1.2em;
+      margin: 1.4em 0;
+    }
+
+    h2 {
+      font-size: 1.1em;
+      margin: 1.0em 0;
+    }
+
+    h3 {
+      font-size: 1em;
+      margin: 1.2em 0;
+    }
+
+    h4 {
+      margin: 1.1em 0;
+    }
+  }
+}
+
+.greentext {
+  color: $fallback--cGreen;
+  color: var(--cGreen, $fallback--cGreen);
+}
+
+.timeline :not(.panel-disabled) > {
+  .status-el:last-child {
+    border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
+    border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
+    border-bottom: none;
+  }
+}
+
+</style>
-- 
cgit v1.2.3-70-g09d2


From 93baa8b664ed7769f5a562953b4b9b93d21ff043 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Fri, 8 May 2020 00:10:49 +0300
Subject: fix gap when not logged in

---
 src/components/react_button/react_button.js  | 2 +-
 src/components/react_button/react_button.vue | 1 -
 src/components/status/status.vue             | 2 +-
 3 files changed, 2 insertions(+), 3 deletions(-)

(limited to 'src/components/status/status.vue')

diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index 19949563..abc3bf07 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -2,7 +2,7 @@ import Popover from '../popover/popover.vue'
 import { mapGetters } from 'vuex'
 
 const ReactButton = {
-  props: ['status', 'loggedIn'],
+  props: ['status'],
   data () {
     return {
       filterWord: ''
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index ab4b4fcd..0b34add1 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -37,7 +37,6 @@
       </div>
     </div>
     <i
-      v-if="loggedIn"
       slot="trigger"
       class="icon-smile button-icon add-reaction-button"
       :title="$t('tool_tip.add_reaction')"
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index ca295640..76b5c5ab 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -404,7 +404,7 @@
               :status="status"
             />
             <ReactButton
-              :logged-in="loggedIn"
+              v-if="loggedIn"
               :status="status"
             />
             <extra-buttons
-- 
cgit v1.2.3-70-g09d2


From b5c1d074f83d08473a19a3885f6ff5eeb95274e5 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 25 May 2020 23:38:31 +0300
Subject: fix reprööted posts not being muted properly. fix muted posts making
 desktop notifications
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/status/status.js             | 57 ++++++++++++++++++++++-------
 src/components/status/status.vue            | 18 ++++++++-
 src/i18n/en.json                            |  4 +-
 src/modules/statuses.js                     | 10 ++++-
 src/services/status_parser/status_parser.js | 11 ++++++
 5 files changed, 83 insertions(+), 17 deletions(-)

(limited to 'src/components/status/status.vue')

diff --git a/src/components/status/status.js b/src/components/status/status.js
index 9cd9d61c..95278968 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -12,7 +12,8 @@ import StatusPopover from '../status_popover/status_popover.vue'
 import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
-import { filter, unescape, uniqBy } from 'lodash'
+import { muteWordHits } from '../../services/status_parser/status_parser.js'
+import { unescape, uniqBy } from 'lodash'
 import { mapGetters, mapState } from 'vuex'
 
 const Status = {
@@ -44,6 +45,12 @@ const Status = {
     muteWords () {
       return this.mergedConfig.muteWords
     },
+    showReasonMutedThread () {
+      return (
+        this.status.thread_muted ||
+          (this.status.reblog && this.status.reblog.thread_muted)
+      ) && !this.inConversation
+    },
     repeaterClass () {
       const user = this.statusoid.user
       return highlightClass(user)
@@ -93,20 +100,44 @@ const Status = {
       return !!this.currentUser
     },
     muteWordHits () {
-      const statusText = this.status.text.toLowerCase()
-      const statusSummary = this.status.summary.toLowerCase()
-      const hits = filter(this.muteWords, (muteWord) => {
-        return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
-      })
-
-      return hits
+      return muteWordHits(this.status, this.muteWords)
     },
     muted () {
-      const relationship = this.$store.getters.relationship(this.status.user.id)
-      return !this.unmuted && (
-        (!(this.inProfile && this.status.user.id === this.profileUserId) && relationship.muting) ||
-        (!this.inConversation && this.status.thread_muted) ||
-        this.muteWordHits.length > 0)
+      const { status } = this
+      const { reblog } = status
+      const relationship = this.$store.getters.relationship(status.user.id)
+      const relationshipReblog = reblog && this.$store.getters.relationship(reblog.user.id)
+      const reasonsToMute = (
+        // Post is muted according to BE
+        status.muted ||
+        // Reprööt of a muted post according to BE
+        (reblog && reblog.muted) ||
+        // Muted user
+        relationship.muting ||
+        // Muted user of a reprööt
+        (relationshipReblog && relationshipReblog.muting) ||
+        // Thread is muted
+        status.thread_muted ||
+        // Wordfiltered
+        this.muteWordHits.length > 0
+      )
+      const excusesNotToMute = (
+        // Currently showing status
+        this.unmuted ||
+        (
+          this.inProfile && (
+            // Don't mute user's posts on user timeline (except reblogs)
+            (!reblog && status.user.id === this.profileUserId) ||
+            // Same as above but also allow self-reblogs
+            (reblog && reblog.user.id === this.profileUserId)
+          )
+        ) ||
+        // Don't mute statuses in muted conversation when said conversation is opened
+        (this.inConversation && status.thread_muted)
+        // No excuses if post has muted words
+      ) && !this.muteWordHits.length > 0
+
+      return !excusesNotToMute && reasonsToMute
     },
     hideFilteredStatuses () {
       return this.mergedConfig.hideFilteredStatuses
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index e4c7545b..1c287aa7 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -18,10 +18,26 @@
     <template v-if="muted && !isPreview">
       <div class="media status container muted">
         <small>
+          <i
+            v-if="muted && retweet"
+            class="button-icon icon-retweet"
+          />
           <router-link :to="userProfileLink">
             {{ status.user.screen_name }}
           </router-link>
         </small>
+        <small
+          v-if="showReasonMutedThread && muteWordHits.length === 0"
+          class="mutedThread"
+        >
+          {{ $t('status.thread_muted') }}
+        </small>
+        <small
+          v-if="showReasonMutedThread && muteWordHits.length > 0"
+          class="mutedThread"
+        >
+          {{ $t('status.thread_muted_and_words') }}
+        </small>
         <small class="muteWords">{{ muteWordHits.join(', ') }}</small>
         <a
           href="#"
@@ -642,7 +658,7 @@ $status-margin: 0.75em;
     margin-left: auto;
   }
 
-  .muteWords {
+  .mutedThread, .muteWords {
     margin-left: 10px;
   }
 }
diff --git a/src/i18n/en.json b/src/i18n/en.json
index f5d1ce7c..61b818a6 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -621,7 +621,9 @@
     "mute_conversation": "Mute conversation",
     "unmute_conversation": "Unmute conversation",
     "status_unavailable": "Status unavailable",
-    "copy_link": "Copy link to status"
+    "copy_link": "Copy link to status",
+    "thread_muted": "Conversation muted",
+    "thread_muted_and_words": "Conversation muted, contains filtered words:"
   },
   "user_card": {
     "approve": "Approve",
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index cd8c1dba..f73fde25 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -15,7 +15,7 @@ import {
 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'
+import { muteWordHits } from '../services/status_parser/status_parser.js'
 
 const emptyTl = (userId = 0) => ({
   statuses: [],
@@ -381,7 +381,13 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
           notifObj.image = status.attachments[0].url
         }
 
-        if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
+        if (
+          !notification.seen &&
+            !state.notifications.desktopNotificationSilence &&
+            visibleNotificationTypes.includes(notification.type) &&
+            !status.muted &&
+            muteWordHits(status, rootGetters.mergedConfig.muteWords).length === 0
+        ) {
           let desktopNotification = new window.Notification(title, notifObj)
           // Chrome is known for not closing notifications automatically
           // according to MDN, anyway.
diff --git a/src/services/status_parser/status_parser.js b/src/services/status_parser/status_parser.js
index 900cd56e..3d517e3c 100644
--- a/src/services/status_parser/status_parser.js
+++ b/src/services/status_parser/status_parser.js
@@ -1,3 +1,4 @@
+import { filter } from 'lodash'
 import sanitize from 'sanitize-html'
 
 export const removeAttachmentLinks = (html) => {
@@ -12,4 +13,14 @@ export const parse = (html) => {
   return removeAttachmentLinks(html)
 }
 
+export const muteWordHits = (status, muteWords) => {
+  const statusText = status.text.toLowerCase()
+  const statusSummary = status.summary.toLowerCase()
+  const hits = filter(muteWords, (muteWord) => {
+    return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
+  })
+
+  return hits
+}
+
 export default parse
-- 
cgit v1.2.3-70-g09d2


From de3a376bebb25fefb1fe513c3491ee6c4e48af0f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 2 Jun 2020 01:16:10 +0300
Subject: fixed case in class name

---
 src/components/status/status.vue | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

(limited to 'src/components/status/status.vue')

diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 1c287aa7..3137cb9d 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -28,17 +28,17 @@
         </small>
         <small
           v-if="showReasonMutedThread && muteWordHits.length === 0"
-          class="mutedThread"
+          class="muted-thread"
         >
           {{ $t('status.thread_muted') }}
         </small>
         <small
           v-if="showReasonMutedThread && muteWordHits.length > 0"
-          class="mutedThread"
+          class="muted-Thread"
         >
           {{ $t('status.thread_muted_and_words') }}
         </small>
-        <small class="muteWords">{{ muteWordHits.join(', ') }}</small>
+        <small class="mute-words">{{ muteWordHits.join(', ') }}</small>
         <a
           href="#"
           class="unmute"
@@ -658,7 +658,7 @@ $status-margin: 0.75em;
     margin-left: auto;
   }
 
-  .mutedThread, .muteWords {
+  .muted-thread, .mute-words {
     margin-left: 10px;
   }
 }
-- 
cgit v1.2.3-70-g09d2


From ebf2ce84fdd7e175f5715452400ec3bdf789310b Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sat, 6 Jun 2020 23:08:52 +0300
Subject: alignment fixes

---
 src/components/notifications/notifications.scss |  6 +--
 src/components/status/status.vue                | 62 +++++++++++++++++++------
 src/i18n/en.json                                |  4 +-
 3 files changed, 52 insertions(+), 20 deletions(-)

(limited to 'src/components/status/status.vue')

diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 9efcfcf8..b675af5a 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -36,6 +36,8 @@
   border-bottom: 1px solid;
   border-color: $fallback--border;
   border-color: var(--border, $fallback--border);
+  word-wrap: break-word;
+  word-break: break-word;
 
   &:hover .animated.avatar {
     canvas {
@@ -46,10 +48,6 @@
     }
   }
 
-  .muted {
-    padding: .25em .6em;
-  }
-
   .non-mention {
     display: flex;
     flex: 1;
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 3137cb9d..336f912a 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -17,7 +17,7 @@
     </div>
     <template v-if="muted && !isPreview">
       <div class="media status container muted">
-        <small>
+        <small class="username">
           <i
             v-if="muted && retweet"
             class="button-icon icon-retweet"
@@ -27,18 +27,23 @@
           </router-link>
         </small>
         <small
-          v-if="showReasonMutedThread && muteWordHits.length === 0"
-          class="muted-thread"
+          v-if="showReasonMutedThread"
+          class="mute-thread"
         >
           {{ $t('status.thread_muted') }}
         </small>
         <small
           v-if="showReasonMutedThread && muteWordHits.length > 0"
-          class="muted-Thread"
+          class="mute-thread"
         >
           {{ $t('status.thread_muted_and_words') }}
         </small>
-        <small class="mute-words">{{ muteWordHits.join(', ') }}</small>
+        <small
+          class="mute-words"
+          :title="muteWordHits.join(', ')"
+        >
+          {{ muteWordHits.join(', ') }}
+        </small>
         <a
           href="#"
           class="unmute"
@@ -653,19 +658,48 @@ $status-margin: 0.75em;
 }
 
 .muted {
-  padding: 0.25em 0.5em;
-  button {
-    margin-left: auto;
+  padding: .25em .6em;
+  height: 1.2em;
+  line-height: 1.2em;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  display: flex;
+  flex-wrap: nowrap;
+
+  .username, .mute-thread, .mute-words {
+    word-wrap: normal;
+    word-break: normal;
+    white-space: nowrap;
   }
 
-  .muted-thread, .mute-words {
-    margin-left: 10px;
+  .username, .mute-words {
+    text-overflow: ellipsis;
+    overflow: hidden;
   }
-}
 
-a.unmute {
-  display: block;
-  margin-left: auto;
+  .username {
+    flex: 0 1 auto;
+    margin-right: .2em;
+  }
+
+  .mute-thread {
+    flex: 0 0 auto;
+  }
+
+  .mute-words {
+    flex: 1 0 5em;
+    margin-left: .2em;
+    &::before {
+      content: ' '
+    }
+  }
+
+  .unmute {
+    flex: 0 0 auto;
+    margin-left: auto;
+    display: block;
+    margin-left: auto;
+  }
 }
 
 .reply-body {
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 61b818a6..5bcf074b 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -622,8 +622,8 @@
     "unmute_conversation": "Unmute conversation",
     "status_unavailable": "Status unavailable",
     "copy_link": "Copy link to status",
-    "thread_muted": "Conversation muted",
-    "thread_muted_and_words": "Conversation muted, contains filtered words:"
+    "thread_muted": "Thread muted",
+    "thread_muted_and_words": ", has words:"
   },
   "user_card": {
     "approve": "Approve",
-- 
cgit v1.2.3-70-g09d2

