diff options
Diffstat (limited to 'src')
91 files changed, 552 insertions, 536 deletions
@@ -46,7 +46,7 @@ export default { this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) window.addEventListener('resize', this.updateMobileState) }, - destroyed () { + unmounted () { window.removeEventListener('resize', this.updateMobileState) }, computed: { diff --git a/src/App.scss b/src/App.scss index bc027f4f..180c0daf 100644 --- a/src/App.scss +++ b/src/App.scss @@ -572,7 +572,7 @@ nav { .fade-enter-active, .fade-leave-active { transition: opacity .2s } -.fade-enter, .fade-leave-active { +.fade-enter-from, .fade-leave-active { opacity: 0 } diff --git a/src/App.vue b/src/App.vue index eb65b548..bee98751 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,11 +1,11 @@ <template> <div - id="app" - :style="bgStyle" + id="app-loaded" > <div id="app_bg_wrapper" class="app-bg-wrapper" + :style="bgStyle" /> <MobileNav v-if="isMobileLayout" /> <DesktopNav v-else /> @@ -59,7 +59,7 @@ <UserReportingModal /> <PostStatusModal /> <SettingsModal /> - <portal-target name="modal" /> + <div id="modal" /> <GlobalNoticeList /> </div> </template> diff --git a/src/boot/after_store.js b/src/boot/after_store.js index c4a0a800..91a13174 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -1,7 +1,13 @@ -import Vue from 'vue' -import VueRouter from 'vue-router' -import routes from './routes' +import { createApp, configureCompat } from 'vue' +import { createRouter, createWebHistory } from 'vue-router' +import vClickOutside from 'click-outside-vue3' + +import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' + import App from '../App.vue' +import routes from './routes' +import VBodyScrollLock from 'src/directives/body_scroll_lock' + import { windowWidth } from '../services/window_utils/window_utils' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' @@ -9,6 +15,13 @@ import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import { applyTheme } from '../services/style_setter/style_setter.js' import FaviconService from '../services/favicon_service/favicon_service.js' +// disable compat for certain features +configureCompat({ + COMPONENT_V_MODEL: false, + INSTANCE_SET: false, + RENDER_FUNCTION: false +}) + let staticInitialResults = null const parsedInitialResults = () => { @@ -367,25 +380,32 @@ const afterStoreSetup = async ({ store, i18n }) => { getTOS({ store }) getStickers({ store }) - const router = new VueRouter({ - mode: 'history', + const router = createRouter({ + history: createWebHistory(), routes: routes(store), scrollBehavior: (to, _from, savedPosition) => { if (to.matched.some(m => m.meta.dontScroll)) { return false } - return savedPosition || { x: 0, y: 0 } + return savedPosition || { left: 0, top: 0 } } }) - /* eslint-disable no-new */ - return new Vue({ - router, - store, - i18n, - el: '#app', - render: h => h(App) - }) + const app = createApp(App) + + app.use(router) + app.use(store) + app.use(i18n) + + app.use(vClickOutside) + app.use(VBodyScrollLock) + + app.component('FAIcon', FontAwesomeIcon) + app.component('FALayers', FontAwesomeLayers) + + app.mount('#app') + + return app } export default afterStoreSetup diff --git a/src/boot/routes.js b/src/boot/routes.js index 1bc1f9f7..905ffe41 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -46,7 +46,7 @@ export default (store) => { { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'remote-user-profile-acct', - path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)', + path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', component: RemoteUserResolver, beforeEnter: validateAuthenticatedRoute }, @@ -69,7 +69,7 @@ export default (store) => { { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, { 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 } + { name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile } ] if (store.state.instance.pleromaChatMessagesAvailable) { diff --git a/src/components/auth_form/auth_form.js b/src/components/auth_form/auth_form.js index e9a6e2d5..a86a3dca 100644 --- a/src/components/auth_form/auth_form.js +++ b/src/components/auth_form/auth_form.js @@ -1,3 +1,4 @@ +import { h, resolveComponent } from 'vue' import LoginForm from '../login_form/login_form.vue' import MFARecoveryForm from '../mfa_form/recovery_form.vue' import MFATOTPForm from '../mfa_form/totp_form.vue' @@ -5,8 +6,8 @@ import { mapGetters } from 'vuex' const AuthForm = { name: 'AuthForm', - render (createElement) { - return createElement('component', { is: this.authForm }) + render () { + return h(resolveComponent(this.authForm)) }, computed: { authForm () { diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index 53deb1df..eeca7828 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -4,7 +4,7 @@ <UserAvatar class="avatar" :user="user" - @click.prevent.native="toggleUserExpanded" + @click.prevent="toggleUserExpanded" /> </router-link> <div diff --git a/src/components/bookmark_timeline/bookmark_timeline.js b/src/components/bookmark_timeline/bookmark_timeline.js index 64b69e5d..5ac43d90 100644 --- a/src/components/bookmark_timeline/bookmark_timeline.js +++ b/src/components/bookmark_timeline/bookmark_timeline.js @@ -9,7 +9,7 @@ const Bookmarks = { components: { Timeline }, - destroyed () { + unmounted () { this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) } } diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index b54f5fb2..aef11712 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -57,7 +57,7 @@ const Chat = { }) this.setChatLayout() }, - destroyed () { + unmounted () { window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('resize', this.handleLayoutChange) this.unsetChatLayout() diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue index 94a0097c..493c5d5a 100644 --- a/src/components/chat/chat.vue +++ b/src/components/chat/chat.vue @@ -26,73 +26,71 @@ /> </div> </div> - <template> + <div + ref="scrollable" + class="scrollable-message-list" + :style="{ height: scrollableContainerHeight }" + @scroll="handleScroll" + > + <template v-if="!errorLoadingChat"> + <ChatMessage + v-for="chatViewItem in chatViewItems" + :key="chatViewItem.id" + :author="recipient" + :chat-view-item="chatViewItem" + :hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId" + @hover="onMessageHover" + /> + </template> <div - ref="scrollable" - class="scrollable-message-list" - :style="{ height: scrollableContainerHeight }" - @scroll="handleScroll" + v-else + class="chat-loading-error" > - <template v-if="!errorLoadingChat"> - <ChatMessage - v-for="chatViewItem in chatViewItems" - :key="chatViewItem.id" - :author="recipient" - :chat-view-item="chatViewItem" - :hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId" - @hover="onMessageHover" - /> - </template> - <div - v-else - class="chat-loading-error" - > - <div class="alert error"> - {{ $t('chats.error_loading_chat') }} - </div> + <div class="alert error"> + {{ $t('chats.error_loading_chat') }} </div> </div> + </div> + <div + ref="footer" + class="panel-body footer" + > <div - ref="footer" - class="panel-body footer" + class="jump-to-bottom-button" + :class="{ 'visible': jumpToBottomButtonVisible }" + @click="scrollDown({ behavior: 'smooth' })" > - <div - class="jump-to-bottom-button" - :class="{ 'visible': jumpToBottomButtonVisible }" - @click="scrollDown({ behavior: 'smooth' })" - > - <span> - <FAIcon icon="chevron-down" /> - <div - v-if="newMessageCount" - class="badge badge-notification unread-chat-count unread-message-count" - > - {{ newMessageCount }} - </div> - </span> - </div> - <PostStatusForm - :disable-subject="true" - :disable-scope-selector="true" - :disable-notice="true" - :disable-lock-warning="true" - :disable-polls="true" - :disable-sensitivity-checkbox="true" - :disable-submit="errorLoadingChat || !currentChat" - :disable-preview="true" - :optimistic-posting="true" - :post-handler="sendMessage" - :submit-on-enter="!mobileLayout" - :preserve-focus="!mobileLayout" - :auto-focus="!mobileLayout" - :placeholder="formPlaceholder" - :file-limit="1" - max-height="160" - emoji-picker-placement="top" - @resize="handleResize" - /> + <span> + <FAIcon icon="chevron-down" /> + <div + v-if="newMessageCount" + class="badge badge-notification unread-chat-count unread-message-count" + > + {{ newMessageCount }} + </div> + </span> </div> - </template> + <PostStatusForm + :disable-subject="true" + :disable-scope-selector="true" + :disable-notice="true" + :disable-lock-warning="true" + :disable-polls="true" + :disable-sensitivity-checkbox="true" + :disable-submit="errorLoadingChat || !currentChat" + :disable-preview="true" + :optimistic-posting="true" + :post-handler="sendMessage" + :submit-on-enter="!mobileLayout" + :preserve-focus="!mobileLayout" + :auto-focus="!mobileLayout" + :placeholder="formPlaceholder" + :file-limit="1" + max-height="160" + emoji-picker-placement="top" + @resize="handleResize" + /> + </div> </div> </div> </div> diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js index edfbe7a4..f6e299ad 100644 --- a/src/components/chat_title/chat_title.js +++ b/src/components/chat_title/chat_title.js @@ -1,11 +1,12 @@ -import Vue from 'vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import UserAvatar from '../user_avatar/user_avatar.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' -export default Vue.component('chat-title', { +export default { name: 'ChatTitle', components: { - UserAvatar + UserAvatar, + RichContent }, props: [ 'user', 'withAvatar' @@ -23,4 +24,4 @@ export default Vue.component('chat-title', { return generateProfileLink(user.id, user.screen_name) } } -}) +} diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index d28c2cfd..83695912 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -6,9 +6,9 @@ <input type="checkbox" :disabled="disabled" - :checked="checked" - :indeterminate.prop="indeterminate" - @change="$emit('change', $event.target.checked)" + :checked="modelValue" + :indeterminate="indeterminate" + @change="$emit('update:modelValue', $event.target.checked)" > <i class="checkbox-indicator" /> <span @@ -22,12 +22,9 @@ <script> export default { - model: { - prop: 'checked', - event: 'change' - }, + emits: ['update:modelValue'], props: [ - 'checked', + 'modelValue', 'indeterminate', 'disabled' ] diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue index 8fb16113..b2acbc3d 100644 --- a/src/components/color_input/color_input.vue +++ b/src/components/color_input/color_input.vue @@ -11,28 +11,28 @@ </label> <Checkbox v-if="typeof fallback !== 'undefined' && showOptionalTickbox" - :checked="present" + :modelValue="present" :disabled="disabled" class="opt" - @change="$emit('input', typeof value === 'undefined' ? fallback : undefined)" + @change="$emit('update:modelValue', typeof value === 'undefined' ? fallback : undefined)" /> <div class="input color-input-field"> <input :id="name + '-t'" class="textColor unstyled" type="text" - :value="value || fallback" + :value="modelValue || fallback" :disabled="!present || disabled" - @input="$emit('input', $event.target.value)" + @input="$emit('update:modelValue', $event.target.value)" > <input v-if="validColor" :id="name" class="nativeColor unstyled" type="color" - :value="value || fallback" + :value="modelValue || fallback" :disabled="!present || disabled" - @input="$emit('input', $event.target.value)" + @input="$emit('update:modelValue', $event.target.value)" > <div v-if="transparentColor" @@ -67,7 +67,7 @@ export default { }, // Color value, should be required but vue cannot tell the difference // between "property missing" and "property set to undefined" - value: { + modelValue: { required: false, type: String, default: undefined @@ -93,16 +93,16 @@ export default { }, computed: { present () { - return typeof this.value !== 'undefined' + return typeof this.modelValue !== 'undefined' }, validColor () { - return hex2rgb(this.value || this.fallback) + return hex2rgb(this.modelValue || this.fallback) }, transparentColor () { - return this.value === 'transparent' + return this.modelValue === 'transparent' }, computedColor () { - return this.value && this.value.startsWith('--') + return this.modelValue && this.modelValue.startsWith('--') } } } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 7628ceaa..39c5db33 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -27,20 +27,23 @@ v-if="shouldShowAllConversationButton" class="conversation-dive-to-top-level-box" > - <i18n - path="status.show_all_conversation_with_icon" + <i18n-t + keypath="status.show_all_conversation_with_icon" tag="button" class="button-unstyled -link" @click.prevent="diveToTopLevel" > - <FAIcon - place="icon" - icon="angle-double-left" - /> - <span place="text"> - {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }} - </span> - </i18n> + <template #icon> + <FAIcon + icon="angle-double-left" + /> + </template> + <template #text> + <span> + {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }} + </span> + </template> + </i18n-t> </div> <div v-if="shouldShowAncestors" @@ -96,20 +99,23 @@ <div class="thread-ancestor-dive-box-inner" > - <i18n + <i18n-t tag="button" - path="status.ancestor_follow_with_icon" + keypath="status.ancestor_follow_with_icon" class="button-unstyled -link thread-tree-show-replies-button" @click.prevent="diveIntoStatus(status.id)" > - <FAIcon - place="icon" - icon="angle-double-right" - /> - <span place="text"> - {{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }} - </span> - </i18n> + <template #icon> + <FAIcon + icon="angle-double-right" + /> + </template> + <template #text> + <span> + {{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }} + </span> + </template> + </i18n-t> </div> </div> </div> diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index 304baf9d..bab3ca81 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -34,7 +34,7 @@ <search-bar v-if="currentUser || !privateMode" @toggled="onSearchBarToggled" - @click.stop.native + @click.stop /> <button class="button-unstyled nav-icon" diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index 902ec384..e772bf75 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -31,6 +31,7 @@ library.add( */ const EmojiInput = { + emits: ['update:modelValue'], props: { suggest: { /** @@ -57,8 +58,7 @@ const EmojiInput = { required: true, type: Function }, - // TODO VUE3: change to modelValue, change 'input' event to 'input' - value: { + modelValue: { /** * Used for v-model */ @@ -137,8 +137,8 @@ const EmojiInput = { return (this.wordAtCaret || {}).word || '' }, wordAtCaret () { - if (this.value && this.caret) { - const word = Completion.wordAtPosition(this.value, this.caret - 1) || {} + if (this.modelValue && this.caret) { + const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {} return word } } @@ -189,8 +189,11 @@ const EmojiInput = { img: imageUrl || '' })) }, - suggestions (newValue) { - this.$nextTick(this.resize) + suggestions: { + handler (newValue) { + this.$nextTick(this.resize) + }, + deep: true } }, methods: { @@ -225,13 +228,13 @@ const EmojiInput = { } }, replace (replacement) { - const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement) - this.$emit('input', newValue) + const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement) + this.$emit('update:modelValue', newValue) this.caret = 0 }, insert ({ insertion, keepOpen, surroundingSpace = true }) { - const before = this.value.substring(0, this.caret) || '' - const after = this.value.substring(this.caret) || '' + const before = this.modelValue.substring(0, this.caret) || '' + const after = this.modelValue.substring(this.caret) || '' /* Using a bit more smart approach to padding emojis with spaces: * - put a space before cursor if there isn't one already, unless we @@ -259,7 +262,7 @@ const EmojiInput = { after ].join('') this.keepOpen = keepOpen - this.$emit('input', newValue) + this.$emit('update:modelValue', newValue) const position = this.caret + (insertion + spaceAfter + spaceBefore).length if (!keepOpen) { this.input.focus() @@ -278,8 +281,8 @@ const EmojiInput = { if (len > 0 || suggestion) { const chosenSuggestion = suggestion || this.suggestions[this.highlighted] const replacement = chosenSuggestion.replacement - const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement) - this.$emit('input', newValue) + const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement) + this.$emit('update:modelValue', newValue) this.highlighted = 0 const position = this.wordAtCaret.start + replacement.length @@ -455,7 +458,7 @@ const EmojiInput = { this.showPicker = false this.setCaret(e) this.resize() - this.$emit('input', e.target.value) + this.$emit('update:modelValue', e.target.value) }, onClickInput (e) { this.showPicker = false diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index 2716d93f..6b589079 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -1,3 +1,4 @@ +import { defineAsyncComponent } from 'vue' import Checkbox from '../checkbox/checkbox.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -57,7 +58,7 @@ const EmojiPicker = { } }, components: { - StickerPicker: () => import('../sticker_picker/sticker_picker.vue'), + StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')), Checkbox }, methods: { @@ -79,7 +80,7 @@ const EmojiPicker = { }, highlight (key) { const ref = this.$refs['group-' + key] - const top = ref[0].offsetTop + const top = ref.offsetTop this.setShowStickers(false) this.activeGroup = key this.$nextTick(() => { @@ -96,7 +97,7 @@ const EmojiPicker = { } }, triggerLoadMore (target) { - const ref = this.$refs['group-end-custom'][0] + const ref = this.$refs['group-end-custom'] if (!ref) return const bottom = ref.offsetTop + ref.offsetHeight @@ -119,7 +120,7 @@ const EmojiPicker = { this.$nextTick(() => { this.emojisView.forEach(group => { const ref = this.$refs['group-' + group.id] - if (ref[0].offsetTop <= top) { + if (ref.offsetTop <= top) { this.activeGroup = group.id } }) diff --git a/src/components/exporter/exporter.js b/src/components/exporter/exporter.js index 51912ac3..fc75372e 100644 --- a/src/components/exporter/exporter.js +++ b/src/components/exporter/exporter.js @@ -15,18 +15,8 @@ const Exporter = { type: String, default: 'export.csv' }, - exportButtonLabel: { - type: String, - default () { - return this.$t('exporter.export') - } - }, - processingMessage: { - type: String, - default () { - return this.$t('exporter.processing') - } - } + exportButtonLabel: { type: String }, + processingMessage: { type: String } }, data () { return { diff --git a/src/components/exporter/exporter.vue b/src/components/exporter/exporter.vue index d6a03088..79defdf6 100644 --- a/src/components/exporter/exporter.vue +++ b/src/components/exporter/exporter.vue @@ -7,14 +7,14 @@ spin /> - <span>{{ processingMessage }}</span> + <span>{{ processingMessage || $t('exporter.processing') }}</span> </div> <button v-else class="btn button-default" @click="process" > - {{ exportButtonLabel }} + {{ exportButtonLabel || $t('exporter.export') }} </button> </div> </template> diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js index 137ef9c0..24bad937 100644 --- a/src/components/font_control/font_control.js +++ b/src/components/font_control/font_control.js @@ -1,4 +1,4 @@ -import { set } from 'vue' +import { set } from 'lodash' import Select from '../select/select.vue' export default { diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue index 29605084..2006c7f6 100644 --- a/src/components/font_control/font_control.vue +++ b/src/components/font_control/font_control.vue @@ -14,14 +14,15 @@ :id="name + '-o'" class="opt exlcude-disabled" type="checkbox" - :checked="present" - @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)" + :modelValue="present" + @input="$emit('update:modelValue', typeof value === 'undefined' ? fallback : undefined)" > <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'" /> + {{ ' ' }} <Select :id="name + '-font-switcher'" v-model="preset" diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js index 094b3e57..4e1bda55 100644 --- a/src/components/gallery/gallery.js +++ b/src/components/gallery/gallery.js @@ -1,5 +1,5 @@ import Attachment from '../attachment/attachment.vue' -import { sumBy } from 'lodash' +import { sumBy, set } from 'lodash' const Gallery = { props: [ @@ -85,7 +85,7 @@ const Gallery = { }, methods: { onNaturalSizeLoad ({ id, width, height }) { - this.$set(this.sizes, id, { width, height }) + set(this.sizes, id, { width, height }) }, rowStyle (row) { if (row.audio) { diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js index e8d5ec6d..e72ed0e8 100644 --- a/src/components/image_cropper/image_cropper.js +++ b/src/components/image_cropper/image_cropper.js @@ -66,7 +66,7 @@ const ImageCropper = { } }, methods: { - destroy () { + unmounted () { if (this.cropper) { this.cropper.destroy() } @@ -117,7 +117,7 @@ const ImageCropper = { const fileInput = this.$refs.input fileInput.addEventListener('change', this.readFile) }, - beforeDestroy: function () { + beforeUnmount: function () { // remove the event listeners const trigger = this.getTriggerDOM() if (trigger) { diff --git a/src/components/importer/importer.js b/src/components/importer/importer.js index 59f9beb1..da86a223 100644 --- a/src/components/importer/importer.js +++ b/src/components/importer/importer.js @@ -15,24 +15,9 @@ const Importer = { type: Function, required: true }, - submitButtonLabel: { - type: String, - default () { - return this.$t('importer.submit') - } - }, - successMessage: { - type: String, - default () { - return this.$t('importer.success') - } - }, - errorMessage: { - type: String, - default () { - return this.$t('importer.error') - } - } + submitButtonLabel: { type: String }, + successMessage: { type: String }, + errorMessage: { type: String } }, data () { return { diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue index 210823f5..ddc3010e 100644 --- a/src/components/importer/importer.vue +++ b/src/components/importer/importer.vue @@ -18,21 +18,21 @@ class="btn button-default" @click="submit" > - {{ submitButtonLabel }} + {{ submitButtonLabel || $t('importer.submit') }} </button> <div v-if="success"> <FAIcon icon="times" @click="dismiss" /> - <p>{{ successMessage }}</p> + <p>{{ successMessage || $t('importer.success') }}</p> </div> <div v-else-if="error"> <FAIcon icon="times" @click="dismiss" /> - <p>{{ errorMessage }}</p> + <p>{{ errorMessage || $t('importer.error') }}</p> </div> </div> </template> diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js index 7fe5e76d..c5ceb63d 100644 --- a/src/components/interactions/interactions.js +++ b/src/components/interactions/interactions.js @@ -1,4 +1,5 @@ import Notifications from '../notifications/notifications.vue' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' const tabModeDict = { mentions: ['mention'], @@ -20,7 +21,8 @@ const Interactions = { } }, components: { - Notifications + Notifications, + TabSwitcher } } diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index cf307a24..6d1f83c4 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -3,6 +3,7 @@ <label for="interface-language-switcher"> {{ $t('settings.interfaceLanguage') }} </label> + {{ ' ' }} <Select id="interface-language-switcher" v-model="language" diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 01a90377..ff993664 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -142,7 +142,7 @@ const MediaModal = { document.addEventListener('keyup', this.handleKeyupEvent) document.addEventListener('keydown', this.handleKeydownEvent) }, - destroyed () { + unmounted () { window.removeEventListener('popstate', this.hide) document.removeEventListener('keyup', this.handleKeyupEvent) document.removeEventListener('keydown', this.handleKeydownEvent) 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 index d27fb3b8..4866ac57 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.js +++ b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -29,7 +29,7 @@ const MobilePostStatusButton = { } window.addEventListener('resize', this.handleOSK) }, - destroyed () { + unmounted () { if (this.autohideFloatingPostButton) { this.deactivateFloatingPostButtonAutohide() } diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue index 37becf4c..9fcdf6f8 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.vue +++ b/src/components/mobile_post_status_button/mobile_post_status_button.vue @@ -1,13 +1,12 @@ <template> - <div v-if="isLoggedIn"> - <button - class="button-default new-status-button" - :class="{ 'hidden': isHidden, 'always-show': isPersistent }" - @click="openPostForm" - > - <FAIcon icon="pen" /> - </button> - </div> + <button + v-if="isLoggedIn" + class="MobilePostButton button-default new-status-button" + :class="{ 'hidden': isHidden, 'always-show': isPersistent }" + @click="openPostForm" + > + <FAIcon icon="pen" /> + </button> </template> <script src="./mobile_post_status_button.js"></script> @@ -15,25 +14,27 @@ <style lang="scss"> @import '../../_variables.scss'; -.new-status-button { - width: 5em; - height: 5em; - border-radius: 100%; - position: fixed; - bottom: 1.5em; - right: 1.5em; - // TODO: this needs its own color, it has to stand out enough and link color - // is not very optimal for this particular use. - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); - display: flex; - justify-content: center; - align-items: center; - box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3); - z-index: 10; - - transition: 0.35s transform; - transition-timing-function: cubic-bezier(0, 1, 0.5, 1); +.MobilePostButton { + &.button-default { + width: 5em; + height: 5em; + border-radius: 100%; + position: fixed; + bottom: 1.5em; + right: 1.5em; + // TODO: this needs its own color, it has to stand out enough and link color + // is not very optimal for this particular use. + background-color: $fallback--fg; + background-color: var(--btn, $fallback--fg); + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3); + z-index: 10; + + transition: 0.35s transform; + transition-timing-function: cubic-bezier(0, 1, 0.5, 1); + } &.hidden { transform: translateY(150%); diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index 96476abe..96b8c3a3 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -132,7 +132,7 @@ </button> </template> </Popover> - <portal to="modal"> + <teleport to="#modal"> <DialogModal v-if="showDeleteUserDialog" :on-cancel="deleteUserDialog.bind(this, false)" @@ -156,7 +156,7 @@ </button> </template> </DialogModal> - </portal> + </teleport> </div> </template> diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index c7305b78..23157a51 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -33,7 +33,7 @@ > <a class="avatar-container" - :href="notification.from_profile.statusnet_profile_url" + :href="$router.resolve(userProfileLink).href" @click.stop.prevent.capture="toggleUserExpanded" > <UserAvatar @@ -65,12 +65,16 @@ v-else class="username" :title="'@'+notification.from_profile.screen_name_ui" - >{{ notification.from_profile.name }}</span> + > + {{ notification.from_profile.name }} + </span> + {{ ' ' }} <span v-if="notification.type === 'like'"> <FAIcon class="type-icon" icon="star" /> + {{ ' ' }} <small>{{ $t('notifications.favorited_you') }}</small> </span> <span v-if="notification.type === 'repeat'"> @@ -79,6 +83,7 @@ icon="retweet" :title="$t('tool_tip.repeat')" /> + {{ ' ' }} <small>{{ $t('notifications.repeated_you') }}</small> </span> <span v-if="notification.type === 'follow'"> @@ -86,6 +91,7 @@ class="type-icon" icon="user-plus" /> + {{ ' ' }} <small>{{ $t('notifications.followed_you') }}</small> </span> <span v-if="notification.type === 'follow_request'"> @@ -93,6 +99,7 @@ class="type-icon" icon="user" /> + {{ ' ' }} <small>{{ $t('notifications.follow_request') }}</small> </span> <span v-if="notification.type === 'move'"> @@ -100,13 +107,14 @@ class="type-icon" icon="suitcase-rolling" /> + {{ ' ' }} <small>{{ $t('notifications.migrated_to') }}</small> </span> <span v-if="notification.type === 'pleroma:emoji_reaction'"> <small> - <i18n path="notifications.reacted_with"> + <i18n-t keypath="notifications.reacted_with"> <span class="emoji-reaction-emoji">{{ notification.emoji }}</span> - </i18n> + </i18n-t> </small> </span> </div> diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue index 3cc3942b..faf07288 100644 --- a/src/components/opacity_input/opacity_input.vue +++ b/src/components/opacity_input/opacity_input.vue @@ -11,10 +11,10 @@ </label> <Checkbox v-if="typeof fallback !== 'undefined'" - :checked="present" + :modelValue="present" :disabled="disabled" class="opt" - @change="$emit('input', !present ? fallback : undefined)" + @change="$emit('update:modelValue', !present ? fallback : undefined)" /> <input :id="name" @@ -25,7 +25,7 @@ max="1" min="0" step=".05" - @input="$emit('input', $event.target.value)" + @input="$emit('update:modelValue', $event.target.value)" > </div> </template> diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js index a69b7886..eda1733a 100644 --- a/src/components/poll/poll.js +++ b/src/components/poll/poll.js @@ -21,7 +21,7 @@ export default { } this.$store.dispatch('trackPoll', this.pollId) }, - destroyed () { + unmounted () { this.$store.dispatch('untrackPoll', this.pollId) }, computed: { diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index 63b44e4f..e949c653 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -71,13 +71,13 @@ {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }} · </template> </div> - <i18n :path="expired ? 'polls.expired' : 'polls.expires_in'"> + <i18n-t :keypath="expired ? 'polls.expired' : 'polls.expires_in'"> <Timeago :time="expiresAt" :auto-update="60" :now-threshold="0" /> - </i18n> + </i18n-t> </div> </div> </template> diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue index 3620075a..f269d60e 100644 --- a/src/components/poll/poll_form.vue +++ b/src/components/poll/poll_form.vue @@ -72,6 +72,7 @@ :max="maxExpirationInCurrentUnit" @change="expiryAmountChange" > + {{ ' ' }} <Select v-model="expiryUnit" unstyled="true" diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js index 6ccf32f0..a30a37c9 100644 --- a/src/components/popover/popover.js +++ b/src/components/popover/popover.js @@ -178,7 +178,7 @@ const Popover = { created () { document.addEventListener('click', this.onClickOutside) }, - destroyed () { + unmounted () { document.removeEventListener('click', this.onClickOutside) this.hidePopover() } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 2e0980a2..45ec9656 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -18,9 +18,9 @@ <FAIcon :icon="uploadFileLimitReached ? 'ban' : 'upload'" /> </div> <div class="form-group"> - <i18n + <i18n-t v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private' && !disableLockWarning" - path="post_status.account_not_locked_warning" + keypath="post_status.account_not_locked_warning" tag="p" class="visibility-notice" > @@ -30,7 +30,7 @@ > {{ $t('post_status.account_not_locked_warning_link') }} </button> - </i18n> + </i18n-t> <p v-if="!hideScopeNotice && newStatus.visibility === 'public'" class="visibility-notice notice-dismissible" diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.js b/src/components/public_and_external_timeline/public_and_external_timeline.js index cbd4491b..bfcce6ae 100644 --- a/src/components/public_and_external_timeline/public_and_external_timeline.js +++ b/src/components/public_and_external_timeline/public_and_external_timeline.js @@ -9,7 +9,7 @@ const PublicAndExternalTimeline = { created () { this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' }) }, - destroyed () { + unmounted () { this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal') } } diff --git a/src/components/public_timeline/public_timeline.js b/src/components/public_timeline/public_timeline.js index 66c40d3a..30693544 100644 --- a/src/components/public_timeline/public_timeline.js +++ b/src/components/public_timeline/public_timeline.js @@ -9,7 +9,7 @@ const PublicTimeline = { created () { this.$store.dispatch('startFetchingTimeline', { timeline: 'public' }) }, - destroyed () { + unmounted () { this.$store.dispatch('stopFetchingTimeline', 'public') } diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue index 5857a5c1..7fe58e80 100644 --- a/src/components/range_input/range_input.vue +++ b/src/components/range_input/range_input.vue @@ -14,8 +14,8 @@ :id="name + '-o'" class="opt" type="checkbox" - :checked="present" - @input="$emit('input', !present ? fallback : undefined)" + :modelValue="present" + @input="$emit('update:modelValue', !present ? fallback : undefined)" > <label v-if="typeof fallback !== 'undefined'" @@ -31,7 +31,7 @@ :max="max || hardMax || 100" :min="min || hardMin || 0" :step="step || 1" - @input="$emit('input', $event.target.value)" + @input="$emit('update:modelValue', $event.target.value)" > <input :id="name" @@ -42,7 +42,7 @@ :max="hardMax" :min="hardMin" :step="step || 1" - @input="$emit('input', $event.target.value)" + @input="$emit('update:modelValue', $event.target.value)" > </div> </template> diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js index 1ac8e8be..531cc480 100644 --- a/src/components/registration/registration.js +++ b/src/components/registration/registration.js @@ -1,9 +1,9 @@ -import { validationMixin } from 'vuelidate' -import { required, requiredIf, sameAs } from 'vuelidate/lib/validators' +import useVuelidate from '@vuelidate/core' +import { required, requiredIf, sameAs } from '@vuelidate/validators' import { mapActions, mapState } from 'vuex' const registration = { - mixins: [validationMixin], + setup () { return { v$: useVuelidate() } }, data: () => ({ user: { email: '', diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index 46bc661a..41e287e4 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -1,4 +1,3 @@ -import Vue from 'vue' import { unescape, flattenDeep } from 'lodash' import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js' import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js' @@ -27,7 +26,7 @@ import './rich_content.scss' * * Apart from that one small hiccup with emit in render this _should_ be vue3-ready */ -export default Vue.component('RichContent', { +export default { name: 'RichContent', props: { // Original html content @@ -58,7 +57,7 @@ export default Vue.component('RichContent', { } }, // NEVER EVER TOUCH DATA INSIDE RENDER - render (h) { + render () { // Pre-process HTML const { newHtml: html } = preProcessPerLine(this.html, this.greentext) let currentMentions = null // Current chain of mentions, we group all mentions together @@ -76,18 +75,18 @@ export default Vue.component('RichContent', { const renderImage = (tag) => { return <StillImage - {...{ attrs: getAttrs(tag) }} + {...getAttrs(tag)} class="img" /> } const renderHashtag = (attrs, children, encounteredTextReverse) => { - const linkData = getLinkData(attrs, children, tagsIndex++) + const { index, ...linkData } = getLinkData(attrs, children, tagsIndex++) writtenTags.push(linkData) if (!encounteredTextReverse) { lastTags.push(linkData) } - return <HashtagLink {...{ props: linkData }}/> + return <HashtagLink { ...linkData }/> } const renderMention = (attrs, children) => { @@ -222,7 +221,7 @@ export default Vue.component('RichContent', { attrs.target = '_blank' const newChildren = [...children].reverse().map(processItemReverse).reverse() - return <a {...{ attrs }}> + return <a {...attrs}> { newChildren } </a> } @@ -235,7 +234,7 @@ export default Vue.component('RichContent', { const newChildren = Array.isArray(children) ? [...children].reverse().map(processItemReverse).reverse() : children - return <Tag {...{ attrs: getAttrs(opener) }}> + return <Tag {...getAttrs(opener)}> { newChildren } </Tag> } else { @@ -266,7 +265,7 @@ export default Vue.component('RichContent', { return result } -}) +} const getLinkData = (attrs, children, index) => { const stripTags = (item) => { diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue index a01242fc..f3bee183 100644 --- a/src/components/scope_selector/scope_selector.vue +++ b/src/components/scope_selector/scope_selector.vue @@ -16,6 +16,7 @@ class="fa-scale-110 fa-old-padding" /> </button> + {{ ' ' }} <button v-if="showPrivate" class="button-unstyled scope" @@ -29,6 +30,7 @@ class="fa-scale-110 fa-old-padding" /> </button> + {{ ' ' }} <button v-if="showUnlisted" class="button-unstyled scope" @@ -42,6 +44,7 @@ class="fa-scale-110 fa-old-padding" /> </button> + {{ ' ' }} <button v-if="showPublic" class="button-unstyled scope" diff --git a/src/components/select/select.js b/src/components/select/select.js index 49535d07..ec571a14 100644 --- a/src/components/select/select.js +++ b/src/components/select/select.js @@ -8,12 +8,9 @@ library.add( ) export default { - model: { - prop: 'value', - event: 'change' - }, + emits: ['update:modelValue'], props: [ - 'value', + 'modelValue', 'disabled', 'unstyled', 'kind' diff --git a/src/components/select/select.vue b/src/components/select/select.vue index 8d6528ff..ea8c8b69 100644 --- a/src/components/select/select.vue +++ b/src/components/select/select.vue @@ -1,4 +1,3 @@ - <template> <label class="Select input" @@ -6,11 +5,12 @@ > <select :disabled="disabled" - :value="value" - @change="$emit('change', $event.target.value)" + :value="modelValue" + @change="$emit('update:modelValue', $event.target.value)" > <slot /> </select> + {{ ' ' }} <FAIcon class="select-down-icon" icon="chevron-down" @@ -23,7 +23,8 @@ <style lang="scss"> @import '../../_variables.scss'; -.Select { +/* TODO fix order of styles */ +label.Select { padding: 0; select { diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue index 3f885881..cf7cec27 100644 --- a/src/components/selectable_list/selectable_list.vue +++ b/src/components/selectable_list/selectable_list.vue @@ -6,7 +6,7 @@ > <div class="selectable-list-checkbox-wrapper"> <Checkbox - :checked="allSelected" + :modelValue="allSelected" :indeterminate="someSelected" @change="toggleAll" > @@ -31,7 +31,7 @@ > <div class="selectable-list-checkbox-wrapper"> <Checkbox - :checked="isSelected(item)" + :modelValue="isSelected(item)" @change="checked => toggle(checked, item)" /> </div> diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue index e0d825f2..3808bc74 100644 --- a/src/components/settings_modal/helpers/boolean_setting.vue +++ b/src/components/settings_modal/helpers/boolean_setting.vue @@ -4,9 +4,9 @@ class="BooleanSetting" > <Checkbox - :checked="state" + :modelValue="state" :disabled="disabled" - @change="update" + @update:modelValue="update" > <span v-if="!!$slots.default" @@ -14,6 +14,7 @@ > <slot /> </span> + {{ ' ' }} <ModifiedIndicator :changed="isChanged" /><ServerSideIndicator :server-side="isServerSide" /> </Checkbox> </label> </template> diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue index 54f5d0a7..b6aa853a 100644 --- a/src/components/settings_modal/helpers/choice_setting.vue +++ b/src/components/settings_modal/helpers/choice_setting.vue @@ -4,10 +4,11 @@ class="ChoiceSetting" > <slot /> + {{ ' ' }} <Select - :value="state" + :modelValue="state" :disabled="disabled" - @change="update" + @update:modelValue="update" > <option v-for="option in options" diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue index 408b0925..e661a025 100644 --- a/src/components/settings_modal/helpers/integer_setting.vue +++ b/src/components/settings_modal/helpers/integer_setting.vue @@ -16,6 +16,7 @@ :value="state" @change="update" > + {{ ' ' }} <ModifiedIndicator :changed="isChanged" /> </span> </template> diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue index b8de7e7e..28f85423 100644 --- a/src/components/settings_modal/settings_modal.vue +++ b/src/components/settings_modal/settings_modal.vue @@ -11,7 +11,7 @@ {{ $t('settings.settings') }} </span> <transition name="fade"> - <template v-if="currentSaveStateNotice"> + <div v-if="currentSaveStateNotice"> <div v-if="currentSaveStateNotice.error" class="alert error" @@ -27,7 +27,7 @@ > {{ $t('settings.saving_ok') }} </div> - </template> + </div> </transition> <button class="btn button-default" @@ -68,6 +68,7 @@ :title="$t('general.close')" > <span>{{ $t("settings.file_export_import.backup_restore") }}</span> + {{ ' ' }} <FAIcon icon="chevron-down" /> @@ -109,7 +110,7 @@ </template> </Popover> - <Checkbox v-model="expertLevel"> + <Checkbox :modelValue="!!expertLevel" @update:modelValue="expertLevel = Number($event)"> {{ $t("settings.expert_mode") }} </Checkbox> <portal-target diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_content.js index 9dcf1b5a..9ac0301f 100644 --- a/src/components/settings_modal/settings_modal_content.js +++ b/src/components/settings_modal/settings_modal_content.js @@ -1,4 +1,4 @@ -import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import DataImportExportTab from './tabs/data_import_export_tab.vue' import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue' @@ -53,6 +53,9 @@ const SettingsModalContent = { }, open () { return this.$store.state.interface.settingsModalState !== 'hidden' + }, + bodyLock () { + return this.$store.state.interface.settingsModalState === 'visible' } }, methods: { @@ -60,8 +63,8 @@ const SettingsModalContent = { const targetTab = this.$store.state.interface.settingsModalTargetTab // We're being told to open in specific tab if (targetTab) { - const tabIndex = this.$refs.tabSwitcher.$slots.default.findIndex(elm => { - return elm.data && elm.data.attrs['data-tab-name'] === targetTab + const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => { + return elm.props && elm.props['data-tab-name'] === targetTab }) if (tabIndex >= 0) { this.$refs.tabSwitcher.setTab(tabIndex) diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_content.vue index c9ed2a38..0be76d22 100644 --- a/src/components/settings_modal/settings_modal_content.vue +++ b/src/components/settings_modal/settings_modal_content.vue @@ -4,6 +4,7 @@ class="settings_tab-switcher" :side-tab-bar="true" :scrollable-tabs="true" + :body-scroll-lock="bodyLock" > <div :label="$t('settings.general')" diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue index dc48902f..97046ff0 100644 --- a/src/components/settings_modal/tabs/filtering_tab.vue +++ b/src/components/settings_modal/tabs/filtering_tab.vue @@ -72,22 +72,10 @@ <div>{{ $t('settings.filtering_explanation') }}</div> </li> <h3>{{ $t('settings.attachments') }}</h3> - <li v-if="expertLevel > 0"> - <label for="maxThumbnails"> - {{ $t('settings.max_thumbnails') }} - </label> - <input - id="maxThumbnails" - path.number="maxThumbnails" - class="number-input" - type="number" - min="0" - step="1" - > - </li> <li> <IntegerSetting path="maxThumbnails" + expert="1" :min="0" > {{ $t('settings.max_thumbnails') }} diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js index 40a87b81..6cfeea35 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js @@ -2,7 +2,7 @@ import get from 'lodash/get' import map from 'lodash/map' import reject from 'lodash/reject' import Autosuggest from 'src/components/autosuggest/autosuggest.vue' -import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import BlockCard from 'src/components/block_card/block_card.vue' import MuteCard from 'src/components/mute_card/mute_card.vue' import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue' diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue index 7ac7b9d3..36613d20 100644 --- a/src/components/settings_modal/tabs/theme_tab/preview.vue +++ b/src/components/settings_modal/tabs/theme_tab/preview.vue @@ -29,14 +29,14 @@ {{ $t('settings.style.preview.content') }} </h4> - <i18n path="settings.style.preview.text"> + <i18n-t keypath="settings.style.preview.text"> <code style="font-family: var(--postCodeFont)"> {{ $t('settings.style.preview.mono') }} </code> <a style="color: var(--link)"> {{ $t('settings.style.preview.link') }} </a> - </i18n> + </i18n-t> <div class="icons"> <FAIcon @@ -72,15 +72,15 @@ :^) </div> <div class="content"> - <i18n - path="settings.style.preview.fine_print" + <i18n-t + keypath="settings.style.preview.fine_print" tag="span" class="faint" > <a style="color: var(--faintLink)"> {{ $t('settings.style.preview.faint_link') }} </a> - </i18n> + </i18n-t> </div> </div> <div class="separator" /> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js index 6d140b6c..7e1da7ab 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -1,4 +1,3 @@ -import { set, delete as del } from 'vue' import { rgb2hex, hex2rgb, @@ -34,7 +33,7 @@ import OpacityInput from 'src/components/opacity_input/opacity_input.vue' import ShadowControl from 'src/components/shadow_control/shadow_control.vue' import FontControl from 'src/components/font_control/font_control.vue' import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue' -import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import Checkbox from 'src/components/checkbox/checkbox.vue' import Select from 'src/components/select/select.vue' @@ -320,9 +319,9 @@ export default { }, set (val) { if (val) { - set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _))) + this.shadowsLocal[this.shadowSelected] = this.currentShadowFallback.map(_ => Object.assign({}, _)) } else { - del(this.shadowsLocal, this.shadowSelected) + delete this.shadowsLocal[this.shadowSelected] } } }, @@ -334,7 +333,7 @@ export default { return this.shadowsLocal[this.shadowSelected] }, set (v) { - set(this.shadowsLocal, this.shadowSelected, v) + this.shadowsLocal[this.shadowSelected] = v } }, themeValid () { @@ -561,7 +560,7 @@ export default { .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal')) .filter(_ => !v1OnlyNames.includes(_)) .forEach(key => { - set(this.$data, key, undefined) + this.$data[key] = undefined }) }, @@ -569,7 +568,7 @@ export default { Object.keys(this.$data) .filter(_ => _.endsWith('RadiusLocal')) .forEach(key => { - set(this.$data, key, undefined) + this.$data[key] = undefined }) }, @@ -577,7 +576,7 @@ export default { Object.keys(this.$data) .filter(_ => _.endsWith('OpacityLocal')) .forEach(key => { - set(this.$data, key, undefined) + this.$data[key] = undefined }) }, diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue index c32f9353..9cdd229a 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -903,6 +903,7 @@ <div class="tab-header shadow-selector"> <div class="select-container"> {{ $t('settings.style.shadows.component') }} + {{ ' ' }} <Select id="shadow-switcher" v-model="shadowSelected" @@ -924,6 +925,7 @@ > {{ $t('settings.style.shadows.override') }} </label> + {{ ' ' }} <input id="override" v-model="currentShadowOverriden" @@ -949,27 +951,27 @@ :fallback="currentShadowFallback" /> <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"> - <i18n - path="settings.style.shadows.filter_hint.always_drop_shadow" + <i18n-t + keypath="settings.style.shadows.filter_hint.always_drop_shadow" tag="p" > <code>filter: drop-shadow()</code> - </i18n> + </i18n-t> <p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p> - <i18n - path="settings.style.shadows.filter_hint.drop_shadow_syntax" + <i18n-t + keypath="settings.style.shadows.filter_hint.drop_shadow_syntax" tag="p" > <code>drop-shadow</code> <code>spread-radius</code> <code>inset</code> - </i18n> - <i18n - path="settings.style.shadows.filter_hint.inset_classic" + </i18n-t> + <i18n-t + keypath="settings.style.shadows.filter_hint.inset_classic" tag="p" > <code>box-shadow</code> - </i18n> + </i18n-t> <p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p> </div> </div> diff --git a/src/components/settings_modal/tabs/version_tab.vue b/src/components/settings_modal/tabs/version_tab.vue index d35ff25e..0330d49f 100644 --- a/src/components/settings_modal/tabs/version_tab.vue +++ b/src/components/settings_modal/tabs/version_tab.vue @@ -28,4 +28,4 @@ </div> </div> </template> -<script src="./version_tab.js"> +<script src="./version_tab.js" /> diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue index 511e07f3..b59ac3bc 100644 --- a/src/components/shadow_control/shadow_control.vue +++ b/src/components/shadow_control/shadow_control.vue @@ -204,12 +204,12 @@ v-model="selected.alpha" :disabled="!present" /> - <i18n - path="settings.style.shadows.hintV3" + <i18n-t + keypath="settings.style.shadows.hintV3" tag="p" > <code>--variable,mod</code> - </i18n> + </i18n-t> </div> </div> </template> diff --git a/src/components/status/status.js b/src/components/status/status.js index e54be241..088ddcc5 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -389,6 +389,9 @@ const Status = { }, threadShowing () { return this.controlledThreadDisplayStatus === 'showing' + }, + visibilityLocalized () { + return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility) } }, methods: { @@ -478,11 +481,6 @@ const Status = { 'isSuspendable': function (val) { this.suspendable = val } - }, - filters: { - capitalize: function (str) { - return str.charAt(0).toUpperCase() + str.slice(1) - } } } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 2387151e..61feb303 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,6 +1,7 @@ <template> <div v-if="!hideStatus" + ref="root" class="Status" :class="[{ '-focused': isFocused }, { '-conversation': inlineExpanded }]" > @@ -100,6 +101,7 @@ :to="retweeterProfileLink" >{{ retweeter }}</router-link> </span> + {{ ' ' }} <FAIcon icon="retweet" class="repeat-icon" @@ -120,9 +122,9 @@ v-if="!noHeading" class="left-side" > - <router-link - :to="userProfileLink" - @click.stop.prevent.capture.native="toggleUserExpanded" + <a + :href="$router.resolve(userProfileLink).href" + @click.stop.prevent.capture="toggleUserExpanded" > <UserAvatar class="post-avatar" @@ -131,7 +133,7 @@ :better-shadow="betterShadow" :user="status.user" /> - </router-link> + </a> </div> <div class="right-side"> <UserCard @@ -191,7 +193,7 @@ <span v-if="status.visibility" class="visibility-icon" - :title="status.visibility | capitalize" + :title="visibilityLocalized" > <FAIcon fixed-width @@ -274,6 +276,7 @@ icon="reply" flip="horizontal" /> + {{ ' ' }} <span class="reply-to-text" > diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js index c47f5631..e0962ccd 100644 --- a/src/components/status_popover/status_popover.js +++ b/src/components/status_popover/status_popover.js @@ -1,6 +1,7 @@ import { find } from 'lodash' import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' +import { defineAsyncComponent } from 'vue' library.add( faCircleNotch @@ -22,8 +23,8 @@ const StatusPopover = { } }, components: { - Status: () => import('../status/status.vue'), - Popover: () => import('../popover/popover.vue') + Status: defineAsyncComponent(() => import('../status/status.vue')), + Popover: defineAsyncComponent(() => import('../popover/popover.vue')) }, methods: { enter () { diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js index 8daf3f07..3a2d3914 100644 --- a/src/components/sticker_picker/sticker_picker.js +++ b/src/components/sticker_picker/sticker_picker.js @@ -1,6 +1,6 @@ /* eslint-env browser */ import statusPosterService from '../../services/status_poster/status_poster.service.js' -import TabSwitcher from '../tab_switcher/tab_switcher.js' +import TabSwitcher from '../tab_switcher/tab_switcher.jsx' const StickerPicker = { components: { diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.jsx index d2a7f5c5..59ff98df 100644 --- a/src/components/tab_switcher/tab_switcher.js +++ b/src/components/tab_switcher/tab_switcher.jsx @@ -1,10 +1,13 @@ -import Vue from 'vue' +// eslint-disable-next-line no-unused +import { h, Fragment } from 'vue' import { mapState } from 'vuex' import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' import './tab_switcher.scss' -export default Vue.component('tab-switcher', { +const findFirstUsable = (slots) => slots.findIndex(_ => _.props) + +export default { name: 'TabSwitcher', props: { renderOnlyFocused: { @@ -31,25 +34,30 @@ export default Vue.component('tab-switcher', { required: false, type: Boolean, default: false + }, + bodyScrollLock: { + required: false, + type: Boolean, + default: false } }, data () { return { - active: this.$slots.default.findIndex(_ => _.tag) + active: findFirstUsable(this.slots()) } }, computed: { activeIndex () { // In case of controlled component if (this.activeTab) { - return this.$slots.default.findIndex(slot => this.activeTab === slot.key) + return this.slots().findIndex(slot => this.activeTab === slot.key) } else { return this.active } }, isActive () { return tabName => { - const isWanted = slot => slot.data && slot.data.attrs['data-tab-name'] === tabName + const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName return this.$slots.default.findIndex(isWanted) === this.activeIndex } }, @@ -61,9 +69,9 @@ export default Vue.component('tab-switcher', { }) }, beforeUpdate () { - const currentSlot = this.$slots.default[this.active] - if (!currentSlot.tag) { - this.active = this.$slots.default.findIndex(_ => _.tag) + const currentSlot = this.slots()[this.active] + if (!currentSlot.props) { + this.active = findFirstUsable(this.slots()) } }, methods: { @@ -73,9 +81,16 @@ export default Vue.component('tab-switcher', { this.setTab(index) } }, + // DO NOT put it to computed, it doesn't work (caching?) + slots () { + if (this.$slots.default()[0].type === Fragment) { + return this.$slots.default()[0].children + } + return this.$slots.default() + }, setTab (index) { if (typeof this.onSwitch === 'function') { - this.onSwitch.call(null, this.$slots.default[index].key) + this.onSwitch.call(null, this.slots()[index].key) } this.active = index if (this.scrollableTabs) { @@ -83,27 +98,28 @@ export default Vue.component('tab-switcher', { } } }, - render (h) { - const tabs = this.$slots.default + render () { + const tabs = this.slots() .map((slot, index) => { - if (!slot.tag) return + const props = slot.props + if (!props) return const classesTab = ['tab', 'button-default'] const classesWrapper = ['tab-wrapper'] if (this.activeIndex === index) { classesTab.push('active') classesWrapper.push('active') } - if (slot.data.attrs.image) { + if (props.image) { return ( <div class={classesWrapper.join(' ')}> <button - disabled={slot.data.attrs.disabled} + disabled={props.disabled} onClick={this.clickTab(index)} class={classesTab.join(' ')} type="button" > - <img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/> - {slot.data.attrs.label ? '' : slot.data.attrs.label} + <img src={props.image} title={props['image-tooltip']}/> + {props.label ? '' : props.label} </button> </div> ) @@ -111,25 +127,26 @@ export default Vue.component('tab-switcher', { return ( <div class={classesWrapper.join(' ')}> <button - disabled={slot.data.attrs.disabled} + disabled={props.disabled} onClick={this.clickTab(index)} class={classesTab.join(' ')} type="button" > - {!slot.data.attrs.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={slot.data.attrs.icon}/>)} + {!props.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={props.icon}/>)} <span class="text"> - {slot.data.attrs.label} + {props.label} </span> </button> </div> ) }) - const contents = this.$slots.default.map((slot, index) => { - if (!slot.tag) return + const contents = this.slots().map((slot, index) => { + const props = slot.props + if (!props) return const active = this.activeIndex === index const classes = [ active ? 'active' : 'hidden' ] - if (slot.data.attrs.fullHeight) { + if (props.fullHeight) { classes.push('full-height') } const renderSlot = (!this.renderOnlyFocused || active) @@ -140,7 +157,7 @@ export default Vue.component('tab-switcher', { <div class={classes}> { this.sideTabBar - ? <h1 class="mobile-label">{slot.data.attrs.label}</h1> + ? <h1 class="mobile-label">{props.label}</h1> : '' } {renderSlot} @@ -153,10 +170,14 @@ export default Vue.component('tab-switcher', { <div class="tabs"> {tabs} </div> - <div ref="contents" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')} v-body-scroll-lock={this.settingsModalVisible}> + <div + ref="contents" + class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')} + v-body-scroll-lock={this.bodyScrollLock} + > {contents} </div> </div> ) } -}) +} diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss index 0ed614b7..575d41e1 100644 --- a/src/components/tab_switcher/tab_switcher.scss +++ b/src/components/tab_switcher/tab_switcher.scss @@ -166,13 +166,6 @@ position: relative; white-space: nowrap; padding: 6px 1em; - background-color: $fallback--fg; - background-color: var(--tab, $fallback--fg); - - &, &:active .tab-icon { - color: $fallback--text; - color: var(--tabText, $fallback--text); - } &:not(.active) { z-index: 4; diff --git a/src/components/tag_timeline/tag_timeline.js b/src/components/tag_timeline/tag_timeline.js index 400c6a4b..bda61ae0 100644 --- a/src/components/tag_timeline/tag_timeline.js +++ b/src/components/tag_timeline/tag_timeline.js @@ -18,7 +18,7 @@ const TagTimeline = { this.$store.dispatch('startFetchingTimeline', { timeline: 'tag', tag: this.tag }) } }, - destroyed () { + unmounted () { this.$store.dispatch('stopFetchingTimeline', 'tag') } } diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue index e64455e0..48942005 100644 --- a/src/components/thread_tree/thread_tree.vue +++ b/src/components/thread_tree/thread_tree.vue @@ -74,36 +74,42 @@ v-if="currentReplies.length && !threadShowing" class="thread-tree-replies thread-tree-replies-hidden" > - <i18n + <i18n-t v-if="simple" tag="button" - path="status.thread_follow_with_icon" + keypath="status.thread_follow_with_icon" class="button-unstyled -link thread-tree-show-replies-button" @click.prevent="dive(status.id)" > - <FAIcon - place="icon" - icon="angle-double-right" - /> - <span place="text"> - {{ $tc('status.thread_follow', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id] }) }} - </span> - </i18n> - <i18n + <template #icon> + <FAIcon + icon="angle-double-right" + /> + </template> + <template #text> + <span> + {{ $tc('status.thread_follow', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id] }) }} + </span> + </template> + </i18n-t> + <i18n-t v-else tag="button" - path="status.thread_show_full_with_icon" + keypath="status.thread_show_full_with_icon" class="button-unstyled -link thread-tree-show-replies-button" @click.prevent="showThreadRecursively(status.id)" > - <FAIcon - place="icon" - icon="angle-double-down" - /> - <span place="text"> - {{ $tc('status.thread_show_full', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }} - </span> - </i18n> + <template #icon> + <FAIcon + icon="angle-double-down" + /> + </template> + <template #text> + <span> + {{ $tc('status.thread_show_full', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }} + </span> + </template> + </i18n-t> </div> </div> </template> diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue index 55a2dd94..bed29020 100644 --- a/src/components/timeago/timeago.vue +++ b/src/components/timeago/timeago.vue @@ -31,7 +31,7 @@ export default { created () { this.refreshRelativeTimeObject() }, - destroyed () { + unmounted () { clearTimeout(this.interval) }, methods: { diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 04f0e7d6..6ede1dc5 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -40,6 +40,12 @@ const Timeline = { TimelineQuickSettings }, computed: { + filteredVisibleStatuses () { + return this.timeline.visibleStatuses.filter(status => this.timelineName !== 'user' || (status.id >= this.timeline.minId && status.id <= this.timeline.maxId)) + }, + filteredPinnedStatusesId () { + return this.pinnedStatusIds.filter(statusId => this.timeline.statusesObject[statusId]) + }, newStatusCount () { return this.timeline.newStatusCount }, @@ -104,7 +110,7 @@ const Timeline = { window.addEventListener('keydown', this.handleShortKey) setTimeout(this.determineVisibleStatuses, 250) }, - destroyed () { + unmounted () { window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('keydown', this.handleShortKey) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index ff16208d..d37a9e2a 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -23,30 +23,26 @@ ref="timeline" class="timeline" > - <template v-for="statusId in pinnedStatusIds"> - <conversation - v-if="timeline.statusesObject[statusId]" - :key="statusId + '-pinned'" - class="status-fadein" - :status-id="statusId" - :collapsable="true" - :pinned-status-ids-object="pinnedStatusIdsObject" - :in-profile="inProfile" - :profile-user-id="userId" - /> - </template> - <template v-for="status in timeline.visibleStatuses"> - <conversation - v-if="timelineName !== 'user' || (status.id >= timeline.minId && status.id <= timeline.maxId)" - :key="status.id" - class="status-fadein" - :status-id="status.id" - :collapsable="true" - :in-profile="inProfile" - :profile-user-id="userId" - :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)" - /> - </template> + <conversation + v-for="statusId in filteredPinnedStatusIds" + :key="statusId + '-pinned'" + class="status-fadein" + :status-id="statusId" + :collapsable="true" + :pinned-status-ids-object="pinnedStatusIdsObject" + :in-profile="inProfile" + :profile-user-id="userId" + /> + <conversation + v-for="status in filteredVisibleStatuses" + :key="status.id" + class="status-fadein" + :status-id="status.id" + :collapsable="true" + :in-profile="inProfile" + :profile-user-id="userId" + :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)" + /> </div> </div> <div :class="classes.footer"> diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 0708f387..14b4643a 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -141,6 +141,7 @@ class="userHighlightCl" type="color" > + {{ ' ' }} <Select :id="'userHighlightSel'+user.id" v-model="userHighlightType" diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js index 32ca2b8d..e24eb9f7 100644 --- a/src/components/user_list_popover/user_list_popover.js +++ b/src/components/user_list_popover/user_list_popover.js @@ -1,3 +1,6 @@ +import { defineAsyncComponent } from 'vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' + import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' @@ -11,8 +14,9 @@ const UserListPopover = { 'users' ], components: { - Popover: () => import('../popover/popover.vue'), - UserAvatar: () => import('../user_avatar/user_avatar.vue') + RichContent, + Popover: defineAsyncComponent(() => import('../popover/popover.vue')), + UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue')) }, computed: { usersCapped () { diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue index 5685916a..50949b98 100644 --- a/src/components/user_panel/user_panel.vue +++ b/src/components/user_panel/user_panel.vue @@ -2,7 +2,7 @@ <div class="user-panel"> <div v-if="signedIn" - key="user-panel" + key="user-panel-signed" class="panel panel-default signed-in" > <UserCard diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 7a475609..eeb6ea40 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -3,7 +3,7 @@ import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue' import Timeline from '../timeline/timeline.vue' import Conversation from '../conversation/conversation.vue' -import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx' import List from '../list/list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' @@ -47,7 +47,7 @@ const UserProfile = { this.load(routeParams.name || routeParams.id) this.tab = get(this.$route, 'query.tab', defaultTabKey) }, - destroyed () { + unmounted () { this.stopFetching() }, computed: { diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue index 1f67a5cc..2993e4ec 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.vue +++ b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -53,7 +53,7 @@ :statusoid="item" /> <Checkbox - :checked="isChecked(item.id)" + :modelValue="isChecked(item.id)" @change="checked => toggleStatus(checked, item.id)" /> </div> diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.jsx index 671b2b6f..705991ca 100644 --- a/src/hocs/with_load_more/with_load_more.js +++ b/src/hocs/with_load_more/with_load_more.jsx @@ -1,4 +1,5 @@ -import Vue from 'vue' +// eslint-disable-next-line no-unused +import { h } from 'vue' import isEmpty from 'lodash/isEmpty' import { getComponentProps } from '../../services/component_utils/component_utils' import './with_load_more.scss' @@ -16,14 +17,14 @@ library.add( const withLoadMore = ({ fetch, // function to fetch entries and return a promise select, // function to select data from store - destroy, // function called at "destroyed" lifecycle + unmounted, // function called at "destroyed" lifecycle childPropName = 'entries', // name of the prop to be passed into the wrapped component additionalPropNames = [] // additional prop name list of the wrapper component }) => (WrappedComponent) => { const originalProps = Object.keys(getComponentProps(WrappedComponent)) const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames) - return Vue.component('withLoadMore', { + return { props, data () { return { @@ -39,9 +40,9 @@ const withLoadMore = ({ this.fetchEntries() } }, - destroyed () { + unmounted () { window.removeEventListener('scroll', this.scrollLoad) - destroy && destroy(this.$props, this.$store) + unmounted && unmounted(this.$props, this.$store) }, methods: { // Entries is not a computed because computed can't track the dynamic @@ -79,16 +80,13 @@ const withLoadMore = ({ } } }, - render (h) { + render () { + console.log(this.$listeners) const props = { - props: { - ...this.$props, - [childPropName]: this.entries - }, - on: this.$listeners, - scopedSlots: this.$scopedSlots + ...this.$props, + [childPropName]: this.entries } - const children = Object.entries(this.$slots).map(([key, value]) => h('template', { slot: key }, value)) + const children = this.$slots return ( <div class="with-load-more"> <WrappedComponent {...props}> @@ -106,7 +104,7 @@ const withLoadMore = ({ </div> ) } - }) + } } export default withLoadMore diff --git a/src/hocs/with_subscription/with_subscription.js b/src/hocs/with_subscription/with_subscription.jsx index b1244276..d3f5506a 100644 --- a/src/hocs/with_subscription/with_subscription.js +++ b/src/hocs/with_subscription/with_subscription.jsx @@ -1,4 +1,5 @@ -import Vue from 'vue' +// eslint-disable-next-line no-unused +import { h } from 'vue' import isEmpty from 'lodash/isEmpty' import { getComponentProps } from '../../services/component_utils/component_utils' import './with_subscription.scss' @@ -22,7 +23,7 @@ const withSubscription = ({ const originalProps = Object.keys(getComponentProps(WrappedComponent)) const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames) - return Vue.component('withSubscription', { + return { props: [ ...props, 'refresh' // boolean saying to force-fetch data whenever created @@ -59,17 +60,13 @@ const withSubscription = ({ } } }, - render (h) { + render () { if (!this.error && !this.loading) { const props = { - props: { - ...this.$props, - [childPropName]: this.fetchedData - }, - on: this.$listeners, - scopedSlots: this.$scopedSlots + ...this.$props, + [childPropName]: this.fetchedData } - const children = Object.entries(this.$slots).map(([key, value]) => h('template', { slot: key }, value)) + const children = this.$slots return ( <div class="with-subscription"> <WrappedComponent {...props}> @@ -88,7 +85,7 @@ const withSubscription = ({ ) } } - }) + } } export default withSubscription diff --git a/src/i18n/en.json b/src/i18n/en.json index 76a871fb..d0af7268 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -85,7 +85,13 @@ }, "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." + "flash_fail": "Failed to load flash content, see console for details.", + "scope_in_timeline": { + "direct": "Direct", + "private": "Followers-only", + "public": "Public", + "unlisted": "Unlisted" + } }, "image_cropper": { "crop_picture": "Crop picture", @@ -502,14 +508,14 @@ "true": "yes" }, "virtual_scrolling": "Optimize timeline rendering", - "use_at_icon": "Display @ symbol as an icon instead of text", + "use_at_icon": "Display {'@'} symbol as an icon instead of text", "mention_link_display": "Display mention links", - "mention_link_display_short": "always as short names (e.g. @foo)", - "mention_link_display_full_for_remote": "as full names only for remote users (e.g. @foo@example.org)", - "mention_link_display_full": "always as full names (e.g. @foo@example.org)", + "mention_link_display_short": "always as short names (e.g. {'@'}foo)", + "mention_link_display_full_for_remote": "as full names only for remote users (e.g. {'@'}foo{'@'}example.org)", + "mention_link_display_full": "always as full names (e.g. {'@'}foo{'@'}example.org)", "mention_link_show_tooltip": "Show full user names as tooltip for remote users", "mention_link_show_avatar": "Show user avatar beside the link", - "mention_link_fade_domain": "Fade domains (e.g. @example.org in @foo@example.org)", + "mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)", "mention_link_bolden_you": "Highlight mention of you when you are mentioned", "fun": "Fun", "greentext": "Meme arrows", diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index 8ecb66a8..24b835da 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -1,6 +1,6 @@ import merge from 'lodash.merge' import localforage from 'localforage' -import { each, get, set } from 'lodash' +import { each, get, set, cloneDeep } from 'lodash' let loaded = false @@ -69,7 +69,7 @@ export default function createPersistedState ({ subscriber(store)((mutation, state) => { try { if (saveImmedeatelyActions.includes(mutation.type)) { - setState(key, reducer(state, paths), storage) + setState(key, reducer(cloneDeep(state), paths), storage) .then(success => { if (typeof success !== 'undefined') { if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') { diff --git a/src/main.js b/src/main.js index bdf8368b..eacd554c 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,4 @@ -import Vue from 'vue' -import VueRouter from 'vue-router' -import Vuex from 'vuex' +import { createStore } from 'vuex' import 'custom-event-polyfill' import './lib/event_target_polyfill.js' @@ -22,36 +20,18 @@ import pollsModule from './modules/polls.js' import postStatusModule from './modules/postStatus.js' import chatsModule from './modules/chats.js' -import VueI18n from 'vue-i18n' +import { createI18n } from 'vue-i18n' import createPersistedState from './lib/persisted_state.js' import pushNotifications from './lib/push_notifications_plugin.js' import messages from './i18n/messages.js' -import VueClickOutside from 'v-click-outside' -import PortalVue from 'portal-vue' -import VBodyScrollLock from './directives/body_scroll_lock' - -import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' - import afterStoreSetup from './boot/after_store.js' const currentLocale = (window.navigator.language || 'en').split('-')[0] -Vue.use(Vuex) -Vue.use(VueRouter) -Vue.use(VueI18n) -Vue.use(VueClickOutside) -Vue.use(PortalVue) -Vue.use(VBodyScrollLock) - -Vue.config.ignoredElements = ['pinch-zoom'] - -Vue.component('FAIcon', FontAwesomeIcon) -Vue.component('FALayers', FontAwesomeLayers) - -const i18n = new VueI18n({ +const i18n = createI18n({ // By default, use the browser locale, we will update it if neccessary locale: 'en', fallbackLocale: 'en', @@ -78,17 +58,18 @@ const persistedStateOptions = { console.error(e) storageError = true } - const store = new Vuex.Store({ + const store = createStore({ modules: { i18n: { getters: { - i18n: () => i18n + i18n: () => i18n.global } }, interface: interfaceModule, instance: instanceModule, - statuses: statusesModule, + // TODO refactor users/statuses modules, they depend on each other users: usersModule, + statuses: statusesModule, api: apiModule, config: configModule, serverSideConfig: serverSideConfigModule, diff --git a/src/modules/chats.js b/src/modules/chats.js index 69d683bd..f28c2603 100644 --- a/src/modules/chats.js +++ b/src/modules/chats.js @@ -1,4 +1,4 @@ -import Vue from 'vue' +import { reactive } from 'vue' import { find, omitBy, orderBy, sumBy } from 'lodash' import chatService from '../services/chat_service/chat_service.js' import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js' @@ -13,8 +13,8 @@ const emptyChatList = () => ({ const defaultState = { chatList: emptyChatList(), chatListFetcher: null, - openedChats: {}, - openedChatMessageServices: {}, + openedChats: reactive({}), + openedChatMessageServices: reactive({}), fetcher: undefined, currentChatId: null, lastReadMessageId: null @@ -137,10 +137,10 @@ const chats = { }, addOpenedChat (state, { _dispatch, chat }) { state.currentChatId = chat.id - Vue.set(state.openedChats, chat.id, chat) + state.openedChats[chat.id] = chat if (!state.openedChatMessageServices[chat.id]) { - Vue.set(state.openedChatMessageServices, chat.id, chatService.empty(chat.id)) + state.openedChatMessageServices[chat.id] = chatService.empty(chat.id) } }, setCurrentChatId (state, { chatId }) { @@ -160,7 +160,7 @@ const chats = { } } else { state.chatList.data.push(updatedChat) - Vue.set(state.chatList.idStore, updatedChat.id, updatedChat) + state.chatList.idStore[updatedChat.id] = updatedChat } }) }, @@ -172,7 +172,7 @@ const chats = { chat.updated_at = updatedChat.updated_at } if (!chat) { state.chatList.data.unshift(updatedChat) } - Vue.set(state.chatList.idStore, updatedChat.id, updatedChat) + state.chatList.idStore[updatedChat.id] = updatedChat }, deleteChat (state, { _dispatch, id, _rootGetters }) { state.chats.data = state.chats.data.filter(conversation => @@ -186,8 +186,8 @@ const chats = { commit('setChatListFetcher', { fetcher: undefined }) for (const chatId in state.openedChats) { chatService.clear(state.openedChatMessageServices[chatId]) - Vue.delete(state.openedChats, chatId) - Vue.delete(state.openedChatMessageServices, chatId) + delete state.openedChats[chatId] + delete state.openedChatMessageServices[chatId] } }, setChatsLoading (state, { value }) { @@ -215,8 +215,8 @@ const chats = { for (const chatId in state.openedChats) { if (currentChatId !== chatId) { chatService.clear(state.openedChatMessageServices[chatId]) - Vue.delete(state.openedChats, chatId) - Vue.delete(state.openedChatMessageServices, chatId) + delete state.openedChats[chatId] + delete state.openedChatMessageServices[chatId] } } }, diff --git a/src/modules/config.js b/src/modules/config.js index ef140bcb..ff5ef270 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -1,4 +1,3 @@ -import { set, delete as del } from 'vue' import { setPreset, applyTheme } from '../services/style_setter/style_setter.js' import messages from '../i18n/messages' @@ -122,14 +121,14 @@ const config = { }, mutations: { setOption (state, { name, value }) { - set(state, name, value) + state[name] = value }, setHighlight (state, { user, color, type }) { const data = this.state.config.highlight[user] if (color || type) { - set(state.highlight, user, { color: color || data.color, type: type || data.type }) + state.highlight[user] = { color: color || data.color, type: type || data.type } } else { - del(state.highlight, user) + delete state.highlight[user] } } }, diff --git a/src/modules/instance.js b/src/modules/instance.js index 79c54096..220463ca 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -1,4 +1,3 @@ -import { set } from 'vue' import { getPreset, applyTheme } from '../services/style_setter/style_setter.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import apiService from '../services/api/api.service.js' @@ -102,7 +101,7 @@ const instance = { mutations: { setInstanceOption (state, { name, value }) { if (typeof value !== 'undefined') { - set(state, name, value) + state[name] = value } }, setKnownDomains (state, domains) { diff --git a/src/modules/interface.js b/src/modules/interface.js index d6db32fd..17277331 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -1,5 +1,3 @@ -import { set, delete as del } from 'vue' - const defaultState = { settingsModalState: 'hidden', settingsModalLoaded: false, @@ -29,11 +27,10 @@ const interfaceMod = { if (state.noticeClearTimeout) { clearTimeout(state.noticeClearTimeout) } - set(state.settings, 'currentSaveStateNotice', { error: false, data: success }) - set(state.settings, 'noticeClearTimeout', - setTimeout(() => del(state.settings, 'currentSaveStateNotice'), 2000)) + state.settings.currentSaveStateNotice = { error: false, data: success } + state.settings.noticeClearTimeout = setTimeout(() => delete state.settings.currentSaveStateNotice, 2000) } else { - set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error }) + state.settings.currentSaveStateNotice = { error: true, errorData: error } } }, setNotificationPermission (state, permission) { @@ -109,7 +106,7 @@ const interfaceMod = { commit('openSettingsModal') }, pushGlobalNotice ( - { commit, dispatch }, + { commit, dispatch, state }, { messageKey, messageArgs = {}, @@ -121,11 +118,14 @@ const interfaceMod = { messageArgs, level } + commit('pushGlobalNotice', notice) + // Adding a new element to array wraps it in a Proxy, which breaks the comparison + // TODO: Generate UUID or something instead or relying on !== operator? + const newNotice = state.globalNotices[state.globalNotices.length - 1] if (timeout) { - setTimeout(() => dispatch('removeGlobalNotice', notice), timeout) + setTimeout(() => dispatch('removeGlobalNotice', newNotice), timeout) } - commit('pushGlobalNotice', notice) - return notice + return newNotice }, removeGlobalNotice ({ commit }, notice) { commit('removeGlobalNotice', notice) diff --git a/src/modules/oauth.js b/src/modules/oauth.js index a2a83450..038bc3f3 100644 --- a/src/modules/oauth.js +++ b/src/modules/oauth.js @@ -1,5 +1,3 @@ -import { delete as del } from 'vue' - const oauth = { state: { clientId: false, @@ -29,7 +27,7 @@ const oauth = { state.userToken = false // state.token is userToken with older name, coming from persistent state // let's clear it as well, since it is being used as a fallback of state.userToken - del(state, 'token') + delete state.token } }, getters: { diff --git a/src/modules/polls.js b/src/modules/polls.js index 92b89a06..1c4f98a4 100644 --- a/src/modules/polls.js +++ b/src/modules/polls.js @@ -1,5 +1,4 @@ import { merge } from 'lodash' -import { set } from 'vue' const polls = { state: { @@ -13,25 +12,25 @@ const polls = { // Make expired-state change trigger re-renders properly poll.expired = Date.now() > Date.parse(poll.expires_at) if (existingPoll) { - set(state.pollsObject, poll.id, merge(existingPoll, poll)) + state.pollsObject[poll.id] = merge(existingPoll, poll) } else { - set(state.pollsObject, poll.id, poll) + state.pollsObject[poll.id] = poll } }, trackPoll (state, pollId) { const currentValue = state.trackedPolls[pollId] if (currentValue) { - set(state.trackedPolls, pollId, currentValue + 1) + state.trackedPolls[pollId] = currentValue + 1 } else { - set(state.trackedPolls, pollId, 1) + state.trackedPolls[pollId] = 1 } }, untrackPoll (state, pollId) { const currentValue = state.trackedPolls[pollId] if (currentValue) { - set(state.trackedPolls, pollId, currentValue - 1) + state.trackedPolls[pollId] = currentValue - 1 } else { - set(state.trackedPolls, pollId, 0) + state.trackedPolls[pollId] = 0 } } }, diff --git a/src/modules/statuses.js b/src/modules/statuses.js index ac5d25c4..a13930e9 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -12,7 +12,6 @@ import { isArray, omitBy } from 'lodash' -import { set } from 'vue' import { isStatusNotification, isValidNotification, @@ -92,7 +91,7 @@ const mergeOrAdd = (arr, obj, item) => { // This is a new item, prepare it prepareStatus(item) arr.push(item) - set(obj, item.id, item) + obj[item.id] = item return { item, new: true } } } @@ -131,7 +130,7 @@ const addStatusToGlobalStorage = (state, data) => { if (conversationsObject[conversationId]) { conversationsObject[conversationId].push(status) } else { - set(conversationsObject, conversationId, [status]) + conversationsObject[conversationId] = [status] } } return result @@ -523,7 +522,7 @@ export const mutations = { }, addEmojiReactionsBy (state, { id, emojiReactions, currentUser }) { const status = state.allStatusesObject[id] - set(status, 'emoji_reactions', emojiReactions) + status['emoji_reactions'] = emojiReactions }, addOwnReaction (state, { id, emoji, currentUser }) { const status = state.allStatusesObject[id] @@ -542,9 +541,9 @@ export const mutations = { // Update count of existing reaction if it exists, otherwise append at the end if (reactionIndex >= 0) { - set(status.emoji_reactions, reactionIndex, newReaction) + status.emoji_reactions[reactionIndex] = newReaction } else { - set(status, 'emoji_reactions', [...status.emoji_reactions, newReaction]) + status['emoji_reactions'] = [...status.emoji_reactions, newReaction] } }, removeOwnReaction (state, { id, emoji, currentUser }) { @@ -563,9 +562,9 @@ export const mutations = { } if (newReaction.count > 0) { - set(status.emoji_reactions, reactionIndex, newReaction) + status.emoji_reactions[reactionIndex] = newReaction } else { - set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.name !== emoji)) + status['emoji_reactions'] = status.emoji_reactions.filter(r => r.name !== emoji) } }, updateStatusWithPoll (state, { id, poll }) { diff --git a/src/modules/users.js b/src/modules/users.js index 05ff44d5..e5889fdb 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -1,7 +1,6 @@ import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import oauthApi from '../services/new_api/oauth.js' import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash' -import { set } from 'vue' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' // TODO: Unify with mergeOrAdd in statuses.js @@ -15,9 +14,9 @@ export const mergeOrAdd = (arr, obj, item) => { } else { // This is a new item, prepare it arr.push(item) - set(obj, item.id, item) + obj[item.id] = item if (item.screen_name && !item.screen_name.includes('@')) { - set(obj, item.screen_name.toLowerCase(), item) + obj[item.screen_name.toLowerCase()] = item } return { item, new: true } } @@ -103,23 +102,23 @@ export const mutations = { const user = state.usersObject[id] const tags = user.tags || [] const newTags = tags.concat([tag]) - set(user, 'tags', newTags) + user['tags'] = newTags }, untagUser (state, { user: { id }, tag }) { const user = state.usersObject[id] const tags = user.tags || [] const newTags = tags.filter(t => t !== tag) - set(user, 'tags', newTags) + user['tags'] = newTags }, updateRight (state, { user: { id }, right, value }) { const user = state.usersObject[id] let newRights = user.rights newRights[right] = value - set(user, 'rights', newRights) + user['rights'] = newRights }, updateActivationStatus (state, { user: { id }, deactivated }) { const user = state.usersObject[id] - set(user, 'deactivated', deactivated) + user['deactivated'] = deactivated }, setCurrentUser (state, user) { state.lastLoginName = user.screen_name @@ -148,26 +147,26 @@ export const mutations = { clearFriends (state, userId) { const user = state.usersObject[userId] if (user) { - set(user, 'friendIds', []) + user['friendIds'] = [] } }, clearFollowers (state, userId) { const user = state.usersObject[userId] if (user) { - set(user, 'followerIds', []) + user['followerIds'] = [] } }, addNewUsers (state, users) { each(users, (user) => { if (user.relationship) { - set(state.relationships, user.relationship.id, user.relationship) + state.relationships[user.relationship.id] = user.relationship } mergeOrAdd(state.users, state.usersObject, user) }) }, updateUserRelationship (state, relationships) { relationships.forEach((relationship) => { - set(state.relationships, relationship.id, relationship) + state.relationships[relationship.id] = relationship }) }, saveBlockIds (state, blockIds) { @@ -222,7 +221,7 @@ export const mutations = { }, setColor (state, { user: { id }, highlighted }) { const user = state.usersObject[id] - set(user, 'highlight', highlighted) + user['highlight'] = highlighted }, signUpPending (state) { state.signUpPending = true diff --git a/src/services/resettable_async_component.js b/src/services/resettable_async_component.js index 517bbd88..e85c540b 100644 --- a/src/services/resettable_async_component.js +++ b/src/services/resettable_async_component.js @@ -1,4 +1,5 @@ -import Vue from 'vue' +// TODO investigate if even necessary since VUE3 +import { reactive } from 'vue' /* By default async components don't have any way to recover, if component is * failed, it is failed forever. This helper tries to remedy that by recreating @@ -13,7 +14,7 @@ function getResettableAsyncComponent (asyncComponent, options) { ...options }) - const observe = Vue.observable({ c: asyncComponentFactory() }) + const observe = reactive({ c: asyncComponentFactory() }) return { functional: true, @@ -3,12 +3,10 @@ import localForage from 'localforage' import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js' import { prepareNotificationObject } from './services/notification_utils/notification_utils.js' -import Vue from 'vue' -import VueI18n from 'vue-i18n' +import { createI18n } from 'vue-i18n' import messages from './i18n/service_worker_messages.js' -Vue.use(VueI18n) -const i18n = new VueI18n({ +const i18n = createI18n({ // By default, use the browser locale, we will update it if neccessary locale: 'en', fallbackLocale: 'en', |
