From cd9a7dd48802fff8942ae607a23677cfb43a7b14 Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Tue, 12 Mar 2019 17:16:57 -0400
Subject: #436: integrate mastoAPI notifications

---
 src/components/notification/notification.vue | 83 ++++++++++++++++------------
 1 file changed, 48 insertions(+), 35 deletions(-)

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

diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 5e9cef97..b281c7cf 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -1,44 +1,57 @@
 <template>
-  <status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
-  <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
-    <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
-      <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
-    </a>
-    <div class='notification-right'>
-      <UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
-      <span class="notification-details">
-        <div class="name-and-action">
-          <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
-          <span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
-          <span v-if="notification.type === 'like'">
-            <i class="fa icon-star lit"></i>
-            <small>{{$t('notifications.favorited_you')}}</small>
-          </span>
-          <span v-if="notification.type === 'repeat'">
-            <i class="fa icon-retweet lit" :title="$t('tool_tip.repeat')"></i>
-            <small>{{$t('notifications.repeated_you')}}</small>
-          </span>
-          <span v-if="notification.type === 'follow'">
-            <i class="fa icon-user-plus lit"></i>
-            <small>{{$t('notifications.followed_you')}}</small>
-          </span>
-        </div>
-        <div class="timeago">
-          <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
-            <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
+  <div class="notification-item">
+    <button @click.prevent="dismiss" class="btn-dismiss">{{$t("notifications.dismiss")}}</button>
+    <status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
+    <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
+      <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
+        <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
+      </a>
+      <div class='notification-right'>
+        <UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
+        <span class="notification-details">
+          <div class="name-and-action">
+            <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
+            <span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
+            <span v-if="notification.type === 'like'">
+              <i class="fa icon-star lit"></i>
+              <small>{{$t('notifications.favorited_you')}}</small>
+            </span>
+            <span v-if="notification.type === 'repeat'">
+              <i class="fa icon-retweet lit" :title="$t('tool_tip.repeat')"></i>
+              <small>{{$t('notifications.repeated_you')}}</small>
+            </span>
+            <span v-if="notification.type === 'follow'">
+              <i class="fa icon-user-plus lit"></i>
+              <small>{{$t('notifications.followed_you')}}</small>
+            </span>
+          </div>
+          <div class="timeago">
+            <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
+              <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
+            </router-link>
+          </div>
+        </span>
+        <div class="follow-text" v-if="notification.type === 'follow'">
+          <router-link :to="userProfileLink(notification.action.user)">
+            @{{notification.action.user.screen_name}}
           </router-link>
         </div>
-      </span>
-      <div class="follow-text" v-if="notification.type === 'follow'">
-        <router-link :to="userProfileLink(notification.action.user)">
-          @{{notification.action.user.screen_name}}
-        </router-link>
+        <template v-else>
+          <status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
+        </template>
       </div>
-      <template v-else>
-        <status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
-      </template>
     </div>
   </div>
 </template>
 
 <script src="./notification.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.notification-item {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+}
+</style>
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 2f7d890ad228a2aeb8643373c9c9a6d925c6ff7f Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Wed, 13 Mar 2019 14:08:03 -0400
Subject: #436: add dismiss button, disable is_seen part

---
 src/components/notification/notification.vue       | 93 +++++++++++-----------
 src/components/status/status.js                    |  6 +-
 src/components/status/status.vue                   |  3 +
 .../entity_normalizer/entity_normalizer.service.js |  6 +-
 .../notifications_fetcher.service.js               | 22 ++---
 5 files changed, 68 insertions(+), 62 deletions(-)

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

diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index b281c7cf..87b0fde2 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -1,57 +1,54 @@
 <template>
-  <div class="notification-item">
-    <button @click.prevent="dismiss" class="btn-dismiss">{{$t("notifications.dismiss")}}</button>
-    <status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
-    <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
-      <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
-        <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
-      </a>
-      <div class='notification-right'>
-        <UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
-        <span class="notification-details">
-          <div class="name-and-action">
-            <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
-            <span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
-            <span v-if="notification.type === 'like'">
-              <i class="fa icon-star lit"></i>
-              <small>{{$t('notifications.favorited_you')}}</small>
-            </span>
-            <span v-if="notification.type === 'repeat'">
-              <i class="fa icon-retweet lit" :title="$t('tool_tip.repeat')"></i>
-              <small>{{$t('notifications.repeated_you')}}</small>
-            </span>
-            <span v-if="notification.type === 'follow'">
-              <i class="fa icon-user-plus lit"></i>
-              <small>{{$t('notifications.followed_you')}}</small>
-            </span>
-          </div>
-          <div class="timeago">
-            <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
-              <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
-            </router-link>
-          </div>
-        </span>
-        <div class="follow-text" v-if="notification.type === 'follow'">
-          <router-link :to="userProfileLink(notification.action.user)">
-            @{{notification.action.user.screen_name}}
+  <status 
+    v-if="notification.type === 'mention'" 
+    :compact="true" 
+    :statusoid="notification.status"
+    isNotification="true"
+    @dismissNotification="dismiss"
+  >
+  </status>
+  <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
+    <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
+      <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
+    </a>
+    <div class='notification-right'>
+      <UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
+      <span class="notification-details">
+        <div class="name-and-action">
+          <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
+          <span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
+          <span v-if="notification.type === 'like'">
+            <i class="fa icon-star lit"></i>
+            <small>{{$t('notifications.favorited_you')}}</small>
+          </span>
+          <span v-if="notification.type === 'repeat'">
+            <i class="fa icon-retweet lit" :title="$t('tool_tip.repeat')"></i>
+            <small>{{$t('notifications.repeated_you')}}</small>
+          </span>
+          <span v-if="notification.type === 'follow'">
+            <i class="fa icon-user-plus lit"></i>
+            <small>{{$t('notifications.followed_you')}}</small>
+          </span>
+        </div>
+        <div class="timeago">
+          <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
+            <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
           </router-link>
         </div>
-        <template v-else>
-          <status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
-        </template>
+        <span class="button-icon" @click.prevent="dismiss">
+          <i class="button-icon icon-cancel delete-status"></i>
+        </span>
+      </span>
+      <div class="follow-text" v-if="notification.type === 'follow'">
+        <router-link :to="userProfileLink(notification.action.user)">
+          @{{notification.action.user.screen_name}}
+        </router-link>
       </div>
+      <template v-else>
+        <status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
+      </template>
     </div>
   </div>
 </template>
 
 <script src="./notification.js"></script>
-
-<style lang="scss">
-@import '../../_variables.scss';
-
-.notification-item {
-  display: flex;
-  flex: 1;
-  flex-direction: column;
-}
-</style>
\ No newline at end of file
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 9e18fe15..4cd26be3 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -25,7 +25,8 @@ const Status = {
     'replies',
     'isPreview',
     'noHeading',
-    'inlineExpanded'
+    'inlineExpanded',
+    'isNotification'
   ],
   data () {
     return {
@@ -365,6 +366,9 @@ const Status = {
     setMedia () {
       const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
       return () => this.$store.dispatch('setMedia', attachments)
+    },
+    dismissNotification () {
+      this.$emit('dismissNotification')
     }
   },
   watch: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 1f6d0325..28a44346 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -52,6 +52,9 @@
                 <a :href="status.external_url" target="_blank" v-if="!status.is_local && !isPreview" class="source_url" title="Source">
                   <i class="button-icon icon-link-ext-alt"></i>
                 </a>
+                <span class="button-icon" v-if="isNotification" @click.prevent="dismissNotification">
+                  <i class="button-icon icon-cancel delete-status"></i>
+                </span>
                 <template v-if="expandable && !isPreview">
                   <a href="#" @click.prevent="toggleExpanded" title="Expand">
                     <i class="button-icon icon-plus-squared"></i>
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 81b88bf0..4e6248bb 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -65,8 +65,7 @@ export const parseUser = (data) => {
       output.muted = pleroma.muted
     }
 
-    // Missing, trying to recover
-    output.is_local = !output.screen_name.includes('@')
+    // TODO: handle is_local
   } else {
     output.screen_name = data.screen_name
 
@@ -179,8 +178,7 @@ export const parseStatus = (data) => {
     output.summary_html = data.spoiler_text
     output.external_url = data.url
 
-    // FIXME missing!!
-    output.is_local = false
+    // TODO: handle is_local
   } else {
     output.favorited = data.favorited
     output.fave_num = data.fave_num
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 3ecdae6a..9eac5ab4 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -16,17 +16,21 @@ const fetchAndUpdate = ({store, credentials, older = false}) => {
       args['until'] = timelineData.minId
     }
   } else {
-    // load unread notifications repeadedly to provide consistency between browser tabs
-    const notifications = timelineData.data
-    const unread = notifications.filter(n => !n.seen).map(n => n.id)
-    if (!unread.length) {
+    if (timelineData.maxId !== Number.POSITIVE_INFINITY) {
       args['since'] = timelineData.maxId
-    } else {
-      args['since'] = Math.min(...unread) - 1
-      if (timelineData.maxId !== Math.max(...unread)) {
-        args['until'] = Math.max(...unread, args['since'] + 20)
-      }
     }
+    // # disabled until is_seen is impelented on the BE
+    // load unread notifications repeadedly to provide consistency between browser tabs
+    // const notifications = timelineData.data
+    // const unread = notifications.filter(n => !n.seen).map(n => n.id)
+    // if (!unread.length) {
+    //   args['since'] = timelineData.maxId
+    // } else {
+    //   args['since'] = Math.min(...unread) - 1
+    //   if (timelineData.maxId !== Math.max(...unread)) {
+    //     args['until'] = Math.max(...unread, args['since'] + 20)
+    //   }
+    // }
   }
 
   args['timeline'] = 'notifications'
-- 
cgit v1.2.3-70-g09d2


From e5e1d4c643d6b69217fb9a62b60f3f9c94ef4714 Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Thu, 14 Mar 2019 07:55:39 -0400
Subject: #436: fix notification order, follow type link issue, duplicate key

---
 src/components/notification/notification.vue                | 7 ++++++-
 src/components/notifications/notifications.vue              | 2 +-
 src/modules/statuses.js                                     | 8 ++++----
 src/services/entity_normalizer/entity_normalizer.service.js | 2 +-
 src/services/notification_utils/notification_utils.js       | 6 +++---
 5 files changed, 15 insertions(+), 10 deletions(-)

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

diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 87b0fde2..6e8e9844 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -30,7 +30,12 @@
             <small>{{$t('notifications.followed_you')}}</small>
           </span>
         </div>
-        <div class="timeago">
+        <div class="timeago" v-if="notification.type === 'follow'">
+          <span class="faint">
+            <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
+          </span>
+        </div>
+        <div class="timeago" v-else>
           <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
             <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
           </router-link>
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 6438361d..5a5f8ed8 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -13,7 +13,7 @@
         <button v-if="notifications.length" @click.prevent="clear" class="read-button">{{$t('notifications.clear')}}</button>
       </div>
       <div class="panel-body">
-        <div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
+        <div v-for="notification in visibleNotifications" :key="notification.action.id + notification.id" class="notification" :class='{"unseen": !notification.seen}'>
           <div class="notification-overlay"></div>
           <notification :notification="notification"></notification>
         </div>
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index fde783a5..4cc3907d 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -269,11 +269,11 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
 }
 
 const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => {
-  const allStatuses = state.allStatuses
-  const allStatusesObject = state.allStatusesObject
+  // const allStatuses = state.allStatuses
+  // const allStatusesObject = state.allStatusesObject
   each(notifications, (notification) => {
-    notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
-    notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
+    // notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
+    // notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
 
     // Only add a new notification if we don't have one for the same action
     if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 5444a1e3..4ca7f56b 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -275,7 +275,7 @@ export const parseNotification = (data) => {
     output.status = output.type === 'follow'
       ? parseFollow(data)
       : parseStatus(data.status)
-    if (data.type === 'reblog') {
+    if (data.type === 'reblog' || data.type === 'favourite') {
       output.status.user = parseUser(data.account)
       output.status.created_at = new Date(data.created_at)
     }
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index cd8f3f9e..8afd114e 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -10,8 +10,8 @@ export const visibleTypes = store => ([
 ].filter(_ => _))
 
 const sortById = (a, b) => {
-  const seqA = Number(a.action.id)
-  const seqB = Number(b.action.id)
+  const seqA = Number(a.id)
+  const seqB = Number(b.id)
   const isSeqA = !Number.isNaN(seqA)
   const isSeqB = !Number.isNaN(seqB)
   if (isSeqA && isSeqB) {
@@ -21,7 +21,7 @@ const sortById = (a, b) => {
   } else if (!isSeqA && isSeqB) {
     return -1
   } else {
-    return a.action.id > b.action.id ? -1 : 1
+    return a.id > b.id ? -1 : 1
   }
 }
 
-- 
cgit v1.2.3-70-g09d2


From 52326e65515317be4815aaef320965313f84d64b Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Thu, 14 Mar 2019 11:57:24 -0400
Subject: #436: remove clear & dismiss UI updates

---
 src/components/notification/notification.js    |  3 ---
 src/components/notification/notification.vue   |  5 ----
 src/components/notifications/notifications.js  |  3 ---
 src/components/notifications/notifications.vue |  3 +--
 src/components/status/status.js                |  6 +----
 src/components/status/status.vue               |  3 ---
 src/i18n/en.json                               |  2 --
 src/modules/statuses.js                        | 36 +-------------------------
 src/services/api/api.service.js                | 25 +-----------------
 9 files changed, 4 insertions(+), 82 deletions(-)

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

diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index c86f22bb..fe5b7018 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -21,9 +21,6 @@ const Notification = {
     },
     userProfileLink (user) {
       return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
-    },
-    dismiss () {
-      this.$store.dispatch('dismissNotifications', { id: this.notification.id })
     }
   },
   computed: {
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 6e8e9844..fa931fb6 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -3,8 +3,6 @@
     v-if="notification.type === 'mention'" 
     :compact="true" 
     :statusoid="notification.status"
-    isNotification="true"
-    @dismissNotification="dismiss"
   >
   </status>
   <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
@@ -40,9 +38,6 @@
             <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
           </router-link>
         </div>
-        <span class="button-icon" @click.prevent="dismiss">
-          <i class="button-icon icon-cancel delete-status"></i>
-        </span>
       </span>
       <div class="follow-text" v-if="notification.type === 'follow'">
         <router-link :to="userProfileLink(notification.action.user)">
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index 68004d54..e1c53c58 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -55,9 +55,6 @@ const Notifications = {
     markAsSeen () {
       this.$store.dispatch('markNotificationsAsSeen')
     },
-    clear () {
-      this.$store.dispatch('clearNotifications')
-    },
     fetchOlderNotifications () {
       const store = this.$store
       const credentials = store.state.users.currentUser.credentials
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 5a5f8ed8..05f51f6c 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -9,8 +9,7 @@
         <div @click.prevent class="loadmore-error alert error" v-if="error">
           {{$t('timeline.error_fetching')}}
         </div>
-        <!-- <button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button> -->
-        <button v-if="notifications.length" @click.prevent="clear" class="read-button">{{$t('notifications.clear')}}</button>
+        <button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button>
       </div>
       <div class="panel-body">
         <div v-for="notification in visibleNotifications" :key="notification.action.id + notification.id" class="notification" :class='{"unseen": !notification.seen}'>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 4cd26be3..9e18fe15 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -25,8 +25,7 @@ const Status = {
     'replies',
     'isPreview',
     'noHeading',
-    'inlineExpanded',
-    'isNotification'
+    'inlineExpanded'
   ],
   data () {
     return {
@@ -366,9 +365,6 @@ const Status = {
     setMedia () {
       const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
       return () => this.$store.dispatch('setMedia', attachments)
-    },
-    dismissNotification () {
-      this.$emit('dismissNotification')
     }
   },
   watch: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 28a44346..1f6d0325 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -52,9 +52,6 @@
                 <a :href="status.external_url" target="_blank" v-if="!status.is_local && !isPreview" class="source_url" title="Source">
                   <i class="button-icon icon-link-ext-alt"></i>
                 </a>
-                <span class="button-icon" v-if="isNotification" @click.prevent="dismissNotification">
-                  <i class="button-icon icon-cancel delete-status"></i>
-                </span>
                 <template v-if="expandable && !isPreview">
                   <a href="#" @click.prevent="toggleExpanded" title="Expand">
                     <i class="button-icon icon-plus-squared"></i>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 38abfaf5..01fe2fba 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -62,8 +62,6 @@
     "load_older": "Load older notifications",
     "notifications": "Notifications",
     "read": "Read!",
-    "clear": "Clear!",
-    "dismiss": "Dismiss!",
     "repeated_you": "repeated your status",
     "no_more_notifications": "No more notifications"
   },
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 4cc3907d..1eb1a3e3 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,4 +1,4 @@
-import { remove, slice, each, find, findIndex, maxBy, minBy, merge, first, last, isArray } from 'lodash'
+import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray } from 'lodash'
 import apiService from '../services/api/api.service.js'
 // import parse from '../services/status_parser/status_parser.js'
 
@@ -390,27 +390,6 @@ export const mutations = {
       notification.seen = true
     })
   },
-  clearNotifications (state) {
-    state.notifications.data = []
-    state.notifications.idStore = {}
-    state.notifications.maxId = 0
-    state.notifications.minId = 0
-  },
-  dismissNotifications (state, { id }) {
-    const { data } = state.notifications
-    const idx = findIndex(data, { id })
-
-    if (idx !== -1) {
-      const notification = data[idx]
-      data.splice(idx, 1)
-      delete state.notifications.idStore[id]
-      if (state.notifications.maxId === notification.id) {
-        state.notifications.maxId = data.length ? maxBy(data, 'id').id : 0
-      } else if (state.notifications.minId === notification.id) {
-        state.notifications.minId = data.length ? minBy(data, 'id').id : 0
-      }
-    }
-  },
   queueFlush (state, { timeline, id }) {
     state.timelines[timeline].flushMarker = id
   }
@@ -495,19 +474,6 @@ const statuses = {
         id: rootState.statuses.notifications.maxId,
         credentials: rootState.users.currentUser.credentials
       })
-    },
-    clearNotifications ({ rootState, commit }) {
-      commit('clearNotifications')
-      apiService.clearNotifications({
-        credentials: rootState.users.currentUser.credentials
-      })
-    },
-    dismissNotifications ({ rootState, commit }, { id }) {
-      commit('dismissNotifications', { id })
-      apiService.dismissNotifications({
-        id,
-        credentials: rootState.users.currentUser.credentials
-      })
     }
   },
   mutations
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 9d3139ce..03553d75 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -43,8 +43,6 @@ const SUGGESTIONS_URL = '/api/v1/suggestions'
 
 const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
 const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
-const MASTODON_USER_NOTIFICATIONS_CLEAR_URL = '/api/v1/notifications/clear'
-const MASTODON_USER_NOTIFICATIONS_DISMISS_URL = '/api/v1/notifications/dismiss'
 
 import { each, map } from 'lodash'
 import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
@@ -577,25 +575,6 @@ const markNotificationsAsSeen = ({id, credentials}) => {
   }).then((data) => data.json())
 }
 
-const clearNotifications = ({ credentials }) => {
-  return fetch(MASTODON_USER_NOTIFICATIONS_CLEAR_URL, {
-    headers: authHeaders(credentials),
-    method: 'POST'
-  }).then((data) => data.json())
-}
-
-const dismissNotifications = ({ id, credentials }) => {
-  const body = new FormData()
-
-  body.append('id', id)
-
-  return fetch(MASTODON_USER_NOTIFICATIONS_DISMISS_URL, {
-    body,
-    headers: authHeaders(credentials),
-    method: 'POST'
-  }).then((data) => data.json())
-}
-
 const apiService = {
   verifyCredentials,
   fetchTimeline,
@@ -636,9 +615,7 @@ const apiService = {
   approveUser,
   denyUser,
   suggestions,
-  markNotificationsAsSeen,
-  clearNotifications,
-  dismissNotifications
+  markNotificationsAsSeen
 }
 
 export default apiService
-- 
cgit v1.2.3-70-g09d2


From 13e727b5eebb60330bc87c7ccc70109683c5b963 Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Mon, 18 Mar 2019 14:42:09 -0400
Subject: #436: sync notification with timeline

---
 src/components/notification/notification.vue             | 16 ++++++++--------
 src/modules/statuses.js                                  |  5 +++++
 .../entity_normalizer/entity_normalizer.service.js       | 10 ++++++----
 3 files changed, 19 insertions(+), 12 deletions(-)

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

diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index fa931fb6..7d88c90b 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -6,15 +6,15 @@
   >
   </status>
   <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
-    <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
-      <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
+    <a class='avatar-container' :href="notification.action.account.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
+      <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.account.profile_image_url_original"/>
     </a>
     <div class='notification-right'>
-      <UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
+      <UserCard :user="notification.action.account" :rounded="true" :bordered="true" v-if="userExpanded"/>
       <span class="notification-details">
         <div class="name-and-action">
-          <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
-          <span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
+          <span class="username" v-if="!!notification.action.account.name_html" :title="'@'+notification.action.account.screen_name" v-html="notification.action.account.name_html"></span>
+          <span class="username" v-else :title="'@'+notification.action.account.screen_name">{{ notification.action.account.name }}</span>
           <span v-if="notification.type === 'like'">
             <i class="fa icon-star lit"></i>
             <small>{{$t('notifications.favorited_you')}}</small>
@@ -30,18 +30,18 @@
         </div>
         <div class="timeago" v-if="notification.type === 'follow'">
           <span class="faint">
-            <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
+            <timeago :since="notification.action.notified_at" :auto-update="240"></timeago>
           </span>
         </div>
         <div class="timeago" v-else>
           <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
-            <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
+            <timeago :since="notification.action.notified_at" :auto-update="240"></timeago>
           </router-link>
         </div>
       </span>
       <div class="follow-text" v-if="notification.type === 'follow'">
         <router-link :to="userProfileLink(notification.action.user)">
-          @{{notification.action.user.screen_name}}
+          @{{notification.action.account.screen_name}}
         </router-link>
       </div>
       <template v-else>
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index e18b0d1f..f14b8703 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -271,7 +271,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
 }
 
 const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => {
+  const allStatuses = state.allStatuses
+  const allStatusesObject = state.allStatusesObject
   each(notifications, (notification) => {
+    notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
+    notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
+
     // Only add a new notification if we don't have one for the same action
     if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
       state.notifications.maxId = notification.id > state.notifications.maxId
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 36a9508a..3bd42b94 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -180,8 +180,8 @@ export const parseStatus = (data) => {
     // Missing!! fix in UI?
     // output.in_reply_to_screen_name = ???
 
-    // Not exactly the same but works
-    output.statusnet_conversation_id = data.id
+    // It breaks the conversation when combined with notification
+    // output.statusnet_conversation_id = data.id
 
     if (output.type === 'retweet') {
       output.retweeted_status = parseStatus(data.reblog)
@@ -218,7 +218,6 @@ export const parseStatus = (data) => {
     output.in_reply_to_status_id = data.in_reply_to_status_id
     output.in_reply_to_user_id = data.in_reply_to_user_id
     output.in_reply_to_screen_name = data.in_reply_to_screen_name
-
     output.statusnet_conversation_id = data.statusnet_conversation_id
 
     if (output.type === 'retweet') {
@@ -266,6 +265,8 @@ export const parseFollow = (data) => {
   output.visibility = true
   output.created_at = new Date(data.created_at)
   output.user = parseUser(data.account)
+  output.notified_at = output.created_at
+  output.account = output.user
 
   return output
 }
@@ -286,7 +287,8 @@ export const parseNotification = (data) => {
       : parseStatus(data.status)
     if (data.type === 'reblog' || data.type === 'favourite') {
       output.status.user = parseUser(data.account)
-      output.status.created_at = new Date(data.created_at)
+      output.status.account = parseUser(data.account)
+      output.status.notified_at = new Date(data.created_at)
     }
     output.action = output.status // not sure
     output.from_profile = parseUser(data.account)
-- 
cgit v1.2.3-70-g09d2


From 0db66e2905a7394a5abf91c85de27cc95ced1c42 Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Sun, 31 Mar 2019 13:41:22 -0400
Subject: #436 - keep original naming

---
 src/components/notification/notification.vue               | 14 +++++++-------
 .../entity_normalizer/entity_normalizer.service.js         |  4 ----
 2 files changed, 7 insertions(+), 11 deletions(-)

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

diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 7d88c90b..282ee6c1 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -6,15 +6,15 @@
   >
   </status>
   <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
-    <a class='avatar-container' :href="notification.action.account.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
-      <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.account.profile_image_url_original"/>
+    <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
+      <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
     </a>
     <div class='notification-right'>
       <UserCard :user="notification.action.account" :rounded="true" :bordered="true" v-if="userExpanded"/>
       <span class="notification-details">
         <div class="name-and-action">
-          <span class="username" v-if="!!notification.action.account.name_html" :title="'@'+notification.action.account.screen_name" v-html="notification.action.account.name_html"></span>
-          <span class="username" v-else :title="'@'+notification.action.account.screen_name">{{ notification.action.account.name }}</span>
+          <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
+          <span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
           <span v-if="notification.type === 'like'">
             <i class="fa icon-star lit"></i>
             <small>{{$t('notifications.favorited_you')}}</small>
@@ -30,18 +30,18 @@
         </div>
         <div class="timeago" v-if="notification.type === 'follow'">
           <span class="faint">
-            <timeago :since="notification.action.notified_at" :auto-update="240"></timeago>
+            <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
           </span>
         </div>
         <div class="timeago" v-else>
           <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
-            <timeago :since="notification.action.notified_at" :auto-update="240"></timeago>
+            <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
           </router-link>
         </div>
       </span>
       <div class="follow-text" v-if="notification.type === 'follow'">
         <router-link :to="userProfileLink(notification.action.user)">
-          @{{notification.action.account.screen_name}}
+          @{{notification.action.user.screen_name}}
         </router-link>
       </div>
       <template v-else>
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 546ae1c6..9d00162f 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -267,8 +267,6 @@ export const parseFollow = (data) => {
   output.visibility = true
   output.created_at = new Date(data.created_at)
   output.user = parseUser(data.account)
-  output.notified_at = output.created_at
-  output.account = output.user
 
   return output
 }
@@ -289,8 +287,6 @@ export const parseNotification = (data) => {
       : parseStatus(data.status)
     if (data.type === 'reblog' || data.type === 'favourite') {
       output.status.user = parseUser(data.account)
-      output.status.account = parseUser(data.account)
-      output.status.notified_at = new Date(data.created_at)
     }
     output.action = output.status // not sure
     output.from_profile = parseUser(data.account)
-- 
cgit v1.2.3-70-g09d2


From ecacc933e78050d67fe1424513911e01308492ca Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Sun, 31 Mar 2019 13:47:23 -0400
Subject: #436 - keep original naming

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

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

diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 282ee6c1..fa931fb6 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -10,7 +10,7 @@
       <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
     </a>
     <div class='notification-right'>
-      <UserCard :user="notification.action.account" :rounded="true" :bordered="true" v-if="userExpanded"/>
+      <UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
       <span class="notification-details">
         <div class="name-and-action">
           <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
-- 
cgit v1.2.3-70-g09d2


From 06fd3a9c5f0c1da263c12317723c0d39e548c8a2 Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Sun, 31 Mar 2019 14:50:34 -0400
Subject: #436 - apply patch

---
 src/components/notification/notification.vue       | 24 +++++++++++-----------
 src/modules/users.js                               |  2 +-
 .../entity_normalizer/entity_normalizer.service.js |  5 +----
 3 files changed, 14 insertions(+), 17 deletions(-)

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

diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index fa931fb6..e5876cfa 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -1,20 +1,20 @@
 <template>
   <status 
-    v-if="notification.type === 'mention'" 
-    :compact="true" 
+    v-if="notification.type === 'mention'"
+    :compact="true"
     :statusoid="notification.status"
   >
   </status>
   <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
-    <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
-      <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
+    <a class='avatar-container' :href="notification.from_profile.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
+      <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.from_profile.profile_image_url_original" />
     </a>
     <div class='notification-right'>
-      <UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
+      <UserCard :user="notification.from_profile" :rounded="true" :bordered="true" v-if="userExpanded" />
       <span class="notification-details">
         <div class="name-and-action">
-          <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
-          <span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
+          <span class="username" v-if="!!notification.from_profile.name_html" :title="'@'+notification.from_profile.screen_name" v-html="notification.from_profile.name_html"></span>
+          <span class="username" v-else :title="'@'+notification.from_profile.screen_name">{{ notification.from_profile.name }}</span>
           <span v-if="notification.type === 'like'">
             <i class="fa icon-star lit"></i>
             <small>{{$t('notifications.favorited_you')}}</small>
@@ -30,22 +30,22 @@
         </div>
         <div class="timeago" v-if="notification.type === 'follow'">
           <span class="faint">
-            <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
+            <timeago :since="notification.created_at" :auto-update="240"></timeago>
           </span>
         </div>
         <div class="timeago" v-else>
           <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
-            <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
+            <timeago :since="notification.created_at" :auto-update="240"></timeago>
           </router-link>
         </div>
       </span>
       <div class="follow-text" v-if="notification.type === 'follow'">
-        <router-link :to="userProfileLink(notification.action.user)">
-          @{{notification.action.user.screen_name}}
+        <router-link :to="userProfileLink(notification.from_profile)">
+          @{{notification.from_profile.screen_name}}
         </router-link>
       </div>
       <template v-else>
-        <status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
+        <status class="faint" :compact="true" :statusoid="notification.action" :noHeading="true"></status>
       </template>
     </div>
   </div>
diff --git a/src/modules/users.js b/src/modules/users.js
index 1a507d31..2c525526 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -123,7 +123,7 @@ export const mutations = {
   },
   setUserForNotification (state, notification) {
     notification.action.user = state.usersObject[notification.action.user.id]
-    notification.from_profile = state.usersObject[notification.action.user.id]
+    notification.from_profile = state.usersObject[notification.from_profile.id]
   },
   setColor (state, { user: { id }, highlighted }) {
     const user = state.usersObject[id]
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 9d00162f..8e3cb066 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -285,10 +285,7 @@ export const parseNotification = (data) => {
     output.status = output.type === 'follow'
       ? parseFollow(data)
       : parseStatus(data.status)
-    if (data.type === 'reblog' || data.type === 'favourite') {
-      output.status.user = parseUser(data.account)
-    }
-    output.action = output.status // not sure
+    output.action = output.status // TODO: Refactor, this is unneeded
     output.from_profile = parseUser(data.account)
   } else {
     const parsedNotice = parseStatus(data.notice)
-- 
cgit v1.2.3-70-g09d2


From 6482201dc9e54d458bec48794260ffa1749c512b Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Mon, 1 Apr 2019 12:22:49 -0400
Subject: #436 - sync notification & timeline users

---
 src/components/notification/notification.js  | 4 ++--
 src/components/notification/notification.vue | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

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

diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 95fd312e..4ad7de36 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -33,13 +33,13 @@ const Notification = {
       return highlightStyle(highlight[user.screen_name])
     },
     userInStore () {
-      return this.$store.getters.findUser(this.notification.action.user.id)
+      return this.$store.getters.findUser(this.notification.from_profile.id)
     },
     user () {
       if (this.userInStore) {
         return this.userInStore
       }
-      return {}
+      return this.notification.from_profile
     }
   }
 }
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index bf952479..77ad89e1 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -10,7 +10,7 @@
       <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.from_profile.profile_image_url_original" />
     </a>
     <div class='notification-right'>
-      <UserCard :user="notification.from_profile" :rounded="true" :bordered="true" v-if="userExpanded" />
+      <UserCard :user="user" :rounded="true" :bordered="true" v-if="userExpanded" />
       <span class="notification-details">
         <div class="name-and-action">
           <span class="username" v-if="!!notification.from_profile.name_html" :title="'@'+notification.from_profile.screen_name" v-html="notification.from_profile.name_html"></span>
-- 
cgit v1.2.3-70-g09d2


From 6b6878bde06b375b1e715a3557f153acc73a8af0 Mon Sep 17 00:00:00 2001
From: eugenijm <eugenijm@protonmail.com>
Date: Mon, 18 Feb 2019 17:49:32 +0300
Subject: Added moderation menu

---
 package.json                                       |   2 +
 src/App.scss                                       |   8 ++
 src/boot/after_store.js                            |   1 +
 src/components/delete_button/delete_button.js      |   6 +-
 src/components/dialog_modal/dialog_modal.js        |  14 ++
 src/components/dialog_modal/dialog_modal.vue       |  92 ++++++++++++
 .../moderation_tools/moderation_tools.js           | 106 ++++++++++++++
 .../moderation_tools/moderation_tools.vue          | 158 +++++++++++++++++++++
 src/components/notification/notification.js        |   3 +
 src/components/notification/notification.vue       |   2 +-
 src/components/popper/popper.scss                  |  70 +++++++++
 src/components/user_card/user_card.js              |  13 +-
 src/components/user_card/user_card.vue             |   2 +
 src/components/user_profile/user_profile.js        |   4 +-
 src/i18n/en.json                                   |  24 +++-
 src/i18n/ru.json                                   |  24 +++-
 src/modules/statuses.js                            |  10 ++
 src/modules/users.js                               |  22 +++
 src/services/api/api.service.js                    |  90 ++++++++++++
 .../backend_interactor_service.js                  |  30 ++++
 .../entity_normalizer/entity_normalizer.service.js |  19 ++-
 yarn.lock                                          |  10 ++
 22 files changed, 697 insertions(+), 13 deletions(-)
 create mode 100644 src/components/dialog_modal/dialog_modal.js
 create mode 100644 src/components/dialog_modal/dialog_modal.vue
 create mode 100644 src/components/moderation_tools/moderation_tools.js
 create mode 100644 src/components/moderation_tools/moderation_tools.vue
 create mode 100644 src/components/popper/popper.scss

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

diff --git a/package.json b/package.json
index 03228133..c80e0f63 100644
--- a/package.json
+++ b/package.json
@@ -24,12 +24,14 @@
     "node-sass": "^3.10.1",
     "object-path": "^0.11.3",
     "phoenix": "^1.3.0",
+    "popper.js": "^1.14.7",
     "sanitize-html": "^1.13.0",
     "sass-loader": "^4.0.2",
     "vue": "^2.5.13",
     "vue-chat-scroll": "^1.2.1",
     "vue-compose": "^0.7.1",
     "vue-i18n": "^7.3.2",
+    "vue-popperjs": "^2.0.3",
     "vue-router": "^3.0.1",
     "vue-template-compiler": "^2.3.4",
     "vue-timeago": "^3.1.2",
diff --git a/src/App.scss b/src/App.scss
index 5fc0dd27..b1c65ade 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -101,6 +101,14 @@ button {
     background-color: $fallback--bg;
     background-color: var(--bg, $fallback--bg)
   }
+
+  &.danger {
+    // TODO: add better color variable
+    color: $fallback--text;
+    color: var(--alertErrorPanelText, $fallback--text);
+    background-color: $fallback--alertError;
+    background-color: var(--alertError, $fallback--alertError);
+  }
 }
 
 label.select {
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 862a534d..bb593026 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -210,6 +210,7 @@ const getNodeInfo = async ({ store }) => {
 
       const frontendVersion = window.___pleromafe_commit_hash
       store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
+      store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
     } else {
       throw (res)
     }
diff --git a/src/components/delete_button/delete_button.js b/src/components/delete_button/delete_button.js
index f2920666..22f24625 100644
--- a/src/components/delete_button/delete_button.js
+++ b/src/components/delete_button/delete_button.js
@@ -10,7 +10,11 @@ const DeleteButton = {
   },
   computed: {
     currentUser () { return this.$store.state.users.currentUser },
-    canDelete () { return this.currentUser && this.currentUser.rights.delete_others_notice || this.status.user.id === this.currentUser.id }
+    canDelete () {
+      if (!this.currentUser) { return }
+      const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
+      return superuser || this.status.user.id === this.currentUser.id
+    }
   }
 }
 
diff --git a/src/components/dialog_modal/dialog_modal.js b/src/components/dialog_modal/dialog_modal.js
new file mode 100644
index 00000000..f14e3fe9
--- /dev/null
+++ b/src/components/dialog_modal/dialog_modal.js
@@ -0,0 +1,14 @@
+const DialogModal = {
+  props: {
+    darkOverlay: {
+      default: true,
+      type: Boolean
+    },
+    onCancel: {
+      default: () => {},
+      type: Function
+    }
+  }
+}
+
+export default DialogModal
diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue
new file mode 100644
index 00000000..7621fb20
--- /dev/null
+++ b/src/components/dialog_modal/dialog_modal.vue
@@ -0,0 +1,92 @@
+<template>
+  <span v-bind:class="{ 'dark-overlay': darkOverlay }" @click.self.stop='onCancel()'>
+    <div class="dialog-modal panel panel-default" @click.stop=''>
+      <div class="panel-heading dialog-modal-heading">
+        <div class="title">
+          <slot name="header"></slot>
+        </div>
+      </div>
+      <div class="dialog-modal-content">
+        <slot name="default"></slot>
+      </div>
+      <div class="dialog-modal-footer user-interactions panel-footer">
+        <slot name="footer"></slot>
+      </div>
+    </div>
+  </span>
+</template>
+
+<script src="./dialog_modal.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+// TODO: unify with other modals.
+.dark-overlay {
+  &::before {
+    bottom: 0;
+    content: " ";
+    display: block;
+    cursor: default;
+    left: 0;
+    position: fixed;
+    right: 0;
+    top: 0;
+    background: rgba(27,31,35,.5);
+    z-index: 99;
+  }
+}
+
+.dialog-modal.panel {
+  top: 0;
+  left: 50%;
+  max-height: 80vh;
+  max-width: 90vw;
+  margin: 15vh auto;
+  position: fixed;
+  transform: translateX(-50%);
+  z-index: 999;
+  cursor: default;
+  display: block;
+  background-color: $fallback--bg;
+  background-color: var(--bg, $fallback--bg);
+
+  .dialog-modal-heading {
+    padding: .5em .5em;
+    margin-right: auto;
+    margin-bottom: 0;
+    white-space: nowrap;
+    color: var(--panelText);
+    background-color: $fallback--fg;
+    background-color: var(--panel, $fallback--fg);
+
+    .title {
+      margin-bottom: 0;
+    }
+  }
+
+  .dialog-modal-content {
+    margin: 0;
+    padding: 1rem 1rem;
+    background-color: $fallback--lightBg;
+    background-color: var(--lightBg, $fallback--lightBg);
+    white-space: normal;
+  }
+
+  .dialog-modal-footer {
+    margin: 0;
+    padding: .5em .5em;
+    background-color: $fallback--lightBg;
+    background-color: var(--lightBg, $fallback--lightBg);
+    border-top: 1px solid $fallback--bg;
+    border-top: 1px solid var(--bg, $fallback--bg);
+    justify-content: flex-end;
+
+    button {
+      width: auto;
+      margin-left: .5rem;
+    }
+  }
+}
+
+</style>
diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js
new file mode 100644
index 00000000..3eedeaa1
--- /dev/null
+++ b/src/components/moderation_tools/moderation_tools.js
@@ -0,0 +1,106 @@
+import DialogModal from '../dialog_modal/dialog_modal.vue'
+import Popper from 'vue-popperjs/src/component/popper.js.vue'
+
+const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
+const STRIP_MEDIA = 'mrf_tag:media-strip'
+const FORCE_UNLISTED = 'mrf_tag:force-unlisted'
+const DISABLE_REMOTE_SUBSCRIPTION = 'mrf_tag:disable-remote-subscription'
+const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription'
+const SANDBOX = 'mrf_tag:sandbox'
+const QUARANTINE = 'mrf_tag:quarantine'
+
+const ModerationTools = {
+  props: [
+    'user'
+  ],
+  data () {
+    return {
+      showDropDown: false,
+      tags: {
+        FORCE_NSFW,
+        STRIP_MEDIA,
+        FORCE_UNLISTED,
+        DISABLE_REMOTE_SUBSCRIPTION,
+        DISABLE_ANY_SUBSCRIPTION,
+        SANDBOX,
+        QUARANTINE
+      },
+      showDeleteUserDialog: false
+    }
+  },
+  components: {
+    DialogModal,
+    Popper
+  },
+  computed: {
+    tagsSet () {
+      return new Set(this.user.tags)
+    },
+    hasTagPolicy () {
+      return this.$store.state.instance.tagPolicyAvailable
+    }
+  },
+  methods: {
+    toggleMenu () {
+      this.showDropDown = !this.showDropDown
+    },
+    hasTag (tagName) {
+      return this.tagsSet.has(tagName)
+    },
+    toggleTag (tag) {
+      const store = this.$store
+      if (this.tagsSet.has(tag)) {
+        store.state.api.backendInteractor.untagUser(this.user, tag).then(response => {
+          if (!response.ok) { return }
+          store.commit('untagUser', {user: this.user, tag})
+        })
+      } else {
+        store.state.api.backendInteractor.tagUser(this.user, tag).then(response => {
+          if (!response.ok) { return }
+          store.commit('tagUser', {user: this.user, tag})
+        })
+      }
+    },
+    toggleRight (right) {
+      const store = this.$store
+      if (this.user.rights[right]) {
+        store.state.api.backendInteractor.deleteRight(this.user, right).then(response => {
+          if (!response.ok) { return }
+          store.commit('updateRight', {user: this.user, right: right, value: false})
+        })
+      } else {
+        store.state.api.backendInteractor.addRight(this.user, right).then(response => {
+          if (!response.ok) { return }
+          store.commit('updateRight', {user: this.user, right: right, value: true})
+        })
+      }
+    },
+    toggleActivationStatus () {
+      const store = this.$store
+      const status = !!this.user.deactivated
+      store.state.api.backendInteractor.setActivationStatus(this.user, status).then(response => {
+        if (!response.ok) { return }
+        store.commit('updateActivationStatus', {user: this.user, status: status})
+      })
+    },
+    deleteUserDialog (show) {
+      this.showDeleteUserDialog = show
+    },
+    deleteUser () {
+      const store = this.$store
+      const user = this.user
+      const {id, name} = user
+      store.state.api.backendInteractor.deleteUser(user)
+        .then(e => {
+          this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
+          const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
+          const isTargetUser = this.$route.params.name === name || this.$route.params.id === id
+          if (isProfile && isTargetUser) {
+            window.history.back()
+          }
+        })
+    }
+  }
+}
+
+export default ModerationTools
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
new file mode 100644
index 00000000..c24a2280
--- /dev/null
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -0,0 +1,158 @@
+<template>
+<div class='block' style='position: relative'>
+  <Popper
+    trigger="click"
+    @hide='showDropDown = false'
+    append-to-body
+    :options="{
+      placement: 'bottom-end',
+      modifiers: {
+        arrow: { enabled: true },
+        offset: { offset: '0, 5px' },
+      }
+    }">
+    <div class="popper-wrapper">
+      <div class="dropdown-menu">
+        <span v-if='user.is_local'>
+          <button class="dropdown-item" @click='toggleRight("admin")'>
+            {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
+          </button>
+          <button class="dropdown-item" @click='toggleRight("moderator")'>
+            {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
+          </button>
+          <div role="separator" class="dropdown-divider"></div>
+        </span>
+        <button class="dropdown-item" @click='toggleActivationStatus()'>
+          {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
+        </button>
+        <button class="dropdown-item" @click='deleteUserDialog(true)'>
+          {{ $t('user_card.admin_menu.delete_account') }}
+        </button>
+        <div role="separator" class="dropdown-divider" v-if='hasTagPolicy'></div>
+        <span v-if='hasTagPolicy'>
+          <button class="dropdown-item" @click='toggleTag(tags.FORCE_NSFW)'>
+            {{ $t('user_card.admin_menu.force_nsfw') }}
+            <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"></span>
+          </button>
+          <button class="dropdown-item" @click='toggleTag(tags.STRIP_MEDIA)'>
+            {{ $t('user_card.admin_menu.strip_media') }}
+            <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"></span>
+          </button>
+          <button class="dropdown-item" @click='toggleTag(tags.FORCE_UNLISTED)'>
+            {{ $t('user_card.admin_menu.force_unlisted') }}
+            <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"></span>
+          </button>
+          <button class="dropdown-item" @click='toggleTag(tags.SANDBOX)'>
+            {{ $t('user_card.admin_menu.sandbox') }}
+            <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"></span>
+          </button>
+          <button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)'>
+            {{ $t('user_card.admin_menu.disable_remote_subscription') }}
+            <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"></span>
+          </button>
+          <button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)'>
+            {{ $t('user_card.admin_menu.disable_any_subscription') }}
+            <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"></span>
+          </button>
+          <button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.QUARANTINE)'>
+            {{ $t('user_card.admin_menu.quarantine') }}
+            <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"></span>
+          </button>
+        </span>
+      </div>
+    </div>
+    <button slot="reference" v-bind:class="{ pressed: showDropDown }" @click='toggleMenu'>
+      {{ $t('user_card.admin_menu.moderation') }}
+    </button>
+  </Popper>
+  <DialogModal v-if="showDeleteUserDialog" :onCancel='deleteUserDialog.bind(this, false)'>
+    <span slot="header">{{ $t('user_card.admin_menu.delete_user') }}</span>
+    <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
+    <span slot="footer">
+      <button @click='deleteUserDialog(false)'>
+        {{ $t('general.cancel') }}
+      </button>
+      <button class="danger" @click='deleteUser()'>
+        {{ $t('user_card.admin_menu.delete_user') }}
+      </button>
+    </span>
+  </DialogModal>
+</div>
+</template>
+
+<script src="./moderation_tools.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+@import '../popper/popper.scss';
+
+.dropdown-menu {
+  display: block;
+  padding: .5rem 0;
+  font-size: 1rem;
+  text-align: left;
+  list-style: none;
+  max-width: 100vw;
+  z-index: 10;
+  box-shadow: 1px 1px 4px rgba(0,0,0,.6);
+  box-shadow: var(--panelShadow);
+  border: none;
+  border-radius: $fallback--btnRadius;
+  border-radius: var(--btnRadius, $fallback--btnRadius);
+  background-color: $fallback--bg;
+  background-color: var(--bg, $fallback--bg);
+
+  .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%;
+
+    &:hover {
+      // TODO: improve the look on breeze themes
+      background-color: $fallback--fg;
+      background-color: var(--btn, $fallback--fg);
+      box-shadow: none;
+    }
+  }
+}
+
+.menu-checkbox {
+  float: right;
+  min-width: 22px;
+  max-width: 22px;
+  min-height: 22px;
+  max-height: 22px;
+  line-height: 22px;
+  text-align: center;
+  border-radius: 0px;
+  background-color: $fallback--fg;
+  background-color: var(--input, $fallback--fg);
+  box-shadow: 0px 0px 2px black inset;
+  box-shadow: var(--inputShadow);
+
+  &.menu-checkbox-checked::after {
+    content: '✔';
+  }
+}
+
+</style>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 42a48f3f..6aa6101f 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -21,6 +21,9 @@ const Notification = {
     },
     userProfileLink (user) {
       return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
+    },
+    getUser (notification) {
+      return this.$store.state.users.usersObject[notification.action.user.id]
     }
   },
   computed: {
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 8f532747..cac335ce 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -5,7 +5,7 @@
       <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
     </a>
     <div class='notification-right'>
-      <UserCard :user="user" :rounded="true" :bordered="true" v-if="userExpanded"/>
+      <UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded"/>
       <span class="notification-details">
         <div class="name-and-action">
           <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
new file mode 100644
index 00000000..0c30d625
--- /dev/null
+++ b/src/components/popper/popper.scss
@@ -0,0 +1,70 @@
+@import '../../_variables.scss';
+
+.popper-wrapper {
+  z-index: 8;
+}
+
+.popper-wrapper .popper__arrow {
+  width: 0;
+  height: 0;
+  border-style: solid;
+  position: absolute;
+  margin: 5px;
+}
+
+.popper-wrapper[x-placement^="top"] {
+  margin-bottom: 5px;
+}
+
+.popper-wrapper[x-placement^="top"] .popper__arrow {
+  border-width: 5px 5px 0 5px;
+  border-color: $fallback--bg transparent transparent transparent;
+  border-color: var(--bg, $fallback--bg) transparent transparent transparent;
+  bottom: -5px;
+  left: calc(50% - 5px);
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.popper-wrapper[x-placement^="bottom"] {
+  margin-top: 5px;
+}
+
+.popper-wrapper[x-placement^="bottom"] .popper__arrow {
+  border-width: 0 5px 5px 5px;
+  border-color: transparent transparent $fallback--bg transparent;
+  border-color: transparent transparent var(--bg, $fallback--bg) transparent;
+  top: -5px;
+  left: calc(50% - 5px);
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.popper-wrapper[x-placement^="right"] {
+  margin-left: 5px;
+}
+
+.popper-wrapper[x-placement^="right"] .popper__arrow {
+  border-width: 5px 5px 5px 0;
+  border-color: transparent $fallback--bg transparent transparent;
+  border-color: transparent var(--bg, $fallback--bg) transparent transparent;
+  left: -5px;
+  top: calc(50% - 5px);
+  margin-left: 0;
+  margin-right: 0;
+}
+
+.popper-wrapper[x-placement^="left"] {
+  margin-right: 5px;
+}
+
+.popper-wrapper[x-placement^="left"] .popper__arrow {
+  border-width: 5px 0 5px 5px;
+  border-color: transparent transparent transparent $fallback--bg;
+  border-color: transparent transparent transparent var(--bg, $fallback--bg);
+  right: -5px;
+  top: calc(50% - 5px);
+  margin-left: 0;
+  margin-right: 0;
+}
+
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 197c61d5..1a100de3 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -1,5 +1,6 @@
 import UserAvatar from '../user_avatar/user_avatar.vue'
 import RemoteFollow from '../remote_follow/remote_follow.vue'
+import ModerationTools from '../moderation_tools/moderation_tools.vue'
 import { hex2rgb } from '../../services/color_convert/color_convert.js'
 import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -93,15 +94,17 @@ export default {
       }
     },
     visibleRole () {
-      const validRole = (this.user.role === 'admin' || this.user.role === 'moderator')
-      const showRole = this.isOtherUser || this.user.show_role
-
-      return validRole && showRole && this.user.role
+      const rights = this.user.rights
+      if (!rights) { return }
+      const validRole = rights.admin || rights.moderator
+      const roleTitle = rights.admin ? 'admin' : 'moderator'
+      return validRole && roleTitle
     }
   },
   components: {
     UserAvatar,
-    RemoteFollow
+    RemoteFollow,
+    ModerationTools
   },
   methods: {
     followUser () {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 3259d1c5..b5c7fa24 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -99,6 +99,8 @@
             </button>
           </span>
         </div>
+        <ModerationTools :user='user' v-if='loggedIn.role === "admin"'>
+        </ModerationTools>
       </div>
     </div>
   </div>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 1df06fe6..540de955 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -3,6 +3,7 @@ import get from 'lodash/get'
 import UserCard from '../user_card/user_card.vue'
 import FollowCard from '../follow_card/follow_card.vue'
 import Timeline from '../timeline/timeline.vue'
+import ModerationTools from '../moderation_tools/moderation_tools.vue'
 import withLoadMore from '../../hocs/with_load_more/with_load_more'
 import withList from '../../hocs/with_list/with_list'
 
@@ -155,7 +156,8 @@ const UserProfile = {
     UserCard,
     Timeline,
     FollowerList,
-    FriendList
+    FriendList,
+    ModerationTools
   }
 }
 
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 026546cc..0bb58734 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -22,7 +22,8 @@
     "generic_error": "An error occured",
     "optional": "optional",
     "show_more": "Show more",
-    "show_less": "Show less"
+    "show_less": "Show less",
+    "cancel": "Cancel"
   },
   "image_cropper": {
     "crop_picture": "Crop picture",
@@ -402,7 +403,26 @@
     "block_progress": "Blocking...",
     "unmute": "Unmute",
     "unmute_progress": "Unmuting...",
-    "mute_progress": "Muting..."
+    "mute_progress": "Muting...",
+    "admin_menu": {
+      "moderation": "Moderation",
+      "grant_admin": "Grant Admin",
+      "revoke_admin": "Revoke Admin",
+      "grant_moderator": "Grant Moderator",
+      "revoke_moderator": "Revoke Moderator",
+      "activate_account": "Activate account",
+      "deactivate_account": "Deactivate account",
+      "delete_account": "Delete account",
+      "force_nsfw": "Mark all posts as NSFW",
+      "strip_media": "Remove media from posts",
+      "force_unlisted": "Force posts to be unlisted",
+      "sandbox": "Force posts to be followers-only",
+      "disable_remote_subscription": "Disallow following user from remote instances",
+      "disable_any_subscription": "Disallow following user at all",
+      "quarantine": "Disallow user posts from federating",
+      "delete_user": "Delete user",
+      "delete_user_confirmation": "Are you absolutely sure? This action cannot be undone."
+    }
   },
   "user_profile": {
     "timeline_title": "User Timeline",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 89aa43f4..5450f154 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -8,7 +8,8 @@
   },
   "general": {
     "apply": "Применить",
-    "submit": "Отправить"
+    "submit": "Отправить",
+    "cancel": "Отмена"
   },
   "login": {
     "login": "Войти",
@@ -311,7 +312,26 @@
     "muted": "Игнорирую",
     "per_day": "в день",
     "remote_follow": "Читать удалённо",
-    "statuses": "Статусы"
+    "statuses": "Статусы",
+    "admin_menu": {
+      "moderation": "Опции модератора",
+      "grant_admin": "Сделать администратором",
+      "revoke_admin": "Забрать права администратора",
+      "grant_moderator": "Сделать модератором",
+      "revoke_moderator": "Забрать права модератора",
+      "activate_account": "Активировать аккаунт",
+      "deactivate_account": "Деактивировать аккаунт",
+      "delete_account": "Удалить аккаунт",
+      "force_nsfw": "Отмечать посты пользователя как NSFW",
+      "strip_media": "Убирать вложения из постов пользователя",
+      "force_unlisted": "Не добавлять посты в публичные ленты",
+      "sandbox": "Посты доступны только для подписчиков",
+      "disable_remote_subscription": "Запретить подписываться с удаленных серверов",
+      "disable_any_subscription": "Запретить подписываться на пользователя",
+      "quarantine": "Не федерировать посты пользователя",
+      "delete_user": "Удалить пользователя",
+      "delete_user_confirmation": "Вы уверены? Это действие нельзя отменить."
+    }
   },
   "user_profile": {
     "timeline_title": "Лента пользователя"
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 8e0203e3..4007b0fc 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -378,6 +378,13 @@ export const mutations = {
     const newStatus = state.allStatusesObject[status.id]
     newStatus.deleted = true
   },
+  setManyDeleted (state, condition) {
+    Object.values(state.allStatusesObject).forEach(status => {
+      if (condition(status)) {
+        status.deleted = true
+      }
+    })
+  },
   setLoading (state, { timeline, value }) {
     state.timelines[timeline].loading = value
   },
@@ -438,6 +445,9 @@ const statuses = {
       commit('setDeleted', { status })
       apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
     },
+    markStatusesAsDeleted ({ commit }, condition) {
+      commit('setManyDeleted', condition)
+    },
     favorite ({ rootState, commit }, status) {
       // Optimistic favoriting...
       commit('setFavorited', { status, value: true })
diff --git a/src/modules/users.js b/src/modules/users.js
index 1a507d31..5839574b 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -37,6 +37,28 @@ export const mutations = {
     const user = state.usersObject[id]
     set(user, 'muted', muted)
   },
+  tagUser (state, { user: { id }, tag }) {
+    const user = state.usersObject[id]
+    const tags = user.tags || []
+    const newTags = tags.concat([tag])
+    set(user, 'tags', newTags)
+  },
+  untagUser (state, { user: { id }, tag }) {
+    const user = state.usersObject[id]
+    const tags = user.tags || []
+    const newTags = tags.filter(t => t !== tag)
+    set(user, 'tags', newTags)
+  },
+  updateRight (state, { user: { id }, right, value }) {
+    const user = state.usersObject[id]
+    let newRights = user.rights
+    newRights[right] = value
+    set(user, 'rights', newRights)
+  },
+  updateActivationStatus (state, { user: { id }, status }) {
+    const user = state.usersObject[id]
+    set(user, 'deactivated', !status)
+  },
   setCurrentUser (state, user) {
     state.lastLoginName = user.screen_name
     state.currentUser = merge(state.currentUser || {}, user)
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 030c2f5e..bd331062 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -16,6 +16,10 @@ const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
 const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests'
 const APPROVE_USER_URL = '/api/pleroma/friendships/approve'
 const DENY_USER_URL = '/api/pleroma/friendships/deny'
+const TAG_USER_URL = '/api/pleroma/admin/users/tag'
+const PERMISSION_GROUP_URL = '/api/pleroma/admin/permission_group'
+const ACTIVATION_STATUS_URL = '/api/pleroma/admin/activation_status'
+const ADMIN_USER_URL = '/api/pleroma/admin/user'
 const SUGGESTIONS_URL = '/api/v1/suggestions'
 
 const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
@@ -352,6 +356,86 @@ const fetchStatus = ({id, credentials}) => {
     .then((data) => parseStatus(data))
 }
 
+const tagUser = ({tag, credentials, ...options}) => {
+  const screenName = options.screen_name
+  const form = {
+    nicknames: [screenName],
+    tags: [tag]
+  }
+
+  const headers = authHeaders(credentials)
+  headers['Content-Type'] = 'application/json'
+
+  return fetch(TAG_USER_URL, {
+    method: 'PUT',
+    headers: headers,
+    body: JSON.stringify(form)
+  })
+}
+
+const untagUser = ({tag, credentials, ...options}) => {
+  const screenName = options.screen_name
+  const body = {
+    nicknames: [screenName],
+    tags: [tag]
+  }
+
+  const headers = authHeaders(credentials)
+  headers['Content-Type'] = 'application/json'
+
+  return fetch(TAG_USER_URL, {
+    method: 'DELETE',
+    headers: headers,
+    body: JSON.stringify(body)
+  })
+}
+
+const addRight = ({right, credentials, ...user}) => {
+  const screenName = user.screen_name
+
+  return fetch(`${PERMISSION_GROUP_URL}/${screenName}/${right}`, {
+    method: 'POST',
+    headers: authHeaders(credentials),
+    body: {}
+  })
+}
+
+const deleteRight = ({right, credentials, ...user}) => {
+  const screenName = user.screen_name
+
+  return fetch(`${PERMISSION_GROUP_URL}/${screenName}/${right}`, {
+    method: 'DELETE',
+    headers: authHeaders(credentials),
+    body: {}
+  })
+}
+
+const setActivationStatus = ({status, credentials, ...user}) => {
+  const screenName = user.screen_name
+  const body = {
+    status: status
+  }
+
+  const headers = authHeaders(credentials)
+  headers['Content-Type'] = 'application/json'
+
+  return fetch(`${ACTIVATION_STATUS_URL}/${screenName}.json`, {
+    method: 'PUT',
+    headers: headers,
+    body: JSON.stringify(body)
+  })
+}
+
+const deleteUser = ({credentials, ...user}) => {
+  const screenName = user.screen_name
+  const headers = authHeaders(credentials)
+
+  return fetch(`${ADMIN_USER_URL}.json?nickname=${screenName}`, {
+    method: 'DELETE',
+    headers: headers
+  })
+}
+
 const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => {
   const timelineUrls = {
     public: MASTODON_PUBLIC_TIMELINE,
@@ -666,6 +750,12 @@ const apiService = {
   fetchBlocks,
   fetchOAuthTokens,
   revokeOAuthToken,
+  tagUser,
+  untagUser,
+  deleteUser,
+  addRight,
+  deleteRight,
+  setActivationStatus,
   register,
   getCaptcha,
   updateAvatar,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 71e78d2f..badcbb06 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -62,6 +62,30 @@ const backendInteractorService = (credentials) => {
     return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag})
   }
 
+  const tagUser = ({screen_name}, tag) => {
+    return apiService.tagUser({screen_name, tag, credentials})
+  }
+
+  const untagUser = ({screen_name}, tag) => {
+    return apiService.untagUser({screen_name, tag, credentials})
+  }
+
+  const addRight = ({screen_name}, right) => {
+    return apiService.addRight({screen_name, right, credentials})
+  }
+
+  const deleteRight = ({screen_name}, right) => {
+    return apiService.deleteRight({screen_name, right, credentials})
+  }
+
+  const setActivationStatus = ({screen_name}, status) => {
+    return apiService.setActivationStatus({screen_name, status, credentials})
+  }
+
+  const deleteUser = ({screen_name}) => {
+    return apiService.deleteUser({screen_name, credentials})
+  }
+
   const fetchMutes = () => apiService.fetchMutes({credentials})
   const muteUser = (id) => apiService.muteUser({credentials, id})
   const unmuteUser = (id) => apiService.unmuteUser({credentials, id})
@@ -104,6 +128,12 @@ const backendInteractorService = (credentials) => {
     fetchBlocks,
     fetchOAuthTokens,
     revokeOAuthToken,
+    tagUser,
+    untagUser,
+    addRight,
+    deleteRight,
+    deleteUser,
+    setActivationStatus,
     register,
     getCaptcha,
     updateAvatar,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index ea57e6b2..8aa4b352 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -67,6 +67,11 @@ export const parseUser = (data) => {
         output.statusnet_blocking = relationship.blocking
         output.muted = relationship.muting
       }
+
+      output.rights = {
+        moderator: data.pleroma.is_moderator,
+        admin: data.pleroma.is_admin
+      }
     }
 
     // Missing, trying to recover
@@ -103,7 +108,12 @@ export const parseUser = (data) => {
 
     // QVITTER ONLY FOR NOW
     // Really only applies to logged in user, really.. I THINK
-    output.rights = data.rights
+    if (data.rights) {
+      output.rights = {
+        moderator: data.rights.delete_others_notice,
+        admin: data.rights.admin
+      }
+    }
     output.no_rich_text = data.no_rich_text
     output.default_scope = data.default_scope
     output.hide_follows = data.hide_follows
@@ -125,6 +135,13 @@ export const parseUser = (data) => {
     output.follow_request_count = data.pleroma.follow_request_count
   }
 
+  if (data.pleroma) {
+    output.tags = data.pleroma.tags
+    output.deactivated = data.pleroma.deactivated
+  }
+
+  output.tags = output.tags || []
+
   return output
 }
 
diff --git a/yarn.lock b/yarn.lock
index 0fe45479..58007622 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4831,6 +4831,10 @@ pluralize@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
 
+popper.js@^1.14.7:
+  version "1.14.7"
+  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e"
+
 posix-character-classes@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -6457,6 +6461,12 @@ vue-loader@^11.1.0:
     vue-style-loader "^2.0.0"
     vue-template-es2015-compiler "^1.2.2"
 
+vue-popperjs@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/vue-popperjs/-/vue-popperjs-2.0.3.tgz#7c446d0ba7c63170ccb33a02669d0df4efc3d8cd"
+  dependencies:
+    popper.js "^1.14.7"
+
 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

