Bladeren bron

Merge remote-tracking branch 'origin/develop' into develop

Weblate 2 jaren geleden
bovenliggende
commit
71d9bbb56b
53 gewijzigde bestanden met toevoegingen van 838 en 516 verwijderingen
  1. 11 1
      CHANGELOG.md
  2. 12 12
      package.json
  3. 1 2
      src/App.js
  4. 9 2
      src/App.scss
  5. 2 1
      src/App.vue
  6. 3 0
      src/boot/after_store.js
  7. 2 10
      src/components/basic_user_card/basic_user_card.js
  8. 14 17
      src/components/basic_user_card/basic_user_card.vue
  9. 3 5
      src/components/chat_message/chat_message.js
  10. 4 4
      src/components/chat_message/chat_message.vue
  11. 3 7
      src/components/chat_title/chat_title.js
  12. 3 3
      src/components/chat_title/chat_title.vue
  13. 1 0
      src/components/desktop_nav/desktop_nav.scss
  14. 1 1
      src/components/desktop_nav/desktop_nav.vue
  15. 2 1
      src/components/emoji_picker/emoji_picker.scss
  16. 3 0
      src/components/extra_buttons/extra_buttons.js
  17. 26 24
      src/components/extra_buttons/extra_buttons.vue
  18. 1 1
      src/components/global_notice_list/global_notice_list.vue
  19. 1 1
      src/components/media_modal/media_modal.vue
  20. 21 3
      src/components/mention_link/mention_link.js
  21. 15 14
      src/components/mention_link/mention_link.scss
  22. 38 46
      src/components/mention_link/mention_link.vue
  23. 1 2
      src/components/mentions_line/mentions_line.vue
  24. 4 2
      src/components/mobile_nav/mobile_nav.vue
  25. 4 1
      src/components/modal/modal.vue
  26. 2 0
      src/components/nav_panel/nav_panel.vue
  27. 3 1
      src/components/notification/notification.js
  28. 13 12
      src/components/notification/notification.vue
  29. 10 0
      src/components/notifications/notifications.js
  30. 1 1
      src/components/poll/poll_form.vue
  31. 173 48
      src/components/popover/popover.js
  32. 28 20
      src/components/popover/popover.vue
  33. 3 2
      src/components/react_button/react_button.vue
  34. 5 5
      src/components/settings_modal/helpers/modified_indicator.vue
  35. 5 5
      src/components/settings_modal/helpers/server_side_indicator.vue
  36. 18 12
      src/components/settings_modal/tabs/general_tab.vue
  37. 1 1
      src/components/shout_panel/shout_panel.vue
  38. 1 1
      src/components/side_drawer/side_drawer.vue
  39. 3 3
      src/components/status/status.js
  40. 14 18
      src/components/status/status.vue
  41. 7 0
      src/components/status_popover/status_popover.js
  42. 2 2
      src/components/status_popover/status_popover.vue
  43. 18 32
      src/components/timeline_menu/timeline_menu.vue
  44. 24 6
      src/components/user_card/user_card.js
  45. 38 13
      src/components/user_card/user_card.scss
  46. 41 9
      src/components/user_card/user_card.vue
  47. 23 0
      src/components/user_popover/user_popover.js
  48. 33 0
      src/components/user_popover/user_popover.vue
  49. 1 1
      src/components/user_profile/user_profile.vue
  50. 3 1
      src/i18n/en.json
  51. 2 0
      src/modules/config.js
  52. 12 16
      test/unit/specs/components/rich_content.spec.js
  53. 169 147
      yarn.lock

+ 11 - 1
CHANGELOG.md

@@ -16,17 +16,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Attachments are ALWAYS in same order as user uploaded, no more "videos first"
 - Attachment description is prefilled with backend-provided default when uploading
 - Proper visual feedback that next image is loading when browsing
+- UI no longer lags when switching between mobile and desktop mode
+- Popovers no longer constrained by DOM hierarchy, shouldn't be cut off by anything
+- "Always show mobile button" is working now
 
 ### Changed
+- Using Vue 3 now
 - (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)
 - User highlight background now also covers the `@`
 - Reverted back to textual `@`, svg version is opt-in.
-- Settings window has been throughly rearranged to make make more sense and make navication settings easier.
+- Settings window has been thoroughly rearranged to make more sense and make navigation settings easier.
 - Uploaded attachments are uniform with displayed attachments
 - Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues)
 - Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post.
+- Slight width/spacing adjustments
+- More sizing stuff is font-size dependent now
+- Scrollbars are styled/colorized now
+- Scrollbars are toggleable (for stuff that didn't have visible scrollbars before) (opt-in)
 
 ### Added
+- 3 column mode: only enables when there's space for it (opt-out, customizable)
 - Options to show domains in mentions
 - Option to show user avatars in mention links (opt-in)
 - Option to disable the tooltip for mentions
@@ -37,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Media modal now also displays description and counter position in gallery (i.e. 1/5)
 - Ability to rearrange order of attachments when uploading
 - Enabled users to zoom and pan images in media viewer with mouse and touch
+- Timelines/panels and conversations have sticky headers now
 - Added frontend ui for account migration
 
 

+ 12 - 12
package.json

@@ -18,15 +18,15 @@
   "dependencies": {
     "@babel/runtime": "7.18.9",
     "@chenfengyuan/vue-qrcode": "2.0.0",
-    "@fortawesome/fontawesome-svg-core": "6.1.1",
-    "@fortawesome/free-regular-svg-icons": "6.1.1",
-    "@fortawesome/free-solid-svg-icons": "6.1.1",
+    "@fortawesome/fontawesome-svg-core": "6.1.2",
+    "@fortawesome/free-regular-svg-icons": "6.1.2",
+    "@fortawesome/free-solid-svg-icons": "6.1.2",
     "@fortawesome/vue-fontawesome": "3.0.1",
     "@kazvmoe-infra/pinch-zoom-element": "1.2.0",
     "@ruffle-rs/ruffle": "^0.1.0-nightly.2022.7.12",
     "@vuelidate/core": "2.0.0-alpha.43",
-    "@vuelidate/validators": "2.0.0-alpha.30",
-    "body-scroll-lock": "2.7.1",
+    "@vuelidate/validators": "2.0.0-alpha.31",
+    "body-scroll-lock": "3.1.5",
     "chromatism": "3.0.0",
     "click-outside-vue3": "4.0.1",
     "cropperjs": "1.5.12",
@@ -39,10 +39,10 @@
     "punycode.js": "2.1.0",
     "qrcode": "1",
     "utf8": "^3.0.0",
-    "vue": "^3.2.31",
+    "vue": "3.2.37",
     "vue-i18n": "9.2.0-beta.40",
     "vue-router": "4.1.2",
-    "vue-template-compiler": "2.6.11",
+    "vue-template-compiler": "2.7.8",
     "vuex": "4.0.2"
   },
   "devDependencies": {
@@ -54,15 +54,15 @@
     "@ungap/event-target": "0.2.3",
     "@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
     "@vue/babel-plugin-jsx": "1.1.1",
-    "@vue/compiler-sfc": "^3.1.0",
-    "@vue/test-utils": "2.0.0-rc.17",
+    "@vue/compiler-sfc": "3.2.37",
+    "@vue/test-utils": "2.0.2",
     "autoprefixer": "6.7.7",
     "babel-eslint": "7.2.3",
     "babel-loader": "8.2.5",
     "babel-plugin-lodash": "3.3.4",
     "chai": "3.5.0",
     "chalk": "1.1.3",
-    "chromedriver": "87.0.7",
+    "chromedriver": "103.0.0",
     "connect-history-api-fallback": "1.6.0",
     "copy-webpack-plugin": "6.4.1",
     "cross-spawn": "4.0.2",
@@ -105,7 +105,7 @@
     "ora": "0.4.1",
     "postcss-loader": "3.0.0",
     "raw-loader": "0.5.1",
-    "sass": "1.53.0",
+    "sass": "1.54.0",
     "sass-loader": "7.3.1",
     "selenium-server": "2.53.1",
     "semver": "5.7.1",
@@ -118,7 +118,7 @@
     "stylelint-rscss": "0.4.0",
     "url-loader": "1.1.2",
     "vue-loader": "^16.0.0",
-    "vue-style-loader": "4.1.2",
+    "vue-style-loader": "4.1.3",
     "webpack": "4.46.0",
     "webpack-dev-middleware": "3.7.3",
     "webpack-hot-middleware": "2.25.1",

+ 1 - 2
src/App.js

@@ -4,7 +4,6 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
 import FeaturesPanel from './components/features_panel/features_panel.vue'
 import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
 import ShoutPanel from './components/shout_panel/shout_panel.vue'
-import SettingsModal from './components/settings_modal/settings_modal.vue'
 import MediaModal from './components/media_modal/media_modal.vue'
 import SideDrawer from './components/side_drawer/side_drawer.vue'
 import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
@@ -32,7 +31,7 @@ export default {
     MobilePostStatusButton,
     MobileNav,
     DesktopNav,
-    SettingsModal,
+    SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
     UserReportingModal,
     PostStatusModal,
     GlobalNoticeList

+ 9 - 2
src/App.scss

@@ -4,6 +4,13 @@
 :root {
   --navbar-height: 3.5rem;
   --post-line-height: 1.4;
+  // Z-Index stuff
+  --ZI_media_modal: 90000;
+  --ZI_modals_popovers: 85000;
+  --ZI_modals: 80000;
+  --ZI_navbar_popovers: 75000;
+  --ZI_navbar: 70000;
+  --ZI_popovers: 60000;
 }
 
 html {
@@ -117,7 +124,7 @@ i[class*=icon-],
 }
 
 nav {
-  z-index: 1000;
+  z-index: var(--ZI_navbar);
   color: var(--topBarText);
   background-color: $fallback--fg;
   background-color: var(--topBar, $fallback--fg);
@@ -828,7 +835,7 @@ option {
 // Vue transitions
 .fade-enter-active,
 .fade-leave-active {
-  transition: opacity 0.2s;
+  transition: opacity 0.3s;
 }
 
 .fade-enter-from,

+ 2 - 1
src/App.vue

@@ -42,7 +42,7 @@
       </div>
       <div id="notifs-column" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }"/>
     </div>
-    <media-modal />
+    <MediaModal />
     <shout-panel
       v-if="currentUser && shout && !hideShoutbox"
       :floating="true"
@@ -55,6 +55,7 @@
     <SettingsModal />
     <div id="modal" />
     <GlobalNoticeList />
+    <div id="popovers" />
   </div>
 </template>
 

+ 3 - 0
src/boot/after_store.js

@@ -396,6 +396,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
   app.component('FAIcon', FontAwesomeIcon)
   app.component('FALayers', FontAwesomeLayers)
 
+  // remove after vue 3.3
+  app.config.unwrapInjectedRef = true
+
   app.mount('#app')
 
   return app

+ 2 - 10
src/components/basic_user_card/basic_user_card.js

@@ -1,4 +1,4 @@
-import UserCard from '../user_card/user_card.vue'
+import UserPopover from '../user_popover/user_popover.vue'
 import UserAvatar from '../user_avatar/user_avatar.vue'
 import RichContent from 'src/components/rich_content/rich_content.jsx'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -7,20 +7,12 @@ const BasicUserCard = {
   props: [
     'user'
   ],
-  data () {
-    return {
-      userExpanded: false
-    }
-  },
   components: {
-    UserCard,
+    UserPopover,
     UserAvatar,
     RichContent
   },
   methods: {
-    toggleUserExpanded () {
-      this.userExpanded = !this.userExpanded
-    },
     userProfileLink (user) {
       return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
     }

+ 14 - 17
src/components/basic_user_card/basic_user_card.vue

@@ -1,24 +1,19 @@
 <template>
   <div class="basic-user-card">
-    <router-link :to="userProfileLink(user)">
-      <UserAvatar
-        class="avatar"
-        :user="user"
-        @click.prevent="toggleUserExpanded"
-      />
+    <router-link @click.prevent :to="userProfileLink(user)">
+      <UserPopover
+        :userId="user.id"
+        :overlayCenters="true"
+        overlayCentersSelector=".avatar"
+      >
+        <UserAvatar
+          class="user-avatar avatar"
+          :user="user"
+          @click.prevent
+        />
+      </UserPopover>
     </router-link>
     <div
-      v-if="userExpanded"
-      class="basic-user-card-expanded-content"
-    >
-      <UserCard
-        :user-id="user.id"
-        :rounded="true"
-        :bordered="true"
-      />
-    </div>
-    <div
-      v-else
       class="basic-user-card-collapsed-content"
     >
       <div
@@ -53,6 +48,8 @@
   margin: 0;
   padding: 0.6em 1em;
 
+   --emoji-size: 14px;
+
   &-collapsed-content {
     margin-left: 0.7em;
     text-align: left;

+ 3 - 5
src/components/chat_message/chat_message.js

@@ -6,7 +6,7 @@ import Gallery from '../gallery/gallery.vue'
 import LinkPreview from '../link-preview/link-preview.vue'
 import StatusContent from '../status_content/status_content.vue'
 import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
-import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import { defineAsyncComponent } from 'vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faTimes,
@@ -35,7 +35,8 @@ const ChatMessage = {
     UserAvatar,
     Gallery,
     LinkPreview,
-    ChatMessageDate
+    ChatMessageDate,
+    UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
   },
   computed: {
     // Returns HH:MM (hours and minutes) in local time.
@@ -49,9 +50,6 @@ const ChatMessage = {
     message () {
       return this.chatViewItem.data
     },
-    userProfileLink () {
-      return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames)
-    },
     isMessage () {
       return this.chatViewItem.type === 'message'
     },

+ 4 - 4
src/components/chat_message/chat_message.vue

@@ -14,16 +14,16 @@
         v-if="!isCurrentUser"
         class="avatar-wrapper"
       >
-        <router-link
+        <UserPopover
           v-if="chatViewItem.isHead"
-          :to="userProfileLink"
+          :userId="author.id"
         >
           <UserAvatar
             :compact="true"
             :better-shadow="betterShadow"
             :user="author"
           />
-        </router-link>
+        </UserPopover>
       </div>
       <div class="chat-message-inner">
         <div
@@ -44,7 +44,7 @@
               <Popover
                 trigger="click"
                 placement="top"
-                :bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'"
+                bound-to-selector=".chat-view-inner"
                 :bound-to="{ x: 'container' }"
                 :margin="popoverMarginStyle"
                 @show="menuOpened = true"

+ 3 - 7
src/components/chat_title/chat_title.js

@@ -1,12 +1,13 @@
-import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 import UserAvatar from '../user_avatar/user_avatar.vue'
 import RichContent from 'src/components/rich_content/rich_content.jsx'
+import { defineAsyncComponent } from 'vue'
 
 export default {
   name: 'ChatTitle',
   components: {
     UserAvatar,
-    RichContent
+    RichContent,
+    UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
   },
   props: [
     'user', 'withAvatar'
@@ -18,10 +19,5 @@ export default {
     htmlTitle () {
       return this.user ? this.user.name_html : ''
     }
-  },
-  methods: {
-    getUserProfileLink (user) {
-      return generateProfileLink(user.id, user.screen_name)
-    }
   }
 }

+ 3 - 3
src/components/chat_title/chat_title.vue

@@ -3,16 +3,16 @@
     class="chat-title"
     :title="title"
   >
-    <router-link
+    <UserPopover
       class="avatar-container"
       v-if="withAvatar && user"
-      :to="getUserProfileLink(user)"
+      :userId="user.id"
     >
       <UserAvatar
         class="titlebar-avatar"
         :user="user"
       />
-    </router-link>
+    </UserPopover>
     <RichContent
       v-if="user"
       class="username"

+ 1 - 0
src/components/desktop_nav/desktop_nav.scss

@@ -2,6 +2,7 @@
 
 .DesktopNav {
   width: 100%;
+  z-index: var(--ZI_navbar);
 
   input {
     color: var(--inputTopbarText, var(--inputText));

+ 1 - 1
src/components/desktop_nav/desktop_nav.vue

@@ -38,7 +38,7 @@
         />
         <button
           class="button-unstyled nav-icon"
-          @click.stop="openSettingsModal"
+          @click="openSettingsModal"
         >
           <FAIcon
             fixed-width

+ 2 - 1
src/components/emoji_picker/emoji_picker.scss

@@ -7,7 +7,8 @@
   right: 0;
   left: 0;
   margin: 0 !important;
-  z-index: 100;
+  // TODO: actually use popover in emoji picker
+  z-index: var(--ZI_popovers);
   background-color: $fallback--bg;
   background-color: var(--popover, $fallback--bg);
   color: $fallback--link;

+ 3 - 0
src/components/extra_buttons/extra_buttons.js

@@ -89,6 +89,9 @@ const ExtraButtons = {
     canMute () {
       return !!this.currentUser
     },
+    canBookmark () {
+      return !!this.currentUser
+    },
     statusLink () {
       return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
     }

+ 26 - 24
src/components/extra_buttons/extra_buttons.vue

@@ -51,28 +51,30 @@
             icon="thumbtack"
           /><span>{{ $t("status.unpin") }}</span>
         </button>
-        <button
-          v-if="!status.bookmarked"
-          class="button-default dropdown-item dropdown-item-icon"
-          @click.prevent="bookmarkStatus"
-          @click="close"
-        >
-          <FAIcon
-            fixed-width
-            :icon="['far', 'bookmark']"
-          /><span>{{ $t("status.bookmark") }}</span>
-        </button>
-        <button
-          v-if="status.bookmarked"
-          class="button-default dropdown-item dropdown-item-icon"
-          @click.prevent="unbookmarkStatus"
-          @click="close"
-        >
-          <FAIcon
-            fixed-width
-            icon="bookmark"
-          /><span>{{ $t("status.unbookmark") }}</span>
-        </button>
+        <template v-if="canBookmark">
+          <button
+            v-if="!status.bookmarked"
+            class="button-default dropdown-item dropdown-item-icon"
+            @click.prevent="bookmarkStatus"
+            @click="close"
+          >
+            <FAIcon
+              fixed-width
+              :icon="['far', 'bookmark']"
+            /><span>{{ $t("status.bookmark") }}</span>
+          </button>
+          <button
+            v-if="status.bookmarked"
+            class="button-default dropdown-item dropdown-item-icon"
+            @click.prevent="unbookmarkStatus"
+            @click="close"
+          >
+            <FAIcon
+              fixed-width
+              icon="bookmark"
+            /><span>{{ $t("status.unbookmark") }}</span>
+          </button>
+        </template>
         <button
           v-if="canDelete"
           class="button-default dropdown-item dropdown-item-icon"
@@ -119,12 +121,12 @@
       </div>
     </template>
     <template v-slot:trigger>
-      <button class="button-unstyled popover-trigger">
+      <span class="button-unstyled popover-trigger">
         <FAIcon
           class="fa-scale-110 fa-old-padding"
           icon="ellipsis-h"
         />
-      </button>
+      </span>
     </template>
   </Popover>
 </template>

+ 1 - 1
src/components/global_notice_list/global_notice_list.vue

@@ -32,7 +32,7 @@
   top: 50px;
   width: 100%;
   pointer-events: none;
-  z-index: 1001;
+  z-index: var(--ZI_popovers);
   display: flex;
   flex-direction: column;
   align-items: center;

+ 1 - 1
src/components/media_modal/media_modal.vue

@@ -121,7 +121,7 @@ $modal-view-button-icon-width: 3em;
 $modal-view-button-icon-margin: 0.5em;
 
 .modal-view.media-modal-view {
-  z-index: 9000;
+  z-index: var(--ZI_media_modal);
   flex-direction: column;
 
   .modal-view-button-arrow,

+ 21 - 3
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 { defineAsyncComponent } from 'vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faAt
@@ -14,7 +15,8 @@ library.add(
 const MentionLink = {
   name: 'MentionLink',
   components: {
-    UserAvatar
+    UserAvatar,
+    UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
   },
   props: {
     url: {
@@ -34,15 +36,30 @@ const MentionLink = {
       type: String
     }
   },
+  data () {
+    return {
+      hasSelection: false
+    }
+  },
   methods: {
     onClick () {
+      if (this.shouldShowTooltip) return
       const link = generateProfileLink(
         this.userId || this.user.id,
         this.userScreenName || this.user.screen_name
       )
       this.$router.push(link)
+    },
+    handleSelection () {
+      this.hasSelection = document.getSelection().containsNode(this.$refs.full, true)
     }
   },
+  mounted () {
+    document.addEventListener('selectionchange', this.handleSelection)
+  },
+  unmounted () {
+    document.removeEventListener('selectionchange', this.handleSelection)
+  },
   computed: {
     user () {
       return this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
@@ -88,7 +105,8 @@ const MentionLink = {
       return [
         {
           '-you': this.isYou && this.shouldBoldenYou,
-          '-highlighted': this.highlight
+          '-highlighted': this.highlight,
+          '-has-selection': this.hasSelection
         },
         this.highlightType
       ]
@@ -110,7 +128,7 @@ const MentionLink = {
       }
     },
     shouldShowTooltip () {
-      return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote
+      return this.mergedConfig.mentionLinkShowTooltip
     },
     shouldShowAvatar () {
       return this.mergedConfig.mentionLinkShowAvatar

+ 15 - 14
src/components/mention_link/mention_link.scss

@@ -55,11 +55,14 @@
 
   .new {
     &.-you {
-      & .shortName,
-      & .full {
+      .shortName {
         font-weight: 600;
       }
     }
+    &.-has-selection {
+      color: var(--alertNeutralText, $fallback--text);
+      background-color: var(--alertNeutral, $fallback--fg);
+    }
 
     .at {
       color: var(--link);
@@ -72,8 +75,7 @@
     }
 
     &.-striped {
-      & .shortName,
-      & .full {
+      & .shortName {
         background-image:
           repeating-linear-gradient(
             135deg,
@@ -86,30 +88,29 @@
     }
 
     &.-solid {
-      & .shortName,
-      & .full {
+      .shortName {
         background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));
       }
     }
 
     &.-side {
-      & .shortName,
-      & .userNameFull {
+      .shortName {
         box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);
       }
     }
   }
 
-  &:hover .new .full {
-    opacity: 1;
-    pointer-events: initial;
+  .full {
+    pointer-events: none;
   }
 
   .serverName.-faded {
     color: var(--faintLink, $fallback--link);
   }
+}
 
-  .full .-faded {
-    color: var(--faint, $fallback--faint);
-  }
+.mention-link-popover {
+  max-width: 70ch;
+  max-height: 20rem;
+  overflow: hidden;
 }

+ 38 - 46
src/components/mention_link/mention_link.vue

@@ -9,66 +9,58 @@
       class="original"
       target="_blank"
       v-html="content"
-    /><!-- eslint-enable vue/no-v-html --><span
-      v-if="user"
-      class="new"
-      :style="style"
-      :class="classnames"
+    /><!-- eslint-enable vue/no-v-html -->
+    <UserPopover
+      v-else
+      :userId="user.id"
+      :disabled="!shouldShowTooltip"
     >
-      <a
-        class="short button-unstyled"
-        :class="{ '-with-tooltip': shouldShowTooltip }"
-        :href="url"
-        @click.prevent="onClick"
+      <span
+        v-if="user"
+        class="new"
+        :style="style"
+        :class="classnames"
       >
-        <!-- eslint-disable vue/no-v-html -->
-        <UserAvatar
-          v-if="shouldShowAvatar"
-          class="mention-avatar"
-          :user="user"
-        /><span
-          class="shortName"
-        ><FAIcon
-          v-if="useAtIcon"
-          size="sm"
-          icon="at"
-          class="at"
-        />{{ !useAtIcon ? '@' : '' }}<span
-          class="userName"
-          v-html="userName"
-        /><span
-          v-if="shouldShowFullUserName"
-          class="serverName"
-          :class="{ '-faded': shouldFadeDomain }"
-          v-html="'@' + serverName"
-        />
-        </span>
-        <span
-          v-if="isYou && shouldShowYous"
-          :class="{ '-you': shouldBoldenYou }"
-        > {{ ' ' + $t('status.you') }}</span>
-        <!-- eslint-enable vue/no-v-html -->
-      </a><span
-        v-if="shouldShowTooltip"
-        class="full popover-default"
-        :class="[highlightType]"
-      >
-        <span
-          class="userNameFull"
+        <a
+          class="short button-unstyled"
+          :class="{ '-with-tooltip': shouldShowTooltip }"
+          :href="url"
+          @click.prevent="onClick"
         >
           <!-- eslint-disable vue/no-v-html -->
-          @<span
+          <UserAvatar
+            v-if="shouldShowAvatar"
+            class="mention-avatar"
+            :user="user"
+          /><span
+            class="shortName"
+          ><FAIcon
+            v-if="useAtIcon"
+            size="sm"
+            icon="at"
+            class="at"
+          />{{ !useAtIcon ? '@' : '' }}<span
             class="userName"
             v-html="userName"
           /><span
+            v-if="shouldShowFullUserName"
             class="serverName"
             :class="{ '-faded': shouldFadeDomain }"
             v-html="'@' + serverName"
           />
+          </span>
+          <span
+            v-if="isYou && shouldShowYous"
+            :class="{ '-you': shouldBoldenYou }"
+          > {{ ' ' + $t('status.you') }}</span>
           <!-- eslint-enable vue/no-v-html -->
+        </a><span class="full" ref="full">
+            <!-- eslint-disable vue/no-v-html -->
+            @<span v-html="userName" /><span v-html="'@' + serverName" />
+            <!-- eslint-enable vue/no-v-html -->
         </span>
       </span>
-    </span>
+    </UserPopover>
   </span>
 </template>
 

+ 1 - 2
src/components/mentions_line/mentions_line.vue

@@ -13,8 +13,7 @@
       <span
         v-if="expanded"
         class="fullExtraMentions"
-      >
-        <MentionLink
+      >{{ ' ' }}<MentionLink
           v-for="mention in extraMentions"
           :key="mention.index"
           class="mention-link"

+ 4 - 2
src/components/mobile_nav/mobile_nav.vue

@@ -86,6 +86,8 @@
 @import '../../_variables.scss';
 
 .MobileNav {
+  z-index: var(--ZI_navbar);
+
   .mobile-nav {
     display: grid;
     line-height: var(--navbar-height);
@@ -147,7 +149,7 @@
     transition-property: transform;
     transition-duration: 0.25s;
     transform: translateX(0);
-    z-index: 1001;
+    z-index: var(--ZI_navbar);
     -webkit-overflow-scrolling: touch;
 
     &.-closed {
@@ -160,7 +162,7 @@
     display: flex;
     align-items: center;
     justify-content: space-between;
-    z-index: 1;
+    z-index: calc(var(--ZI_navbar) + 100);
     width: 100%;
     height: 50px;
     line-height: 50px;

+ 4 - 1
src/components/modal/modal.vue

@@ -22,6 +22,9 @@ export default {
       default: false
     }
   },
+  provide: {
+    popoversZLayer: 'modals'
+  },
   computed: {
     classes () {
       return {
@@ -35,7 +38,7 @@ export default {
 
 <style lang="scss">
 .modal-view {
-  z-index: 2000;
+  z-index: var(--ZI_modals);
   position: fixed;
   top: 0;
   left: 0;

+ 2 - 0
src/components/nav_panel/nav_panel.vue

@@ -113,7 +113,9 @@
     border-color: $fallback--border;
     border-color: var(--border, $fallback--border);
     padding: 0;
+  }
 
+  > li {
     &:first-child .menu-item {
       border-top-right-radius: $fallback--panelRadius;
       border-top-right-radius: var(--panelRadius, $fallback--panelRadius);

+ 3 - 1
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 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'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -46,7 +47,8 @@ const Notification = {
     UserCard,
     Timeago,
     Status,
-    RichContent
+    RichContent,
+    UserPopover
   },
   methods: {
     toggleUserExpanded () {

+ 13 - 12
src/components/notification/notification.vue

@@ -34,21 +34,22 @@
       <a
         class="avatar-container"
         :href="$router.resolve(userProfileLink).href"
-        @click.stop.prevent.capture="toggleUserExpanded"
+        @click.prevent
       >
-        <UserAvatar
-          :compact="true"
-          :better-shadow="betterShadow"
-          :user="notification.from_profile"
-        />
+        <UserPopover
+          :userId="notification.from_profile.id"
+          :overlayCenters="true"
+        >
+          <UserAvatar
+            class="post-avatar"
+            :bot="botIndicator"
+            :compact="true"
+            :better-shadow="betterShadow"
+            :user="notification.from_profile"
+          />
+        </UserPopover>
       </a>
       <div class="notification-right">
-        <UserCard
-          v-if="userExpanded"
-          :user-id="getUser(notification).id"
-          :rounded="true"
-          :bordered="true"
-        />
         <span class="notification-details">
           <div class="name-and-action">
             <!-- eslint-disable vue/no-v-html -->

+ 10 - 0
src/components/notifications/notifications.js

@@ -1,3 +1,4 @@
+import { computed } from 'vue'
 import { mapGetters } from 'vuex'
 import Notification from '../notification/notification.vue'
 import NotificationFilters from './notification_filters.vue'
@@ -40,6 +41,11 @@ const Notifications = {
       seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
     }
   },
+  provide () {
+    return {
+      popoversZLayer: computed(() => this.popoversZLayer)
+    }
+  },
   computed: {
     mainClass () {
       return this.minimalMode ? '' : 'panel panel-default'
@@ -77,6 +83,10 @@ const Notifications = {
       }
       return map[layoutType] || '#notifs-sidebar'
     },
+    popoversZLayer () {
+      const { layoutType } = this.$store.state.interface
+      return layoutType === 'mobile' ? 'navbar' : null
+    },
     notificationsToDisplay () {
       return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
     },

+ 1 - 1
src/components/poll/poll_form.vue

@@ -84,7 +84,7 @@
             :key="unit"
             :value="unit"
           >
-            {{ $t(`time.${unit}_short`, ['']) }}
+            {{ $tc(`time.unit.${unit}_short`, expiryAmount, ['']) }}
           </option>
         </Select>
       </div>

+ 173 - 48
src/components/popover/popover.js

@@ -31,13 +31,35 @@ const Popover = {
 
     // If true, subtract padding when calculating position for the popover,
     // use it when popover offset looks to be different on top vs bottom.
-    removePadding: Boolean
+    removePadding: Boolean,
+
+    // self-explanatory (i hope)
+    disabled: Boolean,
+
+    // Instead of putting popover next to anchor, overlay popover's center on top of anchor's center
+    overlayCenters: Boolean,
+
+    // What selector (witin popover!) to use for determining center of popover
+    overlayCentersSelector: String,
+
+    // Lets hover popover stay when clicking inside of it
+    stayOnClick: Boolean
   },
+  inject: ['popoversZLayer'], // override popover z layer
   data () {
     return {
+      // lockReEntry is a flag that is set when mouse cursor is leaving the popover's content
+      // so that if mouse goes back into popover it won't be re-shown again to prevent annoyance
+      // with popovers refusing to be hidden when user wants to interact with something in below popover
+      lockReEntry: false,
       hidden: true,
-      styles: { opacity: 0 },
-      oldSize: { width: 0, height: 0 }
+      styles: {},
+      oldSize: { width: 0, height: 0 },
+      scrollable: null,
+      // used to avoid blinking if hovered onto popover
+      graceTimeout: null,
+      parentPopover: null,
+      childrenShown: new Set()
     }
   },
   methods: {
@@ -47,9 +69,7 @@ const Popover = {
     },
     updateStyles () {
       if (this.hidden) {
-        this.styles = {
-          opacity: 0
-        }
+        this.styles = {}
         return
       }
 
@@ -57,14 +77,26 @@ const Popover = {
       // its children are what are inside the slot. Expect only one v-slot:trigger.
       const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
       // SVGs don't have offsetWidth/Height, use fallback
-      const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
       const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
-      const screenBox = anchorEl.getBoundingClientRect()
-      // Screen position of the origin point for popover
-      const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
+      const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
+      const anchorScreenBox = anchorEl.getBoundingClientRect()
+
+      const anchorStyle = getComputedStyle(anchorEl)
+      const topPadding = parseFloat(anchorStyle.paddingTop)
+      const bottomPadding = parseFloat(anchorStyle.paddingBottom)
+
+      // Screen position of the origin point for popover = center of the anchor
+      const origin = {
+        x: anchorScreenBox.left + anchorWidth * 0.5,
+        y: anchorScreenBox.top + anchorHeight * 0.5
+      }
       const content = this.$refs.content
+      const overlayCenter = this.overlayCenters
+        ? this.$refs.content.querySelector(this.overlayCentersSelector)
+        : null
+
       // Minor optimization, don't call a slow reflow call if we don't have to
-      const parentBounds = this.boundTo &&
+      const parentScreenBox = this.boundTo &&
         (this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
         this.containerBoundingClientRect()
 
@@ -73,81 +105,151 @@ const Popover = {
       // What are the screen bounds for the popover? Viewport vs container
       // when using viewport, using default margin values to dodge the navbar
       const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
-        min: parentBounds.left + (margin.left || 0),
-        max: parentBounds.right - (margin.right || 0)
+        min: parentScreenBox.left + (margin.left || 0),
+        max: parentScreenBox.right - (margin.right || 0)
       } : {
         min: 0 + (margin.left || 10),
         max: window.innerWidth - (margin.right || 10)
       }
 
       const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
-        min: parentBounds.top + (margin.top || 0),
-        max: parentBounds.bottom - (margin.bottom || 0)
+        min: parentScreenBox.top + (margin.top || 0),
+        max: parentScreenBox.bottom - (margin.bottom || 0)
       } : {
         min: 0 + (margin.top || 50),
         max: window.innerHeight - (margin.bottom || 5)
       }
 
       let horizOffset = 0
+      let vertOffset = 0
+
+      if (overlayCenter) {
+        const box = content.getBoundingClientRect()
+        const overlayCenterScreenBox = overlayCenter.getBoundingClientRect()
+        const leftInnerOffset = overlayCenterScreenBox.left - box.left
+        const topInnerOffset = overlayCenterScreenBox.top - box.top
+        horizOffset = -leftInnerOffset - overlayCenter.offsetWidth * 0.5
+        vertOffset = -topInnerOffset - overlayCenter.offsetHeight * 0.5
+      } else {
+        horizOffset = content.offsetWidth * -0.5
+        vertOffset = content.offsetHeight * -0.5
+      }
+
+      const leftBorder = origin.x + horizOffset
+      const rightBorder = leftBorder + content.offsetWidth
+      const topBorder = origin.y + vertOffset
+      const bottomBorder = topBorder + content.offsetHeight
 
       // If overflowing from left, move it so that it doesn't
-      if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {
-        horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min
+      if (leftBorder < xBounds.min) {
+        horizOffset += xBounds.min - leftBorder
       }
 
       // If overflowing from right, move it so that it doesn't
-      if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {
-        horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max
+      if (rightBorder > xBounds.max) {
+        horizOffset -= rightBorder - xBounds.max
       }
 
-      // Default to whatever user wished with placement prop
-      let usingTop = this.placement !== 'bottom'
-
-      // Handle special cases, first force to displaying on top if there's not space on bottom,
-      // regardless of what placement value was. Then check if there's not space on top, and
-      // force to bottom, again regardless of what placement value was.
-      if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
-      if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
+      // If overflowing from top, move it so that it doesn't
+      if (topBorder < yBounds.min) {
+        vertOffset += yBounds.min - topBorder
+      }
 
-      let vPadding = 0
-      if (this.removePadding && usingTop) {
-        const anchorStyle = getComputedStyle(anchorEl)
-        vPadding = parseFloat(anchorStyle.paddingTop) + parseFloat(anchorStyle.paddingBottom)
+      // If overflowing from bottom, move it so that it doesn't
+      if (bottomBorder > yBounds.max) {
+        vertOffset -= bottomBorder - yBounds.max
       }
 
-      const yOffset = (this.offset && this.offset.y) || 0
-      const translateY = usingTop
-        ? -anchorHeight + vPadding - yOffset - content.offsetHeight
-        : yOffset
+      let translateX = 0
+      let translateY = 0
+
+      if (overlayCenter) {
+        translateX = origin.x + horizOffset
+        translateY = origin.y + vertOffset
+      } else {
+        // Default to whatever user wished with placement prop
+        let usingTop = this.placement !== 'bottom'
+
+        // Handle special cases, first force to displaying on top if there's not space on bottom,
+        // regardless of what placement value was. Then check if there's not space on top, and
+        // force to bottom, again regardless of what placement value was.
+        const topBoundary = origin.y - anchorHeight * 0.5 + (this.removePadding ? topPadding : 0)
+        const bottomBoundary = origin.y + anchorHeight * 0.5 - (this.removePadding ? bottomPadding : 0)
+        if (bottomBoundary + content.offsetHeight > yBounds.max) usingTop = true
+        if (topBoundary - content.offsetHeight < yBounds.min) usingTop = false
 
-      const xOffset = (this.offset && this.offset.x) || 0
-      const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset
+        const yOffset = (this.offset && this.offset.y) || 0
+        translateY = usingTop
+          ? topBoundary - yOffset - content.offsetHeight
+          : bottomBoundary + yOffset
+
+        const xOffset = (this.offset && this.offset.x) || 0
+        translateX = origin.x + horizOffset + xOffset
+      }
 
-      // Note, separate translateX and translateY avoids blurry text on chromium,
-      // single translate or translate3d resulted in blurry text.
       this.styles = {
-        opacity: 1,
-        transform: `translateX(${Math.round(translateX)}px) translateY(${Math.round(translateY)}px)`
+        left: `${Math.round(translateX)}px`,
+        top: `${Math.round(translateY)}px`
+      }
+
+      if (this.popoversZLayer) {
+        this.styles['--ZI_popover_override'] = `var(--ZI_${this.popoversZLayer}_popovers)`
+      }
+      if (parentScreenBox) {
+        this.styles.maxWidth = `${Math.round(parentScreenBox.width)}px`
       }
     },
     showPopover () {
+      if (this.disabled) return
       const wasHidden = this.hidden
       this.hidden = false
+      this.parentPopover && this.parentPopover.onChildPopoverState(this, true)
+      if (this.trigger === 'click' || this.stayOnClick) {
+        document.addEventListener('click', this.onClickOutside)
+      }
+      this.scrollable.addEventListener('scroll', this.onScroll)
+      this.scrollable.addEventListener('resize', this.onResize)
       this.$nextTick(() => {
         if (wasHidden) this.$emit('show')
         this.updateStyles()
       })
     },
     hidePopover () {
+      if (this.disabled) return
       if (!this.hidden) this.$emit('close')
       this.hidden = true
-      this.styles = { opacity: 0 }
+      this.parentPopover && this.parentPopover.onChildPopoverState(this, false)
+      if (this.trigger === 'click') {
+        document.removeEventListener('click', this.onClickOutside)
+      }
+      this.scrollable.removeEventListener('scroll', this.onScroll)
+      this.scrollable.removeEventListener('resize', this.onResize)
     },
     onMouseenter (e) {
-      if (this.trigger === 'hover') this.showPopover()
+      if (this.trigger === 'hover') {
+        this.lockReEntry = false
+        clearTimeout(this.graceTimeout)
+        this.graceTimeout = null
+        this.showPopover()
+      }
     },
     onMouseleave (e) {
-      if (this.trigger === 'hover') this.hidePopover()
+      if (this.trigger === 'hover' && this.childrenShown.size === 0) {
+        this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
+      }
+    },
+    onMouseenterContent (e) {
+      if (this.trigger === 'hover' && !this.lockReEntry) {
+        this.lockReEntry = true
+        clearTimeout(this.graceTimeout)
+        this.graceTimeout = null
+        this.showPopover()
+      }
+    },
+    onMouseleaveContent (e) {
+      if (this.trigger === 'hover' && this.childrenShown.size === 0) {
+        this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
+      }
     },
     onClick (e) {
       if (this.trigger === 'click') {
@@ -160,8 +262,24 @@ const Popover = {
     },
     onClickOutside (e) {
       if (this.hidden) return
+      if (this.$refs.content && this.$refs.content.contains(e.target)) return
       if (this.$el.contains(e.target)) return
+      if (this.childrenShown.size > 0) return
       this.hidePopover()
+      if (this.parentPopover) this.parentPopover.onClickOutside(e)
+    },
+    onScroll (e) {
+      this.updateStyles()
+    },
+    onResize (e) {
+      this.updateStyles()
+    },
+    onChildPopoverState (childRef, state) {
+      if (state) {
+        this.childrenShown.add(childRef)
+      } else {
+        this.childrenShown.delete(childRef)
+      }
     }
   },
   updated () {
@@ -175,11 +293,18 @@ const Popover = {
       this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
     }
   },
-  created () {
-    document.addEventListener('click', this.onClickOutside)
+  mounted () {
+    let scrollable = this.$refs.trigger.closest('.column.-scrollable') ||
+        this.$refs.trigger.closest('.mobile-notifications')
+    if (!scrollable) scrollable = window
+    this.scrollable = scrollable
+    let parent = this.$parent
+    while (parent && parent.$.type.name !== 'Popover') {
+      parent = parent.$parent
+    }
+    this.parentPopover = parent
   },
-  unmounted () {
-    document.removeEventListener('click', this.onClickOutside)
+  beforeUnmount () {
     this.hidePopover()
   }
 }

+ 28 - 20
src/components/popover/popover.vue

@@ -1,5 +1,5 @@
 <template>
-  <div
+  <span
     @mouseenter="onMouseenter"
     @mouseleave="onMouseleave"
   >
@@ -11,20 +11,27 @@
     >
       <slot name="trigger" />
     </button>
-    <div
-      v-if="!hidden"
-      ref="content"
-      :style="styles"
-      class="popover"
-      :class="popoverClass || 'popover-default'"
-    >
-      <slot
-        name="content"
-        class="popover-inner"
-        :close="hidePopover"
-      />
-    </div>
-  </div>
+    <teleport to="#popovers">
+      <transition name="fade">
+        <div
+          v-if="!hidden"
+          ref="content"
+          :style="styles"
+          class="popover"
+          :class="popoverClass || 'popover-default'"
+          @mouseenter="onMouseenterContent"
+          @mouseleave="onMouseleaveContent"
+          @click="onClickContent"
+        >
+          <slot
+            name="content"
+            class="popover-inner"
+            :close="hidePopover"
+          />
+        </div>
+      </transition>
+    </teleport>
+  </span>
 </template>
 
 <script src="./popover.js" />
@@ -37,14 +44,15 @@
 }
 
 .popover {
-  z-index: 500;
-  position: absolute;
+  z-index: var(--ZI_popover_override, var(--ZI_popovers));
+  position: fixed;
   min-width: 0;
+  max-width: calc(100vw - 20px);
+  box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
+  box-shadow: var(--popupShadow);
 }
 
 .popover-default {
-  transition: opacity 0.3s;
-
   &:after {
     content: '';
     position: absolute;
@@ -80,7 +88,7 @@
   text-align: left;
   list-style: none;
   max-width: 100vw;
-  z-index: 200;
+  z-index: var(--ZI_popover_override, var(--ZI_popovers));
   white-space: nowrap;
 
   .dropdown-divider {

+ 3 - 2
src/components/react_button/react_button.vue

@@ -6,6 +6,7 @@
     :offset="{ y: 5 }"
     :bound-to="{ x: 'container' }"
     remove-padding
+    popover-class="ReactButton popover-default"
     @show="focusInput"
   >
     <template v-slot:content="{close}">
@@ -41,7 +42,7 @@
       </div>
     </template>
     <template v-slot:trigger>
-      <button
+      <span
         class="button-unstyled popover-trigger"
         :title="$t('tool_tip.add_reaction')"
       >
@@ -49,7 +50,7 @@
           class="fa-scale-110 fa-old-padding"
           :icon="['far', 'smile-beam']"
         />
-      </button>
+      </span>
     </template>
   </Popover>
 </template>

+ 5 - 5
src/components/settings_modal/helpers/modified_indicator.vue

@@ -41,11 +41,11 @@ export default {
 .ModifiedIndicator {
   display: inline-block;
   position: relative;
+}
 
-  .modified-tooltip {
-    margin: 0.5em 1em;
-    min-width: 10em;
-    text-align: center;
-  }
+.modified-tooltip {
+  margin: 0.5em 1em;
+  min-width: 10em;
+  text-align: center;
 }
 </style>

+ 5 - 5
src/components/settings_modal/helpers/server_side_indicator.vue

@@ -41,11 +41,11 @@ export default {
 .ServerSideIndicator {
   display: inline-block;
   position: relative;
+}
 
-  .serverside-tooltip {
-    margin: 0.5em 1em;
-    min-width: 10em;
-    text-align: center;
-  }
+.serverside-tooltip {
+  margin: 0.5em 1em;
+  min-width: 10em;
+  text-align: center;
 }
 </style>

+ 18 - 12
src/components/settings_modal/tabs/general_tab.vue

@@ -74,6 +74,16 @@
             {{ $t('settings.show_scrollbars') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="userPopoverZoom" expert="1">
+            {{ $t('settings.user_popover_avatar_zoom') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="userPopoverOverlay" expert="1">
+            {{ $t('settings.user_popover_avatar_overlay') }}
+          </BooleanSetting>
+        </li>
         <li>
           <ChoiceSetting
             v-if="user"
@@ -261,18 +271,14 @@
             {{ $t('settings.mention_link_display') }}
           </ChoiceSetting>
         </li>
-        <ul
-          class="setting-list suboptions"
-        >
-          <li v-if="mentionLinkDisplay === 'short'">
-            <BooleanSetting
-              path="mentionLinkShowTooltip"
-              expert="1"
-            >
-              {{ $t('settings.mention_link_show_tooltip') }}
-            </BooleanSetting>
-          </li>
-        </ul>
+        <li>
+          <BooleanSetting
+            path="mentionLinkShowTooltip"
+            expert="1"
+          >
+            {{ $t('settings.mention_link_use_tooltip') }}
+          </BooleanSetting>
+        </li>
         <li>
           <BooleanSetting
             path="useAtIcon"

+ 1 - 1
src/components/shout_panel/shout_panel.vue

@@ -80,7 +80,7 @@
 .floating-shout {
   position: fixed;
   bottom: 0.5em;
-  z-index: 1000;
+  z-index: var(--ZI_popovers);
   max-width: 25em;
 
   &.-left {

+ 1 - 1
src/components/side_drawer/side_drawer.vue

@@ -211,7 +211,7 @@
 
 .side-drawer-container {
   position: fixed;
-  z-index: 1000;
+  z-index: var(--ZI_navbar);
   top: 0;
   left: 0;
   width: 100%;

+ 3 - 3
src/components/status/status.js

@@ -4,13 +4,13 @@ import ReactButton from '../react_button/react_button.vue'
 import RetweetButton from '../retweet_button/retweet_button.vue'
 import ExtraButtons from '../extra_buttons/extra_buttons.vue'
 import PostStatusForm from '../post_status_form/post_status_form.vue'
-import UserCard from '../user_card/user_card.vue'
 import UserAvatar from '../user_avatar/user_avatar.vue'
 import AvatarList from '../avatar_list/avatar_list.vue'
 import Timeago from '../timeago/timeago.vue'
 import StatusContent from '../status_content/status_content.vue'
 import RichContent from 'src/components/rich_content/rich_content.jsx'
 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 MentionsLine from 'src/components/mentions_line/mentions_line.vue'
@@ -105,7 +105,6 @@ const Status = {
     RetweetButton,
     ExtraButtons,
     PostStatusForm,
-    UserCard,
     UserAvatar,
     AvatarList,
     Timeago,
@@ -115,7 +114,8 @@ const Status = {
     StatusContent,
     RichContent,
     MentionLink,
-    MentionsLine
+    MentionsLine,
+    UserPopover
   },
   props: [
     'statusoid',

+ 14 - 18
src/components/status/status.vue

@@ -122,27 +122,22 @@
           v-if="!noHeading"
           class="left-side"
         >
-          <a
-            :href="$router.resolve(userProfileLink).href"
-            @click.stop.prevent.capture="toggleUserExpanded"
-          >
-            <UserAvatar
-              class="post-avatar"
-              :bot="botIndicator"
-              :compact="compact"
-              :better-shadow="betterShadow"
-              :user="status.user"
-            />
+          <a :href="$router.resolve(userProfileLink).href" @click.prevent>
+            <UserPopover
+              :userId="status.user.id"
+              :overlayCenters="true"
+            >
+              <UserAvatar
+                class="post-avatar"
+                :bot="botIndicator"
+                :compact="compact"
+                :better-shadow="betterShadow"
+                :user="status.user"
+              />
+            </UserPopover>
           </a>
         </div>
         <div class="right-side">
-          <UserCard
-            v-if="userExpanded"
-            :user-id="status.user.id"
-            :rounded="true"
-            :bordered="true"
-            class="usercard"
-          />
           <div
             v-if="!noHeading"
             class="status-heading"
@@ -322,6 +317,7 @@
                   class="mentions-line-first"
                 />
               </span>
+              {{ ' ' }}
               <MentionsLine
                 v-if="hasMentionsLine"
                 :mentions="mentionsLine.slice(1)"

+ 7 - 0
src/components/status_popover/status_popover.js

@@ -38,6 +38,13 @@ const StatusPopover = {
           .catch(e => (this.error = true))
       }
     }
+  },
+  watch: {
+    status (newStatus, oldStatus) {
+      if (newStatus !== oldStatus) {
+        this.$nextTick(() => this.$refs.popover.updateStyles())
+      }
+    }
   }
 }
 

+ 2 - 2
src/components/status_popover/status_popover.vue

@@ -1,9 +1,11 @@
 <template>
   <Popover
     trigger="hover"
+    :stay-on-click="true"
     popover-class="popover-default status-popover"
     :bound-to="{ x: 'container' }"
     @show="enter"
+    ref="popover"
   >
     <template v-slot:trigger>
       <slot />
@@ -52,8 +54,6 @@
   border-width: 1px;
   border-radius: $fallback--tooltipRadius;
   border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
-  box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
-  box-shadow: var(--popupShadow);
 
   /* TODO cleanup this */
   .Status.Status {

+ 18 - 32
src/components/timeline_menu/timeline_menu.vue

@@ -3,19 +3,17 @@
     trigger="click"
     class="TimelineMenu"
     :class="{ 'open': isOpen }"
-    :margin="{ left: -15, right: -200 }"
     :bound-to="{ x: 'container' }"
-    popover-class="timeline-menu-popover-wrap"
+    bound-to-selector=".Timeline"
+    popover-class="timeline-menu-popover popover-default"
     @show="openMenu"
     @close="() => isOpen = false"
   >
     <template v-slot:content>
-      <div class="timeline-menu-popover popover-default">
-        <TimelineMenuContent />
-      </div>
+      <TimelineMenuContent />
     </template>
     <template v-slot:trigger>
-      <button class="button-unstyled title timeline-menu-title">
+      <span class="button-unstyled title timeline-menu-title">
         <span class="timeline-title">{{ timelineName() }}</span>
         <span>
           <FAIcon
@@ -27,7 +25,7 @@
           class="click-blocker"
           @click="blockOpen"
         />
-      </button>
+      </span>
     </template>
   </Popover>
 </template>
@@ -38,42 +36,18 @@
 @import '../../_variables.scss';
 
 .TimelineMenu {
-  flex-shrink: 1;
   margin-right: auto;
   min-width: 0;
-  width: 24rem;
 
   .popover-trigger-button {
     vertical-align: bottom;
   }
 
-  .timeline-menu-popover-wrap {
-    overflow: hidden;
-    // Match panel heading padding to line up menu with bottom of heading
-    margin-top: 0.6rem;
-    padding: 0 15px 15px 15px;
-  }
-
-  .timeline-menu-popover {
-    width: 24rem;
-    max-width: 100vw;
-    margin: 0;
-    font-size: 1rem;
-    border-top-right-radius: 0;
-    border-top-left-radius: 0;
-    transform: translateY(-100%);
-    transition: transform 100ms;
-  }
-
   .panel::after {
     border-top-right-radius: 0;
     border-top-left-radius: 0;
   }
 
-  &.open .timeline-menu-popover {
-    transform: translateY(0);
-  }
-
   .timeline-menu-title {
     margin: 0;
     cursor: pointer;
@@ -108,6 +82,16 @@
     box-shadow: var(--popoverShadow);
   }
 
+}
+
+.timeline-menu-popover {
+  min-width: 24rem;
+  max-width: 100vw;
+  margin-top: 0.6rem;
+  font-size: 1rem;
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+
   ul {
     list-style: none;
     margin: 0;
@@ -134,7 +118,9 @@
 
   a {
     display: block;
-    padding: 0.6em 0.65em;
+    padding: 0 0.65em;
+    height: 3.5em;
+    line-height: 3.5em;
 
     &:hover {
       background-color: $fallback--lightBg;

+ 24 - 6
src/components/user_card/user_card.js

@@ -14,7 +14,9 @@ import {
   faRss,
   faSearchPlus,
   faExternalLinkAlt,
-  faEdit
+  faEdit,
+  faTimes,
+  faExpandAlt
 } from '@fortawesome/free-solid-svg-icons'
 
 library.add(
@@ -22,12 +24,21 @@ library.add(
   faBell,
   faSearchPlus,
   faExternalLinkAlt,
-  faEdit
+  faEdit,
+  faTimes,
+  faExpandAlt
 )
 
 export default {
   props: [
-    'userId', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
+    'userId',
+    'switcher',
+    'selected',
+    'hideBio',
+    'rounded',
+    'bordered',
+    'avatarAction', // default - open profile, 'zoom' - zoom, function - call function
+    'onClose'
   ],
   data () {
     return {
@@ -47,9 +58,10 @@ export default {
     },
     classes () {
       return [{
-        'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
-        'user-card-rounded': this.rounded === true, // set border-radius for all sides
-        'user-card-bordered': this.bordered === true // set border for all sides
+        '-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
+        '-rounded': this.rounded === true, // set border-radius for all sides
+        '-bordered': this.bordered === true, // set border for all sides
+        '-popover': !!this.onClose // set popover rounding
       }]
     },
     style () {
@@ -170,6 +182,12 @@ export default {
     },
     mentionUser () {
       this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
+    },
+    onAvatarClickHandler (e) {
+      if (this.onAvatarClick) {
+        e.preventDefault()
+        this.onAvatarClick()
+      }
     }
   }
 }

+ 38 - 13
src/components/user_card/user_card.scss

@@ -42,8 +42,10 @@
     mask-composite: exclude;
     background-size: cover;
     mask-size: 100% 60%;
-    border-top-left-radius: calc(var(--panelRadius) - 1px);
-    border-top-right-radius: calc(var(--panelRadius) - 1px);
+    border-top-left-radius: calc(var(--__roundnessTop, --panelRadius) - 1px);
+    border-top-right-radius: calc(var(--__roundnessTop, --panelRadius) - 1px);
+    border-bottom-left-radius: calc(var(--__roundnessBottom, --panelRadius) - 1px);
+    border-bottom-right-radius: calc(var(--__roundnessBottom, --panelRadius) - 1px);
     background-color: var(--profileBg);
     z-index: -2;
 
@@ -72,21 +74,33 @@
     }
   }
 
-  // Modifiers
-
-  &-rounded-t {
+  &.-rounded-t {
     border-top-left-radius: $fallback--panelRadius;
     border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
     border-top-right-radius: $fallback--panelRadius;
     border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
+
+    --__roundnessTop: var(--panelRadius);
+    --__roundnessBottom: 0;
   }
 
-  &-rounded {
+  &.-rounded {
     border-radius: $fallback--panelRadius;
     border-radius: var(--panelRadius, $fallback--panelRadius);
+
+    --__roundnessTop: var(--panelRadius);
+    --__roundnessBottom: var(--panelRadius);
   }
 
-  &-bordered {
+  &.-popover {
+    border-radius: $fallback--tooltipRadius;
+    border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+
+    --__roundnessTop: var(--tooltipRadius);
+    --__roundnessBottom: var(--tooltipRadius);
+  }
+
+  &.-bordered {
     border-width: 1px;
     border-style: solid;
     border-color: $fallback--border;
@@ -99,6 +113,15 @@
   color: var(--lightText, $fallback--lightText);
   padding: 0 26px;
 
+  a {
+    color: $fallback--lightText;
+    color: var(--lightText, $fallback--lightText);
+
+    &:hover {
+      color: var(--icon);
+    }
+  }
+
   .container {
     min-width: 0;
     padding: 16px 0 6px;
@@ -110,23 +133,27 @@
       min-width: 0;
     }
 
+    > a {
+      vertical-align: middle;
+      display: flex;
+    }
+
     .Avatar {
       --_avatarShadowBox: var(--avatarShadow);
       --_avatarShadowFilter: var(--avatarShadowFilter);
       --_avatarShadowInset: var(--avatarShadowInset);
 
-      flex: 1 0 100%;
       width: 56px;
       height: 56px;
       object-fit: cover;
     }
   }
 
-  &-avatar-link {
+  &-avatar {
     position: relative;
     cursor: pointer;
 
-    &-overlay {
+    &.-overlay {
       position: absolute;
       left: 0;
       top: 0;
@@ -146,7 +173,7 @@
       }
     }
 
-    &:hover &-overlay {
+    &:hover &.-overlay {
       opacity: 1;
     }
   }
@@ -206,8 +233,6 @@
       flex: 0 1 auto;
       text-overflow: ellipsis;
       overflow: hidden;
-      color: $fallback--lightText;
-      color: var(--lightText, $fallback--lightText);
     }
 
     .dailyAvg {

+ 41 - 9
src/components/user_card/user_card.vue

@@ -8,25 +8,32 @@
       :style="style"
       class="background-image"
     />
-    <div class="panel-heading -flexible-height">
+    <div :class="onClose ? '' : panel-heading -flexible-height">
       <div class="user-info">
         <div class="container">
           <a
-            v-if="allowZoomingAvatar"
-            class="user-info-avatar-link"
+            v-if="avatarAction === 'zoom'"
+            class="user-info-avatar -link"
             @click="zoomAvatar"
           >
             <UserAvatar
               :better-shadow="betterShadow"
               :user="user"
             />
-            <div class="user-info-avatar-link-overlay">
+            <div class="user-info-avatar -link -overlay">
               <FAIcon
                 class="fa-scale-110 fa-old-padding"
                 icon="search-plus"
               />
             </div>
           </a>
+          <UserAvatar
+            v-else-if="typeof avatarAction === 'function'"
+            @click="avatarAction"
+            class="user-info-avatar"
+            :better-shadow="betterShadow"
+            :user="user"
+          />
           <router-link
             v-else
             :to="userProfileLink(user)"
@@ -38,12 +45,16 @@
           </router-link>
           <div class="user-summary">
             <div class="top-line">
-              <RichContent
-                :title="user.name"
+              <router-link
+                :to="userProfileLink(user)"
                 class="user-name"
-                :html="user.name"
-                :emoji="user.emoji"
-              />
+              >
+                <RichContent
+                  :title="user.name"
+                  :html="user.name"
+                  :emoji="user.emoji"
+                />
+              </router-link>
               <button
                 v-if="!isOtherUser && user.is_local"
                 class="button-unstyled edit-profile-button"
@@ -72,6 +83,27 @@
                 :user="user"
                 :relationship="relationship"
               />
+              <router-link
+                v-if="onClose"
+                :to="userProfileLink(user)"
+                class="button-unstyled external-link-button"
+                @click="onClose"
+              >
+                <FAIcon
+                  class="icon"
+                  icon="expand-alt"
+                />
+              </router-link>
+              <button
+                v-if="onClose"
+                class="button-unstyled external-link-button"
+                @click="onClose"
+              >
+                <FAIcon
+                  class="icon"
+                  icon="times"
+                />
+              </button>
             </div>
             <div class="bottom-line">
               <router-link

+ 23 - 0
src/components/user_popover/user_popover.js

@@ -0,0 +1,23 @@
+import UserCard from '../user_card/user_card.vue'
+import { defineAsyncComponent } from 'vue'
+
+const UserPopover = {
+  name: 'UserPopover',
+  props: [
+    'userId', 'overlayCenters', 'disabled', 'overlayCentersSelector'
+  ],
+  components: {
+    UserCard,
+    Popover: defineAsyncComponent(() => import('../popover/popover.vue'))
+  },
+  computed: {
+    userPopoverZoom () {
+      return this.$store.getters.mergedConfig.userPopoverZoom
+    },
+    userPopoverOverlay () {
+      return this.$store.getters.mergedConfig.userPopoverOverlay
+    }
+  }
+}
+
+export default UserPopover

+ 33 - 0
src/components/user_popover/user_popover.vue

@@ -0,0 +1,33 @@
+<template>
+<Popover
+  trigger="click"
+  popover-class="popover-default user-popover"
+  :overlay-centers-selector="overlayCentersSelector || '.user-info .Avatar'"
+  :overlay-centers="overlayCenters && userPopoverOverlay"
+  :disabled="disabled"
+>
+  <template v-slot:trigger>
+    <slot />
+  </template>
+  <template v-slot:content={close}>
+    <UserCard
+      class="user-popover"
+      :user-id="userId"
+      :hide-bio="true"
+      :avatar-action="userPopoverZoom ? 'zoom' : close"
+      :on-close="close"
+    />
+  </template>
+</Popover>
+</template>
+
+<script src="./user_popover.js" ></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+/* popover styles load on-demand, so we need to override */
+.user-popover.popover {
+}
+
+</style>

+ 1 - 1
src/components/user_profile/user_profile.vue

@@ -8,7 +8,7 @@
         :user-id="userId"
         :switcher="true"
         :selected="timeline.viewing"
-        :allow-zooming-avatar="true"
+        avatar-action="zoom"
         rounded="top"
       />
       <div

+ 3 - 1
src/i18n/en.json

@@ -546,10 +546,12 @@
     "mention_link_display_short": "always as short names (e.g. {'@'}foo)",
     "mention_link_display_full_for_remote": "as full names only for remote users (e.g. {'@'}foo{'@'}example.org)",
     "mention_link_display_full": "always as full names (e.g. {'@'}foo{'@'}example.org)",
-    "mention_link_show_tooltip": "Show full user names as tooltip for remote users",
+    "mention_link_use_tooltip": "Show user card when clicking mention links",
     "mention_link_show_avatar": "Show user avatar beside the link",
     "mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)",
     "mention_link_bolden_you": "Highlight mention of you when you are mentioned",
+    "user_popover_avatar_zoom": "Clicking on user avatar in popover zooms it instead of closing the popover",
+    "user_popover_avatar_overlay": "Show user popover over user avatar",
     "fun": "Fun",
     "greentext": "Meme arrows",
     "show_yous": "Show (You)s",

+ 2 - 0
src/modules/config.js

@@ -81,6 +81,8 @@ export const defaultState = {
   useContainFit: true,
   disableStickyHeaders: false,
   showScrollbars: false,
+  userPopoverZoom: false,
+  userPopoverOverlay: true,
   greentext: undefined, // instance default
   useAtIcon: undefined, // instance default
   mentionLinkDisplay: undefined, // instance default

+ 12 - 16
test/unit/specs/components/rich_content.spec.js

@@ -4,7 +4,15 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
 const attentions = []
 const global = {
   mocks: {
-    '$store': null
+    '$store': {
+      state: {},
+      getters: {
+        mergedConfig: () => ({
+          mentionLinkShowTooltip: true
+        }),
+        findUserByUrl: () => null
+      }
+    }
   },
   stubs: {
     FAIcon: true
@@ -131,8 +139,7 @@ describe('RichContent', () => {
       ].join(''),
       [
         makeMention('John'),
-        makeMention('Josh'),
-        makeMention('Jeremy')
+        makeMention('Josh'), makeMention('Jeremy')
       ].join('')
     ].join('\n')
 
@@ -349,7 +356,6 @@ describe('RichContent', () => {
       p(
         '<span class="MentionsLine">',
         '<span class="MentionLink mention-link">',
-        '<!-- eslint-disable vue/no-v-html -->',
         '<a href="lol" class="original" target="_blank">',
         '<span>',
         'https://</span>',
@@ -358,10 +364,7 @@ describe('RichContent', () => {
         '<span>',
         '</span>',
         '</a>',
-        '<!-- eslint-enable vue/no-v-html -->',
-        '<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
         '</span>',
-        '<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff
         '</span>'
       ),
       p(
@@ -380,7 +383,7 @@ describe('RichContent', () => {
       }
     })
 
-    expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected))
+    expect(wrapper.html().replace(/\n/g, '').replace(/<!--.*?-->/g, '')).to.eql(compwrap(expected))
   })
 
   it('rich contents of nested mentions are handled properly', () => {
@@ -412,7 +415,6 @@ describe('RichContent', () => {
       '<span class="poast-style">',
       '<span class="MentionsLine">',
       '<span class="MentionLink mention-link">',
-      '<!-- eslint-disable vue/no-v-html -->',
       '<a href="lol" class="original" target="_blank">',
       '<span>',
       'https://</span>',
@@ -421,11 +423,8 @@ describe('RichContent', () => {
       '<span>',
       '</span>',
       '</a>',
-      '<!-- eslint-enable vue/no-v-html -->',
-      '<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
       '</span>',
       '<span class="MentionLink mention-link">',
-      '<!-- eslint-disable vue/no-v-html -->',
       '<a href="lol" class="original" target="_blank">',
       '<span>',
       'https://</span>',
@@ -434,10 +433,7 @@ describe('RichContent', () => {
       '<span>',
       '</span>',
       '</a>',
-      '<!-- eslint-enable vue/no-v-html -->',
-      '<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
       '</span>',
-      '<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff
       '</span>',
       ' ',
       '</span>',
@@ -455,7 +451,7 @@ describe('RichContent', () => {
       }
     })
 
-    expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected))
+    expect(wrapper.html().replace(/\n/g, '').replace(/<!--.*?-->/g, '')).to.eql(compwrap(expected))
   })
 
   it('rich contents of a link are handled properly', () => {

+ 169 - 147
yarn.lock

@@ -1294,31 +1294,31 @@
   resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
   integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
 
-"@fortawesome/fontawesome-common-types@6.1.1":
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz#7dc996042d21fc1ae850e3173b5c67b0549f9105"
-  integrity sha512-wVn5WJPirFTnzN6tR95abCx+ocH+3IFLXAgyavnf9hUmN0CfWoDjPT/BAWsUVwSlYYVBeCLJxaqi7ZGe4uSjBA==
+"@fortawesome/fontawesome-common-types@6.1.2":
+  version "6.1.2"
+  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.2.tgz#c1095b1bbabf19f37f9ff0719db38d92a410bcfe"
+  integrity sha512-wBaAPGz1Awxg05e0PBRkDRuTsy4B3dpBm+zreTTyd9TH4uUM27cAL4xWyWR0rLJCrRwzVsQ4hF3FvM6rqydKPA==
 
-"@fortawesome/fontawesome-svg-core@6.1.1":
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz#3424ec6182515951816be9b11665d67efdce5b5f"
-  integrity sha512-NCg0w2YIp81f4V6cMGD9iomfsIj7GWrqmsa0ZsPh59G7PKiGN1KymZNxmF00ssuAlo/VZmpK6xazsGOwzKYUMg==
+"@fortawesome/fontawesome-svg-core@6.1.2":
+  version "6.1.2"
+  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.2.tgz#11e2e8583a7dea75d734e4d0e53d91c63fae7511"
+  integrity sha512-853G/Htp0BOdXnPoeCPTjFrVwyrJHpe8MhjB/DYE9XjwhnNDfuBCd3aKc2YUYbEfHEcBws4UAA0kA9dymZKGjA==
   dependencies:
-    "@fortawesome/fontawesome-common-types" "6.1.1"
+    "@fortawesome/fontawesome-common-types" "6.1.2"
 
-"@fortawesome/free-regular-svg-icons@6.1.1":
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.1.1.tgz#3f2f58262a839edf0643cbacee7a8a8230061c98"
-  integrity sha512-xXiW7hcpgwmWtndKPOzG+43fPH7ZjxOaoeyooptSztGmJxCAflHZxXNK0GcT0uEsR4jTGQAfGklDZE5NHoBhKg==
+"@fortawesome/free-regular-svg-icons@6.1.2":
+  version "6.1.2"
+  resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.1.2.tgz#9f04009098addcc11d0d185126f058ed042c3099"
+  integrity sha512-xR4hA+tAwsaTHGfb+25H1gVU/aJ0Rzu+xIUfnyrhaL13yNQ7TWiI2RvzniAaB+VGHDU2a+Pk96Ve+pkN3/+TTQ==
   dependencies:
-    "@fortawesome/fontawesome-common-types" "6.1.1"
+    "@fortawesome/fontawesome-common-types" "6.1.2"
 
-"@fortawesome/free-solid-svg-icons@6.1.1":
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.1.1.tgz#3369e673f8fe8be2fba30b1ec274d47490a830a6"
-  integrity sha512-0/5exxavOhI/D4Ovm2r3vxNojGZioPwmFrKg0ZUH69Q68uFhFPs6+dhAToh6VEQBntxPRYPuT5Cg1tpNa9JUPg==
+"@fortawesome/free-solid-svg-icons@6.1.2":
+  version "6.1.2"
+  resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.1.2.tgz#491d668b8a6603698d0ce1ac620f66fd22b74c84"
+  integrity sha512-lTgZz+cMpzjkHmCwOG3E1ilUZrnINYdqMmrkv30EC3XbRsGlbIOL8H9LaNp5SV4g0pNJDfQ4EdTWWaMvdwyLiQ==
   dependencies:
-    "@fortawesome/fontawesome-common-types" "6.1.1"
+    "@fortawesome/fontawesome-common-types" "6.1.2"
 
 "@fortawesome/vue-fontawesome@3.0.1":
   version "3.0.1"
@@ -1499,10 +1499,10 @@
     remark "^13.0.0"
     unist-util-find-all-after "^3.0.2"
 
-"@testim/chrome-version@^1.0.7":
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.0.7.tgz#0cd915785ec4190f08a3a6acc9b61fc38fb5f1a9"
-  integrity sha512-8UT/J+xqCYfn3fKtOznAibsHpiuDshCb0fwgWxRazTT19Igp9ovoXMPhXyLD6m3CKQGTMHgqoxaFfMWaL40Rnw==
+"@testim/chrome-version@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.2.tgz#092005c5b77bd3bb6576a4677110a11485e11864"
+  integrity sha512-1c4ZOETSRpI0iBfIFUqU4KqwBAB2lHUAlBjZz/YqOHqwM9dTTzjV6Km0ZkiEiSCx/tLr1BtESIKyWWMww+RUqw==
 
 "@types/color-name@^1.1.1":
   version "1.1.1"
@@ -1625,47 +1625,47 @@
     html-tags "^3.1.0"
     svg-tags "^1.0.0"
 
-"@vue/compiler-core@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.31.tgz#d38f06c2cf845742403b523ab4596a3fda152e89"
-  integrity sha512-aKno00qoA4o+V/kR6i/pE+aP+esng5siNAVQ422TkBNM6qA4veXiZbSe8OTXHXquEi/f6Akc+nLfB4JGfe4/WQ==
+"@vue/compiler-core@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz#b3c42e04c0e0f2c496ff1784e543fbefe91e215a"
+  integrity sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/shared" "3.2.31"
+    "@vue/shared" "3.2.37"
     estree-walker "^2.0.2"
     source-map "^0.6.1"
 
-"@vue/compiler-dom@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz#b1b7dfad55c96c8cc2b919cd7eb5fd7e4ddbf00e"
-  integrity sha512-60zIlFfzIDf3u91cqfqy9KhCKIJgPeqxgveH2L+87RcGU/alT6BRrk5JtUso0OibH3O7NXuNOQ0cDc9beT0wrg==
+"@vue/compiler-dom@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz#10d2427a789e7c707c872da9d678c82a0c6582b5"
+  integrity sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==
   dependencies:
-    "@vue/compiler-core" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/compiler-core" "3.2.37"
+    "@vue/shared" "3.2.37"
 
-"@vue/compiler-sfc@3.2.31", "@vue/compiler-sfc@^3.1.0":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.31.tgz#d02b29c3fe34d599a52c5ae1c6937b4d69f11c2f"
-  integrity sha512-748adc9msSPGzXgibHiO6T7RWgfnDcVQD+VVwYgSsyyY8Ans64tALHZANrKtOzvkwznV/F4H7OAod/jIlp/dkQ==
+"@vue/compiler-sfc@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz#3103af3da2f40286edcd85ea495dcb35bc7f5ff4"
+  integrity sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/compiler-core" "3.2.31"
-    "@vue/compiler-dom" "3.2.31"
-    "@vue/compiler-ssr" "3.2.31"
-    "@vue/reactivity-transform" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/compiler-core" "3.2.37"
+    "@vue/compiler-dom" "3.2.37"
+    "@vue/compiler-ssr" "3.2.37"
+    "@vue/reactivity-transform" "3.2.37"
+    "@vue/shared" "3.2.37"
     estree-walker "^2.0.2"
     magic-string "^0.25.7"
     postcss "^8.1.10"
     source-map "^0.6.1"
 
-"@vue/compiler-ssr@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.31.tgz#4fa00f486c9c4580b40a4177871ebbd650ecb99c"
-  integrity sha512-mjN0rqig+A8TVDnsGPYJM5dpbjlXeHUm2oZHZwGyMYiGT/F4fhJf/cXy8QpjnLQK4Y9Et4GWzHn9PS8AHUnSkw==
+"@vue/compiler-ssr@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz#4899d19f3a5fafd61524a9d1aee8eb0505313cff"
+  integrity sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==
   dependencies:
-    "@vue/compiler-dom" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/compiler-dom" "3.2.37"
+    "@vue/shared" "3.2.37"
 
 "@vue/devtools-api@^6.0.0-beta.11":
   version "6.1.3"
@@ -1677,58 +1677,58 @@
   resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz#6f2948ff002ec46df01420dfeff91de16c5b4092"
   integrity sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==
 
-"@vue/reactivity-transform@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz#0f5b25c24e70edab2b613d5305c465b50fc00911"
-  integrity sha512-uS4l4z/W7wXdI+Va5pgVxBJ345wyGFKvpPYtdSgvfJfX/x2Ymm6ophQlXXB6acqGHtXuBqNyyO3zVp9b1r0MOA==
+"@vue/reactivity-transform@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz#0caa47c4344df4ae59f5a05dde2a8758829f8eca"
+  integrity sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/compiler-core" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/compiler-core" "3.2.37"
+    "@vue/shared" "3.2.37"
     estree-walker "^2.0.2"
     magic-string "^0.25.7"
 
-"@vue/reactivity@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.31.tgz#fc90aa2cdf695418b79e534783aca90d63a46bbd"
-  integrity sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==
+"@vue/reactivity@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.37.tgz#5bc3847ac58828e2b78526e08219e0a1089f8848"
+  integrity sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==
   dependencies:
-    "@vue/shared" "3.2.31"
+    "@vue/shared" "3.2.37"
 
-"@vue/runtime-core@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.31.tgz#9d284c382f5f981b7a7b5971052a1dc4ef39ac7a"
-  integrity sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA==
+"@vue/runtime-core@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz#7ba7c54bb56e5d70edfc2f05766e1ca8519966e3"
+  integrity sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==
   dependencies:
-    "@vue/reactivity" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/reactivity" "3.2.37"
+    "@vue/shared" "3.2.37"
 
-"@vue/runtime-dom@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz#79ce01817cb3caf2c9d923f669b738d2d7953eff"
-  integrity sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g==
+"@vue/runtime-dom@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz#002bdc8228fa63949317756fb1e92cdd3f9f4bbd"
+  integrity sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==
   dependencies:
-    "@vue/runtime-core" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/runtime-core" "3.2.37"
+    "@vue/shared" "3.2.37"
     csstype "^2.6.8"
 
-"@vue/server-renderer@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.31.tgz#201e9d6ce735847d5989403af81ef80960da7141"
-  integrity sha512-8CN3Zj2HyR2LQQBHZ61HexF5NReqngLT3oahyiVRfSSvak+oAvVmu8iNLSu6XR77Ili2AOpnAt1y8ywjjqtmkg==
+"@vue/server-renderer@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz#840a29c8dcc29bddd9b5f5ffa22b95c0e72afdfc"
+  integrity sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==
   dependencies:
-    "@vue/compiler-ssr" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/compiler-ssr" "3.2.37"
+    "@vue/shared" "3.2.37"
 
-"@vue/shared@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.31.tgz#c90de7126d833dcd3a4c7534d534be2fb41faa4e"
-  integrity sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==
+"@vue/shared@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702"
+  integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==
 
-"@vue/test-utils@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.0-rc.17.tgz#e6dcf5b5bd3ae23595bdb154b9b578ebcdffd698"
-  integrity sha512-7LHZKsFRV/HqDoMVY+cJamFzgHgsrmQFalROHC5FMWrzPzd+utG5e11krj1tVsnxYufGA2ABShX4nlcHXED+zQ==
+"@vue/test-utils@2.0.2":
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.2.tgz#0b5edd683366153d5bc5a91edc62f292118710eb"
+  integrity sha512-E2P4oXSaWDqTZNbmKZFVLrNN/siVN78YkEqs7pHryWerrlZR9bBFLWdJwRoguX45Ru6HxIflzKl4vQvwRMwm5g==
 
 "@vuelidate/core@2.0.0-alpha.43":
   version "2.0.0-alpha.43"
@@ -1737,12 +1737,12 @@
   dependencies:
     vue-demi "^0.13.4"
 
-"@vuelidate/validators@2.0.0-alpha.30":
-  version "2.0.0-alpha.30"
-  resolved "https://registry.yarnpkg.com/@vuelidate/validators/-/validators-2.0.0-alpha.30.tgz#978e676b5b5dc160e6a83fdf8c1bf26052f46e88"
-  integrity sha512-XH0oIU1+6bTZ1Kd1RNf7AMDsAahj1hUjLhbFUIrDhKIUKMFvG4658pqYATePNqhAegENFA+RDAPhsDXV/MB2wQ==
+"@vuelidate/validators@2.0.0-alpha.31":
+  version "2.0.0-alpha.31"
+  resolved "https://registry.yarnpkg.com/@vuelidate/validators/-/validators-2.0.0-alpha.31.tgz#04d63307bc0a12db9f7ad94243350b83aacee998"
+  integrity sha512-+MFA9nZ7Y9zCpq383/voPDk/hiAmu6KqiJJhLOYB/FmrUPVoyKnuKnI9Bwiq8ok9GZlVkI8BnIrKPKGj9QpwiQ==
   dependencies:
-    vue-demi "^0.12.0"
+    vue-demi "^0.13.4"
 
 "@webassemblyjs/ast@1.9.0":
   version "1.9.0"
@@ -2229,6 +2229,11 @@ async@^2.5.0:
   dependencies:
     lodash "^4.17.10"
 
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
 atob@^2.1.1:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
@@ -2258,12 +2263,13 @@ autoprefixer@^9.8.6:
     postcss "^7.0.32"
     postcss-value-parser "^4.1.0"
 
-axios@^0.21.1:
-  version "0.21.4"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
-  integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
+axios@^0.27.2:
+  version "0.27.2"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
+  integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
   dependencies:
-    follow-redirects "^1.14.0"
+    follow-redirects "^1.14.9"
+    form-data "^4.0.0"
 
 babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
   version "6.26.0"
@@ -2540,10 +2546,10 @@ body-parser@^1.19.0:
     raw-body "2.4.3"
     type-is "~1.6.18"
 
-body-scroll-lock@2.7.1:
-  version "2.7.1"
-  resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-2.7.1.tgz#caf3f9c91773af1ffb684cd66ed9137b5b737014"
-  integrity sha512-hS53SQ8RhM0e4DsQ3PKz6Gr2O7Kpdh59TWU98GHjaQznL7y4dFycEPk7pFQAikqBaUSCArkc5E3pe7CWIt2fZA==
+body-scroll-lock@3.1.5:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz#c1392d9217ed2c3e237fee1e910f6cdd80b7aaec"
+  integrity sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==
 
 boolbase@~1.0.0:
   version "1.0.0"
@@ -3033,17 +3039,16 @@ chrome-trace-event@^1.0.2:
   resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
   integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
 
-chromedriver@87.0.7:
-  version "87.0.7"
-  resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-87.0.7.tgz#74041e02ff7f633e91b98eb707e2476f713dc4ca"
-  integrity sha512-7J7iN2rJuSDsKb9BUUMewJt07PuTlZYd809D10dUCT1rjMD3i2jUw7dum9RxdC1xO3aFwMd8TwZ5NR82T+S+Dg==
+chromedriver@103.0.0:
+  version "103.0.0"
+  resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-103.0.0.tgz#2ef086d62076e3ff6df6cfb84895d11d2c18d9cf"
+  integrity sha512-7BHf6HWt0PeOHCzWO8qlnD13sARzr5AKTtG8Csn+czsuAsajwPxdLNtry5GPh8HYFyl+i0M+yg3bT43AGfgU9w==
   dependencies:
-    "@testim/chrome-version" "^1.0.7"
-    axios "^0.21.1"
+    "@testim/chrome-version" "^1.1.2"
+    axios "^0.27.2"
     del "^6.0.0"
     extract-zip "^2.0.1"
     https-proxy-agent "^5.0.0"
-    mkdirp "^1.0.4"
     proxy-from-env "^1.1.0"
     tcp-port-used "^1.0.1"
 
@@ -3211,6 +3216,13 @@ colors@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
 
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
 commander@2.17.x, commander@~2.17.1:
   version "2.17.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
@@ -3756,6 +3768,11 @@ del@^6.0.0:
     rimraf "^3.0.2"
     slash "^3.0.0"
 
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
 delegates@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -4756,15 +4773,24 @@ follow-redirects@^1.0.0:
   dependencies:
     debug "=3.1.0"
 
-follow-redirects@^1.14.0:
-  version "1.14.9"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
-  integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
+follow-redirects@^1.14.9:
+  version "1.15.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
+  integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
 
 for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
 
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
 formatio@1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
@@ -5230,7 +5256,7 @@ he@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
 
-he@1.2.x, he@^1.1.0:
+he@1.2.x, he@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@@ -6948,19 +6974,19 @@ mime-db@1.52.0:
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
   integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
 
-mime-types@~2.1.24:
-  version "2.1.24"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
-  dependencies:
-    mime-db "1.40.0"
-
-mime-types@~2.1.34:
+mime-types@^2.1.12, mime-types@~2.1.34:
   version "2.1.35"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
   integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
   dependencies:
     mime-db "1.52.0"
 
+mime-types@~2.1.24:
+  version "2.1.24"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
+  dependencies:
+    mime-db "1.40.0"
+
 mime@1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
@@ -9026,10 +9052,10 @@ sass-loader@7.3.1:
     pify "^4.0.1"
     semver "^6.3.0"
 
-sass@1.53.0:
-  version "1.53.0"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.53.0.tgz#eab73a7baac045cc57ddc1d1ff501ad2659952eb"
-  integrity sha512-zb/oMirbKhUgRQ0/GFz8TSAwRq2IlR29vOUJZOx0l8sV+CkHUfHa4u5nqrG+1VceZp7Jfj59SVW9ogdhTvJDcQ==
+sass@1.54.0:
+  version "1.54.0"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.0.tgz#24873673265e2a4fe3d3a997f714971db2fba1f4"
+  integrity sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==
   dependencies:
     chokidar ">=3.0.0 <4.0.0"
     immutable "^4.0.0"
@@ -10342,11 +10368,6 @@ void-elements@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
 
-vue-demi@^0.12.0:
-  version "0.12.4"
-  resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.4.tgz#420dd17628f95f1bbce1102ad3c51074713a8049"
-  integrity sha512-ztPDkFt0TSUdoq1ZI6oD730vgztBkiByhUW7L1cOTebiSBqSYfSQgnhYakYigBkyAybqCTH7h44yZuDJf2xILQ==
-
 vue-demi@^0.13.4:
   version "0.13.5"
   resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.5.tgz#d5eddbc9eaefb89ce5995269d1fa6b0486312092"
@@ -10389,31 +10410,32 @@ vue-router@4.1.2:
   dependencies:
     "@vue/devtools-api" "^6.1.4"
 
-vue-style-loader@4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8"
+vue-style-loader@4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35"
+  integrity sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==
   dependencies:
     hash-sum "^1.0.2"
     loader-utils "^1.0.2"
 
-vue-template-compiler@2.6.11:
-  version "2.6.11"
-  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080"
-  integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==
+vue-template-compiler@2.7.8:
+  version "2.7.8"
+  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.8.tgz#eadd54ed8fbff55b7deb07093a976c07f451a1dc"
+  integrity sha512-eQqdcUpJKJpBRPDdxCNsqUoT0edNvdt1jFjtVnVS/LPPmr0BU2jWzXlrf6BVMeODtdLewB3j8j3WjNiB+V+giw==
   dependencies:
     de-indent "^1.0.2"
-    he "^1.1.0"
-
-vue@^3.2.31:
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.31.tgz#e0c49924335e9f188352816788a4cca10f817ce6"
-  integrity sha512-odT3W2tcffTiQCy57nOT93INw1auq5lYLLYtWpPYQQYQOOdHiqFct9Xhna6GJ+pJQaF67yZABraH47oywkJgFw==
-  dependencies:
-    "@vue/compiler-dom" "3.2.31"
-    "@vue/compiler-sfc" "3.2.31"
-    "@vue/runtime-dom" "3.2.31"
-    "@vue/server-renderer" "3.2.31"
-    "@vue/shared" "3.2.31"
+    he "^1.2.0"
+
+vue@3.2.37:
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e"
+  integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==
+  dependencies:
+    "@vue/compiler-dom" "3.2.37"
+    "@vue/compiler-sfc" "3.2.37"
+    "@vue/runtime-dom" "3.2.37"
+    "@vue/server-renderer" "3.2.37"
+    "@vue/shared" "3.2.37"
 
 vuex@4.0.2:
   version "4.0.2"