diff options
| author | Shpuld Shpludson <shp@cock.li> | 2019-07-26 12:44:32 +0000 |
|---|---|---|
| committer | Shpuld Shpludson <shp@cock.li> | 2019-07-26 12:44:32 +0000 |
| commit | d3f6b581d1bbe64d26fceae3f00e9d471ca44dfe (patch) | |
| tree | 371e786ac95c83d914aa04f22aa9aabfb7b5dc4a | |
| parent | 3370dd80dc4644f2bff053b97b18698cd2abb550 (diff) | |
| parent | 4827e4d972f8ee11e606693e24ae4ca21711c6b1 (diff) | |
Merge branch 'develop' into 'feat/conversation-muting'
# Conflicts:
# src/components/extra_buttons/extra_buttons.js
# src/components/extra_buttons/extra_buttons.vue
28 files changed, 638 insertions, 177 deletions
diff --git a/build/dev-server.js b/build/dev-server.js index 59dd2c4d..48574214 100644 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -24,9 +24,6 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, { stats: { colors: true, chunks: false - }, - headers: { - 'content-security-policy': "base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; script-src 'self' 'unsafe-eval';" } }) diff --git a/package.json b/package.json index 25bc419c..28f3beba 100644 --- a/package.json +++ b/package.json @@ -25,14 +25,13 @@ "localforage": "^1.5.0", "object-path": "^0.11.3", "phoenix": "^1.3.0", - "popper.js": "^1.14.7", "portal-vue": "^2.1.4", "sanitize-html": "^1.13.0", "v-click-outside": "^2.1.1", + "v-tooltip": "^2.0.2", "vue": "^2.5.13", "vue-chat-scroll": "^1.2.1", "vue-i18n": "^7.3.2", - "vue-popperjs": "^2.0.3", "vue-router": "^3.0.1", "vue-template-compiler": "^2.3.4", "vuelidate": "^0.7.4", @@ -81,8 +80,8 @@ "json-loader": "^0.5.4", "karma": "^3.0.0", "karma-coverage": "^1.1.1", - "karma-mocha": "^1.2.0", "karma-firefox-launcher": "^1.1.0", + "karma-mocha": "^1.2.0", "karma-sinon-chai": "^2.0.2", "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "0.0.26", diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 3fcbc246..3799359f 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -148,6 +148,37 @@ const getInstancePanel = async ({ store }) => { } } +const getStickers = async ({ store }) => { + try { + const res = await window.fetch('/static/stickers.json') + if (res.ok) { + const values = await res.json() + const stickers = (await Promise.all( + Object.entries(values).map(async ([name, path]) => { + const resPack = await window.fetch(path + 'pack.json') + var meta = {} + if (resPack.ok) { + meta = await resPack.json() + } + return { + pack: name, + path, + meta + } + }) + )).sort((a, b) => { + return a.meta.title.localeCompare(b.meta.title) + }) + store.dispatch('setInstanceOption', { name: 'stickers', value: stickers }) + } else { + throw (res) + } + } catch (e) { + console.warn("Can't load stickers") + console.warn(e) + } +} + const getStaticEmoji = async ({ store }) => { try { const res = await window.fetch('/static/emoji.json') @@ -286,6 +317,7 @@ const afterStoreSetup = async ({ store, i18n }) => { setConfig({ store }), getTOS({ store }), getInstancePanel({ store }), + getStickers({ store }), getStaticEmoji({ store }), getCustomEmoji({ store }), getNodeInfo({ store }) diff --git a/src/boot/routes.js b/src/boot/routes.js index 22641f83..7dc4b2a5 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -19,6 +19,14 @@ import WhoToFollow from 'components/who_to_follow/who_to_follow.vue' import About from 'components/about/about.vue' export default (store) => { + const validateAuthenticatedRoute = (to, from, next) => { + if (store.state.users.currentUser) { + next() + } else { + next(store.state.instance.redirectRootNoLogin || '/main/all') + } + } + return [ { name: 'root', path: '/', @@ -30,23 +38,23 @@ export default (store) => { }, { name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline }, { name: 'public-timeline', path: '/main/public', component: PublicTimeline }, - { name: 'friends', path: '/main/friends', component: FriendsTimeline }, + { name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute }, { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'external-user-profile', path: '/users/:id', component: UserProfile }, - { name: 'interactions', path: '/users/:username/interactions', component: Interactions }, - { name: 'dms', path: '/users/:username/dms', component: DMs }, + { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute }, + { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'settings', path: '/settings', component: Settings }, { name: 'registration', path: '/registration', component: Registration }, { name: 'registration-token', path: '/registration/:token', component: Registration }, - { name: 'friend-requests', path: '/friend-requests', component: FollowRequests }, - { name: 'user-settings', path: '/user-settings', component: UserSettings }, - { name: 'notifications', path: '/:username/notifications', component: Notifications }, + { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, + { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute }, + { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute }, { name: 'login', path: '/login', component: AuthForm }, { name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) }, { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, - { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow }, + { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, { name: 'about', path: '/about', component: About }, { name: 'user-profile', path: '/(users/)?:name', component: UserProfile } ] diff --git a/src/components/emoji-input/suggestor.js b/src/components/emoji-input/suggestor.js index a7ac203e..aec5c39d 100644 --- a/src/components/emoji-input/suggestor.js +++ b/src/components/emoji-input/suggestor.js @@ -1,20 +1,27 @@ +import { debounce } from 'lodash' /** * suggest - generates a suggestor function to be used by emoji-input * data: object providing source information for specific types of suggestions: * data.emoji - optional, an array of all emoji available i.e. * (state.instance.emoji + state.instance.customEmoji) * data.users - optional, an array of all known users + * updateUsersList - optional, a function to search and append to users * * Depending on data present one or both (or none) can be present, so if field * doesn't support user linking you can just provide only emoji. */ + +const debounceUserSearch = debounce((data, input) => { + data.updateUsersList(input) +}, 500, { leading: true, trailing: false }) + export default data => input => { const firstChar = input[0] if (firstChar === ':' && data.emoji) { return suggestEmoji(data.emoji)(input) } if (firstChar === '@' && data.users) { - return suggestUsers(data.users)(input) + return suggestUsers(data)(input) } return [] } @@ -38,9 +45,11 @@ export const suggestEmoji = emojis => input => { }) } -export const suggestUsers = users => input => { +export const suggestUsers = data => input => { const noPrefix = input.toLowerCase().substr(1) - return users.filter( + const users = data.users + + const newUsers = users.filter( user => user.screen_name.toLowerCase().startsWith(noPrefix) || user.name.toLowerCase().startsWith(noPrefix) @@ -75,5 +84,11 @@ export const suggestUsers = users => input => { imageUrl: profile_image_url_original, replacement: '@' + screen_name + ' ' })) + + // BE search users if there are no matches + if (newUsers.length === 0 && data.updateUsersList) { + debounceUserSearch(data, noPrefix) + } + return newUsers /* eslint-enable camelcase */ } diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index 56b2c41e..8d123293 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -1,35 +1,18 @@ -import Popper from 'vue-popperjs/src/component/popper.js.vue' - const ExtraButtons = { props: [ 'status' ], - components: { - Popper - }, - data () { - return { - showDropDown: false, - showPopper: true - } - }, methods: { deleteStatus () { - this.refreshPopper() const confirmed = window.confirm(this.$t('status.delete_confirm')) if (confirmed) { this.$store.dispatch('deleteStatus', { id: this.status.id }) } }, - toggleMenu () { - this.showDropDown = !this.showDropDown - }, pinStatus () { - this.refreshPopper() this.$store.dispatch('pinStatus', this.status.id) .then(() => this.$emit('onSuccess')) .catch(err => this.$emit('onError', err.error.error)) }, unpinStatus () { - this.refreshPopper() this.$store.dispatch('unpinStatus', this.status.id) .then(() => this.$emit('onSuccess')) .catch(err => this.$emit('onError', err.error.error)) @@ -45,13 +28,6 @@ const ExtraButtons = { this.$store.dispatch('unmuteConversation', this.status.id) .then(() => this.$emit('onSuccess')) .catch(err => this.$emit('onError', err.error.error)) - }, - refreshPopper () { - this.showPopper = false - this.showDropDown = false - setTimeout(() => { - this.showPopper = true - }) } }, computed: { diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index 5027be1b..fc800072 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -1,18 +1,13 @@ <template> - <Popper - v-if="showPopper" + <v-popover + v-if="enabled" trigger="click" - append-to-body - :options="{ - placement: 'top', - modifiers: { - arrow: { enabled: true }, - offset: { offset: '0, 5px' }, - } - }" - @hide="showDropDown = false" + placement="top" + class="extra-button-popover" + :offset="5" + :container="false" > - <div class="popper-wrapper"> + <div slot="popover"> <div class="dropdown-menu"> <button v-if="!status.muted" @@ -30,6 +25,7 @@ </button> <button v-if="!status.pinned && canPin" + v-close-popover class="dropdown-item dropdown-item-icon" @click.prevent="pinStatus" > @@ -37,6 +33,7 @@ </button> <button v-if="status.pinned && canPin" + v-close-popover class="dropdown-item dropdown-item-icon" @click.prevent="unpinStatus" > @@ -44,6 +41,7 @@ </button> <button v-if="canDelete" + v-close-popover class="dropdown-item dropdown-item-icon" @click.prevent="deleteStatus" > @@ -51,17 +49,10 @@ </button> </div> </div> - <div - slot="reference" - class="button-icon" - @click="toggleMenu" - > - <i - class="icon-ellipsis" - :class="{'icon-clicked': showDropDown}" - /> + <div class="button-icon"> + <i class="icon-ellipsis" /> </div> - </Popper> + </v-popover> </template> <script src="./extra_buttons.js" ></script> @@ -73,7 +64,8 @@ .icon-ellipsis { cursor: pointer; - &:hover, &.icon-clicked { + &:hover, + .extra-button-popover.open & { color: $fallback--text; color: var(--text, $fallback--text); } diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js index 11de9f93..8aadc8c5 100644 --- a/src/components/moderation_tools/moderation_tools.js +++ b/src/components/moderation_tools/moderation_tools.js @@ -1,5 +1,4 @@ import DialogModal from '../dialog_modal/dialog_modal.vue' -import Popper from 'vue-popperjs/src/component/popper.js.vue' const FORCE_NSFW = 'mrf_tag:media-force-nsfw' const STRIP_MEDIA = 'mrf_tag:media-strip' @@ -29,8 +28,7 @@ const ModerationTools = { } }, components: { - DialogModal, - Popper + DialogModal }, computed: { tagsSet () { @@ -41,9 +39,6 @@ const ModerationTools = { } }, methods: { - toggleMenu () { - this.showDropDown = !this.showDropDown - }, hasTag (tagName) { return this.tagsSet.has(tagName) }, diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index f1ab67a6..d97ca3aa 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -1,18 +1,15 @@ <template> <div> - <Popper + <v-popover trigger="click" - append-to-body - :options="{ - placement: 'bottom-end', - modifiers: { - arrow: { enabled: true }, - offset: { offset: '0, 5px' }, - } - }" + class="moderation-tools-popover" + :container="false" + placement="bottom-end" + :offset="5" + @show="showDropDown = true" @hide="showDropDown = false" > - <div class="popper-wrapper"> + <div slot="popover"> <div class="dropdown-menu"> <span v-if="user.is_local"> <button @@ -127,14 +124,12 @@ </div> </div> <button - slot="reference" class="btn btn-default btn-block" :class="{ pressed: showDropDown }" - @click="toggleMenu" > {{ $t('user_card.admin_menu.moderation') }} </button> - </Popper> + </v-popover> <portal to="modal"> <DialogModal v-if="showDeleteUserDialog" @@ -188,4 +183,11 @@ } } +.moderation-tools-popover { + height: 100%; + .trigger { + display: flex !important; + height: 100%; + } +} </style> diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss index cfc5c8e7..279b01be 100644 --- a/src/components/popper/popper.scss +++ b/src/components/popper/popper.scss @@ -1,71 +1,99 @@ @import '../../_variables.scss'; -.popper-wrapper { +.tooltip.popover { z-index: 8; -} -.popper-wrapper .popper__arrow { - width: 0; - height: 0; - border-style: solid; - position: absolute; - margin: 5px; -} + .popover-inner { + box-shadow: 1px 1px 4px rgba(0,0,0,.6); + box-shadow: var(--panelShadow); + border-radius: $fallback--btnRadius; + border-radius: var(--btnRadius, $fallback--btnRadius); + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + } -.popper-wrapper[x-placement^="top"] { - margin-bottom: 5px; -} + .popover-arrow { + width: 0; + height: 0; + border-style: solid; + position: absolute; + margin: 5px; + border-color: $fallback--bg; + border-color: var(--bg, $fallback--bg); + z-index: 1; + } -.popper-wrapper[x-placement^="top"] .popper__arrow { - border-width: 5px 5px 0 5px; - border-color: $fallback--bg transparent transparent transparent; - border-color: var(--bg, $fallback--bg) transparent transparent transparent; - bottom: -5px; - left: calc(50% - 5px); - margin-top: 0; - margin-bottom: 0; -} + &[x-placement^="top"] { + margin-bottom: 5px; + + .popover-arrow { + border-width: 5px 5px 0 5px; + border-left-color: transparent !important; + border-right-color: transparent !important; + border-bottom-color: transparent !important; + bottom: -5px; + left: calc(50% - 5px); + margin-top: 0; + margin-bottom: 0; + } + } -.popper-wrapper[x-placement^="bottom"] { - margin-top: 5px; -} + &[x-placement^="bottom"] { + margin-top: 5px; + + .popover-arrow { + border-width: 0 5px 5px 5px; + border-left-color: transparent !important; + border-right-color: transparent !important; + border-top-color: transparent !important; + top: -5px; + left: calc(50% - 5px); + margin-top: 0; + margin-bottom: 0; + } + } -.popper-wrapper[x-placement^="bottom"] .popper__arrow { - border-width: 0 5px 5px 5px; - border-color: transparent transparent $fallback--bg transparent; - border-color: transparent transparent var(--bg, $fallback--bg) transparent; - top: -5px; - left: calc(50% - 5px); - margin-top: 0; - margin-bottom: 0; -} + &[x-placement^="right"] { + margin-left: 5px; + + .popover-arrow { + border-width: 5px 5px 5px 0; + border-left-color: transparent !important; + border-top-color: transparent !important; + border-bottom-color: transparent !important; + left: -5px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; + } + } -.popper-wrapper[x-placement^="right"] { - margin-left: 5px; -} + &[x-placement^="left"] { + margin-right: 5px; -.popper-wrapper[x-placement^="right"] .popper__arrow { - border-width: 5px 5px 5px 0; - border-color: transparent $fallback--bg transparent transparent; - border-color: transparent var(--bg, $fallback--bg) transparent transparent; - left: -5px; - top: calc(50% - 5px); - margin-left: 0; - margin-right: 0; -} + .popover-arrow { + border-width: 5px 0 5px 5px; + border-top-color: transparent !important; + border-right-color: transparent !important; + border-bottom-color: transparent !important; + right: -5px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; + } + } -.popper-wrapper[x-placement^="left"] { - margin-right: 5px; -} + &[aria-hidden='true'] { + visibility: hidden; + opacity: 0; + transition: opacity .15s, visibility .15s; + } -.popper-wrapper[x-placement^="left"] .popper__arrow { - border-width: 5px 0 5px 5px; - border-color: transparent transparent transparent $fallback--bg; - border-color: transparent transparent transparent var(--bg, $fallback--bg); - right: -5px; - top: calc(50% - 5px); - margin-left: 0; - margin-right: 0; + &[aria-hidden='false'] { + visibility: visible; + opacity: 1; + transition: opacity .15s; + } } .dropdown-menu { @@ -76,13 +104,6 @@ list-style: none; max-width: 100vw; z-index: 10; - box-shadow: 1px 1px 4px rgba(0,0,0,.6); - box-shadow: var(--panelShadow); - border: none; - border-radius: $fallback--btnRadius; - border-radius: var(--btnRadius, $fallback--btnRadius); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); .dropdown-divider { height: 0; diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index ef6b0fce..40bbf6d4 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -3,6 +3,7 @@ import MediaUpload from '../media_upload/media_upload.vue' import ScopeSelector from '../scope_selector/scope_selector.vue' import EmojiInput from '../emoji-input/emoji-input.vue' import PollForm from '../poll/poll_form.vue' +import StickerPicker from '../sticker_picker/sticker_picker.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import { reject, map, uniqBy } from 'lodash' import suggestor from '../emoji-input/suggestor.js' @@ -34,6 +35,7 @@ const PostStatusForm = { MediaUpload, EmojiInput, PollForm, + StickerPicker, ScopeSelector }, mounted () { @@ -82,7 +84,8 @@ const PostStatusForm = { contentType }, caret: 0, - pollFormVisible: false + pollFormVisible: false, + stickerPickerVisible: false } }, computed: { @@ -104,7 +107,8 @@ const PostStatusForm = { ...this.$store.state.instance.emoji, ...this.$store.state.instance.customEmoji ], - users: this.$store.state.users.users + users: this.$store.state.users.users, + updateUsersList: (input) => this.$store.dispatch('searchUsers', input) }) }, emojiSuggestor () { @@ -157,6 +161,12 @@ const PostStatusForm = { safeDMEnabled () { return this.$store.state.instance.safeDM }, + stickersAvailable () { + if (this.$store.state.instance.stickers) { + return this.$store.state.instance.stickers.length > 0 + } + return 0 + }, pollsAvailable () { return this.$store.state.instance.pollsAvailable && this.$store.state.instance.pollLimits.max_options >= 2 @@ -212,6 +222,7 @@ const PostStatusForm = { poll: {} } this.pollFormVisible = false + this.stickerPickerVisible = false this.$refs.mediaUpload.clearFile() this.clearPollForm() this.$emit('posted') @@ -228,6 +239,7 @@ const PostStatusForm = { addMediaFile (fileInfo) { this.newStatus.files.push(fileInfo) this.enableSubmit() + this.stickerPickerVisible = false }, removeMediaFile (fileInfo) { let index = this.newStatus.files.indexOf(fileInfo) @@ -287,6 +299,14 @@ const PostStatusForm = { changeVis (visibility) { this.newStatus.visibility = visibility }, + toggleStickerPicker () { + this.stickerPickerVisible = !this.stickerPickerVisible + }, + clearStickerPicker () { + if (this.$refs.stickerPicker) { + this.$refs.stickerPicker.clear() + } + }, togglePollForm () { this.pollFormVisible = !this.pollFormVisible }, diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 1f389eda..d29d47e4 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -158,6 +158,17 @@ @upload-failed="uploadFailed" /> <div + v-if="stickersAvailable" + class="sticker-icon" + > + <i + :title="$t('stickers.add_sticker')" + class="icon-picture btn btn-default" + :class="{ selected: stickerPickerVisible }" + @click="toggleStickerPicker" + /> + </div> + <div v-if="pollsAvailable" class="poll-icon" > @@ -169,7 +180,6 @@ /> </div> </div> - <button v-if="posting" disabled @@ -248,6 +258,11 @@ <label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label> </div> </form> + <sticker-picker + v-if="stickerPickerVisible" + ref="stickerPicker" + @uploaded="addMediaFile" + /> </div> </template> @@ -310,7 +325,7 @@ } } - .poll-icon { + .poll-icon, .sticker-icon { font-size: 26px; flex: 1; @@ -320,6 +335,11 @@ } } + .sticker-icon { + flex: 0; + min-width: 50px; + } + .icon-chart-bar { cursor: pointer; } diff --git a/src/components/status/status.js b/src/components/status/status.js index 7911d40d..3c172e5b 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -110,8 +110,9 @@ const Status = { }, muteWordHits () { const statusText = this.status.text.toLowerCase() + const statusSummary = this.status.summary.toLowerCase() const hits = filter(this.muteWords, (muteWord) => { - return statusText.includes(muteWord.toLowerCase()) + return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase()) }) return hits @@ -280,6 +281,11 @@ const Status = { }, tags () { return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ') + }, + hidePostStats () { + return typeof this.$store.state.config.hidePostStats === 'undefined' + ? this.$store.state.instance.hidePostStats + : this.$store.state.config.hidePostStats } }, components: { @@ -316,11 +322,8 @@ const Status = { this.error = undefined }, linkClicked (event) { - let { target } = event - if (target.tagName === 'SPAN') { - target = target.parentNode - } - if (target.tagName === 'A') { + const target = event.target.closest('.status-content a') + if (target) { if (target.className.match(/mention/)) { const href = target.href const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href)) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 30969256..ab506632 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -344,7 +344,7 @@ <transition name="fade"> <div - v-if="isFocused && combinedFavsAndRepeatsUsers.length > 0" + v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0" class="favs-repeated-users" > <div class="stats"> @@ -820,11 +820,12 @@ $status-margin: 0.75em; } .status-actions { + position: relative; width: 100%; display: flex; margin-top: $status-margin; - div, favorite-button { + > * { max-width: 4em; flex: 1; } diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js new file mode 100644 index 00000000..a6dcded3 --- /dev/null +++ b/src/components/sticker_picker/sticker_picker.js @@ -0,0 +1,52 @@ +/* eslint-env browser */ +import statusPosterService from '../../services/status_poster/status_poster.service.js' +import TabSwitcher from '../tab_switcher/tab_switcher.js' + +const StickerPicker = { + components: [ + TabSwitcher + ], + data () { + return { + meta: { + stickers: [] + }, + path: '' + } + }, + computed: { + pack () { + return this.$store.state.instance.stickers || [] + } + }, + methods: { + clear () { + this.meta = { + stickers: [] + } + }, + pick (sticker, name) { + const store = this.$store + // TODO remove this workaround by finding a way to bypass reuploads + fetch(sticker) + .then((res) => { + res.blob().then((blob) => { + var file = new File([blob], name, { mimetype: 'image/png' }) + var formData = new FormData() + formData.append('file', file) + statusPosterService.uploadMedia({ store, formData }) + .then((fileData) => { + this.$emit('uploaded', fileData) + this.clear() + }, (error) => { + console.warn("Can't attach sticker") + console.warn(error) + this.$emit('upload-failed', 'default') + }) + }) + }) + } + } +} + +export default StickerPicker diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue new file mode 100644 index 00000000..938204c8 --- /dev/null +++ b/src/components/sticker_picker/sticker_picker.vue @@ -0,0 +1,62 @@ +<template> + <div + class="sticker-picker" + > + <div + class="sticker-picker-panel" + > + <tab-switcher + :render-only-focused="true" + > + <div + v-for="stickerpack in pack" + :key="stickerpack.path" + :image-tooltip="stickerpack.meta.title" + :image="stickerpack.path + stickerpack.meta.tabIcon" + class="sticker-picker-content" + > + <div + v-for="sticker in stickerpack.meta.stickers" + :key="sticker" + class="sticker" + @click="pick(stickerpack.path + sticker, stickerpack.meta.title)" + > + <img + :src="stickerpack.path + sticker" + > + </div> + </div> + </tab-switcher> + </div> + </div> +</template> + +<script src="./sticker_picker.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.sticker-picker { + .sticker-picker-panel { + display: inline-block; + width: 100%; + .sticker-picker-content { + max-height: 300px; + overflow-y: scroll; + overflow-x: auto; + .sticker { + display: inline-block; + width: 20%; + height: 20%; + img { + width: 100%; + &:hover { + filter: drop-shadow(0 0 5px var(--link, $fallback--link)); + } + } + } + } + } +} + +</style> diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js index 67835231..a5fe019c 100644 --- a/src/components/tab_switcher/tab_switcher.js +++ b/src/components/tab_switcher/tab_switcher.js @@ -45,7 +45,19 @@ export default Vue.component('tab-switcher', { classesTab.push('active') classesWrapper.push('active') } - + if (slot.data.attrs.image) { + return ( + <div class={ classesWrapper.join(' ')}> + <button + disabled={slot.data.attrs.disabled} + onClick={this.activateTab(index)} + class={classesTab.join(' ')}> + <img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/> + {slot.data.attrs.label ? '' : slot.data.attrs.label} + </button> + </div> + ) + } return ( <div class={ classesWrapper.join(' ')}> <button diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss index f7449439..4eeb42e0 100644 --- a/src/components/tab_switcher/tab_switcher.scss +++ b/src/components/tab_switcher/tab_switcher.scss @@ -53,6 +53,12 @@ background: transparent; z-index: 5; } + + img { + max-height: 26px; + vertical-align: top; + margin-top: -5px; + } } &:not(.active) { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index f987fbbb..9e142480 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -283,7 +283,6 @@ .user-card { background-size: cover; - overflow: hidden; .panel-heading { padding: .5em 0; @@ -298,6 +297,8 @@ word-wrap: break-word; background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%); background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%); + border-bottom-right-radius: inherit; + border-bottom-left-radius: inherit; } p { @@ -503,6 +504,7 @@ } } .user-interactions { + position: relative; display: flex; flex-flow: row wrap; justify-content: space-between; diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index 0c3b2aef..b5a7f0df 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -91,7 +91,8 @@ const UserSettings = { ...this.$store.state.instance.emoji, ...this.$store.state.instance.customEmoji ], - users: this.$store.state.users.users + users: this.$store.state.users.users, + updateUsersList: (input) => this.$store.dispatch('searchUsers', input) }) }, emojiSuggestor () { diff --git a/src/i18n/en.json b/src/i18n/en.json index ec90cfcf..6230675c 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -106,6 +106,9 @@ "expired": "Poll ended {0} ago", "not_enough_options": "Too few unique options in poll" }, + "stickers": { + "add_sticker": "Add Sticker" + }, "interactions": { "favs_repeats": "Repeats and Favorites", "follows": "New follows", diff --git a/src/i18n/es.json b/src/i18n/es.json index 2e38f859..bf87937a 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -27,7 +27,11 @@ "optional": "opcional", "show_more": "Mostrar más", "show_less": "Mostrar menos", - "cancel": "Cancelar" + "cancel": "Cancelar", + "disable": "Inhabilitar", + "enable": "Habilitar", + "confirm": "Confirmar", + "verify": "Verificar" }, "image_cropper": { "crop_picture": "Recortar la foto", @@ -48,7 +52,15 @@ "placeholder": "p.ej. lain", "register": "Registrar", "username": "Usuario", - "hint": "Inicia sesión para unirte a la discusión" + "hint": "Inicia sesión para unirte a la discusión", + "authentication_code": "Código de autentificación", + "enter_recovery_code": "Inserta el código de recuperación", + "enter_two_factor_code": "Inserta el código de doble factor", + "recovery_code": "Código de recuperación", + "heading" : { + "totp" : "Autentificación de doble factor", + "recovery" : "Recuperación de doble factor" + } }, "media_modal": { "previous": "Anterior", @@ -60,11 +72,13 @@ "chat": "Chat Local", "friend_requests": "Solicitudes de amistad", "mentions": "Menciones", + "interactions": "Interacciones", "dms": "Mensajes Directo", "public_tl": "Línea Temporal Pública", "timeline": "Línea Temporal", "twkn": "Toda La Red Conocida", "user_search": "Búsqueda de Usuarios", + "search": "Buscar", "who_to_follow": "A quién seguir", "preferences": "Preferencias" }, @@ -78,6 +92,25 @@ "repeated_you": "repite tu estado", "no_more_notifications": "No hay más notificaciones" }, + "polls": { + "add_poll": "Añadir encuesta", + "add_option": "Añadir opción", + "option": "Opción", + "votes": "votos", + "vote": "Votar", + "type": "Tipo de encuesta", + "single_choice": "Elección única", + "multiple_choices": "Múltiples elecciones", + "expiry": "Tiempo de vida de la encuesta", + "expires_in": "La encuensta termina en {0}", + "expired": "La encuesta terminó hace {0}", + "not_enough_options": "Muy pocas opciones únicas en la encuesta" + }, + "interactions": { + "favs_repeats": "Favoritos y Repetidos", + "follows": "Nuevos seguidores", + "load_older": "Cargar interacciones antiguas" + }, "post_status": { "new_status": "Publicar un nuevo estado", "account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.", @@ -91,9 +124,14 @@ }, "content_warning": "Tema (opcional)", "default": "Acabo de aterrizar en L.A.", - "direct_warning": "Esta publicación solo será visible para los usuarios mencionados.", + "direct_warning_to_all": "Esta publicación será visible para todos los usarios mencionados.", "direct_warning_to_first_only": "Esta publicación solo será visible para los usuarios mencionados al comienzo del mensaje.", "posting": "Publicando", + "scope_notice": { + "public": "Esta publicación será visible para todo el mundo", + "private": "Esta publicación solo será visible para tus seguidores.", + "unlisted": "Esta publicación no será visible en la Línea Temporal Pública ni en Toda La Red Conocida" + }, "scope": { "direct": "Directo - Solo para los usuarios mencionados.", "private": "Solo-Seguidores - Solo tus seguidores leeran la publicación", @@ -127,6 +165,29 @@ }, "settings": { "app_name": "Nombre de la aplicación", + "security": "Seguridad", + "enter_current_password_to_confirm": "Introduce la contraseña actual para confirmar tu identidad", + "mfa": { + "otp" : "OTP", + "setup_otp" : "Configurar OTP", + "wait_pre_setup_otp" : "preconfiguración OTP", + "confirm_and_enable" : "Confirmar y habilitar OTP", + "title": "Autentificación de Doble Factor", + "generate_new_recovery_codes" : "Generar nuevos códigos de recuperación", + "warning_of_generate_new_codes" : "Cuando generas nuevos códigos de recuperación, los antiguos dejarán de funcionar.", + "recovery_codes" : "Códigos de recuperación.", + "waiting_a_recovery_codes": "Recibiendo códigos de respaldo", + "recovery_codes_warning" : "Anote los códigos o guárdelos en un lugar seguro, de lo contrario no los volverá a ver. Si pierde el acceso a su aplicación 2FA y los códigos de recuperación, su cuenta quedará bloqueada.", + "authentication_methods" : "Métodos de autentificación", + "scan": { + "title": "Escanear", + "desc": "Usando su aplicación de doble factor, escanee este código QR o ingrese la clave de texto:", + "secret_code": "Clave" + }, + "verify": { + "desc": "Para habilitar la autenticación de doble factor, ingrese el código de su aplicación 2FA:" + } + }, "attachmentRadius": "Adjuntos", "attachments": "Adjuntos", "autoload": "Activar carga automática al llegar al final de la página", @@ -233,6 +294,7 @@ "reply_visibility_all": "Mostrar todas las réplicas", "reply_visibility_following": "Solo mostrar réplicas para mí o usuarios a los que sigo", "reply_visibility_self": "Solo mostrar réplicas para mí", + "autohide_floating_post_button": "Ocultar automáticamente el botón 'Nueva Publicación' (móvil)", "saving_err": "Error al guardar los ajustes", "saving_ok": "Ajustes guardados", "search_user_to_block": "Buscar usuarios a bloquear", @@ -265,6 +327,13 @@ "true": "sí" }, "notifications": "Notificaciones", + "notification_setting": "Recibir notificaciones de:", + "notification_setting_follows": "Usuarios que sigues", + "notification_setting_non_follows": "Usuarios que no sigues", + "notification_setting_followers": "Usuarios que te siguen", + "notification_setting_non_followers": "Usuarios que no te siguen", + "notification_mutes": "Para dejar de recibir notificaciones de un usuario específico, siléncialo.", + "notification_blocks": "El bloqueo de un usuario detiene todas las notificaciones y también las cancela.", "enable_web_push_notifications": "Habilitar las notificiaciones en el navegador", "style": { "switcher": { @@ -381,6 +450,40 @@ "frontend_version": "Versión del Frontend" } }, + "time": { + "day": "{0} día", + "days": "{0} días", + "day_short": "{0}d", + "days_short": "{0}d", + "hour": "{0} hora", + "hours": "{0} horas", + "hour_short": "{0}h", + "hours_short": "{0}h", + "in_future": "en {0}", + "in_past": "hace {0}", + "minute": "{0} minuto", + "minutes": "{0} minutos", + "minute_short": "{0}min", + "minutes_short": "{0}min", + "month": "{0} mes", + "months": "{0} meses", + "month_short": "{0}m", + "months_short": "{0}m", + "now": "justo ahora", + "now_short": "ahora", + "second": "{0} segundo", + "seconds": "{0} segundos", + "second_short": "{0}s", + "seconds_short": "{0}s", + "week": "{0} semana", + "weeks": "{0} semana", + "week_short": "{0}sem", + "weeks_short": "{0}sem", + "year": "{0} año", + "years": "{0} años", + "year_short": "{0}a", + "years_short": "{0}a" + }, "timeline": { "collapse": "Colapsar", "conversation": "Conversación", @@ -396,6 +499,11 @@ "status": { "favorites": "Favoritos", "repeats": "Repetidos", + "delete": "Eliminar publicación", + "pin": "Fijar en tu perfil", + "unpin": "Desclavar de tu perfil", + "pinned": "Fijado", + "delete_confirm": "¿Realmente quieres borrar la publicación?", "reply_to": "Responder a", "replies_list": "Respuestas:" }, @@ -422,6 +530,8 @@ "remote_follow": "Seguir", "report": "Reportar", "statuses": "Estados", + "subscribe": "Suscribirse", + "unsubscribe": "Desuscribirse", "unblock": "Desbloquear", "unblock_progress": "Desbloqueando...", "block_progress": "Bloqueando...", @@ -486,5 +596,12 @@ "GiB": "GiB", "TiB": "TiB" } + }, + "search": { + "people": "Personas", + "hashtags": "Hashtags", + "person_talking": "{count} personas hablando", + "people_talking": "{count} gente hablando", + "no_results": "Sin resultados" } }
\ No newline at end of file diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 559bb4c9..c77f28a6 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -27,7 +27,11 @@ "optional": "かかなくてもよい", "show_more": "つづきをみる", "show_less": "たたむ", - "cancel": "キャンセル" + "cancel": "キャンセル", + "disable": "なし", + "enable": "あり", + "confirm": "たしかめる", + "verify": "たしかめる" }, "image_cropper": { "crop_picture": "がぞうをきりぬく", @@ -48,7 +52,15 @@ "placeholder": "れい: lain", "register": "はじめる", "username": "ユーザーめい", - "hint": "はなしあいにくわわるには、ログインしてください" + "hint": "はなしあいにくわわるには、ログインしてください", + "authentication_code": "にんしょうコード", + "enter_recovery_code": "リカバリーコードをいれてください", + "enter_two_factor_code": "2-ファクターコードをいれてください", + "recovery_code": "リカバリーコード", + "heading" : { + "totp" : "2-ファクターにんしょう", + "recovery" : "2-ファクターリカバリー" + } }, "media_modal": { "previous": "まえ", @@ -79,6 +91,20 @@ "repeated_you": "あなたのステータスがリピートされました", "no_more_notifications": "つうちはありません" }, + "polls": { + "add_poll": "いれふだをはじめる", + "add_option": "オプションをふやす", + "option": "オプション", + "votes": "いれふだ", + "vote": "ふだをいれる", + "type": "いれふだのかた", + "single_choice": "ひとつえらぶ", + "multiple_choices": "いくつでもえらべる", + "expiry": "いれふだのながさ", + "expires_in": "いれふだは {0} で、おわります", + "expired": "いれふだは {0} まえに、おわりました", + "not_enough_options": "ユニークなオプションが、たりません" + }, "interactions": { "favs_repeats": "リピートとおきにいり", "follows": "あたらしいフォロー", @@ -139,6 +165,29 @@ }, "settings": { "app_name": "アプリのなまえ", + "security": "セキュリティ", + "enter_current_password_to_confirm": "あなたのアイデンティティをたしかめるため、あなたのいまのパスワードをかいてください", + "mfa": { + "otp" : "OTP", + "setup_otp" : "OTPをつくる", + "wait_pre_setup_otp" : "OTPをよういしています", + "confirm_and_enable" : "OTPをたしかめて、ゆうこうにする", + "title": "2-ファクターにんしょう", + "generate_new_recovery_codes" : "あたらしいリカバリーコードをつくる", + "warning_of_generate_new_codes" : "あたらしいリカバリーコードをつくったら、ふるいコードはつかえなくなります。", + "recovery_codes" : "リカバリーコード。", + "waiting_a_recovery_codes": "バックアップコードをうけとっています...", + "recovery_codes_warning" : "コードをかきうつすか、ひとにみられないところにセーブしてください。そうでなければ、あなたはこのコードをふたたびみることはできません。もしあなたが、2FAアプリのアクセスをうしなって、なおかつ、リカバリーコードもおもいだせないならば、あなたはあなたのアカウントから、しめだされます。", + "authentication_methods" : "にんしょうメソッド", + "scan": { + "title": "スキャン", + "desc": "あなたの2-ファクターアプリをつかって、このQRコードをスキャンするか、テキストキーをうちこんでください:", + "secret_code": "キー" + }, + "verify": { + "desc": "2-ファクターにんしょうをつかうには、あなたの2-ファクターアプリのコードをいれてください:" + } + }, "attachmentRadius": "ファイル", "attachments": "ファイル", "autoload": "したにスクロールしたとき、じどうてきによみこむ。", diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json index 345e1f1c..992a2fa6 100644 --- a/src/i18n/ja_pedantic.json +++ b/src/i18n/ja_pedantic.json @@ -27,7 +27,11 @@ "optional": "省略可", "show_more": "もっと見る", "show_less": "たたむ", - "cancel": "キャンセル" + "cancel": "キャンセル", + "disable": "無効", + "enable": "有効", + "confirm": "確認", + "verify": "検査" }, "image_cropper": { "crop_picture": "画像を切り抜く", @@ -48,7 +52,15 @@ "placeholder": "例: lain", "register": "登録", "username": "ユーザー名", - "hint": "会話に加わるには、ログインしてください" + "hint": "会話に加わるには、ログインしてください", + "authentication_code": "認証コード", + "enter_recovery_code": "リカバリーコードを入力してください", + "enter_two_factor_code": "2段階認証コードを入力してください", + "recovery_code": "リカバリーコード", + "heading" : { + "totp" : "2段階認証", + "recovery" : "2段階リカバリー" + } }, "media_modal": { "previous": "前", @@ -79,6 +91,20 @@ "repeated_you": "あなたのステータスがリピートされました", "no_more_notifications": "通知はありません" }, + "polls": { + "add_poll": "投票を追加", + "add_option": "選択肢を追加", + "option": "選択肢", + "votes": "票", + "vote": "投票", + "type": "投票の形式", + "single_choice": "択一式", + "multiple_choices": "複数選択式", + "expiry": "投票期間", + "expires_in": "投票は {0} で終了します", + "expired": "投票は {0} 前に終了しました", + "not_enough_options": "相異なる選択肢が不足しています" + }, "interactions": { "favs_repeats": "リピートとお気に入り", "follows": "新しいフォロワー", @@ -139,6 +165,29 @@ }, "settings": { "app_name": "アプリの名称", + "security": "セキュリティ", + "enter_current_password_to_confirm": "あなたのアイデンティティを証明するため、現在のパスワードを入力してください", + "mfa": { + "otp" : "OTP", + "setup_otp" : "OTPのセットアップ", + "wait_pre_setup_otp" : "OTPのプリセット", + "confirm_and_enable" : "OTPの確認と有効化", + "title": "2段階認証", + "generate_new_recovery_codes" : "新しいリカバリーコードを生成", + "warning_of_generate_new_codes" : "新しいリカバリーコードを生成すると、古いコードは使用できなくなります。", + "recovery_codes" : "リカバリーコード。", + "waiting_a_recovery_codes": "バックアップコードを受信しています...", + "recovery_codes_warning" : "コードを紙に書くか、安全な場所に保存してください。そうでなければ、あなたはコードを再び見ることはできません。もし2段階認証アプリのアクセスを喪失し、なおかつ、リカバリーコードもないならば、あなたは自分のアカウントから閉め出されます。", + "authentication_methods" : "認証方法", + "scan": { + "title": "スキャン", + "desc": "あなたの2段階認証アプリを使って、このQRコードをスキャンするか、テキストキーを入力してください:", + "secret_code": "キー" + }, + "verify": { + "desc": "2段階認証を有効にするには、あなたの2段階認証アプリのコードを入力してください:" + } + }, "attachmentRadius": "ファイル", "attachments": "ファイル", "autoload": "下にスクロールしたとき、自動的に読み込む。", diff --git a/src/main.js b/src/main.js index 3287fa2b..b3256e8e 100644 --- a/src/main.js +++ b/src/main.js @@ -26,6 +26,7 @@ import messages from './i18n/messages.js' import VueChatScroll from 'vue-chat-scroll' import VueClickOutside from 'v-click-outside' import PortalVue from 'portal-vue' +import VTooltip from 'v-tooltip' import afterStoreSetup from './boot/after_store.js' @@ -37,6 +38,7 @@ Vue.use(VueI18n) Vue.use(VueChatScroll) Vue.use(VueClickOutside) Vue.use(PortalVue) +Vue.use(VTooltip) const i18n = new VueI18n({ // By default, use the browser locale, we will update it if neccessary diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 670f7fca..1f6e1ebd 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -70,6 +70,7 @@ const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin` const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute` const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute` const MASTODON_SEARCH_2 = `/api/v2/search` +const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' const oldfetch = window.fetch @@ -865,6 +866,18 @@ const reportUser = ({ credentials, userId, statusIds, comment, forward }) => { }) } +const searchUsers = ({ credentials, query }) => { + return promisedRequest({ + url: MASTODON_USER_SEARCH_URL, + params: { + q: query, + resolve: true + }, + credentials + }) + .then((data) => data.map(parseUser)) +} + const search2 = ({ credentials, q, resolve, limit, offset, following }) => { let url = MASTODON_SEARCH_2 let params = [] @@ -974,7 +987,8 @@ const apiService = { fetchRebloggedByUsers, reportUser, updateNotificationSettings, - search2 + search2, + searchUsers } export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 0f40edf1..846d9415 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -152,6 +152,7 @@ const backendInteractorService = credentials => { const unretweet = (id) => apiService.unretweet({ id, credentials }) const search2 = ({ q, resolve, limit, offset, following }) => apiService.search2({ credentials, q, resolve, limit, offset, following }) + const searchUsers = (query) => apiService.searchUsers({ query, credentials }) const backendInteractorServiceInstance = { fetchStatus, @@ -216,7 +217,8 @@ const backendInteractorService = credentials => { retweet, unretweet, updateNotificationSettings, - search2 + search2, + searchUsers } return backendInteractorServiceInstance @@ -5459,9 +5459,10 @@ pngjs@^3.3.0: version "3.3.3" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b" -popper.js@^1.14.7: - version "1.14.7" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e" +popper.js@^1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" + integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== portal-vue@^2.1.4: version "2.1.4" @@ -7198,6 +7199,15 @@ v-click-outside@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7" +v-tooltip@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.2.tgz#8610d9eece2cc44fd66c12ef2f12eec6435cab9b" + integrity sha512-xQ+qzOFfywkLdjHknRPgMMupQNS8yJtf9Utd5Dxiu/0n4HtrxqsgDtN2MLZ0LKbburtSAQgyypuE/snM8bBZhw== + dependencies: + lodash "^4.17.11" + popper.js "^1.15.0" + vue-resize "^0.4.5" + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -7272,11 +7282,10 @@ vue-loader@^14.0.0: vue-style-loader "^4.0.1" vue-template-es2015-compiler "^1.6.0" -vue-popperjs@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/vue-popperjs/-/vue-popperjs-2.0.3.tgz#7c446d0ba7c63170ccb33a02669d0df4efc3d8cd" - dependencies: - popper.js "^1.14.7" +vue-resize@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea" + integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg== vue-router@^3.0.1: version "3.0.2" |
