logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: 887fac5addc2416504593cb62a52a3b08f3638e2
parent af734afe3694ff3c77d4457b93b02309c4f55d6d
Author: Henry Jameson <me@hjkos.com>
Date:   Tue, 30 Aug 2022 23:54:16 +0300

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

* origin/develop: (59 commits)
  a11y
  Use dedicated indicator for non-ascii domain names
  add a favorites "timeline" shortcut
  refactor navigation-entry and use them in other nav items
  Update dependency sinon-chai to v3
  Update dependency semver to v7
  Update dependency vue-router to v4.1.5
  Update dependency eslint to v8.23.0
  Update dependency vue-template-compiler to v2.7.10
  Update dependency @vue/babel-helper-vue-jsx-merge-props to v1.4.0
  Update dependency eslint-plugin-promise to v6.0.1
  fix lists edit page
  change ugly checkbox to a list element that doesn't look too much out of place
  a11y
  squeeze/stretch pinned items as long as there's enough space for it, hide items that won't fitc
  Remove isparta
  lint
  fix being unable to edit timeline pins on mobile
  aria
  fix mobile side drawer causing issues
  ...

Diffstat:

Mpackage.json17++++++++---------
Msrc/App.js4++++
Msrc/App.scss26++++++++++++++++++++++++--
Msrc/App.vue2+-
Msrc/boot/routes.js5++++-
Msrc/components/account_actions/account_actions.js4+++-
Msrc/components/account_actions/account_actions.vue1+
Msrc/components/basic_user_card/basic_user_card.js4+++-
Msrc/components/basic_user_card/basic_user_card.vue8+++-----
Msrc/components/desktop_nav/desktop_nav.scss4++++
Msrc/components/desktop_nav/desktop_nav.vue1+
Msrc/components/emoji_input/emoji_input.js4+++-
Msrc/components/emoji_input/emoji_input.vue16+++++++++++++++-
Msrc/components/emoji_input/suggestor.js11++++++-----
Msrc/components/lists/lists.js7+------
Msrc/components/lists/lists.vue24+++++++++++++-----------
Msrc/components/lists_edit/lists_edit.js110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/components/lists_edit/lists_edit.vue232++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/components/lists_menu/lists_menu_content.js29+++++++++--------------------
Msrc/components/lists_menu/lists_menu_content.vue17++++++-----------
Dsrc/components/lists_new/lists_new.js79-------------------------------------------------------------------------------
Dsrc/components/lists_new/lists_new.vue95-------------------------------------------------------------------------------
Msrc/components/lists_timeline/lists_timeline.js4++--
Msrc/components/lists_user_search/lists_user_search.js7++++++-
Msrc/components/lists_user_search/lists_user_search.vue20+++++++++++---------
Msrc/components/mention_link/mention_link.js2++
Msrc/components/mention_link/mention_link.vue3+++
Msrc/components/mobile_nav/mobile_nav.js9+++++++--
Msrc/components/mobile_nav/mobile_nav.vue29++++++++++++++++-------------
Msrc/components/mobile_post_status_button/mobile_post_status_button.js3++-
Msrc/components/nav_panel/nav_panel.js75++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/components/nav_panel/nav_panel.vue262+++++++++++++++++++++++++++++--------------------------------------------------
Asrc/components/navigation/filter.js18++++++++++++++++++
Asrc/components/navigation/navigation.js75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/navigation/navigation_entry.js47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/navigation/navigation_entry.vue120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/navigation/navigation_pins.js88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/navigation/navigation_pins.vue76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/notification/notification.js4+++-
Msrc/components/notification/notification.vue21++++++++++-----------
Msrc/components/popover/popover.js25+++++++++++++++++++++++--
Msrc/components/popover/popover.vue7+++++++
Msrc/components/search_bar/search_bar.vue2++
Msrc/components/settings_modal/tabs/general_tab.vue5-----
Msrc/components/side_drawer/side_drawer.js13+++++++++++--
Msrc/components/side_drawer/side_drawer.vue14+++++++++++++-
Msrc/components/status/status.js4+++-
Msrc/components/status/status.vue16++++++++--------
Msrc/components/tab_switcher/tab_switcher.scss1+
Msrc/components/timeline/timeline.vue5++++-
Msrc/components/timeline_menu/timeline_menu.js16+++++++++++++---
Msrc/components/timeline_menu/timeline_menu.vue17++++++++++++++---
Dsrc/components/timeline_menu/timeline_menu_content.js29-----------------------------
Dsrc/components/timeline_menu/timeline_menu_content.vue66------------------------------------------------------------------
Asrc/components/unicode_domain_indicator/unicode_domain_indicator.vue26++++++++++++++++++++++++++
Msrc/components/update_notification/update_notification.js4++--
Msrc/components/update_notification/update_notification.vue2+-
Msrc/components/user_card/user_card.js4+++-
Msrc/components/user_card/user_card.vue9+++------
Asrc/components/user_link/user_link.vue38++++++++++++++++++++++++++++++++++++++
Asrc/components/user_list_menu/user_list_menu.js93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/user_list_menu/user_list_menu.vue38++++++++++++++++++++++++++++++++++++++
Msrc/components/user_list_popover/user_list_popover.js2++
Msrc/components/user_list_popover/user_list_popover.vue2+-
Msrc/components/user_reporting_modal/user_reporting_modal.js4+++-
Msrc/components/user_reporting_modal/user_reporting_modal.vue10+++++++---
Msrc/i18n/en.json26++++++++++++++++++++++++--
Msrc/modules/api.js3+++
Msrc/modules/config.js1-
Msrc/modules/lists.js100++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/modules/serverSideStorage.js223++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/modules/users.js17+++++++++++++++++
Msrc/panel.scss13+++++++++++--
Msrc/services/api/api.service.js35++++++++++++++++++++++-------------
Msrc/services/entity_normalizer/entity_normalizer.service.js8++++++--
Mtest/unit/karma.conf.js21---------------------
Mtest/unit/specs/modules/lists.spec.js16++++++++--------
Mtest/unit/specs/modules/serverSideStorage.spec.js148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/unit/specs/services/entity_normalizer/entity_normalizer.spec.js3++-
Myarn.lock647++++++++++++++++---------------------------------------------------------------
80 files changed, 1971 insertions(+), 1305 deletions(-)

diff --git a/package.json b/package.json @@ -43,19 +43,19 @@ "utf8": "3.0.0", "vue": "3.2.37", "vue-i18n": "9.2.2", - "vue-router": "4.1.3", - "vue-template-compiler": "2.7.9", + "vue-router": "4.1.5", + "vue-template-compiler": "2.7.10", "vuex": "4.0.2" }, "devDependencies": { - "@babel/core": "7.18.10", + "@babel/core": "7.18.13", "@babel/eslint-parser": "7.18.9", "@babel/plugin-transform-runtime": "7.18.10", "@babel/preset-env": "7.18.10", "@babel/register": "7.18.9", "@intlify/vue-i18n-loader": "5.0.0", "@ungap/event-target": "0.2.3", - "@vue/babel-helper-vue-jsx-merge-props": "1.2.1", + "@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-plugin-jsx": "1.1.1", "@vue/compiler-sfc": "3.2.37", "@vue/test-utils": "2.0.2", @@ -71,12 +71,12 @@ "css-loader": "6.7.1", "css-minimizer-webpack-plugin": "4.0.0", "custom-event-polyfill": "1.0.7", - "eslint": "8.22.0", + "eslint": "8.23.0", "eslint-config-standard": "17.0.0", "eslint-formatter-friendly": "7.0.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-n": "15.2.5", - "eslint-plugin-promise": "6.0.0", + "eslint-plugin-promise": "6.0.1", "eslint-plugin-vue": "9.4.0", "eslint-webpack-plugin": "3.2.0", "eventsource-polyfill": "0.9.6", @@ -85,7 +85,6 @@ "html-webpack-plugin": "5.5.0", "http-proxy-middleware": "2.0.6", "iso-639-1": "2.1.15", - "isparta-loader": "2.0.0", "json-loader": "0.5.7", "karma": "6.4.0", "karma-coverage": "2.2.0", @@ -108,11 +107,11 @@ "sass": "1.54.5", "sass-loader": "13.0.2", "selenium-server": "2.53.1", - "semver": "5.7.1", + "semver": "7.3.7", "serviceworker-webpack5-plugin": "2.0.0", "shelljs": "0.8.5", "sinon": "2.4.1", - "sinon-chai": "2.14.0", + "sinon-chai": "3.7.0", "stylelint": "13.13.1", "stylelint-config-standard": "20.0.0", "stylelint-rscss": "0.4.0", diff --git a/src/App.js b/src/App.js @@ -92,8 +92,12 @@ export default { isChats () { return this.$route.name === 'chat' || this.$route.name === 'chats' }, + isListEdit () { + return this.$route.name === 'lists-edit' + }, newPostButtonShown () { if (this.isChats) return false + if (this.isListEdit) return false return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, diff --git a/src/App.scss b/src/App.scss @@ -117,12 +117,28 @@ h4 { margin: 0; } +.iconLetter { + display: inline-block; + text-align: center; + font-weight: 1000; +} + i[class*=icon-], -.svg-inline--fa { +.svg-inline--fa, +.iconLetter { color: $fallback--icon; color: var(--icon, $fallback--icon); } +.button-unstyled:hover, +a:hover { + > i[class*=icon-], + > .svg-inline--fa, + > .iconLetter { + color: var(--text); + } +} + nav { z-index: var(--ZI_navbar); color: var(--topBarText); @@ -765,17 +781,23 @@ option { } .fa-scale-110 { - &.svg-inline--fa { + &.svg-inline--fa, + &.iconLetter { font-size: 1.1em; } } .fa-old-padding { + &.iconLetter, &.svg-inline--fa, &-layer { padding: 0 0.3em; } } +.veryfaint { + opacity: 0.25; +} + .login-hint { text-align: center; diff --git a/src/App.vue b/src/App.vue @@ -36,7 +36,7 @@ <div id="main-scroller" class="column main" - :class="{ '-full-height': isChats }" + :class="{ '-full-height': isChats || isListEdit }" > <div v-if="!currentUser" diff --git a/src/boot/routes.js b/src/boot/routes.js @@ -23,6 +23,7 @@ import RemoteUserResolver from 'components/remote_user_resolver/remote_user_reso import Lists from 'components/lists/lists.vue' import ListsTimeline from 'components/lists_timeline/lists_timeline.vue' import ListsEdit from 'components/lists_edit/lists_edit.vue' +import NavPanel from 'src/components/nav_panel/nav_panel.vue' export default (store) => { const validateAuthenticatedRoute = (to, from, next) => { @@ -79,7 +80,9 @@ export default (store) => { { name: 'legacy-user-profile', path: '/:name', component: UserProfile }, { name: 'lists', path: '/lists', component: Lists }, { name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline }, - { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit } + { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit }, + { name: 'lists-new', path: '/lists/new', component: ListsEdit }, + { name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute } ] if (store.state.instance.pleromaChatMessagesAvailable) { diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js @@ -1,6 +1,7 @@ import { mapState } from 'vuex' import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' +import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faEllipsisV @@ -19,7 +20,8 @@ const AccountActions = { }, components: { ProgressButton, - Popover + Popover, + UserListMenu }, methods: { showRepeats () { diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue @@ -28,6 +28,7 @@ class="dropdown-divider" /> </template> + <UserListMenu :user="user" /> <button v-if="relationship.blocking" class="btn button-default btn-block dropdown-item" diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js @@ -1,5 +1,6 @@ import UserPopover from '../user_popover/user_popover.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import UserLink from '../user_link/user_link.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -10,7 +11,8 @@ const BasicUserCard = { components: { UserPopover, UserAvatar, - RichContent + RichContent, + UserLink }, methods: { userProfileLink (user) { diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue @@ -30,12 +30,10 @@ /> </div> <div> - <router-link + <user-link class="basic-user-card-screen-name" - :to="userProfileLink(user)" - > - @{{ user.screen_name_ui }} - </router-link> + :user="user" + /> </div> <slot /> </div> diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss @@ -137,4 +137,8 @@ text-align: right; } } + + .spacer { + width: 1em; + } } diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue @@ -61,6 +61,7 @@ :title="$t('nav.administration')" /> </a> + <span class="spacer" /> <button v-if="currentUser" class="button-unstyled nav-icon" diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js @@ -1,5 +1,6 @@ import Completion from '../../services/completion/completion.js' import EmojiPicker from '../emoji_picker/emoji_picker.vue' +import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' import { take } from 'lodash' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' @@ -120,7 +121,8 @@ const EmojiInput = { } }, components: { - EmojiPicker + EmojiPicker, + UnicodeDomainIndicator }, computed: { padEmoji () { diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue @@ -50,7 +50,21 @@ <span v-else>{{ suggestion.replacement }}</span> </span> <div class="label"> - <span class="displayText">{{ suggestion.displayText }}</span> + <span + v-if="suggestion.user" + class="displayText" + > + {{ suggestion.displayText }}<UnicodeDomainIndicator + :user="suggestion.user" + :at="false" + /> + </span> + <span + v-if="!suggestion.user" + class="displayText" + > + {{ suggestion.displayText }} + </span> <span class="detailText">{{ suggestion.detailText }}</span> </div> </div> diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js @@ -116,11 +116,12 @@ export const suggestUsers = ({ dispatch, state }) => { return diff + nameAlphabetically + screenNameAlphabetically /* eslint-disable camelcase */ - }).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({ - displayText: screen_name_ui, - detailText: name, - imageUrl: profile_image_url_original, - replacement: '@' + screen_name + ' ' + }).map((user) => ({ + user, + displayText: user.screen_name_ui, + detailText: user.name, + imageUrl: user.profile_image_url_original, + replacement: '@' + user.screen_name + ' ' })) /* eslint-enable camelcase */ diff --git a/src/components/lists/lists.js b/src/components/lists/lists.js @@ -1,5 +1,4 @@ import ListsCard from '../lists_card/lists_card.vue' -import ListsNew from '../lists_new/lists_new.vue' const Lists = { data () { @@ -8,11 +7,7 @@ const Lists = { } }, components: { - ListsCard, - ListsNew - }, - created () { - this.$store.dispatch('startFetchingLists') + ListsCard }, computed: { lists () { diff --git a/src/components/lists/lists.vue b/src/components/lists/lists.vue @@ -1,21 +1,15 @@ <template> - <div v-if="isNew"> - <ListsNew @cancel="cancelNewList" /> - </div> - <div - v-else - class="settings panel panel-default" - > + <div class="Lists panel panel-default"> <div class="panel-heading"> <div class="title"> {{ $t('lists.lists') }} </div> - <button - class="button-default" - @click="newList" + <router-link + :to="{ name: 'lists-new' }" + class="button-default btn new-list-button" > {{ $t("lists.new") }} - </button> + </router-link> </div> <div class="panel-body"> <ListsCard @@ -29,3 +23,11 @@ </template> <script src="./lists.js"></script> + +<style lang="scss"> +.Lists { + .new-list-button { + padding: 0 0.5em; + } +} +</style> diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js @@ -1,7 +1,9 @@ import { mapState, mapGetters } from 'vuex' import BasicUserCard from '../basic_user_card/basic_user_card.vue' import ListsUserSearch from '../lists_user_search/lists_user_search.vue' +import PanelLoading from 'src/components/panel_loading/panel_loading.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import { library } from '@fortawesome/fontawesome-svg-core' import { faSearch, @@ -17,22 +19,33 @@ const ListsNew = { components: { BasicUserCard, UserAvatar, - ListsUserSearch + ListsUserSearch, + TabSwitcher, + PanelLoading }, data () { return { title: '', - userIds: [], - selectedUserIds: [] + titleDraft: '', + membersUserIds: [], + removedUserIds: new Set([]), // users we added for members, to undo + searchUserIds: [], + addedUserIds: new Set([]), // users we added from search, to undo + searchLoading: false, + reallyDelete: false } }, created () { - this.$store.dispatch('fetchList', { id: this.id }) - .then(() => { this.title = this.findListTitle(this.id) }) - this.$store.dispatch('fetchListAccounts', { id: this.id }) + if (!this.id) return + this.$store.dispatch('fetchList', { listId: this.id }) .then(() => { - this.selectedUserIds = this.findListAccounts(this.id) - this.selectedUserIds.forEach(userId => { + this.title = this.findListTitle(this.id) + this.titleDraft = this.title + }) + this.$store.dispatch('fetchListAccounts', { listId: this.id }) + .then(() => { + this.membersUserIds = this.findListAccounts(this.id) + this.membersUserIds.forEach(userId => { this.$store.dispatch('fetchUserIfMissing', userId) }) }) @@ -41,11 +54,12 @@ const ListsNew = { id () { return this.$route.params.id }, - users () { - return this.userIds.map(userId => this.findUser(userId)) + membersUsers () { + return [...this.membersUserIds, ...this.addedUserIds] + .map(userId => this.findUser(userId)).filter(user => user) }, - selectedUsers () { - return this.selectedUserIds.map(userId => this.findUser(userId)).filter(user => user) + searchUsers () { + return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user) }, ...mapState({ currentUser: state => state.users.currentUser @@ -56,33 +70,73 @@ const ListsNew = { onInput () { this.search(this.query) }, - selectUser (user) { - if (this.selectedUserIds.includes(user.id)) { - this.removeUser(user.id) + toggleRemoveMember (user) { + if (this.removedUserIds.has(user.id)) { + this.id && this.addUser(user) + this.removedUserIds.delete(user.id) } else { - this.addUser(user) + this.id && this.removeUser(user.id) + this.removedUserIds.add(user.id) } }, - isSelected (user) { - return this.selectedUserIds.includes(user.id) + toggleAddFromSearch (user) { + if (this.addedUserIds.has(user.id)) { + this.id && this.removeUser(user.id) + this.addedUserIds.delete(user.id) + } else { + this.id && this.addUser(user) + this.addedUserIds.add(user.id) + } + }, + isRemoved (user) { + return this.removedUserIds.has(user.id) + }, + isAdded (user) { + return this.addedUserIds.has(user.id) }, addUser (user) { - this.selectedUserIds.push(user.id) + this.$store.dispatch('addListAccount', { accountId: this.user.id, listId: this.id }) }, removeUser (userId) { - this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) + this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id }) }, - onResults (results) { - this.userIds = results + onSearchLoading (results) { + this.searchLoading = true }, - updateList () { - this.$store.dispatch('setList', { id: this.id, title: this.title }) - this.$store.dispatch('setListAccounts', { id: this.id, accountIds: this.selectedUserIds }) - - this.$router.push({ name: 'lists-timeline', params: { id: this.id } }) + onSearchLoadingDone (results) { + this.searchLoading = false + }, + onSearchResults (results) { + this.searchLoading = false + this.searchUserIds = results + }, + updateListTitle () { + this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft }) + .then(() => { + this.title = this.findListTitle(this.id) + }) + }, + createList () { + this.$store.dispatch('createList', { title: this.titleDraft }) + .then((list) => { + return this + .$store + .dispatch('setListAccounts', { listId: list.id, accountIds: [...this.addedUserIds] }) + .then(() => list.id) + }) + .then((listId) => { + this.$router.push({ name: 'lists-timeline', params: { id: listId } }) + }) + .catch((e) => { + this.$store.dispatch('pushGlobalNotice', { + messageKey: 'lists.error', + messageArgs: [e.message], + level: 'error' + }) + }) }, deleteList () { - this.$store.dispatch('deleteList', { id: this.id }) + this.$store.dispatch('deleteList', { listId: this.id }) this.$router.push({ name: 'lists' }) } } diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue @@ -1,8 +1,8 @@ <template> - <div class="panel-default panel list-edit"> + <div class="panel-default panel ListEdit"> <div ref="header" - class="panel-heading" + class="panel-heading list-edit-heading" > <button class="button-unstyled go-back-button" @@ -13,54 +13,151 @@ icon="chevron-left" /> </button> + <div class="title"> + <i18n-t + v-if="id" + keypath="lists.editing_list" + > + <template #listTitle> + {{ title }} + </template> + </i18n-t> + <i18n-t + v-else + keypath="lists.creating_list" + /> + </div> </div> - <div class="input-wrap"> - <input - ref="title" - v-model="title" - :placeholder="$t('lists.title')" + <div class="panel-body"> + <div class="input-wrap"> + <label for="list-edit-title">{{ $t('lists.title') }}</label> + {{ ' ' }} + <input + id="list-edit-title" + ref="title" + v-model="titleDraft" + > + <button + v-if="id" + class="btn button-default follow-button" + @click="updateListTitle" + > + {{ $t('lists.update_title') }} + </button> + </div> + <tab-switcher + class="list-member-management" + :scrollable-tabs="true" > + <div + v-if="id || addedUserIds.size > 0" + :label="$t('lists.manage_members')" + class="members-list" + > + <div class="users-list"> + <div + v-for="user in membersUsers" + :key="user.id" + class="member" + > + <BasicUserCard + :user="user" + > + <button + class="btn button-default follow-button" + @click="toggleRemoveMember(user)" + > + {{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }} + </button> + </BasicUserCard> + </div> + </div> + </div> + + <div + class="search-list" + :label="$t('lists.add_members')" + > + <ListsUserSearch + @results="onSearchResults" + @loading="onSearchLoading" + @loadingDone="onSearchLoadingDone" + /> + <div + v-if="searchLoading" + class="loading" + > + <PanelLoading /> + </div> + <div + v-else + class="users-list" + > + <div + v-for="user in searchUsers" + :key="user.id" + class="member" + > + <BasicUserCard + :user="user" + > + <span + v-if="membersUserIds.includes(user.id)" + > + {{ $t('lists.is_in_list') }} + </span> + <button + v-if="!membersUserIds.includes(user.id)" + class="btn button-default follow-button" + @click="toggleAddFromSearch(user)" + > + {{ isAdded(user) ? $t('general.undo') : $t('lists.add_to_list') }} + </button> + <button + v-else + class="btn button-default follow-button" + @click="toggleRemoveMember(user)" + > + {{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }} + </button> + </BasicUserCard> + </div> + </div> + </div> + </tab-switcher> </div> - <div class="member-list"> - <div - v-for="user in selectedUsers" - :key="user.id" - class="member" + <div class="panel-footer"> + <span class="spacer" /> + <button + v-if="!id" + class="btn button-default footer-button" + @click="createList" > - <BasicUserCard - :user="user" - :class="isSelected(user) ? 'selected' : ''" - @click.capture.prevent="selectUser(user)" - /> - </div> - </div> - <ListsUserSearch @results="onResults" /> - <div class="member-list"> - <div - v-for="user in users" - :key="user.id" - class="member" + {{ $t('lists.create') }} + </button> + <button + v-else-if="!reallyDelete" + class="btn button-default footer-button" + @click="reallyDelete = true" > - <BasicUserCard - :user="user" - :class="isSelected(user) ? 'selected' : ''" - @click.capture.prevent="selectUser(user)" - /> - </div> + {{ $t('lists.delete') }} + </button> + <template v-else> + {{ $t('lists.really_delete') }} + <button + class="btn button-default footer-button" + @click="deleteList" + > + {{ $t('general.yes') }} + </button> + <button + class="btn button-default footer-button" + @click="reallyDelete = false" + > + {{ $t('general.no') }} + </button> + </template> </div> - <button - :disabled="title && title.length === 0" - class="btn button-default" - @click="updateList" - > - {{ $t('lists.save') }} - </button> - <button - class="btn button-default" - @click="deleteList" - > - {{ $t('lists.delete') }} - </button> </div> </template> @@ -69,28 +166,43 @@ <style lang="scss"> @import '../../_variables.scss'; -.list-edit { - .input-wrap { +.ListEdit { + --panel-body-padding: 0.5em; + + height: calc(100vh - var(--navbar-height)); + overflow: hidden; + display: flex; + flex-direction: column; + + .list-edit-heading { + grid-template-columns: auto minmax(50%, 1fr); + } + + .panel-body { display: flex; - margin: 0.7em 0.5em 0.7em 0.5em; + flex: 1; + flex-direction: column; + overflow: hidden; + } - input { - width: 100%; - } + .list-member-management { + flex: 1 0 auto; } .search-icon { margin-right: 0.3em; } - .member-list { + .users-list { padding-bottom: 0.7rem; + overflow-y: auto; } - .basic-user-card:hover, - .basic-user-card.selected { - cursor: pointer; - background-color: var(--selectedPost, $fallback--lightBg); + & .search-list, + & .members-list { + overflow: hidden; + flex-direction: column; + min-height: 0; } .go-back-button { @@ -102,7 +214,15 @@ } .btn { - margin: 0.5em; + margin: 0 0.5em; + } + + .panel-footer { + grid-template-columns: minmax(10%, 1fr); + + .footer-button { + min-width: 9em; + } } } </style> diff --git a/src/components/lists_menu/lists_menu_content.js b/src/components/lists_menu/lists_menu_content.js @@ -1,28 +1,17 @@ import { mapState } from 'vuex' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome -} from '@fortawesome/free-solid-svg-icons' +import NavigationEntry from 'src/components/navigation/navigation_entry.vue' +import { getListEntries } from 'src/components/navigation/filter.js' -library.add( - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome -) - -const ListsMenuContent = { - created () { - this.$store.dispatch('startFetchingLists') +export const ListsMenuContent = { + props: [ + 'showPin' + ], + components: { + NavigationEntry }, computed: { ...mapState({ - lists: state => state.lists.allLists, + lists: getListEntries, currentUser: state => state.users.currentUser, privateMode: state => state.instance.private, federating: state => state.instance.federating diff --git a/src/components/lists_menu/lists_menu_content.vue b/src/components/lists_menu/lists_menu_content.vue @@ -1,16 +1,11 @@ <template> <ul> - <li - v-for="list in lists.slice().reverse()" - :key="list.id" - > - <router-link - class="menu-item" - :to="{ name: 'lists-timeline', params: { id: list.id } }" - > - {{ list.title }} - </router-link> - </li> + <NavigationEntry + v-for="item in lists" + :key="item.name" + :show-pin="showPin" + :item="item" + /> </ul> </template> diff --git a/src/components/lists_new/lists_new.js b/src/components/lists_new/lists_new.js @@ -1,79 +0,0 @@ -import { mapState, mapGetters } from 'vuex' -import BasicUserCard from '../basic_user_card/basic_user_card.vue' -import UserAvatar from '../user_avatar/user_avatar.vue' -import ListsUserSearch from '../lists_user_search/lists_user_search.vue' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faSearch, - faChevronLeft -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faSearch, - faChevronLeft -) - -const ListsNew = { - components: { - BasicUserCard, - UserAvatar, - ListsUserSearch - }, - data () { - return { - title: '', - userIds: [], - selectedUserIds: [] - } - }, - computed: { - users () { - return this.userIds.map(userId => this.findUser(userId)) - }, - selectedUsers () { - return this.selectedUserIds.map(userId => this.findUser(userId)) - }, - ...mapState({ - currentUser: state => state.users.currentUser - }), - ...mapGetters(['findUser']) - }, - methods: { - goBack () { - this.$emit('cancel') - }, - onInput () { - this.search(this.query) - }, - selectUser (user) { - if (this.selectedUserIds.includes(user.id)) { - this.removeUser(user.id) - } else { - this.addUser(user) - } - }, - isSelected (user) { - return this.selectedUserIds.includes(user.id) - }, - addUser (user) { - this.selectedUserIds.push(user.id) - }, - removeUser (userId) { - this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) - }, - onResults (results) { - this.userIds = results - }, - createList () { - // the API has two different endpoints for "creating a list with a name" - // and "updating the accounts on the list". - this.$store.dispatch('createList', { title: this.title }) - .then((list) => { - this.$store.dispatch('setListAccounts', { id: list.id, accountIds: this.selectedUserIds }) - this.$router.push({ name: 'lists-timeline', params: { id: list.id } }) - }) - } - } -} - -export default ListsNew diff --git a/src/components/lists_new/lists_new.vue b/src/components/lists_new/lists_new.vue @@ -1,95 +0,0 @@ -<template> - <div class="panel-default panel list-new"> - <div - ref="header" - class="panel-heading" - > - <button - class="button-unstyled go-back-button" - @click="goBack" - > - <FAIcon - size="lg" - icon="chevron-left" - /> - </button> - </div> - <div class="input-wrap"> - <input - ref="title" - v-model="title" - :placeholder="$t('lists.title')" - > - </div> - - <div class="member-list"> - <div - v-for="user in selectedUsers" - :key="user.id" - class="member" - > - <BasicUserCard - :user="user" - :class="isSelected(user) ? 'selected' : ''" - @click.capture.prevent="selectUser(user)" - /> - </div> - </div> - <ListsUserSearch - @results="onResults" - /> - <div - v-for="user in users" - :key="user.id" - class="member" - > - <BasicUserCard - :user="user" - :class="isSelected(user) ? 'selected' : ''" - @click.capture.prevent="selectUser(user)" - /> - </div> - - <button - :disabled="title && title.length === 0" - class="btn button-default" - @click="createList" - > - {{ $t('lists.create') }} - </button> - </div> -</template> - -<script src="./lists_new.js"></script> - -<style lang="scss"> -@import '../../_variables.scss'; - -.list-new { - .search-icon { - margin-right: 0.3em; - } - - .member-list { - padding-bottom: 0.7rem; - } - - .basic-user-card:hover, - .basic-user-card.selected { - cursor: pointer; - background-color: var(--selectedPost, $fallback--lightBg); - } - - .go-back-button { - text-align: center; - line-height: 1; - height: 100%; - align-self: start; - width: var(--__panel-heading-height-inner); - } - - .btn { - margin: 0.5em; - } -} -</style> diff --git a/src/components/lists_timeline/lists_timeline.js b/src/components/lists_timeline/lists_timeline.js @@ -17,14 +17,14 @@ const ListsTimeline = { this.listId = route.params.id this.$store.dispatch('stopFetchingTimeline', 'list') this.$store.commit('clearTimeline', { timeline: 'list' }) - this.$store.dispatch('fetchList', { id: this.listId }) + this.$store.dispatch('fetchList', { listId: this.listId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) } } }, created () { this.listId = this.$route.params.id - this.$store.dispatch('fetchList', { id: this.listId }) + this.$store.dispatch('fetchList', { listId: this.listId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) }, unmounted () { diff --git a/src/components/lists_user_search/lists_user_search.js b/src/components/lists_user_search/lists_user_search.js @@ -15,6 +15,7 @@ const ListsUserSearch = { components: { Checkbox }, + emits: ['loading', 'loadingDone', 'results'], data () { return { loading: false, @@ -33,12 +34,16 @@ const ListsUserSearch = { } this.loading = true + this.$emit('loading') this.userIds = [] this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly }) .then(data => { - this.loading = false this.$emit('results', data.accounts.map(a => a.id)) }) + .finally(() => { + this.loading = false + this.$emit('loadingDone') + }) } } } diff --git a/src/components/lists_user_search/lists_user_search.vue b/src/components/lists_user_search/lists_user_search.vue @@ -1,5 +1,5 @@ <template> - <div> + <div class="ListsUserSearch"> <div class="input-wrap"> <div class="input-search"> <FAIcon @@ -29,17 +29,19 @@ <style lang="scss"> @import '../../_variables.scss'; -.input-wrap { - display: flex; - margin: 0.7em 0.5em 0.7em 0.5em; +.ListsUserSearch { + .input-wrap { + display: flex; + margin: 0.7em 0.5em 0.7em 0.5em; - input { - width: 100%; + input { + width: 100%; + } } -} -.search-icon { - margin-right: 0.3em; + .search-icon { + margin-right: 0.3em; + } } </style> diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js @@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p import { mapGetters, mapState } from 'vuex' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import UserAvatar from '../user_avatar/user_avatar.vue' +import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' import { defineAsyncComponent } from 'vue' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -16,6 +17,7 @@ const MentionLink = { name: 'MentionLink', components: { UserAvatar, + UnicodeDomainIndicator, UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue')) }, props: { diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue @@ -47,6 +47,9 @@ class="serverName" :class="{ '-faded': shouldFadeDomain }" v-html="'@' + serverName" + /><UnicodeDomainIndicator + v-if="shouldShowFullUserName" + :user="user" /> </span> <span diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js @@ -2,6 +2,7 @@ import SideDrawer from '../side_drawer/side_drawer.vue' import Notifications from '../notifications/notifications.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' +import NavigationPins from 'src/components/navigation/navigation_pins.vue' import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -19,7 +20,8 @@ library.add( const MobileNav = { components: { SideDrawer, - Notifications + Notifications, + NavigationPins }, data: () => ({ notificationsCloseGesture: undefined, @@ -47,7 +49,10 @@ const MobileNav = { isChat () { return this.$route.name === 'chat' }, - ...mapGetters(['unreadChatCount']) + ...mapGetters(['unreadChatCount']), + chatsPinned () { + return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats') + } }, methods: { toggleMobileSidebar () { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue @@ -17,20 +17,12 @@ icon="bars" /> <div - v-if="unreadChatCount" + v-if="unreadChatCount && !chatsPinned" class="alert-dot" /> </button> - <router-link - v-if="!hideSitename" - class="site-name" - :to="{ name: 'root' }" - active-class="home" - > - {{ sitename }} - </router-link> - </div> - <div class="item right"> + <NavigationPins class="pins" /> + </div> <div class="item right"> <button v-if="currentUser" class="button-unstyled mobile-nav-button" @@ -94,6 +86,7 @@ grid-template-columns: 2fr auto; width: 100%; box-sizing: border-box; + a { color: var(--topBarLink, $fallback--link); } @@ -178,13 +171,20 @@ } } + .pins { + flex: 1; + + .pinned-item { + flex-grow: 1; + } + } + .mobile-notifications { margin-top: 50px; width: 100vw; height: calc(100vh - var(--navbar-height)); overflow-x: hidden; overflow-y: scroll; - color: $fallback--text; color: var(--text, $fallback--text); background-color: $fallback--bg; @@ -194,14 +194,17 @@ padding: 0; border-radius: 0; box-shadow: none; + .panel { border-radius: 0; margin: 0; box-shadow: none; } - .panel:after { + + .panel::after { border-radius: 0; } + .panel .panel-heading { border-radius: 0; box-shadow: none; diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -10,7 +10,8 @@ library.add( const HIDDEN_FOR_PAGES = new Set([ 'chats', - 'chat' + 'chat', + 'lists-edit' ]) const MobilePostStatusButton = { diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js @@ -1,6 +1,10 @@ -import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue' -import ListsMenuContent from '../lists_menu/lists_menu_content.vue' +import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue' import { mapState, mapGetters } from 'vuex' +import { TIMELINES, ROOT_ITEMS } from 'src/components/navigation/navigation.js' +import { filterNavigation } from 'src/components/navigation/filter.js' +import NavigationEntry from 'src/components/navigation/navigation_entry.vue' +import NavigationPins from 'src/components/navigation/navigation_pins.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -30,21 +34,23 @@ library.add( faStream, faList ) - const NavPanel = { + props: ['forceExpand', 'forceEditMode'], created () { - if (this.currentUser && this.currentUser.locked) { - this.$store.dispatch('startFetchingFollowRequests') - } }, components: { - TimelineMenuContent, - ListsMenuContent + ListsMenuContent, + NavigationEntry, + NavigationPins, + Checkbox }, data () { return { + editMode: false, showTimelines: false, - showLists: false + showLists: false, + timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })), + rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k })) } }, methods: { @@ -53,19 +59,62 @@ const NavPanel = { }, toggleLists () { this.showLists = !this.showLists + }, + toggleEditMode () { + this.editMode = !this.editMode + }, + toggleCollapse () { + this.$store.commit('setPreference', { path: 'simple.collapseNav', value: !this.collapsed }) + this.$store.dispatch('pushServerSideStorage') + }, + isPinned (item) { + return this.pinnedItems.has(item) + }, + togglePin (item) { + if (this.isPinned(item)) { + this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item }) + } else { + this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item }) + } + this.$store.dispatch('pushServerSideStorage') } }, computed: { - listsNavigation () { - return this.$store.getters.mergedConfig.listsNavigation - }, ...mapState({ currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, privateMode: state => state.instance.private, federating: state => state.instance.federating, - pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems), + collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav }), + timelinesItems () { + return filterNavigation( + Object + .entries({ ...TIMELINES }) + .map(([k, v]) => ({ ...v, name: k })), + { + hasChats: this.pleromaChatMessagesAvailable, + isFederating: this.federating, + isPrivate: this.privateMode, + currentUser: this.currentUser + } + ) + }, + rootItems () { + return filterNavigation( + Object + .entries({ ...ROOT_ITEMS }) + .map(([k, v]) => ({ ...v, name: k })), + { + hasChats: this.pleromaChatMessagesAvailable, + isFederating: this.federating, + isPrivate: this.privateMode, + currentUser: this.currentUser + } + ) + }, ...mapGetters(['unreadChatCount']) } } diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue @@ -1,135 +1,99 @@ <template> <div class="NavPanel"> <div class="panel panel-default"> - <ul> - <li v-if="currentUser || !privateMode"> - <button - class="button-unstyled menu-item" - @click="toggleTimelines" - > - <FAIcon - fixed-width - class="fa-scale-110" - icon="stream" - />{{ $t("nav.timelines") }} - <FAIcon - class="timelines-chevron" - fixed-width - :icon="showTimelines ? 'chevron-up' : 'chevron-down'" - /> - </button> - <div - v-show="showTimelines" - class="timelines-background" - > - <TimelineMenuContent class="timelines" /> - </div> - </li> - <li v-if="currentUser && listsNavigation"> - <button - class="button-unstyled menu-item" - @click="toggleLists" - > - <router-link - :to="{ name: 'lists' }" - @click.stop - > - <FAIcon - fixed-width - class="fa-scale-110" - icon="list" - />{{ $t("nav.lists") }} - </router-link> - <FAIcon - class="timelines-chevron" - fixed-width - :icon="showLists ? 'chevron-up' : 'chevron-down'" + <div + v-if="!forceExpand" + class="panel-heading nav-panel-heading" + > + <NavigationPins :limit="6" /> + <div class="spacer" /> + <button + class="button-unstyled" + @click="toggleCollapse" + > + <FAIcon + class="timelines-chevron" + fixed-width + :icon="collapsed ? 'chevron-down' : 'chevron-up'" + /> + </button> + </div> + <ul + v-if="!collapsed || forceExpand" + class="panel-body" + > + <NavigationEntry + v-if="currentUser || !privateMode" + :show-pin="false" + :item="{ icon: 'stream', label: 'nav.timelines' }" + :aria-expanded="showTimelines ? 'true' : 'false'" + @click="toggleTimelines" + > + <FAIcon + class="timelines-chevron" + fixed-width + :icon="showTimelines ? 'chevron-up' : 'chevron-down'" + /> + </NavigationEntry> + <div + v-show="showTimelines" + class="timelines-background" + > + <div class="timelines"> + <NavigationEntry + v-for="item in timelinesItems" + :key="item.name" + :show-pin="editMode || forceEditMode" + :item="item" /> - </button> - <div - v-show="showLists" - class="timelines-background" - > - <ListsMenuContent class="timelines" /> </div> - </li> - <li v-if="currentUser && !listsNavigation"> + </div> + <NavigationEntry + v-if="currentUser" + :show-pin="false" + :item="{ icon: 'list', label: 'nav.lists' }" + :aria-expanded="showLists ? 'true' : 'false'" + @click="toggleLists" + > <router-link + :title="$t('lists.manage_lists')" + class="extra-button" :to="{ name: 'lists' }" @click.stop > - <button - class="button-unstyled menu-item" - @click="toggleLists" - > - <FAIcon - fixed-width - class="fa-scale-110" - icon="list" - />{{ $t("nav.lists") }} - </button> - </router-link> - </li> - <li v-if="currentUser"> - <router-link - class="menu-item" - :to="{ name: 'interactions', params: { username: currentUser.screen_name } }" - > - <FAIcon - fixed-width - class="fa-scale-110" - icon="bell" - />{{ $t("nav.interactions") }} - </router-link> - </li> - <li v-if="currentUser && pleromaChatMessagesAvailable"> - <router-link - class="menu-item" - :to="{ name: 'chats', params: { username: currentUser.screen_name } }" - > - <div - v-if="unreadChatCount" - class="badge badge-notification" - > - {{ unreadChatCount }} - </div> - <FAIcon - fixed-width - class="fa-scale-110" - icon="comments" - />{{ $t("nav.chats") }} - </router-link> - </li> - <li v-if="currentUser && currentUser.locked"> - <router-link - class="menu-item" - :to="{ name: 'friend-requests' }" - > - <FAIcon - fixed-width - class="fa-scale-110" - icon="user-plus" - />{{ $t("nav.friend_requests") }} - <span - v-if="followRequestCount > 0" - class="badge badge-notification" - > - {{ followRequestCount }} - </span> - </router-link> - </li> - <li> - <router-link - class="menu-item" - :to="{ name: 'about' }" - > <FAIcon + class="extra-button" fixed-width - class="fa-scale-110" - icon="info-circle" - />{{ $t("nav.about") }} + icon="wrench" + /> </router-link> - </li> + <FAIcon + class="timelines-chevron" + fixed-width + :icon="showLists ? 'chevron-up' : 'chevron-down'" + /> + </NavigationEntry> + <div + v-show="showLists" + class="timelines-background" + > + <ListsMenuContent + :show-pin="editMode || forceEditMode" + class="timelines" + /> + </div> + <NavigationEntry + v-for="item in rootItems" + :key="item.name" + :show-pin="editMode || forceEditMode" + :item="item" + /> + <NavigationEntry + v-if="!forceEditMode && currentUser" + :show-pin="false" + :item="{ label: editMode ? $t('nav.edit_finish') : $t('nav.edit_pinned'), icon: editMode ? 'check' : 'wrench' }" + @click="toggleEditMode" + /> </ul> </div> </div> @@ -180,54 +144,23 @@ border: none; } - .menu-item { - display: block; - box-sizing: border-box; - height: 3.5em; - line-height: 3.5em; - padding: 0 1em; - width: 100%; - color: $fallback--link; - color: var(--link, $fallback--link); - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - } - - &.router-link-active { - font-weight: bolder; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - - &:hover { - text-decoration: underline; - } - } - } - .timelines-chevron { margin-left: 0.8em; + margin-right: 0.8em; font-size: 1.1em; } + .menu-item { + .timelines-chevron { + margin-right: 0; + } + } + .timelines-background { padding: 0 0 0 0.6em; background-color: $fallback--lightBg; background-color: var(--selectedMenu, $fallback--lightBg); - border-top: 1px solid; + border-bottom: 1px solid; border-color: $fallback--border; border-color: var(--border, $fallback--border); } @@ -237,14 +170,9 @@ background-color: var(--bg, $fallback--bg); } - .fa-scale-110 { - margin-right: 0.8em; - } - - .badge { - position: absolute; - right: 0.6rem; - top: 1.25em; + .nav-panel-heading { + // breaks without a unit + --panel-heading-height-padding: 0em; } } </style> diff --git a/src/components/navigation/filter.js b/src/components/navigation/filter.js @@ -0,0 +1,18 @@ +export const filterNavigation = (list = [], { hasChats, isFederating, isPrivate, currentUser }) => { + return list.filter(({ criteria, anon, anonRoute }) => { + const set = new Set(criteria || []) + if (!isFederating && set.has('federating')) return false + if (isPrivate && set.has('!private')) return false + if (!currentUser && !(anon || anonRoute)) return false + if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false + if (!hasChats && set.has('chats')) return false + return true + }) +} + +export const getListEntries = state => state.lists.allLists.map(list => ({ + name: 'list-' + list.id, + routeObject: { name: 'lists-timeline', params: { id: list.id } }, + labelRaw: list.title, + iconLetter: list.title[0] +})) diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js @@ -0,0 +1,75 @@ +export const USERNAME_ROUTES = new Set([ + 'bookmarks', + 'dms', + 'interactions', + 'notifications', + 'chat', + 'chats', + 'user-profile' +]) + +export const TIMELINES = { + home: { + route: 'friends', + icon: 'home', + label: 'nav.home_timeline', + criteria: ['!private'] + }, + public: { + route: 'public-timeline', + anon: true, + icon: 'users', + label: 'nav.public_tl', + criteria: ['!private'] + }, + twkn: { + route: 'public-external-timeline', + anon: true, + icon: 'globe', + label: 'nav.twkn', + criteria: ['!private', 'federating'] + }, + bookmarks: { + route: 'bookmarks', + icon: 'bookmark', + label: 'nav.bookmarks' + }, + favorites: { + routeObject: { name: 'user-profile', query: { tab: 'favorites' } }, + icon: 'star', + label: 'user_card.favorites' + }, + dms: { + route: 'dms', + icon: 'envelope', + label: 'nav.dms' + } +} + +export const ROOT_ITEMS = { + interactions: { + route: 'interactions', + icon: 'bell', + label: 'nav.interactions' + }, + chats: { + route: 'chats', + icon: 'comments', + label: 'nav.chats', + badgeGetter: 'unreadChatCount', + criteria: ['chats'] + }, + friendRequests: { + route: 'friend-requests', + icon: 'user-plus', + label: 'nav.friend_requests', + criteria: ['lockedUser'], + badgeGetter: 'followRequestCount' + }, + about: { + route: 'about', + anon: true, + icon: 'info-circle', + label: 'nav.about' + } +} diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js @@ -0,0 +1,47 @@ +import { mapState } from 'vuex' +import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faThumbtack } from '@fortawesome/free-solid-svg-icons' + +library.add(faThumbtack) + +const NavigationEntry = { + props: ['item', 'showPin'], + methods: { + isPinned (value) { + return this.pinnedItems.has(value) + }, + togglePin (value) { + if (this.isPinned(value)) { + this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value }) + } else { + this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value }) + } + this.$store.dispatch('pushServerSideStorage') + } + }, + computed: { + routeTo () { + if (!this.item.route && !this.item.routeObject) return null + let route + if (this.item.routeObject) { + route = this.item.routeObject + } else { + route = { name: (this.item.anon || this.currentUser) ? this.item.route : this.item.anonRoute } + } + if (USERNAME_ROUTES.has(route.name)) { + route.params = { username: this.currentUser.screen_name, name: this.currentUser.screen_name } + } + return route + }, + getters () { + return this.$store.getters + }, + ...mapState({ + currentUser: state => state.users.currentUser, + pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) + }) + } +} + +export default NavigationEntry diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue @@ -0,0 +1,120 @@ +<template> + <li class="NavigationEntry"> + <component + :is="routeTo ? 'router-link' : 'button'" + class="menu-item button-unstyled" + :to="routeTo" + > + <span> + <FAIcon + v-if="item.icon" + fixed-width + class="fa-scale-110 menu-icon" + :icon="item.icon" + /> + </span> + <span + v-if="item.iconLetter" + class="icon iconLetter fa-scale-110 menu-icon" + >{{ item.iconLetter }} + </span> + <span class="label"> + {{ item.labelRaw || $t(item.label) }} + </span> + <slot /> + <div + v-if="item.badgeGetter && getters[item.badgeGetter]" + class="badge badge-notification" + > + {{ getters[item.badgeGetter] }} + </div> + <button + v-if="showPin && currentUser" + type="button" + class="button-unstyled extra-button" + :title="$t(isPinned ? 'general.unpin' : 'general.pin' )" + :aria-pressed="!!isPinned" + @click.stop.prevent="togglePin(item.name)" + > + <FAIcon + v-if="showPin && currentUser" + fixed-width + class="fa-scale-110" + :class="{ 'veryfaint': !isPinned(item.name) }" + :transform="!isPinned(item.name) ? 'rotate-45' : ''" + icon="thumbtack" + /> + </button> + </component> + </li> +</template> + +<script src="./navigation_entry.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.NavigationEntry { + .label { + flex: 1; + } + + .menu-icon { + margin-right: 0.8em; + } + + .extra-button { + width: 3em; + text-align: center; + + &:last-child { + margin-right: -0.8em; + } + } + + .menu-item { + display: flex; + box-sizing: border-box; + align-items: baseline; + height: 3.5em; + line-height: 3.5em; + padding: 0 1em; + width: 100%; + color: $fallback--link; + color: var(--link, $fallback--link); + + &:hover { + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + color: $fallback--link; + color: var(--selectedMenuText, $fallback--link); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); + + .menu-icon { + --icon: var(--text, $fallback--icon); + } + } + + &.router-link-active { + font-weight: bolder; + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + color: $fallback--text; + color: var(--selectedMenuText, $fallback--text); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); + + .menu-icon { + --icon: var(--text, $fallback--icon); + } + + &:hover { + text-decoration: underline; + } + } + } +} +</style> diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js @@ -0,0 +1,88 @@ +import { mapState } from 'vuex' +import { TIMELINES, ROOT_ITEMS, USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faComments, + faBell, + faInfoCircle, + faStream, + faList +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faComments, + faBell, + faInfoCircle, + faStream, + faList +) + +const NavPanel = { + props: ['limit'], + methods: { + getRouteTo (item) { + if (item.routeObject) { + return item.routeObject + } + const route = { name: (item.anon || this.currentUser) ? item.route : item.anonRoute } + if (USERNAME_ROUTES.has(route.name)) { + route.params = { username: this.currentUser.screen_name } + } + return route + } + }, + computed: { + getters () { + return this.$store.getters + }, + ...mapState({ + lists: getListEntries, + currentUser: state => state.users.currentUser, + followRequestCount: state => state.api.followRequests.length, + privateMode: state => state.instance.private, + federating: state => state.instance.federating, + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) + }), + pinnedList () { + if (!this.currentUser) { + return [ + { ...TIMELINES.public, name: 'public' }, + { ...TIMELINES.twkn, name: 'twkn' }, + { ...ROOT_ITEMS.about, name: 'about' } + ] + } + return filterNavigation( + [ + ...Object + .entries({ ...TIMELINES }) + .filter(([k]) => this.pinnedItems.has(k)) + .map(([k, v]) => ({ ...v, name: k })), + ...this.lists.filter((k) => this.pinnedItems.has(k.name)), + ...Object + .entries({ ...ROOT_ITEMS }) + .filter(([k]) => this.pinnedItems.has(k)) + .map(([k, v]) => ({ ...v, name: k })) + ], + { + hasChats: this.pleromaChatMessagesAvailable, + isFederating: this.federating, + isPrivate: this.privateMode, + currentUser: this.currentUser + } + ).slice(0, this.limit) + } + } +} + +export default NavPanel diff --git a/src/components/navigation/navigation_pins.vue b/src/components/navigation/navigation_pins.vue @@ -0,0 +1,76 @@ +<template> + <span class="NavigationPins"> + <router-link + v-for="item in pinnedList" + :key="item.name" + class="pinned-item" + :to="getRouteTo(item)" + :title="item.labelRaw || $t(item.label)" + > + <FAIcon + v-if="item.icon" + fixed-width + :icon="item.icon" + /> + <span + v-if="item.iconLetter" + class="iconLetter fa-scale-110 fa-old-padding" + >{{ item.iconLetter }}</span> + <div + v-if="item.badgeGetter && getters[item.badgeGetter]" + class="alert-dot" + /> + </router-link> + </span> +</template> + +<script src="./navigation_pins.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; +.NavigationPins { + display: flex; + flex-wrap: wrap; + overflow: hidden; + height: 100%; + + .alert-dot { + border-radius: 100%; + height: 0.5em; + width: 0.5em; + position: absolute; + right: calc(50% - 0.25em); + top: calc(50% - 0.25em); + margin-left: 6px; + margin-top: -6px; + background-color: $fallback--cRed; + background-color: var(--badgeNotification, $fallback--cRed); + } + + .pinned-item { + position: relative; + flex: 1 0 3em; + min-width: 2em; + text-align: center; + overflow: visible; + box-sizing: border-box; + height: 100%; + + & .svg-inline--fa, + & .iconLetter { + margin: 0; + } + + &.router-link-active { + color: $fallback--text; + color: var(--selectedMenuText, $fallback--text); + border-bottom: 4px solid; + + & .svg-inline--fa, + & .iconLetter { + color: inherit; + } + } + } +} +</style> diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js @@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' import Report from '../report/report.vue' +import UserLink from '../user_link/user_link.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' import UserPopover from '../user_popover/user_popover.vue' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' @@ -50,7 +51,8 @@ const Notification = { Status, Report, RichContent, - UserPopover + UserPopover, + UserLink }, methods: { toggleUserExpanded () { diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -11,9 +11,10 @@ class="Notification container -muted" > <small> - <router-link :to="userProfileLink"> - {{ notification.from_profile.screen_name_ui }} - </router-link> + <user-link + :user="notification.from_profile" + :at="false" + /> </small> <button class="button-unstyled unmute" @@ -174,12 +175,10 @@ v-if="notification.type === 'follow' || notification.type === 'follow_request'" class="follow-text" > - <router-link - :to="userProfileLink" + <user-link class="follow-name" - > - @{{ notification.from_profile.screen_name_ui }} - </router-link> + :user="notification.from_profile" + /> <div v-if="notification.type === 'follow_request'" style="white-space: nowrap;" @@ -210,9 +209,9 @@ v-else-if="notification.type === 'move'" class="move-text" > - <router-link :to="targetUserProfileLink"> - @{{ notification.target.screen_name_ui }} - </router-link> + <user-link + :user="notification.target" + /> </div> <Report v-else-if="notification.type === 'pleroma:report'" diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js @@ -4,7 +4,7 @@ const Popover = { // Action to trigger popover: either 'hover' or 'click' trigger: String, - // Either 'top' or 'bottom' + // 'top', 'bottom', 'left', 'right' placement: String, // Takes object with properties 'x' and 'y', values of these can be @@ -84,6 +84,8 @@ const Popover = { const anchorStyle = getComputedStyle(anchorEl) const topPadding = parseFloat(anchorStyle.paddingTop) const bottomPadding = parseFloat(anchorStyle.paddingBottom) + const rightPadding = parseFloat(anchorStyle.paddingRight) + const leftPadding = parseFloat(anchorStyle.paddingLeft) // Screen position of the origin point for popover = center of the anchor const origin = { @@ -170,7 +172,7 @@ const Popover = { if (overlayCenter) { translateX = origin.x + horizOffset translateY = origin.y + vertOffset - } else { + } else if (this.placement !== 'right' && this.placement !== 'left') { // Default to whatever user wished with placement prop let usingTop = this.placement !== 'bottom' @@ -189,6 +191,25 @@ const Popover = { const xOffset = (this.offset && this.offset.x) || 0 translateX = origin.x + horizOffset + xOffset + } else { + // Default to whatever user wished with placement prop + let usingRight = this.placement !== 'left' + + // 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 rightBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? rightPadding : 0) + const leftBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? leftPadding : 0) + if (leftBoundary + content.offsetWidth > xBounds.max) usingRight = true + if (rightBoundary - content.offsetWidth < xBounds.min) usingRight = false + + const xOffset = (this.offset && this.offset.x) || 0 + translateX = usingRight + ? rightBoundary - xOffset - content.offsetWidth + : leftBoundary + xOffset + + const yOffset = (this.offset && this.offset.y) || 0 + translateY = origin.y + vertOffset + yOffset } this.styles = { diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue @@ -126,6 +126,13 @@ } } + &.-has-submenu { + .chevron-icon { + margin-right: 0.25rem; + margin-left: 2rem; + } + } + &:active, &:hover { background-color: $fallback--lightBg; background-color: var(--selectedMenuPopover, $fallback--lightBg); diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue @@ -47,6 +47,8 @@ class="cancel-icon fa-scale-110 fa-old-padding" /> </button> + <span class="spacer" /> + <span class="spacer" /> </template> </div> </template> diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue @@ -102,11 +102,6 @@ </BooleanSetting> </li> <li> - <BooleanSetting path="listsNavigation"> - {{ $t('settings.lists_navigation') }} - </BooleanSetting> - </li> - <li> <h3>{{ $t('settings.columns') }}</h3> </li> <li> diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js @@ -2,6 +2,7 @@ import { mapState, mapGetters } from 'vuex' import UserCard from '../user_card/user_card.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' +import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faSignInAlt, @@ -15,6 +16,7 @@ import { faTachometerAlt, faCog, faInfoCircle, + faCompass, faList } from '@fortawesome/free-solid-svg-icons' @@ -30,6 +32,7 @@ library.add( faTachometerAlt, faCog, faInfoCircle, + faCompass, faList ) @@ -80,10 +83,16 @@ const SideDrawer = { return this.$store.state.instance.federating }, timelinesRoute () { + let name if (this.$store.state.interface.lastTimeline) { - return this.$store.state.interface.lastTimeline + name = this.$store.state.interface.lastTimeline + } + name = this.currentUser ? 'friends' : 'public-timeline' + if (USERNAME_ROUTES.has(name)) { + return { name, params: { username: this.currentUser.screen_name } } + } else { + return { name } } - return this.currentUser ? 'friends' : 'public-timeline' }, ...mapState({ pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue @@ -47,7 +47,7 @@ v-if="currentUser || !privateMode" @click="toggleDrawer" > - <router-link :to="{ name: timelinesRoute }"> + <router-link :to="timelinesRoute"> <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -195,6 +195,18 @@ v-if="currentUser" @click="toggleDrawer" > + <router-link :to="{ name: 'edit-navigation' }"> + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="compass" + /> {{ $t("nav.edit_nav_mobile") }} + </router-link> + </li> + <li + v-if="currentUser" + @click="toggleDrawer" + > <button class="button-unstyled -link -fullwidth" @click="doLogout" diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -13,6 +13,7 @@ import StatusPopover from '../status_popover/status_popover.vue' import UserPopover from '../user_popover/user_popover.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' +import UserLink from '../user_link/user_link.vue' import MentionsLine from 'src/components/mentions_line/mentions_line.vue' import MentionLink from 'src/components/mention_link/mention_link.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -115,7 +116,8 @@ const Status = { RichContent, MentionLink, MentionsLine, - UserPopover + UserPopover, + UserLink }, props: [ 'statusoid', diff --git a/src/components/status/status.vue b/src/components/status/status.vue @@ -25,9 +25,10 @@ class="fa-scale-110 fa-old-padding repeat-icon" icon="retweet" /> - <router-link :to="userProfileLink"> - {{ status.user.screen_name_ui }} - </router-link> + <user-link + :user="status.user" + :at="false" + /> </small> <small v-if="showReasonMutedThread" @@ -164,13 +165,12 @@ > {{ status.user.name }} </h4> - <router-link + <user-link class="account-name" :title="status.user.screen_name_ui" - :to="userProfileLink" - > - {{ status.user.screen_name_ui }} - </router-link> + :user="status.user" + :at="false" + /> <img v-if="!!(status.user && status.user.favicon)" class="status-favicon" diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss @@ -17,6 +17,7 @@ overflow-x: auto; padding-top: 5px; flex-direction: row; + flex: 0 0 auto; &::after, &::before { content: ''; diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue @@ -1,7 +1,10 @@ <template> <div :class="['Timeline', classes.root]"> <div :class="classes.header"> - <TimelineMenu v-if="!embedded" /> + <TimelineMenu + v-if="!embedded" + :timeline-name="timelineName" + /> <button v-if="showScrollTop" class="button-unstyled scroll-to-top-button" diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js @@ -1,6 +1,8 @@ import Popover from '../popover/popover.vue' -import TimelineMenuContent from './timeline_menu_content.vue' +import NavigationEntry from 'src/components/navigation/navigation_entry.vue' +import { ListsMenuContent } from '../lists_menu/lists_menu_content.vue' import { library } from '@fortawesome/fontawesome-svg-core' +import { TIMELINES } from 'src/components/navigation/navigation.js' import { faChevronDown } from '@fortawesome/free-solid-svg-icons' @@ -22,11 +24,13 @@ export const timelineNames = () => { const TimelineMenu = { components: { Popover, - TimelineMenuContent + NavigationEntry, + ListsMenuContent }, data () { return { - isOpen: false + isOpen: false, + timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })) } }, created () { @@ -34,6 +38,12 @@ const TimelineMenu = { this.$store.dispatch('setLastTimeline', this.$route.name) } }, + computed: { + useListsMenu () { + const route = this.$route.name + return route === 'lists-timeline' + } + }, methods: { openMenu () { // $nextTick is too fast, animation won't play back but diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue @@ -10,7 +10,19 @@ @close="() => isOpen = false" > <template #content> - <TimelineMenuContent /> + <ListsMenuContent + v-if="useListsMenu" + :show-pin="false" + class="timelines" + /> + <ul v-else> + <NavigationEntry + v-for="item in timelinesList" + :key="item.name" + :show-pin="false" + :item="item" + /> + </ul> </template> <template #trigger> <span class="button-unstyled title timeline-menu-title"> @@ -138,8 +150,7 @@ background-color: $fallback--lightBg; background-color: var(--selectedMenu, $fallback--lightBg); color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - --faint: var(--selectedMenuFaintText, $fallback--faint); + color: var(--selectedMenuText, $fallback--text); --faint: var(--selectedMenuFaintText, $fallback--faint); --faintLink: var(--selectedMenuFaintLink, $fallback--faint); --lightText: var(--selectedMenuLightText, $fallback--lightText); --icon: var(--selectedMenuIcon, $fallback--icon); diff --git a/src/components/timeline_menu/timeline_menu_content.js b/src/components/timeline_menu/timeline_menu_content.js @@ -1,29 +0,0 @@ -import { mapState } from 'vuex' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome -) - -const TimelineMenuContent = { - computed: { - ...mapState({ - currentUser: state => state.users.currentUser, - privateMode: state => state.instance.private, - federating: state => state.instance.federating - }) - } -} - -export default TimelineMenuContent diff --git a/src/components/timeline_menu/timeline_menu_content.vue b/src/components/timeline_menu/timeline_menu_content.vue @@ -1,66 +0,0 @@ -<template> - <ul> - <li v-if="currentUser"> - <router-link - class="menu-item" - :to="{ name: 'friends' }" - > - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="home" - />{{ $t("nav.home_timeline") }} - </router-link> - </li> - <li v-if="currentUser || !privateMode"> - <router-link - class="menu-item" - :to="{ name: 'public-timeline' }" - > - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="users" - />{{ $t("nav.public_tl") }} - </router-link> - </li> - <li v-if="federating && (currentUser || !privateMode)"> - <router-link - class="menu-item" - :to="{ name: 'public-external-timeline' }" - > - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="globe" - />{{ $t("nav.twkn") }} - </router-link> - </li> - <li v-if="currentUser"> - <router-link - class="menu-item" - :to="{ name: 'bookmarks'}" - > - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="bookmark" - />{{ $t("nav.bookmarks") }} - </router-link> - </li> - <li v-if="currentUser"> - <router-link - class="menu-item" - :to="{ name: 'dms', params: { username: currentUser.screen_name } }" - > - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="envelope" - />{{ $t("nav.dms") }} - </router-link> - </li> - </ul> -</template> - -<script src="./timeline_menu_content.js"></script> diff --git a/src/components/unicode_domain_indicator/unicode_domain_indicator.vue b/src/components/unicode_domain_indicator/unicode_domain_indicator.vue @@ -0,0 +1,26 @@ +<template> + <FAIcon + v-if="user && user.screen_name_ui_contains_non_ascii" + icon="code" + :title="$t('unicode_domain_indicator.tooltip')" + /> +</template> + +<script> +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCode +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCode +) + +const UnicodeDomainIndicator = { + props: { + user: Object + } +} + +export default UnicodeDomainIndicator +</script> diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js @@ -38,7 +38,7 @@ const UpdateNotification = { return !this.$store.state.instance.disableUpdateNotification && this.$store.state.users.currentUser && this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER && - !this.$store.state.serverSideStorage.flagStorage.dontShowUpdateNotifs + !this.$store.state.serverSideStorage.prefsStorage.simple.dontShowUpdateNotifs } }, methods: { @@ -48,7 +48,7 @@ const UpdateNotification = { neverShowAgain () { this.toggleShow() this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) - this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 }) + this.$store.commit('setPreference', { path: 'simple.dontShowUpdateNotifs', value: true }) this.$store.dispatch('pushServerSideStorage') }, dismiss () { diff --git a/src/components/update_notification/update_notification.vue b/src/components/update_notification/update_notification.vue @@ -60,7 +60,7 @@ <template #linkToArtist> <a target="_blank" - href="https://post.ebin.club/pipivovott" + href="https://post.ebin.club/users/pipivovott" >pipivovott</a> </template> </i18n-t> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js @@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue' import AccountActions from '../account_actions/account_actions.vue' import Select from '../select/select.vue' +import UserLink from '../user_link/user_link.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { mapGetters } from 'vuex' @@ -134,7 +135,8 @@ export default { ProgressButton, FollowButton, Select, - RichContent + RichContent, + UserLink }, methods: { muteUser () { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -106,13 +106,10 @@ </button> </div> <div class="bottom-line"> - <router-link + <user-link class="user-screen-name" - :title="user.screen_name_ui" - :to="userProfileLink(user)" - > - @{{ user.screen_name_ui }} - </router-link> + :user="user" + /> <template v-if="!hideBio"> <span v-if="user.deactivated" diff --git a/src/components/user_link/user_link.vue b/src/components/user_link/user_link.vue @@ -0,0 +1,38 @@ +<template> + <router-link + :title="user.screen_name_ui" + :to="userProfileLink(user)" + > + {{ at ? '@' : '' }}{{ user.screen_name_ui }}<UnicodeDomainIndicator + :user="user" + /> + </router-link> +</template> + +<script> +import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' + +const UserLink = { + props: { + user: Object, + at: { + type: Boolean, + default: true + } + }, + components: { + UnicodeDomainIndicator + }, + methods: { + userProfileLink (user) { + return generateProfileLink( + user.id, user.screen_name, + this.$store.state.instance.restrictedNicknames + ) + } + } +} + +export default UserLink +</script> diff --git a/src/components/user_list_menu/user_list_menu.js b/src/components/user_list_menu/user_list_menu.js @@ -0,0 +1,93 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faChevronRight } from '@fortawesome/free-solid-svg-icons' +import { mapState } from 'vuex' + +import DialogModal from '../dialog_modal/dialog_modal.vue' +import Popover from '../popover/popover.vue' + +library.add(faChevronRight) + +const UserListMenu = { + props: [ + 'user' + ], + data () { + return {} + }, + components: { + DialogModal, + Popover + }, + created () { + this.$store.dispatch('fetchUserInLists', this.user.id) + }, + computed: { + ...mapState({ + allLists: state => state.lists.allLists + }), + inListsSet () { + return new Set(this.user.inLists.map(x => x.id)) + }, + lists () { + if (!this.user.inLists) return [] + return this.allLists.map(list => ({ + ...list, + inList: this.inListsSet.has(list.id) + })) + } + }, + methods: { + toggleList (listId) { + if (this.inListsSet.has(listId)) { + this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId }).then((response) => { + if (!response.ok) { return } + this.$store.dispatch('fetchUserInLists', this.user.id) + }) + } else { + this.$store.dispatch('addListAccount', { accountId: this.user.id, listId }).then((response) => { + if (!response.ok) { return } + this.$store.dispatch('fetchUserInLists', this.user.id) + }) + } + }, + toggleRight (right) { + const store = this.$store + if (this.user.rights[right]) { + store.state.api.backendInteractor.deleteRight({ user: this.user, right }).then(response => { + if (!response.ok) { return } + store.commit('updateRight', { user: this.user, right, value: false }) + }) + } else { + store.state.api.backendInteractor.addRight({ user: this.user, right }).then(response => { + if (!response.ok) { return } + store.commit('updateRight', { user: this.user, right, value: true }) + }) + } + }, + toggleActivationStatus () { + this.$store.dispatch('toggleActivationStatus', { user: this.user }) + }, + deleteUserDialog (show) { + this.showDeleteUserDialog = show + }, + deleteUser () { + const store = this.$store + const user = this.user + const { id, name } = user + store.state.api.backendInteractor.deleteUser({ user }) + .then(e => { + this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id) + const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile' + const isTargetUser = this.$route.params.name === name || this.$route.params.id === id + if (isProfile && isTargetUser) { + window.history.back() + } + }) + }, + setToggled (value) { + this.toggled = value + } + } +} + +export default UserListMenu diff --git a/src/components/user_list_menu/user_list_menu.vue b/src/components/user_list_menu/user_list_menu.vue @@ -0,0 +1,38 @@ +<template> + <div class="UserListMenu"> + <Popover + trigger="hover" + placement="left" + remove-padding + > + <template #content> + <div class="dropdown-menu"> + <button + v-for="list in lists" + :key="list.id" + class="button-default dropdown-item" + @click="toggleList(list.id)" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': list.inList }" + /> + {{ list.title }} + </button> + </div> + </template> + <template #trigger> + <button class="btn button-default dropdown-item -has-submenu"> + {{ $t('lists.manage_lists') }} + <FAIcon + class="chevron-icon" + size="lg" + icon="chevron-right" + /> + </button> + </template> + </Popover> + </div> +</template> + +<script src="./user_list_menu.js"></script> diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js @@ -1,5 +1,6 @@ import { defineAsyncComponent } from 'vue' import RichContent from 'src/components/rich_content/rich_content.jsx' +import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' @@ -15,6 +16,7 @@ const UserListPopover = { ], components: { RichContent, + UnicodeDomainIndicator, Popover: defineAsyncComponent(() => import('../popover/popover.vue')), UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue')) }, diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue @@ -29,7 +29,7 @@ :emoji="user.emoji" /> <!-- eslint-enable vue/no-v-html --> - <span class="user-list-screen-name">{{ user.screen_name_ui }}</span> + <span class="user-list-screen-name">{{ user.screen_name_ui }}</span><UnicodeDomainIndicator :user="user" /> </div> </div> </template> diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js @@ -2,13 +2,15 @@ import Status from '../status/status.vue' import List from '../list/list.vue' import Checkbox from '../checkbox/checkbox.vue' import Modal from '../modal/modal.vue' +import UserLink from '../user_link/user_link.vue' const UserReportingModal = { components: { Status, List, Checkbox, - Modal + Modal, + UserLink }, data () { return { diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -5,9 +5,13 @@ > <div class="user-reporting-panel panel"> <div class="panel-heading"> - <div class="title"> - {{ $t('user_reporting.title', [user.screen_name_ui]) }} - </div> + <i18n-t + tag="div" + keypath="user_reporting.title" + class="title" + > + <UserLink :user="user" /> + </i18n-t> </div> <div class="panel-body"> <div class="user-reporting-panel-left"> diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -80,11 +80,16 @@ "confirm": "Confirm", "verify": "Verify", "close": "Close", + "undo": "Undo", + "yes": "Yes", + "no": "No", "peek": "Peek", "role": { "admin": "Admin", "moderator": "Moderator" }, + "unpin": "Unpin item", + "pin": "Pin item", "flash_content": "Click to show Flash content using Ruffle (Experimental, may not work).", "flash_security": "Note that this can be potentially dangerous since Flash content is still arbitrary code.", "flash_fail": "Failed to load flash content, see console for details.", @@ -149,7 +154,10 @@ "preferences": "Preferences", "timelines": "Timelines", "chats": "Chats", - "lists": "Lists" + "lists": "Lists", + "edit_nav_mobile": "Customize navigation bar", + "edit_pinned": "Edit pinned items", + "edit_finish": "Done editing" }, "notifications": { "broken_favorite": "Unknown status, searching for it…", @@ -987,7 +995,18 @@ "create": "Create", "save": "Save changes", "delete": "Delete list", - "following_only": "Limit to Following" + "following_only": "Limit to Following", + "manage_lists": "Manage lists", + "manage_members": "Manage list members", + "add_members": "Search for more users", + "remove_from_list": "Remove from list", + "add_to_list": "Add to list", + "is_in_list": "Already in list", + "editing_list": "Editing list {listTitle}", + "creating_list": "Creating new list", + "update_title": "Save Title", + "really_delete": "Really delete list?", + "error": "Error manipulating lists: {0}" }, "file_type": { "audio": "Audio", @@ -1006,5 +1025,8 @@ "update_changelog": "For more details on what's changed, see {theFullChangelog}.", "update_changelog_here": "the full changelog", "art_by": "Art by {linkToArtist}" + }, + "unicode_domain_indicator": { + "tooltip": "This domain contains non-ascii characters." } } diff --git a/src/modules/api.js b/src/modules/api.js @@ -15,6 +15,9 @@ const api = { mastoUserSocketStatus: null, followRequests: [] }, + getters: { + followRequestCount: state => state.api.followRequests.length + }, mutations: { setBackendInteractor (state, backendInteractor) { state.backendInteractor = backendInteractor diff --git a/src/modules/config.js b/src/modules/config.js @@ -89,7 +89,6 @@ export const defaultState = { contentColumnWidth: '45rem', notifsColumnWidth: '25rem', navbarColumnStretch: false, - listsNavigation: false, greentext: undefined, // instance default useAtIcon: undefined, // instance default mentionLinkDisplay: undefined, // instance default diff --git a/src/modules/lists.js b/src/modules/lists.js @@ -9,27 +9,43 @@ export const mutations = { setLists (state, value) { state.allLists = value }, - setList (state, { id, title }) { - if (!state.allListsObject[id]) { - state.allListsObject[id] = {} + setList (state, { listId, title }) { + if (!state.allListsObject[listId]) { + state.allListsObject[listId] = { accountIds: [] } } - state.allListsObject[id].title = title + state.allListsObject[listId].title = title - if (!find(state.allLists, { id })) { - state.allLists.push({ id, title }) + const entry = find(state.allLists, { id: listId }) + if (!entry) { + state.allLists.push({ id: listId, title }) } else { - find(state.allLists, { id }).title = title + entry.title = title } }, - setListAccounts (state, { id, accountIds }) { - if (!state.allListsObject[id]) { - state.allListsObject[id] = {} + setListAccounts (state, { listId, accountIds }) { + if (!state.allListsObject[listId]) { + state.allListsObject[listId] = { accountIds: [] } } - state.allListsObject[id].accountIds = accountIds + state.allListsObject[listId].accountIds = accountIds }, - deleteList (state, { id }) { - delete state.allListsObject[id] - remove(state.allLists, list => list.id === id) + addListAccount (state, { listId, accountId }) { + if (!state.allListsObject[listId]) { + state.allListsObject[listId] = { accountIds: [] } + } + state.allListsObject[listId].accountIds.push(accountId) + }, + removeListAccount (state, { listId, accountId }) { + if (!state.allListsObject[listId]) { + state.allListsObject[listId] = { accountIds: [] } + } + const { accountIds } = state.allListsObject[listId] + const set = new Set(accountIds) + set.delete(accountId) + state.allListsObject[listId].accountIds = [...set] + }, + deleteList (state, { listId }) { + delete state.allListsObject[listId] + remove(state.allLists, list => list.id === listId) } } @@ -40,37 +56,57 @@ const actions = { createList ({ rootState, commit }, { title }) { return rootState.api.backendInteractor.createList({ title }) .then((list) => { - commit('setList', { id: list.id, title }) + commit('setList', { listId: list.id, title }) return list }) }, - fetchList ({ rootState, commit }, { id }) { - return rootState.api.backendInteractor.getList({ id }) - .then((list) => commit('setList', { id: list.id, title: list.title })) + fetchList ({ rootState, commit }, { listId }) { + return rootState.api.backendInteractor.getList({ listId }) + .then((list) => commit('setList', { listId: list.id, title: list.title })) }, - fetchListAccounts ({ rootState, commit }, { id }) { - return rootState.api.backendInteractor.getListAccounts({ id }) - .then((accountIds) => commit('setListAccounts', { id, accountIds })) + fetchListAccounts ({ rootState, commit }, { listId }) { + return rootState.api.backendInteractor.getListAccounts({ listId }) + .then((accountIds) => commit('setListAccounts', { listId, accountIds })) }, - setList ({ rootState, commit }, { id, title }) { - rootState.api.backendInteractor.updateList({ id, title }) - commit('setList', { id, title }) + setList ({ rootState, commit }, { listId, title }) { + rootState.api.backendInteractor.updateList({ listId, title }) + commit('setList', { listId, title }) }, - setListAccounts ({ rootState, commit }, { id, accountIds }) { - const saved = rootState.lists.allListsObject[id].accountIds || [] + setListAccounts ({ rootState, commit }, { listId, accountIds }) { + const saved = rootState.lists.allListsObject[listId].accountIds || [] const added = accountIds.filter(id => !saved.includes(id)) const removed = saved.filter(id => !accountIds.includes(id)) - commit('setListAccounts', { id, accountIds }) + commit('setListAccounts', { listId, accountIds }) if (added.length > 0) { - rootState.api.backendInteractor.addAccountsToList({ id, accountIds: added }) + rootState.api.backendInteractor.addAccountsToList({ listId, accountIds: added }) } if (removed.length > 0) { - rootState.api.backendInteractor.removeAccountsFromList({ id, accountIds: removed }) + rootState.api.backendInteractor.removeAccountsFromList({ listId, accountIds: removed }) } }, - deleteList ({ rootState, commit }, { id }) { - rootState.api.backendInteractor.deleteList({ id }) - commit('deleteList', { id }) + addListAccount ({ rootState, commit }, { listId, accountId }) { + return rootState + .api + .backendInteractor + .addAccountsToList({ listId, accountIds: [accountId] }) + .then((result) => { + commit('addListAccount', { listId, accountId }) + return result + }) + }, + removeListAccount ({ rootState, commit }, { listId, accountId }) { + return rootState + .api + .backendInteractor + .removeAccountsFromList({ listId, accountIds: [accountId] }) + .then((result) => { + commit('removeListAccount', { listId, accountId }) + return result + }) + }, + deleteList ({ rootState, commit }, { listId }) { + rootState.api.backendInteractor.deleteList({ listId }) + commit('deleteList', { listId }) } } diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js @@ -1,5 +1,5 @@ import { toRaw } from 'vue' -import { isEqual, cloneDeep } from 'lodash' +import { isEqual, cloneDeep, set, get, clamp, flatten, groupBy, findLastIndex, takeRight } from 'lodash' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' export const VERSION = 1 @@ -14,14 +14,21 @@ export const defaultState = { // storage of flags - stuff that can only be set and incremented flagStorage: { updateCounter: 0, // Counter for most recent update notification seen - // TODO move to prefsStorage when that becomes a thing since only way - // this can be reset is by complete reset of all flags - dontShowUpdateNotifs: 0, // if user chose to not show update notifications ever again reset: 0 // special flag that can be used to force-reset all flags, debug purposes only // special reset codes: // 1000: trim keys to those known by currently running FE // 1001: same as above + reset everything to 0 }, + prefsStorage: { + _journal: [], + simple: { + dontShowUpdateNotifs: false, + collapseNav: false + }, + collections: { + pinnedNavItems: ['home', 'dms', 'chats'] + } + }, // raw data raw: null, // local cache @@ -33,14 +40,43 @@ export const newUserFlags = { updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification } -const _wrapData = (data) => ({ +export const _moveItemInArray = (array, value, movement) => { + const oldIndex = array.indexOf(value) + const newIndex = oldIndex + movement + const newArray = [...array] + // remove old + newArray.splice(oldIndex, 1) + // add new + newArray.splice(clamp(newIndex, 0, newArray.length + 1), 0, value) + return newArray +} + +const _wrapData = (data, userName) => ({ ...data, + _user: userName, _timestamp: Date.now(), _version: VERSION }) const _checkValidity = (data) => data._timestamp > 0 && data._version > 0 +const _verifyPrefs = (state) => { + state.prefsStorage = state.prefsStorage || { + simple: {}, + collections: {} + } + Object.entries(defaultState.prefsStorage.simple).forEach(([k, v]) => { + if (typeof v === 'number' || typeof v === 'boolean') return + console.warn(`Preference simple.${k} as invalid type, reinitializing`) + set(state.prefsStorage.simple, k, defaultState.prefsStorage.simple[k]) + }) + Object.entries(defaultState.prefsStorage.collections).forEach(([k, v]) => { + if (Array.isArray(v)) return + console.warn(`Preference collections.${k} as invalid type, reinitializing`) + set(state.prefsStorage.collections, k, defaultState.prefsStorage.collections[k]) + }) +} + export const _getRecentData = (cache, live) => { const result = { recent: null, stale: null, needUpload: false } const cacheValid = _checkValidity(cache || {}) @@ -85,6 +121,8 @@ export const _getAllFlags = (recent, stale) => { } export const _mergeFlags = (recent, stale, allFlagKeys) => { + if (!stale.flagStorage) return recent.flagStorage + if (!recent.flagStorage) return stale.flagStorage return Object.fromEntries(allFlagKeys.map(flag => { const recentFlag = recent.flagStorage[flag] const staleFlag = stale.flagStorage[flag] @@ -93,6 +131,88 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => { })) } +const _mergeJournal = (...journals) => { + // Ignore invalid journal entries + const allJournals = flatten( + journals.map(j => Array.isArray(j) ? j : []) + ).filter(entry => + Object.prototype.hasOwnProperty.call(entry, 'path') && + Object.prototype.hasOwnProperty.call(entry, 'operation') && + Object.prototype.hasOwnProperty.call(entry, 'args') && + Object.prototype.hasOwnProperty.call(entry, 'timestamp') + ) + const grouped = groupBy(allJournals, 'path') + const trimmedGrouped = Object.entries(grouped).map(([path, journal]) => { + // side effect + journal.sort((a, b) => a.timestamp > b.timestamp ? 1 : -1) + + if (path.startsWith('collections')) { + const lastRemoveIndex = findLastIndex(journal, ({ operation }) => operation === 'removeFromCollection') + // everything before last remove is unimportant + if (lastRemoveIndex > 0) { + return journal.slice(lastRemoveIndex) + } else { + // everything else doesn't need trimming + return journal + } + } else if (path.startsWith('simple')) { + // Only the last record is important + return takeRight(journal) + } else { + return journal + } + }) + return flatten(trimmedGrouped) + .sort((a, b) => a.timestamp > b.timestamp ? 1 : -1) +} + +export const _mergePrefs = (recent, stale, allFlagKeys) => { + if (!stale) return recent + if (!recent) return stale + const { _journal: recentJournal, ...recentData } = recent + const { _journal: staleJournal } = stale + /** Journal entry format: + * path: path to entry in prefsStorage + * timestamp: timestamp of the change + * operation: operation type + * arguments: array of arguments, depends on operation type + * + * currently only supported operation type is "set" which just sets the value + * to requested one. Intended only to be used with simple preferences (boolean, number) + * shouldn't be used with collections! + */ + const resultOutput = { ...recentData } + const totalJournal = _mergeJournal(staleJournal, recentJournal) + totalJournal.forEach(({ path, timestamp, operation, command, args }) => { + if (path.startsWith('_')) { + console.error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`) + return + } + switch (operation) { + case 'set': + set(resultOutput, path, args[0]) + break + case 'addToCollection': + set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0]))) + break + case 'removeFromCollection': { + const newSet = new Set(get(resultOutput, path)) + newSet.delete(args[0]) + set(resultOutput, path, Array.from(newSet)) + break + } + case 'reorderCollection': { + const [value, movement] = args + set(resultOutput, path, _moveItemInArray(get(resultOutput, path), value, movement)) + break + } + default: + console.error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`) + } + }) + return { ...resultOutput, _journal: totalJournal } +} + export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => { let result = { ...totalFlags } const allFlagKeys = Object.keys(totalFlags) @@ -149,10 +269,17 @@ export const _doMigrations = (cache) => { } export const mutations = { + clearServerSideStorage (state, userData) { + state = { ...cloneDeep(defaultState) } + }, setServerSideStorage (state, userData) { const live = userData.storage state.raw = live let cache = state.cache + if (cache && cache._user !== userData.fqn) { + console.warn('cache belongs to another user! reinitializing local cache!') + cache = null + } cache = _doMigrations(cache) @@ -165,7 +292,8 @@ export const mutations = { if (recent === null) { console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) recent = _wrapData({ - flagStorage: { ...flagsTemplate } + flagStorage: { ...flagsTemplate }, + prefsStorage: { ...defaultState.prefsStorage } }) } @@ -180,17 +308,23 @@ export const mutations = { const allFlagKeys = _getAllFlags(recent, stale) let totalFlags + let totalPrefs if (dirty) { // Merge the flags - console.debug('Merging the flags...') + console.debug('Merging the data...') totalFlags = _mergeFlags(recent, stale, allFlagKeys) + _verifyPrefs(recent) + _verifyPrefs(stale) + totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage) } else { totalFlags = recent.flagStorage + totalPrefs = recent.prefsStorage } totalFlags = _resetFlags(totalFlags) - recent.flagStorage = totalFlags + recent.flagStorage = { ...flagsTemplate, ...totalFlags } + recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs } state.dirty = dirty || needsUpload state.cache = recent @@ -199,10 +333,72 @@ export const mutations = { state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp) } state.flagStorage = state.cache.flagStorage + state.prefsStorage = state.cache.prefsStorage }, setFlag (state, { flag, value }) { state.flagStorage[flag] = value state.dirty = true + }, + setPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + set(state.prefsStorage, path, value) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { operation: 'set', path, args: [value], timestamp: Date.now() } + ] + state.dirty = true + }, + addCollectionPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + const collection = new Set(get(state.prefsStorage, path)) + collection.add(value) + set(state.prefsStorage, path, [...collection]) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { operation: 'addToCollection', path, args: [value], timestamp: Date.now() } + ] + state.dirty = true + }, + removeCollectionPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + const collection = new Set(get(state.prefsStorage, path)) + collection.delete(value) + set(state.prefsStorage, path, [...collection]) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { operation: 'removeFromCollection', path, args: [value], timestamp: Date.now() } + ] + state.dirty = true + }, + reorderCollectionPreference (state, { path, value, movement }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + const collection = get(state.prefsStorage, path) + const newCollection = _moveItemInArray(collection, value, movement) + set(state.prefsStorage, path, newCollection) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { operation: 'arrangeCollection', path, args: [value], timestamp: Date.now() } + ] + state.dirty = true + }, + updateCache (state, { username }) { + state.prefsStorage._journal = _mergeJournal(state.prefsStorage._journal) + state.cache = _wrapData({ + flagStorage: toRaw(state.flagStorage), + prefsStorage: toRaw(state.prefsStorage) + }, username) } } @@ -214,15 +410,16 @@ const serverSideStorage = { actions: { pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { const needPush = state.dirty || force + console.log(needPush) if (!needPush) return - state.cache = _wrapData({ - flagStorage: toRaw(state.flagStorage) - }) + commit('updateCache', { username: rootState.users.currentUser.fqn }) const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } rootState.api.backendInteractor .updateProfile({ params }) - .then((user) => commit('setServerSideStorage', user)) - state.dirty = false + .then((user) => { + commit('setServerSideStorage', user) + state.dirty = false + }) } } } diff --git a/src/modules/users.js b/src/modules/users.js @@ -171,6 +171,9 @@ export const mutations = { state.relationships[relationship.id] = relationship }) }, + updateUserInLists (state, { id, inLists }) { + state.usersObject[id].inLists = inLists + }, saveBlockIds (state, blockIds) { state.currentUser.blockIds = blockIds }, @@ -298,6 +301,12 @@ const users = { .then((relationships) => store.commit('updateUserRelationship', relationships)) } }, + fetchUserInLists (store, id) { + if (store.state.currentUser) { + store.rootState.api.backendInteractor.fetchUserInLists({ id }) + .then((inLists) => store.commit('updateUserInLists', { id, inLists })) + } + }, fetchBlocks (store) { return store.rootState.api.backendInteractor.fetchBlocks() .then((blocks) => { @@ -509,6 +518,7 @@ const users = { store.dispatch('stopFetchingTimeline', 'friends') store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) store.dispatch('stopFetchingNotifications') + store.dispatch('stopFetchingLists') store.dispatch('stopFetchingFollowRequests') store.commit('clearNotifications') store.commit('resetStatuses') @@ -516,6 +526,7 @@ const users = { store.dispatch('setLastTimeline', 'public-timeline') store.dispatch('setLayoutWidth', windowWidth()) store.dispatch('setLayoutHeight', windowHeight()) + store.commit('clearServerSideStorage') }) }, loginUser (store, accessToken) { @@ -562,6 +573,12 @@ const users = { store.dispatch('startFetchingChats') } + store.dispatch('startFetchingLists') + + if (user.locked) { + store.dispatch('startFetchingFollowRequests') + } + if (store.getters.mergedConfig.useStreamingApi) { store.dispatch('fetchTimeline', 'friends', { since: null }) store.dispatch('fetchNotifications', { since: null }) diff --git a/src/panel.scss b/src/panel.scss @@ -46,7 +46,7 @@ .panel-footer { --panel-heading-height-padding: 0.6em; --__panel-heading-height: 3.2em; - --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding)); + --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0)); position: relative; box-sizing: border-box; @@ -57,7 +57,7 @@ grid-column-gap: 0.5em; flex: none; background-size: cover; - padding: 0.6em; + padding: var(--panel-heading-height-padding); height: var(--__panel-heading-height); line-height: var(--__panel-heading-height-inner); z-index: 4; @@ -147,6 +147,15 @@ color: var(--panelLink, $fallback--link); } + .button-unstyled:hover, + a:hover { + i[class*=icon-], + .svg-inline--fa, + .iconLetter { + color: var(--panelText); + } + } + .faint { background-color: transparent; color: $fallback--faint; diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js @@ -53,6 +53,7 @@ const MASTODON_USER_URL = '/api/v1/accounts' const MASTODON_USER_LOOKUP_URL = '/api/v1/accounts/lookup' const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses` +const MASTODON_USER_IN_LISTS = id => `/api/v1/accounts/${id}/lists` const MASTODON_LIST_URL = id => `/api/v1/lists/${id}` const MASTODON_LIST_TIMELINE_URL = id => `/api/v1/timelines/list/${id}` const MASTODON_LIST_ACCOUNTS_URL = id => `/api/v1/lists/${id}/accounts` @@ -263,6 +264,13 @@ const unfollowUser = ({ id, credentials }) => { }).then((data) => data.json()) } +const fetchUserInLists = ({ id, credentials }) => { + const url = MASTODON_USER_IN_LISTS(id) + return fetch(url, { + headers: authHeaders(credentials) + }).then((data) => data.json()) +} + const pinOwnStatus = ({ id, credentials }) => { return promisedRequest({ url: MASTODON_PIN_OWN_STATUS(id), credentials, method: 'POST' }) .then((data) => parseStatus(data)) @@ -428,14 +436,14 @@ const createList = ({ title, credentials }) => { }).then((data) => data.json()) } -const getList = ({ id, credentials }) => { - const url = MASTODON_LIST_URL(id) +const getList = ({ listId, credentials }) => { + const url = MASTODON_LIST_URL(listId) return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) } -const updateList = ({ id, title, credentials }) => { - const url = MASTODON_LIST_URL(id) +const updateList = ({ listId, title, credentials }) => { + const url = MASTODON_LIST_URL(listId) const headers = authHeaders(credentials) headers['Content-Type'] = 'application/json' @@ -446,15 +454,15 @@ const updateList = ({ id, title, credentials }) => { }) } -const getListAccounts = ({ id, credentials }) => { - const url = MASTODON_LIST_ACCOUNTS_URL(id) +const getListAccounts = ({ listId, credentials }) => { + const url = MASTODON_LIST_ACCOUNTS_URL(listId) return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) .then((data) => data.map(({ id }) => id)) } -const addAccountsToList = ({ id, accountIds, credentials }) => { - const url = MASTODON_LIST_ACCOUNTS_URL(id) +const addAccountsToList = ({ listId, accountIds, credentials }) => { + const url = MASTODON_LIST_ACCOUNTS_URL(listId) const headers = authHeaders(credentials) headers['Content-Type'] = 'application/json' @@ -465,8 +473,8 @@ const addAccountsToList = ({ id, accountIds, credentials }) => { }) } -const removeAccountsFromList = ({ id, accountIds, credentials }) => { - const url = MASTODON_LIST_ACCOUNTS_URL(id) +const removeAccountsFromList = ({ listId, accountIds, credentials }) => { + const url = MASTODON_LIST_ACCOUNTS_URL(listId) const headers = authHeaders(credentials) headers['Content-Type'] = 'application/json' @@ -477,8 +485,8 @@ const removeAccountsFromList = ({ id, accountIds, credentials }) => { }) } -const deleteList = ({ id, credentials }) => { - const url = MASTODON_LIST_URL(id) +const deleteList = ({ listId, credentials }) => { + const url = MASTODON_LIST_URL(listId) return fetch(url, { method: 'DELETE', headers: authHeaders(credentials) @@ -1584,7 +1592,8 @@ const apiService = { sendChatMessage, readChat, deleteChatMessage, - setReportState + setReportState, + fetchUserInLists } export default apiService diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js @@ -43,11 +43,13 @@ export const parseUser = (data) => { // case for users in "mentions" property for statuses in MastoAPI const mastoShort = masto && !Object.prototype.hasOwnProperty.call(data, 'avatar') + output.inLists = null output.id = String(data.id) output._original = data // used for server-side settings if (masto) { output.screen_name = data.acct + output.fqn = data.fqn output.statusnet_profile_url = data.url // There's nothing else to get @@ -214,12 +216,14 @@ export const parseUser = (data) => { output.screen_name_ui = output.screen_name if (output.screen_name && output.screen_name.includes('@')) { const parts = output.screen_name.split('@') - let unicodeDomain = punycode.toUnicode(parts[1]) + const unicodeDomain = punycode.toUnicode(parts[1]) if (unicodeDomain !== parts[1]) { // Add some identifier so users can potentially spot spoofing attempts: // lain.com and xn--lin-6cd.com would appear identical otherwise. - unicodeDomain = '🌏' + unicodeDomain + output.screen_name_ui_contains_non_ascii = true output.screen_name_ui = [parts[0], unicodeDomain].join('@') + } else { + output.screen_name_ui_contains_non_ascii = false } } diff --git a/test/unit/karma.conf.js b/test/unit/karma.conf.js @@ -17,11 +17,6 @@ const webpackConfig = merge(baseConfig, { rules: utils.styleLoaders() }, devtool: 'inline-source-map', - // vue: { - // loaders: { - // js: 'isparta' - // } - // }, plugins: [ new webpack.DefinePlugin({ 'process.env': require('../../config/test.env') @@ -37,22 +32,6 @@ const webpackConfig = merge(baseConfig, { // no need for app entry during tests delete webpackConfig.entry -// make sure isparta loader is applied before eslint -// webpackConfig.module.preLoaders = webpackConfig.module.preLoaders || [] -// webpackConfig.module.preLoaders.unshift({ -// test: /\.js$/, -// loader: 'isparta', -// include: path.resolve(projectRoot, 'src') -// }) - -// // only apply babel for test files when using isparta -// webpackConfig.module.loaders.some(function (loader, i) { -// if (loader.loader === 'babel') { -// loader.include = path.resolve(projectRoot, 'test/unit') -// return true -// } -// }) - module.exports = function (config) { config.set({ // to run in additional browsers: diff --git a/test/unit/specs/modules/lists.spec.js b/test/unit/specs/modules/lists.spec.js @@ -17,13 +17,13 @@ describe('The lists module', () => { const list = { id: '1', title: 'testList' } const modList = { id: '1', title: 'anotherTestTitle' } - mutations.setList(state, list) - expect(state.allListsObject[list.id]).to.eql({ title: list.title }) + mutations.setList(state, { listId: list.id, title: list.title }) + expect(state.allListsObject[list.id]).to.eql({ title: list.title, accountIds: [] }) expect(state.allLists).to.have.length(1) expect(state.allLists[0]).to.eql(list) - mutations.setList(state, modList) - expect(state.allListsObject[modList.id]).to.eql({ title: modList.title }) + mutations.setList(state, { listId: modList.id, title: modList.title }) + expect(state.allListsObject[modList.id]).to.eql({ title: modList.title, accountIds: [] }) expect(state.allLists).to.have.length(1) expect(state.allLists[0]).to.eql(modList) }) @@ -33,10 +33,10 @@ describe('The lists module', () => { const list = { id: '1', accountIds: ['1', '2', '3'] } const modList = { id: '1', accountIds: ['3', '4', '5'] } - mutations.setListAccounts(state, list) + mutations.setListAccounts(state, { listId: list.id, accountIds: list.accountIds }) expect(state.allListsObject[list.id]).to.eql({ accountIds: list.accountIds }) - mutations.setListAccounts(state, modList) + mutations.setListAccounts(state, { listId: modList.id, accountIds: modList.accountIds }) expect(state.allListsObject[modList.id]).to.eql({ accountIds: modList.accountIds }) }) @@ -47,9 +47,9 @@ describe('The lists module', () => { 1: { title: 'testList', accountIds: ['1', '2', '3'] } } } - const id = '1' + const listId = '1' - mutations.deleteList(state, { id }) + mutations.deleteList(state, { listId }) expect(state.allLists).to.have.length(0) expect(state.allListsObject).to.eql({}) }) diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js @@ -4,9 +4,11 @@ import { VERSION, COMMAND_TRIM_FLAGS, COMMAND_TRIM_FLAGS_AND_RESET, + _moveItemInArray, _getRecentData, _getAllFlags, _mergeFlags, + _mergePrefs, _resetFlags, mutations, defaultState, @@ -28,6 +30,7 @@ describe('The serverSideStorage module', () => { expect(state.cache._version).to.eql(VERSION) expect(state.cache._timestamp).to.be.a('number') expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) + expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage) }) it('should initialize storage with proper flags for new users if none present', () => { @@ -36,6 +39,7 @@ describe('The serverSideStorage module', () => { expect(state.cache._version).to.eql(VERSION) expect(state.cache._timestamp).to.be.a('number') expect(state.cache.flagStorage).to.eql(newUserFlags) + expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage) }) it('should merge flags even if remote timestamp is older', () => { @@ -57,6 +61,9 @@ describe('The serverSideStorage module', () => { flagStorage: { ...defaultState.flagStorage, updateCounter: 1 + }, + prefsStorage: { + ...defaultState.prefsStorage } } } @@ -99,9 +106,62 @@ describe('The serverSideStorage module', () => { expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) }) }) + describe('setPreference', () => { + const { setPreference, updateCache, addCollectionPreference, removeCollectionPreference } = mutations + + it('should set preference and update journal log accordingly', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.testing', value: 1 }) + expect(state.prefsStorage.simple.testing).to.eql(1) + expect(state.prefsStorage._journal.length).to.eql(1) + expect(state.prefsStorage._journal[0]).to.eql({ + path: 'simple.testing', + operation: 'set', + args: [1], + // should have A timestamp, we don't really care what it is + timestamp: state.prefsStorage._journal[0].timestamp + }) + }) + + it('should keep journal to a minimum', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.testing', value: 1 }) + setPreference(state, { path: 'simple.testing', value: 2 }) + addCollectionPreference(state, { path: 'collections.testing', value: 2 }) + removeCollectionPreference(state, { path: 'collections.testing', value: 2 }) + updateCache(state, { username: 'test' }) + expect(state.prefsStorage.simple.testing).to.eql(2) + expect(state.prefsStorage.collections.testing).to.eql([]) + expect(state.prefsStorage._journal.length).to.eql(2) + expect(state.prefsStorage._journal[0]).to.eql({ + path: 'simple.testing', + operation: 'set', + args: [2], + // should have A timestamp, we don't really care what it is + timestamp: state.prefsStorage._journal[0].timestamp + }) + expect(state.prefsStorage._journal[1]).to.eql({ + path: 'collections.testing', + operation: 'removeFromCollection', + args: [2], + // should have A timestamp, we don't really care what it is + timestamp: state.prefsStorage._journal[1].timestamp + }) + }) + }) }) describe('helper functions', () => { + describe('_moveItemInArray', () => { + it('should move item according to movement value', () => { + expect(_moveItemInArray([1, 2, 3, 4], 4, -1)).to.eql([1, 2, 4, 3]) + expect(_moveItemInArray([1, 2, 3, 4], 1, 2)).to.eql([2, 3, 1, 4]) + }) + it('should clamp movement to within array', () => { + expect(_moveItemInArray([1, 2, 3, 4], 4, -10)).to.eql([4, 1, 2, 3]) + expect(_moveItemInArray([1, 2, 3, 4], 3, 99)).to.eql([1, 2, 4, 3]) + }) + }) describe('_getRecentData', () => { it('should handle nulls correctly', () => { expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true }) @@ -157,6 +217,94 @@ describe('The serverSideStorage module', () => { }) }) + describe('_mergePrefs', () => { + it('should prefer recent and apply journal to it', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { a: 1, b: 0, c: true }, + _journal: [ + { path: 'simple.b', operation: 'set', args: [0], timestamp: 2 }, + { path: 'simple.c', operation: 'set', args: [true], timestamp: 4 } + ] + }, + // STALE + { + simple: { a: 1, b: 1, c: false }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [1], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [1], timestamp: 3 } + ] + } + ) + ).to.eql({ + simple: { a: 1, b: 1, c: true }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [1], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [1], timestamp: 3 }, + { path: 'simple.c', operation: 'set', args: [true], timestamp: 4 } + ] + }) + }) + + it('should allow setting falsy values', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { a: 1, b: 0, c: false }, + _journal: [ + { path: 'simple.b', operation: 'set', args: [0], timestamp: 2 }, + { path: 'simple.c', operation: 'set', args: [false], timestamp: 4 } + ] + }, + // STALE + { + simple: { a: 0, b: 0, c: true }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [0], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [0], timestamp: 3 } + ] + } + ) + ).to.eql({ + simple: { a: 0, b: 0, c: false }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [0], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [0], timestamp: 3 }, + { path: 'simple.c', operation: 'set', args: [false], timestamp: 4 } + ] + }) + }) + + it('should work with strings', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { a: 'foo' }, + _journal: [ + { path: 'simple.a', operation: 'set', args: ['foo'], timestamp: 2 } + ] + }, + // STALE + { + simple: { a: 'bar' }, + _journal: [ + { path: 'simple.a', operation: 'set', args: ['bar'], timestamp: 4 } + ] + } + ) + ).to.eql({ + simple: { a: 'bar' }, + _journal: [ + { path: 'simple.a', operation: 'set', args: ['bar'], timestamp: 4 } + ] + }) + }) + }) + describe('_resetFlags', () => { it('should reset all known flags to 0 when reset flag is set to > 0 and < 9000', () => { const totalFlags = { a: 0, b: 3, reset: 1 } diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -269,7 +269,8 @@ describe('API Entities normalizer', () => { it('converts IDN to unicode and marks it as internatonal', () => { const user = makeMockUserMasto({ acct: 'lain@xn--lin-6cd.com' }) - expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@🌏lаin.com') + expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@lаin.com') + expect(parseUser(user)).to.have.property('screen_name_ui_contains_non_ascii').that.equal(true) }) }) diff --git a/yarn.lock b/yarn.lock @@ -40,7 +40,28 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== -"@babel/core@7.18.10", "@babel/core@^7.12.3": +"@babel/core@7.18.13": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.13.tgz#9be8c44512751b05094a4d3ab05fc53a47ce00ac" + integrity sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.13" + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-module-transforms" "^7.18.9" + "@babel/helpers" "^7.18.9" + "@babel/parser" "^7.18.13" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.18.13" + "@babel/types" "^7.18.13" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/core@^7.12.3": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8" integrity sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw== @@ -109,6 +130,15 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.18.13": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.13.tgz#59550cbb9ae79b8def15587bdfbaa388c4abf212" + integrity sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ== + dependencies: + "@babel/types" "^7.18.13" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + "@babel/generator@^7.18.6", "@babel/generator@^7.18.7": version "7.18.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.7.tgz#2aa78da3c05aadfc82dbac16c99552fc802284bd" @@ -543,6 +573,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.10.tgz#94b5f8522356e69e8277276adf67ed280c90ecc1" integrity sha512-TYk3OA0HKL6qNryUayb5UUEhM/rkOQozIBEA5ITXh5DWrSp0TlUQXMyZmnWxG/DizSWBeeQ0Zbc5z8UGaaqoeg== +"@babel/parser@^7.18.13": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4" + integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg== + "@babel/parser@^7.18.6", "@babel/parser@^7.18.8": version "7.18.8" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.8.tgz#822146080ac9c62dac0823bb3489622e0bc1cbdf" @@ -1266,6 +1301,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.18.13": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.13.tgz#5ab59ef51a997b3f10c4587d648b9696b6cb1a68" + integrity sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.13" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.18.13" + "@babel/types" "^7.18.13" + debug "^4.1.0" + globals "^11.1.0" + "@babel/traverse@^7.18.6", "@babel/traverse@^7.18.8": version "7.18.8" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.8.tgz#f095e62ab46abf1da35e5a2011f43aee72d8d5b0" @@ -1323,6 +1374,15 @@ "@babel/helper-validator-identifier" "^7.18.6" to-fast-properties "^2.0.0" +"@babel/types@^7.18.13": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a" + integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ== + dependencies: + "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + "@babel/types@^7.18.6", "@babel/types@^7.18.7", "@babel/types@^7.18.8": version "7.18.8" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.8.tgz#c5af199951bf41ba4a6a9a6d0d8ad722b30cd42f" @@ -1358,14 +1418,14 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@eslint/eslintrc@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f" - integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw== +"@eslint/eslintrc@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.1.tgz#de0807bfeffc37b964a7d0400e0c348ce5a2543d" + integrity sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.3.2" + espree "^9.4.0" globals "^13.15.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -1418,6 +1478,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d" integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA== +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + "@humanwhocodes/object-schema@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" @@ -1764,10 +1829,10 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@vue/babel-helper-vue-jsx-merge-props@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81" - integrity sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA== +"@vue/babel-helper-vue-jsx-merge-props@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz#8d53a1e21347db8edbe54d339902583176de09f2" + integrity sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA== "@vue/babel-helper-vue-transform-on@^1.0.2": version "1.0.2" @@ -2037,10 +2102,6 @@ version "4.2.2" resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" -abbrev@1, abbrev@1.0.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" - accepts@~1.3.4: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -2130,10 +2191,6 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0: require-from-string "^2.0.2" uri-js "^4.2.2" -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - ansi-align@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" @@ -2202,10 +2259,6 @@ ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" -ansi-styles@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" - ansi-to-html@^0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.7.2.tgz#a92c149e4184b571eb29a0135ca001a8e2d710cb" @@ -2221,12 +2274,6 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - dependencies: - sprintf-js "~1.0.2" - argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -2276,16 +2323,6 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async@1.x: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - -async@^2.5.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - dependencies: - lodash "^4.17.10" - async@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" @@ -2329,58 +2366,6 @@ axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.1.4, babel-core@^6.26.0: - version "6.26.3" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.1" - debug "^2.6.9" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.8" - slash "^1.0.0" - source-map "^0.5.7" - -babel-generator@^6.26.0: - version "6.26.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-loader@8.2.5: version "8.2.5" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.5.tgz#d45f585e654d5a5d90f5350a779d7647c5ed512e" @@ -2391,12 +2376,6 @@ babel-loader@8.2.5: make-dir "^3.1.0" schema-utils "^2.6.5" -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - dependencies: - babel-runtime "^6.22.0" - babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" @@ -2439,62 +2418,6 @@ babel-plugin-polyfill-regenerator@^0.4.0: dependencies: "@babel/helper-define-polyfill-provider" "^0.3.2" -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - bail@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" @@ -2804,7 +2727,7 @@ chai@4.3.6: pathval "^1.1.1" type-detect "^4.0.5" -chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -2838,14 +2761,6 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" - dependencies: - ansi-styles "~1.0.0" - has-color "~0.1.0" - strip-ansi "~0.1.0" - character-entities-legacy@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" @@ -3046,10 +2961,6 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -commander@~2.17.1: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -3089,12 +3000,6 @@ content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" -convert-source-map@^1.5.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" - dependencies: - safe-buffer "~5.1.1" - convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -3144,10 +3049,6 @@ core-js-compat@^3.22.1: browserslist "^4.21.1" semver "7.0.0" -core-js@^2.4.0, core-js@^2.5.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.2.tgz#267988d7268323b349e20b4588211655f0e83944" - core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -3325,7 +3226,7 @@ de-indent@^1.0.2: resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= -debug@2.6.9, debug@^2.6.8, debug@^2.6.9: +debug@2.6.9, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -3408,7 +3309,7 @@ deep-eql@^3.0.1: dependencies: type-detect "^4.0.0" -deep-is@^0.1.3, deep-is@~0.1.3: +deep-is@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -3463,12 +3364,6 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - dependencies: - repeating "^2.0.0" - di@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" @@ -3793,28 +3688,6 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -escodegen@1.8.x: - version "1.8.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" - dependencies: - esprima "^2.7.1" - estraverse "^1.9.1" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.2.0" - -escodegen@^1.6.1: - version "1.11.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" - dependencies: - esprima "^3.1.3" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - eslint-config-standard@17.0.0: version "17.0.0" resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz#fd5b6cf1dcf6ba8d29f200c461de2e19069888cf" @@ -3888,10 +3761,10 @@ eslint-plugin-n@15.2.5: resolve "^1.22.1" semver "^7.3.7" -eslint-plugin-promise@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz#017652c07c9816413a41e11c30adc42c3d55ff18" - integrity sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw== +eslint-plugin-promise@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.0.1.tgz#a8cddf96a67c4059bdabf4d724a29572188ae423" + integrity sha512-uM4Tgo5u3UWQiroOyDEsYcVMOo7re3zmno0IZmB5auxoaQNIceAbXEkSt8RNrKtaYehARHG06pYK6K1JhtP0Zw== eslint-plugin-vue@9.4.0: version "9.4.0" @@ -3962,14 +3835,15 @@ eslint-webpack-plugin@3.2.0: normalize-path "^3.0.0" schema-utils "^4.0.0" -eslint@8.22.0: - version "8.22.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.22.0.tgz#78fcb044196dfa7eef30a9d65944f6f980402c48" - integrity sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA== +eslint@8.23.0: + version "8.23.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.23.0.tgz#a184918d288820179c6041bb3ddcc99ce6eea040" + integrity sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA== dependencies: - "@eslint/eslintrc" "^1.3.0" + "@eslint/eslintrc" "^1.3.1" "@humanwhocodes/config-array" "^0.10.4" "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" + "@humanwhocodes/module-importer" "^1.0.1" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -3979,7 +3853,7 @@ eslint@8.22.0: eslint-scope "^7.1.1" eslint-utils "^3.0.0" eslint-visitor-keys "^3.3.0" - espree "^9.3.3" + espree "^9.4.0" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -4005,7 +3879,6 @@ eslint@8.22.0: strip-ansi "^6.0.1" strip-json-comments "^3.1.0" text-table "^0.2.0" - v8-compile-cache "^2.0.3" espree@^6.0.0: version "6.2.1" @@ -4016,7 +3889,7 @@ espree@^6.0.0: acorn-jsx "^5.2.0" eslint-visitor-keys "^1.1.0" -espree@^9.3.1, espree@^9.3.2: +espree@^9.3.1: version "9.3.2" resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596" integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA== @@ -4025,27 +3898,15 @@ espree@^9.3.1, espree@^9.3.2: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" -espree@^9.3.3: - version "9.3.3" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.3.tgz#2dd37c4162bb05f433ad3c1a52ddf8a49dc08e9d" - integrity sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng== +espree@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.0.tgz#cd4bc3d6e9336c433265fc0aa016fc1aaf182f8a" + integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw== dependencies: acorn "^8.8.0" acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" -esprima@2.7.x, esprima@^2.7.1: - version "2.7.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" - -esprima@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - esquery@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" @@ -4060,11 +3921,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" - -estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" @@ -4193,7 +4050,7 @@ fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" -fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4: +fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== @@ -4493,16 +4350,6 @@ glob@7.2.0, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.15: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^7.0.0, glob@^7.1.1, glob@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" @@ -4553,10 +4400,6 @@ globals@^13.15.0: dependencies: type-fest "^0.20.2" -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - globby@^11.0.1: version "11.0.1" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" @@ -4633,16 +4476,6 @@ growl@1.10.5: resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== -handlebars@^4.0.1: - version "4.0.12" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" - dependencies: - async "^2.5.0" - optimist "^0.6.1" - source-map "^0.6.1" - optionalDependencies: - uglify-js "^3.1.4" - hard-rejection@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" @@ -4659,14 +4492,6 @@ has-bigints@^1.0.1: resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== -has-color@~0.1.0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -4712,13 +4537,6 @@ he@1.2.0, he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - hosted-git-info@^2.1.4: version "2.7.1" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" @@ -4942,12 +4760,6 @@ interpret@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" -invariant@^2.2.2: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - dependencies: - loose-envify "^1.0.0" - ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -5049,12 +4861,6 @@ is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - dependencies: - number-is-nan "^1.0.0" - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -5234,27 +5040,6 @@ isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" -isparta-loader@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isparta-loader/-/isparta-loader-2.0.0.tgz#4425f496c93f765bbceb4dd938576da307566ed1" - integrity sha1-RCX0lsk/dlu8603ZOFdtowdWbtE= - dependencies: - isparta "4.x.x" - -isparta@4.x.x: - version "4.1.1" - resolved "https://registry.yarnpkg.com/isparta/-/isparta-4.1.1.tgz#c92e49672946914ec5407c801160f3374e0b7cb4" - dependencies: - babel-core "^6.1.4" - escodegen "^1.6.1" - esprima "^4.0.0" - istanbul "0.4.5" - mkdirp "^0.5.0" - nomnomnomnom "^2.0.0" - object-assign "^4.0.1" - source-map "^0.5.0" - which "^1.0.9" - istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" @@ -5297,25 +5082,6 @@ istanbul-reports@^3.0.5: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -istanbul@0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" - dependencies: - abbrev "1.0.x" - async "1.x" - escodegen "1.8.x" - esprima "2.7.x" - glob "^5.0.15" - handlebars "^4.0.1" - js-yaml "3.x" - mkdirp "0.5.x" - nopt "3.x" - once "1.x" - resolve "1.1.x" - supports-color "^3.1.0" - which "^1.1.1" - wordwrap "^1.0.0" - jake@^10.8.5: version "10.8.5" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" @@ -5349,21 +5115,10 @@ js-cookie@3.0.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414" integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw== -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" -js-yaml@3.x: - version "3.12.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -5371,10 +5126,6 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -5411,10 +5162,6 @@ json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" -json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -5584,13 +5331,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - lie@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" @@ -5892,7 +5632,7 @@ lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -lodash@^4.17.10, lodash@^4.17.4: +lodash@^4.17.10: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -5942,12 +5682,6 @@ longest-streak@^2.0.0: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - loupe@2.3.4, loupe@^2.3.1: version "2.3.4" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" @@ -6161,7 +5895,7 @@ mini-css-extract-plugin@2.6.1: dependencies: schema-utils "^4.0.0" -"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -6204,7 +5938,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@0.0.8, minimist@~0.0.1: +minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -6222,7 +5956,7 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -6407,19 +6141,6 @@ node-releases@^2.0.5, node-releases@^2.0.6: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== -nomnomnomnom@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/nomnomnomnom/-/nomnomnomnom-2.0.1.tgz#b2239f031c8d04da67e32836e1e3199e12f7a8e2" - dependencies: - chalk "~0.4.0" - underscore "~1.6.0" - -nopt@3.x: - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - dependencies: - abbrev "1" - normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -6469,11 +6190,7 @@ num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - -object-assign@^4, object-assign@^4.0.1: +object-assign@^4: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -6528,7 +6245,7 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -6563,24 +6280,6 @@ opn@5.5.0: dependencies: is-wsl "^1.1.0" -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -optionator@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" - optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -6618,14 +6317,6 @@ ora@5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - -os-tmpdir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -6763,7 +6454,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: +path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -7219,10 +6910,6 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - pretty-error@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" @@ -7231,10 +6918,6 @@ pretty-error@^4.0.0: lodash "^4.17.20" renderkid "^3.0.0" -private@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" @@ -7437,10 +7120,6 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - regenerator-runtime@^0.13.4: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" @@ -7537,12 +7216,6 @@ repeat-string@^1.0.0: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - dependencies: - is-finite "^1.0.0" - replace-ext@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" @@ -7579,7 +7252,7 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@1.1.x, resolve@^1.1.6: +resolve@^1.1.6: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" @@ -7723,11 +7396,6 @@ selenium-webdriver@^4.3.1: version "5.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" -semver@5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - semver@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -7740,18 +7408,18 @@ semver@7.3.5: dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7: +semver@7.3.7, semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -7849,10 +7517,10 @@ signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -sinon-chai@2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.14.0.tgz#da7dd4cc83cd6a260b67cca0f7a9fdae26a1205d" - integrity sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ== +sinon-chai@3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783" + integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g== sinon@2.4.1: version "2.4.1" @@ -7868,10 +7536,6 @@ sinon@2.4.1: text-encoding "0.6.4" type-detect "^4.0.0" -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -7922,12 +7586,6 @@ socket.io@^4.4.1: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - dependencies: - source-map "^0.5.6" - source-map-support@^0.5.16: version "0.5.16" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" @@ -7944,20 +7602,14 @@ source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: +source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" -source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: +source-map@^0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" -source-map@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" - dependencies: - amdefine ">=0.0.4" - sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" @@ -7990,10 +7642,6 @@ specificity@^0.4.1: resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019" integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg== -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" @@ -8105,10 +7753,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" - strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -8230,12 +7874,6 @@ supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" -supports-color@^3.1.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" - dependencies: - has-flag "^1.0.0" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -8337,10 +7975,6 @@ tmp@^0.2.1: dependencies: rimraf "^3.0.0" -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -8362,10 +7996,6 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - trough@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" @@ -8393,12 +8023,6 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - dependencies: - prelude-ls "~1.1.2" - type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -8448,13 +8072,6 @@ ua-parser-js@^0.7.30: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== -uglify-js@^3.1.4: - version "3.4.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" - dependencies: - commander "~2.17.1" - source-map "~0.6.1" - unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" @@ -8465,10 +8082,6 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" -underscore@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" - unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -8587,7 +8200,7 @@ uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" -v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: +v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== @@ -8663,10 +8276,10 @@ vue-loader@17.0.0: hash-sum "^2.0.0" loader-utils "^2.0.0" -vue-router@4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.3.tgz#f8dc7931a2253cc5aa9b740f8b98969d08ca283c" - integrity sha512-XvK81bcYglKiayT7/vYAg/f36ExPC4t90R/HIpzrZ5x+17BOWptXLCrEPufGgZeuq68ww4ekSIMBZY1qdUdfjA== +vue-router@4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.5.tgz#256f597e3f5a281a23352a6193aa6e342c8d9f9a" + integrity sha512-IsvoF5D2GQ/EGTs/Th4NQms9gd2NSqV+yylxIyp/OYp8xOwxmU8Kj/74E9DTSYAyH5LX7idVUngN3JSj1X4xcQ== dependencies: "@vue/devtools-api" "^6.1.4" @@ -8678,10 +8291,10 @@ vue-style-loader@4.1.3: hash-sum "^1.0.2" loader-utils "^1.0.2" -vue-template-compiler@2.7.9: - version "2.7.9" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.9.tgz#ffbeb1769ae6af65cd405a6513df6b1e20e33616" - integrity sha512-NPJxt6OjVlzmkixYg0SdIN2Lw/rMryQskObY89uAMcM9flS/HrmLK5LaN1ReBTuhBgnYuagZZEkSS6FESATQUQ== +vue-template-compiler@2.7.10: + version "2.7.10" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.10.tgz#9e20f35b2fdccacacf732dd7dedb49bf65f4556b" + integrity sha512-QO+8R9YRq1Gudm8ZMdo/lImZLJVUIAM8c07Vp84ojdDAf8HmPJc7XB556PcXV218k2AkKznsRz6xB5uOjAC4EQ== dependencies: de-indent "^1.0.2" he "^1.2.0" @@ -8822,7 +8435,7 @@ which@2.0.2, which@^2.0.1: dependencies: isexe "^2.0.0" -which@^1.0.9, which@^1.1.1, which@^1.3.1: +which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" dependencies: @@ -8840,14 +8453,6 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wordwrap@^1.0.0, wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - workerpool@6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b"