Merge branch 'vue3-again' into shigusegubu-vue3

* vue3-again: (59 commits)
  cleanup console log
  fix i18n at places
  fix all the spacings i could find
  fix spacings in notifications
  fix dupe id
  fix animations
  cleanup
  fix capitalization (and localization of tooltips for scope icon)
  listeners aren't actually used
  fix selects in settings screen
  fix tabs not being able to be "disabled"
  fix avatars not opening inline card
  fix other weird route
  fix routes test
  skip user profile test for now https://github.com/vuejs/test-utils/issues/1382
  fix emoji input tests
  fix richcontent and its tests
  fix tests running
  fix mobile post button being too square
  fix selects
  ...
This commit is contained in:
Henry Jameson 2022-03-24 09:33:00 +02:00
commit f7c2cb95a0
96 changed files with 1375 additions and 1360 deletions

View file

@ -1,5 +1,5 @@
{
"presets": ["@babel/preset-env", "@vue/babel-preset-jsx"],
"plugins": ["@babel/plugin-transform-runtime", "lodash"],
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
"comments": false
}

View file

@ -1,7 +1,7 @@
# This file is a template, and might need editing before it works on your project.
# Official framework image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/node/tags/
image: node:10
image: node:12
stages:
- lint

View file

@ -4,6 +4,7 @@ var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../')
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
var CopyPlugin = require('copy-webpack-plugin');
var { VueLoaderPlugin } = require('vue-loader')
var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
@ -29,12 +30,12 @@ module.exports = {
}
},
resolve: {
extensions: ['.js', '.vue'],
extensions: ['.js', '.jsx', '.vue'],
modules: [
path.join(__dirname, '../node_modules')
],
alias: {
'vue$': 'vue/dist/vue.runtime.common',
'vue': '@vue/compat',
'static': path.resolve(__dirname, '../static'),
'src': path.resolve(__dirname, '../src'),
'assets': path.resolve(__dirname, '../src/assets'),
@ -60,7 +61,14 @@ module.exports = {
},
{
test: /\.vue$/,
use: 'vue-loader'
loader: 'vue-loader',
options: {
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
},
{
test: /\.jsx?$/,
@ -95,6 +103,7 @@ module.exports = {
entry: path.join(__dirname, '..', 'src/sw.js'),
filename: 'sw-pleroma.js'
}),
new VueLoaderPlugin(),
// This copies Ruffle's WASM to a directory so that JS side can access it
new CopyPlugin({
patterns: [

View file

@ -21,7 +21,7 @@
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/vue-fontawesome": "2.0.6",
"@fortawesome/vue-fontawesome": "3.0.0-5",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"body-scroll-lock": "2.7.1",
"chromatism": "3.0.0",
@ -34,13 +34,14 @@
"portal-vue": "2.1.7",
"punycode.js": "2.1.0",
"ruffle-mirror": "2021.4.11",
"v-click-outside": "2.1.5",
"vue": "2.6.11",
"vue-i18n": "7.8.1",
"vue-router": "3.0.2",
"vue-template-compiler": "2.6.11",
"vuelidate": "0.7.7",
"vuex": "3.0.1"
"click-outside-vue3": "4.0.1",
"vue": "^3.1.0",
"@vue/compat": "^3.1.0",
"vue-i18n": "9.1.9",
"vue-router": "4.0.14",
"@vuelidate/core": "2.0.0-alpha.35",
"@vuelidate/validators": "2.0.0-alpha.27",
"vuex": "4.0.2"
},
"devDependencies": {
"@babel/core": "7.17.8",
@ -49,8 +50,9 @@
"@babel/register": "7.17.7",
"@ungap/event-target": "0.2.3",
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
"@vue/babel-preset-jsx": "1.2.4",
"@vue/test-utils": "1.0.0-beta.28",
"@vue/babel-plugin-jsx": "1.1.1",
"@vue/test-utils": "2.0.0-rc.17",
"@vue/compiler-sfc": "^3.1.0",
"autoprefixer": "6.7.7",
"babel-eslint": "7.2.3",
"babel-loader": "8.2.3",
@ -82,10 +84,10 @@
"iso-639-1": "2.1.13",
"isparta-loader": "2.0.0",
"json-loader": "0.5.7",
"karma": "3.1.4",
"karma": "6.3.17",
"karma-coverage": "1.1.2",
"karma-firefox-launcher": "1.3.0",
"karma-mocha": "1.3.0",
"karma-mocha": "2.0.1",
"karma-mocha-reporter": "2.2.5",
"karma-sinon-chai": "2.0.2",
"karma-sourcemap-loader": "0.3.8",
@ -112,7 +114,7 @@
"stylelint-config-standard": "20.0.0",
"stylelint-rscss": "0.4.0",
"url-loader": "1.1.2",
"vue-loader": "14.2.4",
"vue-loader": "^16.0.0",
"vue-style-loader": "4.1.2",
"webpack": "4.46.0",
"webpack-dev-middleware": "3.7.3",

View file

@ -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: {

View file

@ -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
}

View file

@ -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>

View file

@ -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

View file

@ -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) {

View file

@ -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 () {

View file

@ -4,7 +4,7 @@
<UserAvatar
class="avatar"
:user="user"
@click.prevent.native="toggleUserExpanded"
@click.prevent="toggleUserExpanded"
/>
</router-link>
<div

View file

@ -9,7 +9,7 @@ const Bookmarks = {
components: {
Timeline
},
destroyed () {
unmounted () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
}
}

View file

@ -57,7 +57,7 @@ const Chat = {
})
this.setChatLayout()
},
destroyed () {
unmounted () {
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleLayoutChange)
this.unsetChatLayout()

View file

@ -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
v-else
class="chat-loading-error"
>
<div class="alert error">
{{ $t('chats.error_loading_chat') }}
</div>
</div>
</div>
<div
ref="footer"
class="panel-body footer"
>
<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"
<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
v-else
class="chat-loading-error"
>
<div class="alert error">
{{ $t('chats.error_loading_chat') }}
</div>
</div>
</template>
</div>
<div
ref="footer"
class="panel-body footer"
>
<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"
/>
</div>
</div>
</div>
</div>

View file

@ -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)
}
}
})
}

View file

@ -7,7 +7,7 @@
type="checkbox"
:disabled="disabled"
:checked="checked"
:indeterminate.prop="indeterminate"
:indeterminate="indeterminate"
@change="$emit('change', $event.target.checked)"
>
<i class="checkbox-indicator" />

View file

@ -14,25 +14,25 @@
:checked="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('--')
}
}
}

View file

@ -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>

View file

@ -34,7 +34,7 @@
<search-bar
v-if="currentUser || !privateMode"
@toggled="onSearchBarToggled"
@click.stop.native
@click.stop
/>
<button
class="button-unstyled nav-icon"

View file

@ -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

View file

@ -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
}
})

View file

@ -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 {

View file

@ -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>

View file

@ -1,4 +1,4 @@
import { set } from 'vue'
import { set } from 'lodash'
import Select from '../select/select.vue'
export default {

View file

@ -15,13 +15,14 @@
class="opt exlcude-disabled"
type="checkbox"
:checked="present"
@input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
@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"

View file

@ -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) {

View file

@ -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 {

View file

@ -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>

View file

@ -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
}
}

View file

@ -3,6 +3,7 @@
<label for="interface-language-switcher">
{{ $t('settings.interfaceLanguage') }}
</label>
{{ ' ' }}
<Select
id="interface-language-switcher"
v-model="language"

View file

@ -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)

View file

@ -29,7 +29,7 @@ const MobilePostStatusButton = {
}
window.addEventListener('resize', this.handleOSK)
},
destroyed () {
unmounted () {
if (this.autohideFloatingPostButton) {
this.deactivateFloatingPostButtonAutohide()
}

View file

@ -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;
.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);
transition: 0.35s transform;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
&.hidden {
transform: translateY(150%);

View file

@ -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>

View file

@ -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>

View file

@ -14,7 +14,7 @@
:checked="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>

View file

@ -21,7 +21,7 @@ export default {
}
this.$store.dispatch('trackPoll', this.pollId)
},
destroyed () {
unmounted () {
this.$store.dispatch('untrackPoll', this.pollId)
},
computed: {

View file

@ -71,13 +71,13 @@
{{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp;
</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>

View file

@ -72,6 +72,7 @@
:max="maxExpirationInCurrentUnit"
@change="expiryAmountChange"
>
{{ ' ' }}
<Select
v-model="expiryUnit"
unstyled="true"

View file

@ -178,7 +178,7 @@ const Popover = {
created () {
document.addEventListener('click', this.onClickOutside)
},
destroyed () {
unmounted () {
document.removeEventListener('click', this.onClickOutside)
this.hidePopover()
}

View file

@ -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"

View file

@ -9,7 +9,7 @@ const PublicAndExternalTimeline = {
created () {
this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
},
destroyed () {
unmounted () {
this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal')
}
}

View file

@ -9,7 +9,7 @@ const PublicTimeline = {
created () {
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
},
destroyed () {
unmounted () {
this.$store.dispatch('stopFetchingTimeline', 'public')
}

View file

@ -15,7 +15,7 @@
class="opt"
type="checkbox"
:checked="present"
@input="$emit('input', !present ? fallback : undefined)"
@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>

View file

@ -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: '',

View file

@ -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) => {

View file

@ -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"

View file

@ -8,12 +8,9 @@ library.add(
)
export default {
model: {
prop: 'value',
event: 'change'
},
emits: ['update:modelValue'],
props: [
'value',
'modelValue',
'disabled',
'unstyled',
'kind'

View file

@ -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 {

View file

@ -14,6 +14,7 @@
>
<slot />
</span>
{{ ' ' }}
<ModifiedIndicator :changed="isChanged" /><ServerSideIndicator :server-side="isServerSide" /> </Checkbox>
</label>
</template>

View file

@ -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"

View file

@ -16,6 +16,7 @@
:value="state"
@change="update"
>
{{ ' ' }}
<ModifiedIndicator :changed="isChanged" />
</span>
</template>

View file

@ -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 :checked="!!expertLevel" @change="expertLevel = Number($event)">
{{ $t("settings.expert_mode") }}
</Checkbox>
</div>

View file

@ -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: {

View file

@ -4,6 +4,7 @@
class="settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
:body-scroll-lock="bodyLock"
>
<div
:label="$t('settings.general')"

View file

@ -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'

View file

@ -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" />

View file

@ -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 () {
@ -557,7 +556,7 @@ export default {
.filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
.filter(_ => !v1OnlyNames.includes(_))
.forEach(key => {
set(this.$data, key, undefined)
this.$data[key] = undefined
})
},
@ -565,7 +564,7 @@ export default {
Object.keys(this.$data)
.filter(_ => _.endsWith('RadiusLocal'))
.forEach(key => {
set(this.$data, key, undefined)
this.$data[key] = undefined
})
},
@ -573,7 +572,7 @@ export default {
Object.keys(this.$data)
.filter(_ => _.endsWith('OpacityLocal'))
.forEach(key => {
set(this.$data, key, undefined)
this.$data[key] = undefined
})
},

View file

@ -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>

View file

@ -28,4 +28,4 @@
</div>
</div>
</template>
<script src="./version_tab.js">
<script src="./version_tab.js" />

View file

@ -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>

View file

@ -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)
}
}
}

View file

@ -1,6 +1,7 @@
<template>
<div
v-if="!hideStatus"
ref="root"
class="Status"
:class="[{ '-focused': isFocused }, { '-conversation': inlineExpanded }]"
>
@ -120,9 +121,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 +132,7 @@
:better-shadow="betterShadow"
:user="status.user"
/>
</router-link>
</a>
</div>
<div class="right-side">
<UserCard
@ -191,7 +192,7 @@
<span
v-if="status.visibility"
class="visibility-icon"
:title="status.visibility | capitalize"
:title="visibilityLocalized"
>
<FAIcon
fixed-width
@ -274,6 +275,7 @@
icon="reply"
flip="horizontal"
/>
{{ ' ' }}
<span
class="reply-to-text"
>

View file

@ -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 () {

View file

@ -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: {

View file

@ -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,33 +34,35 @@ 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
}
},
settingsModalVisible () {
return this.settingsModalState === 'visible'
},
...mapState({
settingsModalState: state => state.interface.settingsModalState
})
},
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: {
@ -67,9 +72,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) {
@ -77,27 +89,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>
)
@ -105,25 +118,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)
@ -134,7 +148,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}
@ -147,10 +161,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>
)
}
})
}

View file

@ -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;

View file

@ -18,7 +18,7 @@ const TagTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'tag', tag: this.tag })
}
},
destroyed () {
unmounted () {
this.$store.dispatch('stopFetchingTimeline', 'tag')
}
}

View file

@ -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>

View file

@ -31,7 +31,7 @@ export default {
created () {
this.refreshRelativeTimeObject()
},
destroyed () {
unmounted () {
clearTimeout(this.interval)
},
methods: {

View file

@ -104,7 +104,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)

View file

@ -141,6 +141,7 @@
class="userHighlightCl"
type="color"
>
{{ ' ' }}
<Select
:id="'userHighlightSel'+user.id"
v-model="userHighlightType"

View file

@ -1,3 +1,5 @@
import { defineAsyncComponent } from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
@ -11,8 +13,8 @@ const UserListPopover = {
'users'
],
components: {
Popover: () => import('../popover/popover.vue'),
UserAvatar: () => import('../user_avatar/user_avatar.vue')
Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue'))
},
computed: {
usersCapped () {

View file

@ -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

View file

@ -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: {

View file

@ -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'
@ -23,7 +24,7 @@ const withLoadMore = ({
const originalProps = Object.keys(getComponentProps(WrappedComponent))
const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
return Vue.component('withLoadMore', {
return {
props,
data () {
return {
@ -39,7 +40,7 @@ const withLoadMore = ({
this.fetchEntries()
}
},
destroyed () {
unmounted () {
window.removeEventListener('scroll', this.scrollLoad)
destroy && destroy(this.$props, this.$store)
},
@ -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

View file

@ -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

View file

@ -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",

View file

@ -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') {

View file

@ -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,

View file

@ -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]
}
}
},

View file

@ -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]
}
}
},

View file

@ -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) {

View file

@ -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) {

View file

@ -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: {

View file

@ -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
}
}
},

View file

@ -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 }) {

View file

@ -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

View file

@ -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,

View file

@ -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',

View file

@ -1,3 +1,10 @@
import { configureCompat } from 'vue'
// disable compat for certain features
configureCompat({
COMPONENT_V_MODEL: false,
INSTANCE_SET: false,
RENDER_FUNCTION: false
})
// require all test files (files that ends with .spec.js)
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

View file

@ -1,45 +1,40 @@
import Vuex from 'vuex'
import routes from 'src/boot/routes'
import { createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
import { createRouter, createMemoryHistory } from 'vue-router'
import { createStore } from 'vuex'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueRouter)
const store = new Vuex.Store({
const store = createStore({
state: {
instance: {}
}
})
describe('routes', () => {
const router = new VueRouter({
mode: 'abstract',
const router = createRouter({
history: createMemoryHistory(),
routes: routes(store)
})
it('root path', () => {
router.push('/main/all')
it('root path', async () => {
await router.push('/main/all')
const matchedComponents = router.getMatchedComponents()
const matchedComponents = router.currentRoute.value.matched
expect(matchedComponents[0].components.hasOwnProperty('Timeline')).to.eql(true)
expect(matchedComponents[0].components.default.components.hasOwnProperty('Timeline')).to.eql(true)
})
it('user\'s profile', () => {
router.push('/fake-user-name')
it('user\'s profile', async () => {
await router.push('/fake-user-name')
const matchedComponents = router.getMatchedComponents()
const matchedComponents = router.currentRoute.value.matched
expect(matchedComponents[0].components.hasOwnProperty('UserCard')).to.eql(true)
expect(matchedComponents[0].components.default.components.hasOwnProperty('UserCard')).to.eql(true)
})
it('user\'s profile at /users', () => {
router.push('/users/fake-user-name')
it('user\'s profile at /users', async () => {
await router.push('/users/fake-user-name')
const matchedComponents = router.getMatchedComponents()
const matchedComponents = router.currentRoute.value.matched
expect(matchedComponents[0].components.hasOwnProperty('UserCard')).to.eql(true)
expect(matchedComponents[0].components.default.components.hasOwnProperty('UserCard')).to.eql(true)
})
})

View file

@ -1,108 +1,116 @@
import { shallowMount, createLocalVue } from '@vue/test-utils'
import { h } from 'vue'
import { shallowMount } from '@vue/test-utils'
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
import vClickOutside from 'click-outside-vue3'
const generateInput = (value, padEmoji = true) => {
const localVue = createLocalVue()
localVue.directive('click-outside', () => {})
const wrapper = shallowMount(EmojiInput, {
propsData: {
suggest: () => [],
enableEmojiPicker: true,
value
},
mocks: {
$store: {
getters: {
mergedConfig: {
padEmoji
global: {
renderStubDefaultSlot: true,
mocks: {
$store: {
getters: {
mergedConfig: {
padEmoji
}
}
}
},
stubs: {
FAIcon: true
},
directives: {
'click-outside': vClickOutside
}
},
slots: {
default: '<input />'
props: {
suggest: () => [],
enableEmojiPicker: true,
modelValue: value
},
localVue
slots: {
'default': () => h('input', '')
}
})
return [wrapper, localVue]
return wrapper
}
describe('EmojiInput', () => {
describe('insertion mechanism', () => {
it('inserts string at the end with trailing space', () => {
const initialString = 'Testing'
const [wrapper] = generateInput(initialString)
const wrapper = generateInput(initialString)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: initialString.length })
wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
const inputEvents = wrapper.emitted().input
const inputEvents = wrapper.emitted()['update:modelValue']
expect(inputEvents[inputEvents.length - 1][0]).to.eql('Testing (test) ')
})
it('inserts string at the end with trailing space (source has a trailing space)', () => {
const initialString = 'Testing '
const [wrapper] = generateInput(initialString)
const wrapper = generateInput(initialString)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: initialString.length })
wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
const inputEvents = wrapper.emitted().input
const inputEvents = wrapper.emitted()['update:modelValue']
expect(inputEvents[inputEvents.length - 1][0]).to.eql('Testing (test) ')
})
it('inserts string at the begginning without leading space', () => {
const initialString = 'Testing'
const [wrapper] = generateInput(initialString)
const wrapper = generateInput(initialString)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: 0 })
wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
const inputEvents = wrapper.emitted().input
const inputEvents = wrapper.emitted()['update:modelValue']
expect(inputEvents[inputEvents.length - 1][0]).to.eql('(test) Testing')
})
it('inserts string between words without creating extra spaces', () => {
const initialString = 'Spurdo Sparde'
const [wrapper] = generateInput(initialString)
const wrapper = generateInput(initialString)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: 6 })
wrapper.vm.insert({ insertion: ':ebin:', keepOpen: false })
const inputEvents = wrapper.emitted().input
const inputEvents = wrapper.emitted()['update:modelValue']
expect(inputEvents[inputEvents.length - 1][0]).to.eql('Spurdo :ebin: Sparde')
})
it('inserts string between words without creating extra spaces (other caret)', () => {
const initialString = 'Spurdo Sparde'
const [wrapper] = generateInput(initialString)
const wrapper = generateInput(initialString)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: 7 })
wrapper.vm.insert({ insertion: ':ebin:', keepOpen: false })
const inputEvents = wrapper.emitted().input
const inputEvents = wrapper.emitted()['update:modelValue']
expect(inputEvents[inputEvents.length - 1][0]).to.eql('Spurdo :ebin: Sparde')
})
it('inserts string without any padding if padEmoji setting is set to false', () => {
const initialString = 'Eat some spam!'
const [wrapper] = generateInput(initialString, false)
const wrapper = generateInput(initialString, false)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: initialString.length, keepOpen: false })
wrapper.vm.insert({ insertion: ':spam:' })
const inputEvents = wrapper.emitted().input
const inputEvents = wrapper.emitted()['update:modelValue']
expect(inputEvents[inputEvents.length - 1][0]).to.eql('Eat some spam!:spam:')
})
it('correctly sets caret after insertion at beginning', (done) => {
const initialString = '1234'
const [wrapper, vue] = generateInput(initialString)
const wrapper = generateInput(initialString)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: 0 })
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
vue.nextTick(() => {
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.caret).to.eql(5)
done()
})
@ -110,12 +118,12 @@ describe('EmojiInput', () => {
it('correctly sets caret after insertion at end', (done) => {
const initialString = '1234'
const [wrapper, vue] = generateInput(initialString)
const wrapper = generateInput(initialString)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: initialString.length })
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
vue.nextTick(() => {
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.caret).to.eql(10)
done()
})
@ -123,12 +131,12 @@ describe('EmojiInput', () => {
it('correctly sets caret after insertion if padEmoji setting is set to false', (done) => {
const initialString = '1234'
const [wrapper, vue] = generateInput(initialString, false)
const wrapper = generateInput(initialString, false)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: initialString.length })
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
vue.nextTick(() => {
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.caret).to.eql(8)
done()
})

View file

@ -1,8 +1,15 @@
import { mount, shallowMount, createLocalVue } from '@vue/test-utils'
import { mount, shallowMount } from '@vue/test-utils'
import RichContent from 'src/components/rich_content/rich_content.jsx'
const localVue = createLocalVue()
const attentions = []
const global = {
mocks: {
'$store': null
},
stubs: {
FAIcon: true
}
}
const makeMention = (who) => {
attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` })
@ -11,17 +18,17 @@ const makeMention = (who) => {
const p = (...data) => `<p>${data.join('')}</p>`
const compwrap = (...data) => `<span class="RichContent">${data.join('')}</span>`
const mentionsLine = (times) => [
'<mentionsline-stub mentions="',
'<mentions-line-stub mentions="',
new Array(times).fill('[object Object]').join(','),
'"></mentionsline-stub>'
'"></mentions-line-stub>'
].join('')
describe('RichContent', () => {
it('renders simple post without exploding', () => {
const html = p('Hello world!')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
@ -30,7 +37,7 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(html))
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(html))
})
it('unescapes everything as needed', () => {
@ -43,8 +50,8 @@ describe('RichContent', () => {
'Testing \'em all'
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
@ -53,7 +60,7 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected))
})
it('replaces mention with mentionsline', () => {
@ -62,8 +69,8 @@ describe('RichContent', () => {
' how are you doing today?'
)
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
@ -72,7 +79,7 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(p(
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(p(
mentionsLine(1),
' how are you doing today?'
)))
@ -93,17 +100,17 @@ describe('RichContent', () => {
),
// TODO fix this extra line somehow?
p(
'<mentionsline-stub mentions="',
'<mentions-line-stub mentions="',
'[object Object],',
'[object Object],',
'[object Object]',
'"></mentionsline-stub>'
'"></mentions-line-stub>'
)
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
@ -112,7 +119,7 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected))
})
it('Does not touch links if link handling is disabled', () => {
@ -130,8 +137,8 @@ describe('RichContent', () => {
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: false,
greentext: true,
@ -154,8 +161,8 @@ describe('RichContent', () => {
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: false,
greentext: true,
@ -174,8 +181,8 @@ describe('RichContent', () => {
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: false,
greentext: false,
@ -191,12 +198,12 @@ describe('RichContent', () => {
const html = p('Ebin :DDDD :spurdo:')
const expected = p(
'Ebin :DDDD ',
'<anonymous-stub alt=":spurdo:" src="about:blank" title=":spurdo:" class="emoji img"></anonymous-stub>'
'<anonymous-stub src="about:blank" alt=":spurdo:" class="emoji img" title=":spurdo:"></anonymous-stub>'
)
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: false,
greentext: false,
@ -205,15 +212,15 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected))
})
it('Doesn\'t add nonexistent emoji to post', () => {
const html = p('Lol :lol:')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: false,
greentext: false,
@ -222,7 +229,7 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(html))
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(html))
})
it('Greentext + last mentions', () => {
@ -240,8 +247,8 @@ describe('RichContent', () => {
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
@ -272,8 +279,8 @@ describe('RichContent', () => {
].join('<br>')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
@ -282,7 +289,7 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected))
})
it('buggy example/hashtags', () => {
@ -300,16 +307,18 @@ describe('RichContent', () => {
'<p>',
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg" target="_blank">',
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
' <hashtaglink-stub url="https://shitposter.club/tag/nou" content="#nou" tag="nou">',
'</hashtaglink-stub>',
' <hashtaglink-stub url="https://shitposter.club/tag/screencap" content="#screencap" tag="screencap">',
'</hashtaglink-stub>',
' <hashtag-link-stub url="https://shitposter.club/tag/nou" content="#nou" tag="nou">',
'#nou',
'</hashtag-link-stub>',
' <hashtag-link-stub url="https://shitposter.club/tag/screencap" content="#screencap" tag="screencap">',
'#screencap',
'</hashtag-link-stub>',
' </p>'
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
@ -318,7 +327,7 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected))
})
it('rich contents of a mention are handled properly', () => {
@ -342,7 +351,8 @@ describe('RichContent', () => {
p(
'<span class="MentionsLine">',
'<span class="MentionLink mention-link">',
'<a href="lol" target="_blank" class="original">',
'<!-- eslint-disable vue/no-v-html -->',
'<a href="lol" class="original" target="_blank">',
'<span>',
'https://</span>',
'<span>',
@ -350,9 +360,10 @@ describe('RichContent', () => {
'<span>',
'</span>',
'</a>',
'<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'<!-- eslint-enable vue/no-v-html -->',
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'</span>',
'<!---->', // v-if placeholder, mentionsline's extra mentions and stuff
'<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff
'</span>'
),
p(
@ -361,8 +372,8 @@ describe('RichContent', () => {
].join('')
const wrapper = mount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
@ -371,76 +382,73 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected))
})
it('rich contents of nested mentions are handled properly', () => {
attentions.push({ statusnet_profile_url: 'lol' })
const html = [
p(
'<span class="poast-style">',
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
' ',
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
'</span>'
),
p(
'Testing'
)
'<span class="poast-style">',
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
' ',
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
' ',
'</span>',
'Testing'
].join('')
const expected = [
p(
'<span class="poast-style">',
'<span class="MentionsLine">',
'<span class="MentionLink mention-link">',
'<a href="lol" target="_blank" class="original">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
'<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'</span>',
'<span class="MentionLink mention-link">',
'<a href="lol" target="_blank" class="original">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
'<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'</span>',
'<!---->', // v-if placeholder, mentionsline's extra mentions and stuff
'</span>',
'</span>'
),
'<span class="poast-style">',
'<span class="MentionsLine">',
'<span class="MentionLink mention-link">',
'<!-- eslint-disable vue/no-v-html -->',
'<a href="lol" class="original" target="_blank">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
'<!-- eslint-enable vue/no-v-html -->',
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'</span>',
'<span class="MentionLink mention-link">',
'<!-- eslint-disable vue/no-v-html -->',
'<a href="lol" class="original" target="_blank">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
'<!-- eslint-enable vue/no-v-html -->',
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'</span>',
'<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff
'</span>',
' ',
p(
'Testing'
)
'</span>',
'Testing'
].join('')
const wrapper = mount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
@ -449,7 +457,7 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected))
})
it('rich contents of a link are handled properly', () => {
@ -483,8 +491,8 @@ describe('RichContent', () => {
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
@ -493,7 +501,7 @@ describe('RichContent', () => {
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected))
})
it.skip('[INFORMATIVE] Performance testing, 10 000 simple posts', () => {
@ -530,8 +538,8 @@ describe('RichContent', () => {
const t0 = performance.now()
const wrapper = mount(TestComponent, {
localVue,
propsData: {
global,
props: {
attentions,
handleLinks,
vhtml

View file

@ -1,12 +1,9 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import { mount } from '@vue/test-utils'
import { createStore } from 'vuex'
import UserProfile from 'src/components/user_profile/user_profile.vue'
import backendInteractorService from 'src/services/backend_interactor_service/backend_interactor_service.js'
import { getters } from 'src/modules/users.js'
const localVue = createLocalVue()
localVue.use(Vuex)
const mutations = {
clearTimeline: () => {}
}
@ -42,7 +39,7 @@ const extUser = {
screen_name_ui: 'testUser@test.instance'
}
const externalProfileStore = new Vuex.Store({
const externalProfileStore = createStore({
mutations,
actions,
getters: testGetters,
@ -104,7 +101,7 @@ const externalProfileStore = new Vuex.Store({
}
})
const localProfileStore = new Vuex.Store({
const localProfileStore = createStore({
mutations,
actions,
getters: testGetters,
@ -173,17 +170,19 @@ const localProfileStore = new Vuex.Store({
}
})
describe('UserProfile', () => {
// https://github.com/vuejs/test-utils/issues/1382
describe.skip('UserProfile', () => {
it('renders external profile', () => {
const wrapper = mount(UserProfile, {
localVue,
store: externalProfileStore,
mocks: {
$route: {
params: { id: 100 },
name: 'external-user-profile'
},
$t: (msg) => msg
global: {
plugins: [ externalProfileStore ],
mocks: {
$route: {
params: { id: 100 },
name: 'external-user-profile'
},
$t: (msg) => msg
}
}
})
@ -192,14 +191,15 @@ describe('UserProfile', () => {
it('renders local profile', () => {
const wrapper = mount(UserProfile, {
localVue,
store: localProfileStore,
mocks: {
$route: {
params: { name: 'testUser' },
name: 'user-profile'
},
$t: (msg) => msg
global: {
plugins: [ localProfileStore ],
mocks: {
$route: {
params: { name: 'testUser' },
name: 'user-profile'
},
$t: (msg) => msg
}
}
})

1302
yarn.lock

File diff suppressed because it is too large Load diff