From 7ebf3602d5d9a8630ffbe239bfe4431655046821 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Tue, 26 Nov 2019 19:57:27 -0500
Subject: move mention button right next to mute button

---
 src/components/account_actions/account_actions.vue | 18 ++++--------------
 1 file changed, 4 insertions(+), 14 deletions(-)

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

diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 046cba93..d3235be1 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -9,17 +9,7 @@
     >
       <div slot="popover">
         <div class="dropdown-menu">
-          <button
-            class="btn btn-default btn-block dropdown-item"
-            @click="mentionUser"
-          >
-            {{ $t('user_card.mention') }}
-          </button>
           <template v-if="user.following">
-            <div
-              role="separator"
-              class="dropdown-divider"
-            />
             <button
               v-if="user.showing_reblogs"
               class="btn btn-default dropdown-item"
@@ -34,11 +24,11 @@
             >
               {{ $t('user_card.show_repeats') }}
             </button>
+            <div
+              role="separator"
+              class="dropdown-divider"
+            />
           </template>
-          <div
-            role="separator"
-            class="dropdown-divider"
-          />
           <button
             v-if="user.statusnet_blocking"
             class="btn btn-default btn-block dropdown-item"
-- 
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/account_actions/account_actions.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/account_actions/account_actions.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

