From 56d1232588cac825b960782868e2ea3148371f33 Mon Sep 17 00:00:00 2001
From: Ilja <ilja@ilja.space>
Date: Sat, 6 Aug 2022 22:33:38 +0200
Subject: Show moderation drop down menu and items based on privileges

There's a seperator between certain blocks of items. I show/hide the seperator together with the block under it.
When a block with a seperator is at the top, the seperator doesn't show, keeping a consistent look with seperators only between blocks.

I also hide granting roles for deactivated accounts because that doesn't make much sense to me.

For the rest the items are hidden when you're not privileged. When there's no privileges that show items, the menu isn't shown either.
---
 src/components/user_card/user_card.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index ace89c51..043c14d3 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -261,7 +261,7 @@
             </button>
           </div>
           <ModerationTools
-            v-if="loggedIn.role === &quot;admin&quot;"
+            v-if="showModerationMenu"
             :user="user"
           />
         </div>
-- 
cgit v1.2.3-70-g09d2


From 0a79a747730bb4a10eb2544412dab68a10602240 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 29 Aug 2022 18:46:41 -0400
Subject: Use dedicated indicator for non-ascii domain names

---
 src/components/basic_user_card/basic_user_card.js  |  4 ++-
 src/components/basic_user_card/basic_user_card.vue |  8 ++---
 src/components/emoji_input/emoji_input.js          |  4 ++-
 src/components/emoji_input/emoji_input.vue         | 16 ++++++++-
 src/components/emoji_input/suggestor.js            | 11 ++++---
 src/components/mention_link/mention_link.js        |  2 ++
 src/components/mention_link/mention_link.vue       |  3 ++
 src/components/notification/notification.js        |  4 ++-
 src/components/notification/notification.vue       | 21 ++++++------
 src/components/status/status.js                    |  4 ++-
 src/components/status/status.vue                   | 16 ++++-----
 .../unicode_domain_indicator.vue                   | 26 +++++++++++++++
 src/components/user_card/user_card.js              |  4 ++-
 src/components/user_card/user_card.vue             |  9 ++---
 src/components/user_link/user_link.vue             | 38 ++++++++++++++++++++++
 .../user_list_popover/user_list_popover.js         |  2 ++
 .../user_list_popover/user_list_popover.vue        |  2 +-
 .../user_reporting_modal/user_reporting_modal.js   |  4 ++-
 .../user_reporting_modal/user_reporting_modal.vue  | 10 ++++--
 src/i18n/en.json                                   |  3 ++
 .../entity_normalizer/entity_normalizer.service.js |  6 ++--
 .../entity_normalizer/entity_normalizer.spec.js    |  3 +-
 22 files changed, 151 insertions(+), 49 deletions(-)
 create mode 100644 src/components/unicode_domain_indicator/unicode_domain_indicator.vue
 create mode 100644 src/components/user_link/user_link.vue

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

diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js
index 8b1a2c38..31de2d75 100644
--- a/src/components/basic_user_card/basic_user_card.js
+++ b/src/components/basic_user_card/basic_user_card.js
@@ -1,5 +1,6 @@
 import UserPopover from '../user_popover/user_popover.vue'
 import UserAvatar from '../user_avatar/user_avatar.vue'
+import UserLink from '../user_link/user_link.vue'
 import RichContent from 'src/components/rich_content/rich_content.jsx'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 
@@ -10,7 +11,8 @@ const BasicUserCard = {
   components: {
     UserPopover,
     UserAvatar,
-    RichContent
+    RichContent,
+    UserLink
   },
   methods: {
     userProfileLink (user) {
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 9cca7840..418de926 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -30,12 +30,10 @@
         />
       </div>
       <div>
-        <router-link
+        <user-link
           class="basic-user-card-screen-name"
-          :to="userProfileLink(user)"
-        >
-          @{{ user.screen_name_ui }}
-        </router-link>
+          :user="user"
+        />
       </div>
       <slot />
     </div>
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 5ba3907f..b664d6b3 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -1,5 +1,6 @@
 import Completion from '../../services/completion/completion.js'
 import EmojiPicker from '../emoji_picker/emoji_picker.vue'
+import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
 import { take } from 'lodash'
 import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
 
@@ -120,7 +121,8 @@ const EmojiInput = {
     }
   },
   components: {
-    EmojiPicker
+    EmojiPicker,
+    UnicodeDomainIndicator
   },
   computed: {
     padEmoji () {
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 7d95ab7e..81b81913 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -50,7 +50,21 @@
             <span v-else>{{ suggestion.replacement }}</span>
           </span>
           <div class="label">
-            <span class="displayText">{{ suggestion.displayText }}</span>
+            <span
+              v-if="suggestion.user"
+              class="displayText"
+            >
+              {{ suggestion.displayText }}<UnicodeDomainIndicator
+                :user="suggestion.user"
+                :at="false"
+              />
+            </span>
+            <span
+              v-if="!suggestion.user"
+              class="displayText"
+            >
+              {{ suggestion.displayText }}
+            </span>
             <span class="detailText">{{ suggestion.detailText }}</span>
           </div>
         </div>
diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index e8efbd1e..0ddb4d68 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -116,11 +116,12 @@ export const suggestUsers = ({ dispatch, state }) => {
 
       return diff + nameAlphabetically + screenNameAlphabetically
       /* eslint-disable camelcase */
-    }).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
-      displayText: screen_name_ui,
-      detailText: name,
-      imageUrl: profile_image_url_original,
-      replacement: '@' + screen_name + ' '
+    }).map((user) => ({
+      user,
+      displayText: user.screen_name_ui,
+      detailText: user.name,
+      imageUrl: user.profile_image_url_original,
+      replacement: '@' + user.screen_name + ' '
     }))
     /* eslint-enable camelcase */
 
diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 4a74fbe2..6515bd11 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
 import { mapGetters, mapState } from 'vuex'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
 import UserAvatar from '../user_avatar/user_avatar.vue'
+import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
 import { defineAsyncComponent } from 'vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
@@ -16,6 +17,7 @@ const MentionLink = {
   name: 'MentionLink',
   components: {
     UserAvatar,
+    UnicodeDomainIndicator,
     UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
   },
   props: {
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 3af502ef..869a3257 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -47,6 +47,9 @@
             class="serverName"
             :class="{ '-faded': shouldFadeDomain }"
             v-html="'@' + serverName"
+          /><UnicodeDomainIndicator
+            v-if="shouldShowFullUserName"
+            :user="user"
           />
           </span>
           <span
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index f60a1a6a..ddba560e 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
 import UserCard from '../user_card/user_card.vue'
 import Timeago from '../timeago/timeago.vue'
 import Report from '../report/report.vue'
+import UserLink from '../user_link/user_link.vue'
 import RichContent from 'src/components/rich_content/rich_content.jsx'
 import UserPopover from '../user_popover/user_popover.vue'
 import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
@@ -50,7 +51,8 @@ const Notification = {
     Status,
     Report,
     RichContent,
-    UserPopover
+    UserPopover,
+    UserLink
   },
   methods: {
     toggleUserExpanded () {
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 6a5417b3..26b174ff 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -11,9 +11,10 @@
       class="Notification container -muted"
     >
       <small>
-        <router-link :to="userProfileLink">
-          {{ notification.from_profile.screen_name_ui }}
-        </router-link>
+        <user-link
+          :user="notification.from_profile"
+          :at="false"
+        />
       </small>
       <button
         class="button-unstyled unmute"
@@ -174,12 +175,10 @@
           v-if="notification.type === 'follow' || notification.type === 'follow_request'"
           class="follow-text"
         >
-          <router-link
-            :to="userProfileLink"
+          <user-link
             class="follow-name"
-          >
-            @{{ notification.from_profile.screen_name_ui }}
-          </router-link>
+            :user="notification.from_profile"
+          />
           <div
             v-if="notification.type === 'follow_request'"
             style="white-space: nowrap;"
@@ -210,9 +209,9 @@
           v-else-if="notification.type === 'move'"
           class="move-text"
         >
-          <router-link :to="targetUserProfileLink">
-            @{{ notification.target.screen_name_ui }}
-          </router-link>
+          <user-link
+            :user="notification.target"
+          />
         </div>
         <Report
           v-else-if="notification.type === 'pleroma:report'"
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 384063a7..19356664 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -13,6 +13,7 @@ import StatusPopover from '../status_popover/status_popover.vue'
 import UserPopover from '../user_popover/user_popover.vue'
 import UserListPopover from '../user_list_popover/user_list_popover.vue'
 import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
+import UserLink from '../user_link/user_link.vue'
 import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
 import MentionLink from 'src/components/mention_link/mention_link.vue'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -115,7 +116,8 @@ const Status = {
     RichContent,
     MentionLink,
     MentionsLine,
-    UserPopover
+    UserPopover,
+    UserLink
   },
   props: [
     'statusoid',
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 967a966c..91f5fffa 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -25,9 +25,10 @@
             class="fa-scale-110 fa-old-padding repeat-icon"
             icon="retweet"
           />
-          <router-link :to="userProfileLink">
-            {{ status.user.screen_name_ui }}
-          </router-link>
+          <user-link
+            :user="status.user"
+            :at="false"
+          />
         </small>
         <small
           v-if="showReasonMutedThread"
@@ -164,13 +165,12 @@
                 >
                   {{ status.user.name }}
                 </h4>
-                <router-link
+                <user-link
                   class="account-name"
                   :title="status.user.screen_name_ui"
-                  :to="userProfileLink"
-                >
-                  {{ status.user.screen_name_ui }}
-                </router-link>
+                  :user="status.user"
+                  :at="false"
+                />
                 <img
                   v-if="!!(status.user && status.user.favicon)"
                   class="status-favicon"
diff --git a/src/components/unicode_domain_indicator/unicode_domain_indicator.vue b/src/components/unicode_domain_indicator/unicode_domain_indicator.vue
new file mode 100644
index 00000000..8f35245f
--- /dev/null
+++ b/src/components/unicode_domain_indicator/unicode_domain_indicator.vue
@@ -0,0 +1,26 @@
+<template>
+  <FAIcon
+    v-if="user && user.screen_name_ui_contains_non_ascii"
+    icon="code"
+    :title="$t('unicode_domain_indicator.tooltip')"
+  />
+</template>
+
+<script>
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+  faCode
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+  faCode
+)
+
+const UnicodeDomainIndicator = {
+  props: {
+    user: Object
+  }
+}
+
+export default UnicodeDomainIndicator
+</script>
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 4c81e2c7..37458ba2 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue'
 import ModerationTools from '../moderation_tools/moderation_tools.vue'
 import AccountActions from '../account_actions/account_actions.vue'
 import Select from '../select/select.vue'
+import UserLink from '../user_link/user_link.vue'
 import RichContent from 'src/components/rich_content/rich_content.jsx'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 import { mapGetters } from 'vuex'
@@ -134,7 +135,8 @@ export default {
     ProgressButton,
     FollowButton,
     Select,
-    RichContent
+    RichContent,
+    UserLink
   },
   methods: {
     muteUser () {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index ace89c51..b84ff27d 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -106,13 +106,10 @@
               </button>
             </div>
             <div class="bottom-line">
-              <router-link
+              <user-link
                 class="user-screen-name"
-                :title="user.screen_name_ui"
-                :to="userProfileLink(user)"
-              >
-                @{{ user.screen_name_ui }}
-              </router-link>
+                :user="user"
+              />
               <template v-if="!hideBio">
                 <span
                   v-if="user.deactivated"
diff --git a/src/components/user_link/user_link.vue b/src/components/user_link/user_link.vue
new file mode 100644
index 00000000..efd96e12
--- /dev/null
+++ b/src/components/user_link/user_link.vue
@@ -0,0 +1,38 @@
+<template>
+  <router-link
+    :title="user.screen_name_ui"
+    :to="userProfileLink(user)"
+  >
+    {{ at ? '@' : '' }}{{ user.screen_name_ui }}<UnicodeDomainIndicator
+      :user="user"
+    />
+  </router-link>
+</template>
+
+<script>
+import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
+import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+
+const UserLink = {
+  props: {
+    user: Object,
+    at: {
+      type: Boolean,
+      default: true
+    }
+  },
+  components: {
+    UnicodeDomainIndicator
+  },
+  methods: {
+    userProfileLink (user) {
+      return generateProfileLink(
+        user.id, user.screen_name,
+        this.$store.state.instance.restrictedNicknames
+      )
+    }
+  }
+}
+
+export default UserLink
+</script>
diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js
index e24eb9f7..046e0abd 100644
--- a/src/components/user_list_popover/user_list_popover.js
+++ b/src/components/user_list_popover/user_list_popover.js
@@ -1,5 +1,6 @@
 import { defineAsyncComponent } from 'vue'
 import RichContent from 'src/components/rich_content/rich_content.jsx'
+import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
 
 import { library } from '@fortawesome/fontawesome-svg-core'
 import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
@@ -15,6 +16,7 @@ const UserListPopover = {
   ],
   components: {
     RichContent,
+    UnicodeDomainIndicator,
     Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
     UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue'))
   },
diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue
index a3ce54c3..635dc7f6 100644
--- a/src/components/user_list_popover/user_list_popover.vue
+++ b/src/components/user_list_popover/user_list_popover.vue
@@ -29,7 +29,7 @@
                 :emoji="user.emoji"
               />
               <!-- eslint-enable vue/no-v-html -->
-              <span class="user-list-screen-name">{{ user.screen_name_ui }}</span>
+              <span class="user-list-screen-name">{{ user.screen_name_ui }}</span><UnicodeDomainIndicator :user="user" />
             </div>
           </div>
         </template>
diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js
index 85ffc661..67fde084 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.js
+++ b/src/components/user_reporting_modal/user_reporting_modal.js
@@ -2,13 +2,15 @@ import Status from '../status/status.vue'
 import List from '../list/list.vue'
 import Checkbox from '../checkbox/checkbox.vue'
 import Modal from '../modal/modal.vue'
+import UserLink from '../user_link/user_link.vue'
 
 const UserReportingModal = {
   components: {
     Status,
     List,
     Checkbox,
-    Modal
+    Modal,
+    UserLink
   },
   data () {
     return {
diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue
index 429a66e2..8c42ab7b 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.vue
+++ b/src/components/user_reporting_modal/user_reporting_modal.vue
@@ -5,9 +5,13 @@
   >
     <div class="user-reporting-panel panel">
       <div class="panel-heading">
-        <div class="title">
-          {{ $t('user_reporting.title', [user.screen_name_ui]) }}
-        </div>
+        <i18n-t
+          tag="div"
+          keypath="user_reporting.title"
+          class="title"
+        >
+          <UserLink :user="user" />
+        </i18n-t>
       </div>
       <div class="panel-body">
         <div class="user-reporting-panel-left">
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 91722d9a..4ce56678 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1006,5 +1006,8 @@
     "update_changelog": "For more details on what's changed, see {theFullChangelog}.",
     "update_changelog_here": "the full changelog",
     "art_by": "Art by {linkToArtist}"
+  },
+  "unicode_domain_indicator": {
+    "tooltip": "This domain contains non-ascii characters."
   }
 }
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index e9cbcfe6..ce316832 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -214,12 +214,14 @@ export const parseUser = (data) => {
   output.screen_name_ui = output.screen_name
   if (output.screen_name && output.screen_name.includes('@')) {
     const parts = output.screen_name.split('@')
-    let unicodeDomain = punycode.toUnicode(parts[1])
+    const unicodeDomain = punycode.toUnicode(parts[1])
     if (unicodeDomain !== parts[1]) {
       // Add some identifier so users can potentially spot spoofing attempts:
       // lain.com and xn--lin-6cd.com would appear identical otherwise.
-      unicodeDomain = '🌏' + unicodeDomain
+      output.screen_name_ui_contains_non_ascii = true
       output.screen_name_ui = [parts[0], unicodeDomain].join('@')
+    } else {
+      output.screen_name_ui_contains_non_ascii = false
     }
   }
 
diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
index 98bb05a8..3923596b 100644
--- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -269,7 +269,8 @@ describe('API Entities normalizer', () => {
     it('converts IDN to unicode and marks it as internatonal', () => {
       const user = makeMockUserMasto({ acct: 'lain@xn--lin-6cd.com' })
 
-      expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@🌏lаin.com')
+      expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@lаin.com')
+      expect(parseUser(user)).to.have.property('screen_name_ui_contains_non_ascii').that.equal(true)
     })
   })
 
-- 
cgit v1.2.3-70-g09d2


From 1101305ffb72bacd68698bfbe6e961953a9043de Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 20 Aug 2022 13:02:27 -0400
Subject: Add ui for note editing

---
 src/components/user_card/user_card.js   |  4 +-
 src/components/user_card/user_card.scss |  4 ++
 src/components/user_card/user_card.vue  |  6 +++
 src/components/user_note/user_note.js   | 31 ++++++++++++
 src/components/user_note/user_note.vue  | 88 +++++++++++++++++++++++++++++++++
 src/i18n/en.json                        |  7 ++-
 6 files changed, 138 insertions(+), 2 deletions(-)
 create mode 100644 src/components/user_note/user_note.js
 create mode 100644 src/components/user_note/user_note.vue

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

diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 8b64a07e..56e978bf 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -4,6 +4,7 @@ import ProgressButton from '../progress_button/progress_button.vue'
 import FollowButton from '../follow_button/follow_button.vue'
 import ModerationTools from '../moderation_tools/moderation_tools.vue'
 import AccountActions from '../account_actions/account_actions.vue'
+import UserNote from '../user_note/user_note.vue'
 import Select from '../select/select.vue'
 import UserLink from '../user_link/user_link.vue'
 import RichContent from 'src/components/rich_content/rich_content.jsx'
@@ -140,7 +141,8 @@ export default {
     FollowButton,
     Select,
     RichContent,
-    UserLink
+    UserLink,
+    UserNote
   },
   methods: {
     muteUser () {
diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss
index a0bbc6a6..cdb8cb57 100644
--- a/src/components/user_card/user_card.scss
+++ b/src/components/user_card/user_card.scss
@@ -315,6 +315,10 @@
       margin: 0;
     }
   }
+
+  .user-note {
+    margin: 0 .75em .6em 0;
+  }
 }
 
 .sidebar .edit-profile-button {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 897d89f9..83dfed4e 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -268,6 +268,12 @@
         >
           <RemoteFollow :user="user" />
         </div>
+        <UserNote
+          v-if="loggedIn && isOtherUser"
+          :user="user"
+          :relationship="relationship"
+          :editing="isEditingNote"
+        />
       </div>
     </div>
     <div
diff --git a/src/components/user_note/user_note.js b/src/components/user_note/user_note.js
new file mode 100644
index 00000000..6a479800
--- /dev/null
+++ b/src/components/user_note/user_note.js
@@ -0,0 +1,31 @@
+const UserNote = {
+  props: {
+    user: Object,
+    relationship: Object
+  },
+  data () {
+    return {
+      localNote: '',
+      editing: false
+    }
+  },
+  computed: {
+    shouldShow () {
+      return this.relationship.note || this.editing
+    }
+  },
+  methods: {
+    startEditing () {
+      this.localNote = this.relationship.note
+      this.editing = true
+    },
+    cancelEditing () {
+      this.editing = false
+    },
+    finalizeEditing () {
+      this.editing = false
+    }
+  }
+}
+
+export default UserNote
diff --git a/src/components/user_note/user_note.vue b/src/components/user_note/user_note.vue
new file mode 100644
index 00000000..46db1ca7
--- /dev/null
+++ b/src/components/user_note/user_note.vue
@@ -0,0 +1,88 @@
+<template>
+  <div
+    class="user-note"
+  >
+    <div class="heading">
+      <span>{{ $t('user_card.note') }}</span>
+      <div class="buttons">
+        <button
+          v-show="!editing"
+          class="button-default btn"
+          @click="startEditing"
+        >
+          {{ $t('user_card.edit_note') }}
+        </button>
+        <button
+          v-show="editing"
+          class="button-default btn"
+          @click="finalizeEditing"
+        >
+          {{ $t('user_card.edit_note_apply') }}
+        </button>
+        <button
+          v-show="editing"
+          class="button-default btn"
+          @click="cancelEditing"
+        >
+          {{ $t('user_card.edit_note_cancel') }}
+        </button>
+      </div>
+    </div>
+    <input
+      v-show="editing"
+      class="note-text"
+      type="string"
+      :model="localNote"
+    >
+    <span
+      v-show="!editing"
+      class="note-text"
+      :class="{ '-blank': !relationship.note }"
+    >
+      {{ relationship.note || $t('user_card.note_blank') }}
+    </span>
+  </div>
+</template>
+
+<script src="./user_note.js"></script>
+
+<style lang="scss">
+@import '../../variables';
+
+.user-note {
+  display: flex;
+  flex-direction: column;
+
+  .heading {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 0.75em;
+
+    .btn {
+      min-width: 95px;
+    }
+
+    .buttons {
+      display: flex;
+      flex-direction: row;
+      justify-content: right;
+
+      .btn {
+        margin-left: 0.5em;
+      }
+    }
+  }
+
+  .note-text {
+    line-height: 2;
+    align-self: stretch;
+  }
+
+  .note-text.-blank {
+    font-style: italic;
+    color: var(--faint, $fallback--faint);
+  }
+}
+</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 5793c3c8..187d4dcd 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -957,7 +957,12 @@
       "solid": "Solid bg",
       "striped": "Striped bg",
       "side": "Side stripe"
-    }
+    },
+    "note": "Note",
+    "note_blank": "(None)",
+    "edit_note": "Edit note",
+    "edit_note_apply": "Apply",
+    "edit_note_cancel": "Cancel"
   },
   "user_profile": {
     "timeline_title": "User timeline",
-- 
cgit v1.2.3-70-g09d2


From 9ab3f27f72c74a61763534eb53b92a3172bb5f5d Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 20 Aug 2022 13:20:59 -0400
Subject: Test if server supports note

---
 src/components/user_card/user_card.js  | 3 +++
 src/components/user_card/user_card.vue | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

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

diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 56e978bf..65140d4e 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -130,6 +130,9 @@ export default {
       const privileges = this.loggedIn.privileges
       return this.loggedIn.role === 'admin' || privileges.includes('users_manage_activation_state') || privileges.includes('users_delete') || privileges.includes('users_manage_tags')
     },
+    supportsNote () {
+      return 'note' in this.relationship
+    },
     ...mapGetters(['mergedConfig'])
   },
   components: {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 83dfed4e..88434c59 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -269,7 +269,7 @@
           <RemoteFollow :user="user" />
         </div>
         <UserNote
-          v-if="loggedIn && isOtherUser"
+          v-if="loggedIn && isOtherUser && supportsNote"
           :user="user"
           :relationship="relationship"
           :editing="isEditingNote"
-- 
cgit v1.2.3-70-g09d2


From 2e2512019230f299c2c9eae18afec3c8218d2697 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Wed, 7 Sep 2022 18:46:18 -0400
Subject: Make notes editable only on user profile

---
 src/components/user_card/user_card.js        | 6 +++++-
 src/components/user_card/user_card.vue       | 4 ++--
 src/components/user_note/user_note.js        | 3 ++-
 src/components/user_note/user_note.vue       | 3 +--
 src/components/user_profile/user_profile.vue | 1 +
 5 files changed, 11 insertions(+), 6 deletions(-)

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

diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 65140d4e..67879307 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -40,7 +40,8 @@ export default {
     'rounded',
     'bordered',
     'avatarAction', // default - open profile, 'zoom' - zoom, function - call function
-    'onClose'
+    'onClose',
+    'hasNoteEditor'
   ],
   data () {
     return {
@@ -130,6 +131,9 @@ export default {
       const privileges = this.loggedIn.privileges
       return this.loggedIn.role === 'admin' || privileges.includes('users_manage_activation_state') || privileges.includes('users_delete') || privileges.includes('users_manage_tags')
     },
+    hasNote () {
+      return this.relationship.note
+    },
     supportsNote () {
       return 'note' in this.relationship
     },
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 88434c59..349c7cb1 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -269,10 +269,10 @@
           <RemoteFollow :user="user" />
         </div>
         <UserNote
-          v-if="loggedIn && isOtherUser && supportsNote"
+          v-if="loggedIn && isOtherUser && (hasNote || (hasNoteEditor && supportsNote))"
           :user="user"
           :relationship="relationship"
-          :editing="isEditingNote"
+          :editable="hasNoteEditor"
         />
       </div>
     </div>
diff --git a/src/components/user_note/user_note.js b/src/components/user_note/user_note.js
index dc5b8e71..830b2e59 100644
--- a/src/components/user_note/user_note.js
+++ b/src/components/user_note/user_note.js
@@ -1,7 +1,8 @@
 const UserNote = {
   props: {
     user: Object,
-    relationship: Object
+    relationship: Object,
+    editable: Boolean
   },
   data () {
     return {
diff --git a/src/components/user_note/user_note.vue b/src/components/user_note/user_note.vue
index 328e2350..4286e017 100644
--- a/src/components/user_note/user_note.vue
+++ b/src/components/user_note/user_note.vue
@@ -6,7 +6,7 @@
       <span>{{ $t('user_card.note') }}</span>
       <div class="buttons">
         <button
-          v-show="!editing"
+          v-show="!editing && editable"
           class="button-default btn"
           @click="startEditing"
         >
@@ -77,7 +77,6 @@
   }
 
   .note-text {
-    line-height: 2;
     align-self: stretch;
   }
 
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index d0da2b5b..d5e8d230 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -10,6 +10,7 @@
         :selected="timeline.viewing"
         avatar-action="zoom"
         rounded="top"
+        :has-note-editor="true"
       />
       <div
         v-if="user.fields_html && user.fields_html.length > 0"
-- 
cgit v1.2.3-70-g09d2

