Compare commits

...

17 commits

378 changed files with 9439 additions and 6639 deletions

View file

@ -5,14 +5,9 @@ module.exports = {
sourceType: 'module' sourceType: 'module'
}, },
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [ extends: ['plugin:vue/recommended', 'plugin:prettier/recommended'],
'plugin:vue/recommended'
],
// required to lint *.vue files // required to lint *.vue files
plugins: [ plugins: ['vue', 'import'],
'vue',
'import'
],
// add your custom rules here // add your custom rules here
rules: { rules: {
// allow paren-less arrow functions // allow paren-less arrow functions

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"trailingComma": "none",
"singleQuote": true,
"semi": false,
"singleAttributePerLine": true
}

View file

@ -56,7 +56,7 @@
"@vue/babel-plugin-jsx": "1.1.1", "@vue/babel-plugin-jsx": "1.1.1",
"@vue/compiler-sfc": "^3.1.0", "@vue/compiler-sfc": "^3.1.0",
"@vue/test-utils": "^2.0.2", "@vue/test-utils": "^2.0.2",
"autoprefixer": "6.7.7", "autoprefixer": "^10.4.13",
"babel-loader": "^9.1.0", "babel-loader": "^9.1.0",
"babel-plugin-lodash": "3.3.4", "babel-plugin-lodash": "3.3.4",
"chai": "^4.3.7", "chai": "^4.3.7",
@ -67,11 +67,13 @@
"css-loader": "^6.7.2", "css-loader": "^6.7.2",
"custom-event-polyfill": "^1.0.7", "custom-event-polyfill": "^1.0.7",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard": "^17.0.0", "eslint-config-standard": "^17.0.0",
"eslint-friendly-formatter": "^4.0.1", "eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^4.0.2", "eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-standard": "^5.0.0", "eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^9.7.0", "eslint-plugin-vue": "^9.7.0",
@ -101,9 +103,11 @@
"nightwatch": "0.9.21", "nightwatch": "0.9.21",
"opn": "4.0.2", "opn": "4.0.2",
"ora": "0.4.1", "ora": "0.4.1",
"postcss": "^8.4.19",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-loader": "3.0.0", "postcss-loader": "^7.0.2",
"postcss-sass": "^0.5.0", "postcss-sass": "^0.5.0",
"prettier": "2.8.1",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"sass": "^1.56.0", "sass": "^1.56.0",
"sass-loader": "^13.2.0", "sass-loader": "^13.2.0",

View file

@ -1,5 +1,3 @@
module.exports = { module.exports = {
plugins: [ plugins: [require('autoprefixer')]
require('autoprefixer')
]
} }

View file

@ -24,7 +24,9 @@ export default {
components: { components: {
UserPanel, UserPanel,
NavPanel, NavPanel,
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')), Notifications: defineAsyncComponent(() =>
import('./components/notifications/notifications.vue')
),
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
@ -47,7 +49,10 @@ export default {
created() { created() {
// Load the locale from the storage // Load the locale from the storage
const val = this.$store.getters.mergedConfig.interfaceLanguage const val = this.$store.getters.mergedConfig.interfaceLanguage
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.$store.dispatch('setOption', {
name: 'interfaceLanguage',
value: val
})
window.addEventListener('resize', this.updateMobileState) window.addEventListener('resize', this.updateMobileState)
}, },
unmounted() { unmounted() {
@ -64,14 +69,20 @@ export default {
'-' + this.layoutType '-' + this.layoutType
] ]
}, },
currentUser () { return this.$store.state.users.currentUser }, currentUser() {
userBackground () { return this.currentUser.background_image }, return this.$store.state.users.currentUser
},
userBackground() {
return this.currentUser.background_image
},
instanceBackground() { instanceBackground() {
return this.mergedConfig.hideInstanceWallpaper return this.mergedConfig.hideInstanceWallpaper
? null ? null
: this.$store.state.instance.background : this.$store.state.instance.background
}, },
background () { return this.userBackground || this.instanceBackground }, background() {
return this.userBackground || this.instanceBackground
},
bgStyle() { bgStyle() {
if (this.background) { if (this.background) {
return { return {
@ -79,29 +90,51 @@ export default {
} }
} }
}, },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, suggestionsEnabled() {
return this.$store.state.instance.suggestionsEnabled
},
showInstanceSpecificPanel() { showInstanceSpecificPanel() {
return this.$store.state.instance.showInstanceSpecificPanel && return (
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP && !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
)
}, },
newPostButtonShown() { newPostButtonShown() {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' return (
this.$store.getters.mergedConfig.alwaysShowNewPostButton ||
this.layoutType === 'mobile'
)
},
showFeaturesPanel() {
return this.$store.state.instance.showFeaturesPanel
},
editingAvailable() {
return this.$store.state.instance.editingAvailable
},
layoutType() {
return this.$store.state.interface.layoutType
},
privateMode() {
return this.$store.state.instance.private
}, },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
editingAvailable () { return this.$store.state.instance.editingAvailable },
layoutType () { return this.$store.state.interface.layoutType },
privateMode () { return this.$store.state.instance.private },
reverseLayout() { reverseLayout() {
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig const { thirdColumnMode, sidebarRight: reverseSetting } =
this.$store.getters.mergedConfig
if (this.layoutType !== 'wide') { if (this.layoutType !== 'wide') {
return reverseSetting return reverseSetting
} else { } else {
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting return thirdColumnMode === 'notifications'
? reverseSetting
: !reverseSetting
} }
}, },
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, noSticky() {
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars }, return this.$store.getters.mergedConfig.disableStickyHeaders
},
showScrollbars() {
return this.$store.getters.mergedConfig.showScrollbars
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
methods: { methods: {

View file

@ -12,8 +12,8 @@ html {
} }
body { body {
font-family: sans-serif; font-family: $system-sans-serif;
font-family: var(--interfaceFont, sans-serif); font-family: var(--interfaceFont, $system-sans-serif);
margin: 0; margin: 0;
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
@ -22,84 +22,13 @@ body {
overscroll-behavior-y: none; overscroll-behavior-y: none;
overflow-x: clip; overflow-x: clip;
overflow-y: scroll; overflow-y: scroll;
background: var(--bg);
&.hidden { &.hidden {
display: none; display: none;
} }
} }
// ## Custom scrollbars
// Only show custom scrollbars on devices which
// have a cursor/pointer to operate them
@media (any-pointer: fine) {
* {
scrollbar-color: var(--btn) transparent;
&::-webkit-scrollbar {
background: transparent;
}
&::-webkit-scrollbar-button,
&::-webkit-scrollbar-thumb {
background-color: var(--btn);
box-shadow: var(--buttonShadow);
border-radius: var(--btnRadius);
}
// horizontal/vertical/increment/decrement are webkit-specific stuff
// that indicates whether we're affecting vertical scrollbar, increase button etc
// stylelint-disable selector-pseudo-class-no-unknown
&::-webkit-scrollbar-button {
--___bgPadding: 2px;
color: var(--btnText);
background-repeat: no-repeat, no-repeat;
&:horizontal {
background-size: 50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding));
&:increment {
background-image:
linear-gradient(45deg, var(--btnText) 50%, transparent 51%),
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%);
background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding);
}
&:decrement {
background-image:
linear-gradient(45deg, transparent 50%, var(--btnText) 51%),
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%);
background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding);
}
}
&:vertical {
background-size: calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%;
&:increment {
background-image:
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%),
linear-gradient(45deg, transparent 50%, var(--btnText) 51%);
background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%;
}
&:decrement {
background-image:
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%),
linear-gradient(45deg, var(--btnText) 50%, transparent 51%);
background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%;
}
}
}
// stylelint-enable selector-pseudo-class-no-unknown
}
// Body should have background to scrollbar otherwise it will use white (body color?)
html {
scrollbar-color: var(--selectedMenu) var(--wallpaper);
background: var(--wallpaper);
}
}
a { a {
text-decoration: none; text-decoration: none;
color: $fallback--link; color: $fallback--link;
@ -110,7 +39,7 @@ h4 {
margin: 0; margin: 0;
} }
i[class*=icon-], i[class*='icon-'],
.svg-inline--fa { .svg-inline--fa {
color: $fallback--icon; color: $fallback--icon;
color: var(--icon, $fallback--icon); color: var(--icon, $fallback--icon);
@ -126,8 +55,9 @@ nav {
box-shadow: 0 0 4px rgba(0, 0, 0, 0.6); box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--topBarShadow); box-shadow: var(--topBarShadow);
box-sizing: border-box; box-sizing: border-box;
height: var(--navbar-height); height: calc(var(--navbar-height) + 1px);
position: fixed; position: fixed;
backdrop-filter: blur(12px) saturate(1.2);
} }
#sidebar { #sidebar {
@ -182,7 +112,7 @@ nav {
position: relative; position: relative;
display: grid; display: grid;
grid-template-columns: var(--miniColumn) var(--maxiColumn); grid-template-columns: var(--miniColumn) var(--maxiColumn);
grid-template-areas: "sidebar content"; grid-template-areas: 'sidebar content';
grid-template-rows: 1fr; grid-template-rows: 1fr;
box-sizing: border-box; box-sizing: border-box;
margin: 0 auto; margin: 0 auto;
@ -228,7 +158,9 @@ nav {
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
margin-left: calc(var(--___paddingIncrease) * -1); margin-left: calc(var(--___paddingIncrease) * -1);
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2); padding-left: calc(
var(--___paddingIncrease) + var(--___columnMargin) / 2
);
// On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode // On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode
// might implement old style of hiding scrollbars later if there's demand // might implement old style of hiding scrollbars later if there's demand
@ -236,7 +168,9 @@ nav {
&:not(.-show-scrollbar) { &:not(.-show-scrollbar) {
scrollbar-width: none; scrollbar-width: none;
margin-right: calc(var(--___paddingIncrease) * -1); margin-right: calc(var(--___paddingIncrease) * -1);
padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2); padding-right: calc(
var(--___paddingIncrease) + var(--___columnMargin) / 2
);
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: block; display: block;
@ -276,21 +210,21 @@ nav {
&.-reverse:not(.-wide):not(.-mobile) { &.-reverse:not(.-wide):not(.-mobile) {
grid-template-columns: var(--maxiColumn) var(--miniColumn); grid-template-columns: var(--maxiColumn) var(--miniColumn);
grid-template-areas: "content sidebar"; grid-template-areas: 'content sidebar';
} }
&.-wide { &.-wide {
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn); grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
grid-template-areas: "sidebar content notifs"; grid-template-areas: 'sidebar content notifs';
&.-reverse { &.-reverse {
grid-template-areas: "notifs content sidebar"; grid-template-areas: 'notifs content sidebar';
} }
} }
&.-mobile { &.-mobile {
grid-template-columns: 100vw; grid-template-columns: 100vw;
grid-template-areas: "content"; grid-template-areas: 'content';
padding: 0; padding: 0;
.column { .column {
@ -347,7 +281,7 @@ nav {
background: transparent; background: transparent;
} }
i[class*=icon-], i[class*='icon-'],
.svg-inline--fa { .svg-inline--fa {
color: $fallback--text; color: $fallback--text;
color: var(--btnText, $fallback--text); color: var(--btnText, $fallback--text);
@ -363,7 +297,9 @@ nav {
} }
&:active { &:active {
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset; box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow); box-shadow: var(--buttonPressedShadow);
color: $fallback--text; color: $fallback--text;
color: var(--btnPressedText, $fallback--text); color: var(--btnPressedText, $fallback--text);
@ -396,7 +332,9 @@ nav {
color: var(--btnToggledText, $fallback--text); color: var(--btnToggledText, $fallback--text);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--btnToggled, $fallback--fg); background-color: var(--btnToggled, $fallback--fg);
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset; box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow); box-shadow: var(--buttonPressedShadow);
svg, svg,
@ -461,7 +399,8 @@ textarea,
border: none; border: none;
border-radius: $fallback--inputRadius; border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius); border-radius: var(--inputRadius, $fallback--inputRadius);
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset; box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
box-shadow: var(--inputShadow); box-shadow: var(--inputShadow);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--input, $fallback--fg); background-color: var(--input, $fallback--fg);
@ -479,13 +418,13 @@ textarea,
padding: 0 var(--_padding); padding: 0 var(--_padding);
&:disabled, &:disabled,
&[disabled=disabled], &[disabled='disabled'],
&.disabled { &.disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5; opacity: 0.5;
} }
&[type=range] { &[type='range'] {
background: none; background: none;
border: none; border: none;
margin: 0; margin: 0;
@ -493,7 +432,7 @@ textarea,
flex: 1; flex: 1;
} }
&[type=radio] { &[type='radio'] {
display: none; display: none;
&:checked + label::before { &:checked + label::before {
@ -533,7 +472,7 @@ textarea,
} }
} }
&[type=checkbox] { &[type='checkbox'] {
display: none; display: none;
&:checked + label::before { &:checked + label::before {
@ -594,8 +533,8 @@ option {
.hide-number-spinner { .hide-number-spinner {
-moz-appearance: textfield; -moz-appearance: textfield;
&[type=number]::-webkit-inner-spin-button, &[type='number']::-webkit-inner-spin-button,
&[type=number]::-webkit-outer-spin-button { &[type='number']::-webkit-outer-spin-button {
opacity: 0; opacity: 0;
display: none; display: none;
} }

View file

@ -43,7 +43,7 @@
:to="{ name: 'login' }" :to="{ name: 'login' }"
class="panel-body" class="panel-body"
> >
{{ $t("login.hint") }} {{ $t('login.hint') }}
</router-link> </router-link>
</div> </div>
<router-view /> <router-view />

View file

@ -4,7 +4,7 @@ $darkened-background: whitesmoke;
$fallback--bg: #121a24; $fallback--bg: #121a24;
$fallback--fg: #182230; $fallback--fg: #182230;
$fallback--faint: rgba(185, 185, 186, .5); $fallback--faint: rgba(185, 185, 186, 0.5);
$fallback--text: #b9b9ba; $fallback--text: #b9b9ba;
$fallback--link: #d8a070; $fallback--link: #d8a070;
$fallback--icon: #666; $fallback--icon: #666;
@ -16,8 +16,8 @@ $fallback--cBlue: #0095ff;
$fallback--cGreen: #0fa00f; $fallback--cGreen: #0fa00f;
$fallback--cOrange: orange; $fallback--cOrange: orange;
$fallback--alertError: rgba(211,16,20,.5); $fallback--alertError: rgba(211, 16, 20, 0.5);
$fallback--alertWarning: rgba(111,111,20,.5); $fallback--alertWarning: rgba(111, 111, 20, 0.5);
$fallback--panelRadius: 10px; $fallback--panelRadius: 10px;
$fallback--checkboxRadius: 2px; $fallback--checkboxRadius: 2px;
@ -28,6 +28,14 @@ $fallback--avatarRadius: 4px;
$fallback--avatarAltRadius: 10px; $fallback--avatarAltRadius: 10px;
$fallback--attachmentRadius: 10px; $fallback--attachmentRadius: 10px;
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; $fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1),
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
$status-margin: 0.75em; $status-margin: 0.75em;
$system-sans-serif: -apple-system, BlinkMacSystemFont, avenir next, avenir,
segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial,
sans-serif;
$system-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,
monospace;

View file

@ -3,13 +3,19 @@ import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3' import vClickOutside from 'click-outside-vue3'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' import {
FontAwesomeIcon,
FontAwesomeLayers
} from '@fortawesome/vue-fontawesome'
import App from '../App.vue' import App from '../App.vue'
import routes from './routes' import routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock' import VBodyScrollLock from 'src/directives/body_scroll_lock'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import {
windowWidth,
windowHeight
} from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
@ -23,7 +29,9 @@ const parsedInitialResults = () => {
return null return null
} }
if (!staticInitialResults) { if (!staticInitialResults) {
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent) staticInitialResults = JSON.parse(
document.getElementById('initial-results').textContent
)
} }
return staticInitialResults return staticInitialResults
} }
@ -71,18 +79,30 @@ const getInstanceConfig = async ({ store }) => {
const textlimit = data.max_toot_chars const textlimit = data.max_toot_chars
const vapidPublicKey = data.pleroma.vapid_public_key const vapidPublicKey = data.pleroma.vapid_public_key
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) name: 'textlimit',
value: textlimit
})
store.dispatch('setInstanceOption', {
name: 'accountApprovalRequired',
value: data.approval_required
})
// don't override cookie if set // don't override cookie if set
if (!Cookies.get('userLanguage')) { if (!Cookies.get('userLanguage')) {
store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) }) store.dispatch('setOption', {
name: 'interfaceLanguage',
value: resolveLanguage(data.languages)
})
} }
if (vapidPublicKey) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) store.dispatch('setInstanceOption', {
name: 'vapidPublicKey',
value: vapidPublicKey
})
} }
} else { } else {
throw (res) throw res
} }
} catch (error) { } catch (error) {
console.error('Could not load instance config, potentially fatal') console.error('Could not load instance config, potentially fatal')
@ -97,10 +117,12 @@ const getBackendProvidedConfig = async ({ store }) => {
const data = await res.json() const data = await res.json()
return data.pleroma_fe return data.pleroma_fe
} else { } else {
throw (res) throw res
} }
} catch (error) { } catch (error) {
console.error('Could not load backend-provided frontend config, potentially fatal') console.error(
'Could not load backend-provided frontend config, potentially fatal'
)
console.error(error) console.error(error)
} }
} }
@ -111,7 +133,7 @@ const getStaticConfig = async () => {
if (res.ok) { if (res.ok) {
return res.json() return res.json()
} else { } else {
throw (res) throw res
} }
} catch (error) { } catch (error) {
console.warn('Failed to load static/config.json, continuing without it.') console.warn('Failed to load static/config.json, continuing without it.')
@ -154,16 +176,12 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'logoMask', name: 'logoMask',
value: typeof config.logoMask === 'undefined' value: typeof config.logoMask === 'undefined' ? true : config.logoMask
? true
: config.logoMask
}) })
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'logoMargin', name: 'logoMargin',
value: typeof config.logoMargin === 'undefined' value: typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin
? 0
: config.logoMargin
}) })
copyInstanceOption('logoLeft') copyInstanceOption('logoLeft')
store.commit('authFlow/setInitialStrategy', config.loginMethod) store.commit('authFlow/setInitialStrategy', config.loginMethod)
@ -191,7 +209,7 @@ const getTOS = async ({ store }) => {
const html = await res.text() const html = await res.text()
store.dispatch('setInstanceOption', { name: 'tos', value: html }) store.dispatch('setInstanceOption', { name: 'tos', value: html })
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn("Can't load TOS") console.warn("Can't load TOS")
@ -204,9 +222,12 @@ const getInstancePanel = async ({ store }) => {
const res = await preloadFetch('/instance/panel.html') const res = await preloadFetch('/instance/panel.html')
if (res.ok) { if (res.ok) {
const html = await res.text() const html = await res.text()
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) store.dispatch('setInstanceOption', {
name: 'instanceSpecificPanelContent',
value: html
})
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn("Can't load instance panel") console.warn("Can't load instance panel")
@ -219,7 +240,8 @@ const getStickers = async ({ store }) => {
const res = await window.fetch('/static/stickers.json') const res = await window.fetch('/static/stickers.json')
if (res.ok) { if (res.ok) {
const values = await res.json() const values = await res.json()
const stickers = (await Promise.all( const stickers = (
await Promise.all(
Object.entries(values).map(async ([name, path]) => { Object.entries(values).map(async ([name, path]) => {
const resPack = await window.fetch(path + 'pack.json') const resPack = await window.fetch(path + 'pack.json')
var meta = {} var meta = {}
@ -232,12 +254,16 @@ const getStickers = async ({ store }) => {
meta meta
} }
}) })
)).sort((a, b) => { )
).sort((a, b) => {
return a.meta.title.localeCompare(b.meta.title) return a.meta.title.localeCompare(b.meta.title)
}) })
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers }) store.dispatch('setInstanceOption', {
name: 'stickers',
value: stickers
})
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn("Can't load stickers") console.warn("Can't load stickers")
@ -252,13 +278,19 @@ const getAppSecret = async ({ store }) => {
.then((app) => getClientToken({ ...app, instance: instance.server })) .then((app) => getClientToken({ ...app, instance: instance.server }))
.then((token) => { .then((token) => {
commit('setAppToken', token.access_token) commit('setAppToken', token.access_token)
commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) commit(
'setBackendInteractor',
backendInteractorService(store.getters.getToken())
)
}) })
} }
const resolveStaffAccounts = ({ store, accounts }) => { const resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map(uri => uri.split('/').pop()) const nicknames = accounts.map((uri) => uri.split('/').pop())
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames }) store.dispatch('setInstanceOption', {
name: 'staffAccounts',
value: nicknames
})
} }
const getNodeInfo = async ({ store }) => { const getNodeInfo = async ({ store }) => {
@ -268,65 +300,137 @@ const getNodeInfo = async ({ store }) => {
const data = await res.json() const data = await res.json()
const metadata = data.metadata const metadata = data.metadata
const features = metadata.features const features = metadata.features
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) name: 'name',
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) value: metadata.nodeName
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') }) name: 'registrationOpen',
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) value: data.openRegistrations
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) })
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') }) store.dispatch('setInstanceOption', {
name: 'mediaProxyAvailable',
value: features.includes('media_proxy')
})
store.dispatch('setInstanceOption', {
name: 'safeDM',
value: features.includes('safe_dm_mentions')
})
store.dispatch('setInstanceOption', {
name: 'pollsAvailable',
value: features.includes('polls')
})
store.dispatch('setInstanceOption', {
name: 'editingAvailable',
value: features.includes('editing')
})
store.dispatch('setInstanceOption', {
name: 'pollLimits',
value: metadata.pollLimits
})
store.dispatch('setInstanceOption', {
name: 'mailerEnabled',
value: metadata.mailerEnabled
})
store.dispatch('setInstanceOption', {
name: 'translationEnabled',
value: features.includes('akkoma:machine_translation')
})
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) }) name: 'uploadlimit',
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) }) value: parseInt(uploadLimits.general)
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) }) })
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits }) store.dispatch('setInstanceOption', {
name: 'avatarlimit',
value: parseInt(uploadLimits.avatar)
})
store.dispatch('setInstanceOption', {
name: 'backgroundlimit',
value: parseInt(uploadLimits.background)
})
store.dispatch('setInstanceOption', {
name: 'bannerlimit',
value: parseInt(uploadLimits.banner)
})
store.dispatch('setInstanceOption', {
name: 'fieldsLimits',
value: metadata.fieldsLimits
})
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) name: 'restrictedNicknames',
value: metadata.restrictedNicknames
})
store.dispatch('setInstanceOption', {
name: 'postFormats',
value: metadata.postFormats
})
const suggestions = metadata.suggestions const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web }) name: 'suggestionsEnabled',
value: suggestions.enabled
})
store.dispatch('setInstanceOption', {
name: 'suggestionsWeb',
value: suggestions.web
})
const software = data.software const software = data.software
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' }) name: 'backendVersion',
value: software.version
})
store.dispatch('setInstanceOption', {
name: 'pleromaBackend',
value: software.name === 'pleroma'
})
const priv = metadata.private const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv }) store.dispatch('setInstanceOption', { name: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) store.dispatch('setInstanceOption', {
name: 'frontendVersion',
value: frontendVersion
})
const federation = metadata.federation const federation = metadata.federation
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'tagPolicyAvailable', name: 'tagPolicyAvailable',
value: typeof federation.mrf_policies === 'undefined' value:
typeof federation.mrf_policies === 'undefined'
? false ? false
: metadata.federation.mrf_policies.includes('TagPolicy') : metadata.federation.mrf_policies.includes('TagPolicy')
}) })
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances }) name: 'federationPolicy',
value: federation
})
store.dispatch('setInstanceOption', {
name: 'localBubbleInstances',
value: metadata.localBubbleInstances
})
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'federating', name: 'federating',
value: typeof federation.enabled === 'undefined' value:
? true typeof federation.enabled === 'undefined' ? true : federation.enabled
: federation.enabled
}) })
const accountActivationRequired = metadata.accountActivationRequired const accountActivationRequired = metadata.accountActivationRequired
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired }) store.dispatch('setInstanceOption', {
name: 'accountActivationRequired',
value: accountActivationRequired
})
const accounts = metadata.staffAccounts const accounts = metadata.staffAccounts
resolveStaffAccounts({ store, accounts }) resolveStaffAccounts({ store, accounts })
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn('Could not load nodeinfo') console.warn('Could not load nodeinfo')
@ -336,11 +440,16 @@ const getNodeInfo = async ({ store }) => {
const setConfig = async ({ store }) => { const setConfig = async ({ store }) => {
// apiConfig, staticConfig // apiConfig, staticConfig
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()]) const configInfos = await Promise.all([
getBackendProvidedConfig({ store }),
getStaticConfig()
])
const apiConfig = configInfos[0] const apiConfig = configInfos[0]
const staticConfig = configInfos[1] const staticConfig = configInfos[1]
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store })) await setSettings({ store, apiConfig, staticConfig }).then(
getAppSecret({ store })
)
} }
const checkOAuthToken = async ({ store }) => { const checkOAuthToken = async ({ store }) => {
@ -363,7 +472,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
FaviconService.initFaviconService() FaviconService.initFaviconService()
const overrides = window.___pleromafe_dev_overrides || {} const overrides = window.___pleromafe_dev_overrides || {}
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin const server =
typeof overrides.target !== 'undefined'
? overrides.target
: window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server }) store.dispatch('setInstanceOption', { name: 'server', value: server })
await setConfig({ store }) await setConfig({ store })
@ -373,7 +485,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
const customThemePresent = customThemeSource || customTheme const customThemePresent = customThemeSource || customTheme
if (customThemePresent) { if (customThemePresent) {
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) { if (
customThemeSource &&
customThemeSource.themeEngineVersion === CURRENT_VERSION
) {
applyTheme(customThemeSource) applyTheme(customThemeSource)
} else { } else {
applyTheme(customTheme) applyTheme(customTheme)
@ -404,7 +519,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
history: createWebHistory(), history: createWebHistory(),
routes: routes(store), routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => { scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) { if (to.matched.some((m) => m.meta.dontScroll)) {
return {} return {}
} }

View file

@ -35,51 +35,145 @@ export default (store) => {
} }
let routes = [ let routes = [
{ name: 'root', {
name: 'root',
path: '/', path: '/',
redirect: _to => { redirect: (_to) => {
return (store.state.users.currentUser return (
(store.state.users.currentUser
? store.state.instance.redirectRootLogin ? store.state.instance.redirectRootLogin
: store.state.instance.redirectRootNoLogin) || '/main/all' : store.state.instance.redirectRootNoLogin) || '/main/all'
)
} }
}, },
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline }, {
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline }, name: 'public-external-timeline',
{ name: 'bubble-timeline', path: '/main/bubble', component: BubbleTimeline }, path: '/main/all',
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute }, component: PublicAndExternalTimeline
},
{
name: 'public-timeline',
path: '/main/public',
component: PublicTimeline
},
{
name: 'bubble-timeline',
path: '/main/bubble',
component: BubbleTimeline
},
{
name: 'friends',
path: '/main/friends',
component: FriendsTimeline,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, {
{ name: 'remote-user-profile-acct', 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, component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute beforeEnter: validateAuthenticatedRoute
}, },
{ name: 'remote-user-profile', {
name: 'remote-user-profile',
path: '/remote-users/:hostname/:username', path: '/remote-users/:hostname/:username',
component: RemoteUserResolver, component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute beforeEnter: validateAuthenticatedRoute
}, },
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile, meta: { dontScroll: true } }, {
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute }, name: 'external-user-profile',
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, path: '/users/:id',
component: UserProfile,
meta: { dontScroll: true }
},
{
name: 'interactions',
path: '/users/:username/interactions',
component: Interactions,
beforeEnter: validateAuthenticatedRoute
},
{
name: 'dms',
path: '/users/:username/dms',
component: DMs,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
{ name: 'registration-request-sent', path: '/registration-request-sent', component: RegistrationRequestSent }, {
{ name: 'awaiting-email-confirmation', path: '/awaiting-email-confirmation', component: AwaitingEmailConfirmation }, name: 'registration-request-sent',
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true }, path: '/registration-request-sent',
{ name: 'registration-token', path: '/registration/:token', component: Registration }, component: RegistrationRequestSent
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, },
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute }, {
name: 'awaiting-email-confirmation',
path: '/awaiting-email-confirmation',
component: AwaitingEmailConfirmation
},
{
name: 'password-reset',
path: '/password-reset',
component: PasswordReset,
props: true
},
{
name: 'registration-token',
path: '/registration/:token',
component: Registration
},
{
name: 'friend-requests',
path: '/friend-requests',
component: FollowRequests,
beforeEnter: validateAuthenticatedRoute
},
{
name: 'notifications',
path: '/:username/notifications',
component: Notifications,
props: () => ({ disableTeleport: true }),
beforeEnter: validateAuthenticatedRoute
},
{ name: 'login', path: '/login', component: AuthForm }, { name: 'login', path: '/login', component: AuthForm },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, {
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, name: 'oauth-callback',
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, path: '/oauth-callback',
component: OAuthCallback,
props: (route) => ({ code: route.query.code })
},
{
name: 'search',
path: '/search',
component: Search,
props: (route) => ({ query: route.query.query })
},
{
name: 'who-to-follow',
path: '/who-to-follow',
component: WhoToFollow,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'about', path: '/about', component: About }, { name: 'about', path: '/about', component: About },
{ name: 'lists', path: '/lists', component: Lists }, { name: 'lists', path: '/lists', component: Lists },
{ name: 'list-timeline', path: '/lists/:id', component: ListTimeline }, { name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
{ name: 'list-edit', path: '/lists/:id/edit', component: ListEdit }, { name: 'list-edit', path: '/lists/:id/edit', component: ListEdit },
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage }, {
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile, meta: { dontScroll: true } } name: 'announcements',
path: '/announcements',
component: AnnouncementsPage
},
{
name: 'user-profile',
path: '/:_(users)?/:name',
component: UserProfile,
meta: { dontScroll: true }
}
] ]
return routes return routes

View file

@ -15,11 +15,15 @@ const About = {
LocalBubblePanel LocalBubblePanel
}, },
computed: { computed: {
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, showFeaturesPanel() {
return this.$store.state.instance.showFeaturesPanel
},
showInstanceSpecificPanel() { showInstanceSpecificPanel() {
return this.$store.state.instance.showInstanceSpecificPanel && return (
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP && !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
)
}, },
showLocalBubblePanel() { showLocalBubblePanel() {
return this.$store.state.instance.localBubbleInstances.length > 0 return this.$store.state.instance.localBubbleInstances.length > 0

View file

@ -11,5 +11,4 @@
<script src="./about.js"></script> <script src="./about.js"></script>
<style lang="scss"> <style lang="scss"></style>
</style>

View file

@ -3,18 +3,12 @@ import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
faEllipsisV
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faEllipsisV)
faEllipsisV
)
const AccountActions = { const AccountActions = {
props: [ props: ['user', 'relationship'],
'user', 'relationship'
],
data() { data() {
return { return {
showingConfirmBlock: false showingConfirmBlock: false
@ -62,11 +56,13 @@ const AccountActions = {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
}, },
muteDomain() { muteDomain() {
this.$store.dispatch('muteDomain', this.user.screen_name.split('@')[1]) this.$store
.dispatch('muteDomain', this.user.screen_name.split('@')[1])
.then(() => this.refetchRelationship()) .then(() => this.refetchRelationship())
}, },
unmuteDomain() { unmuteDomain() {
this.$store.dispatch('unmuteDomain', this.user.screen_name.split('@')[1]) this.$store
.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
.then(() => this.refetchRelationship()) .then(() => this.refetchRelationship())
} }
}, },
@ -75,7 +71,8 @@ const AccountActions = {
return this.$store.getters.mergedConfig.modalOnBlock return this.$store.getters.mergedConfig.modalOnBlock
}, },
...mapState({ ...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable pleromaChatMessagesAvailable: (state) =>
state.instance.pleromaChatMessagesAvailable
}) })
} }
} }

View file

@ -6,7 +6,7 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template v-slot:content> <template #content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<template v-if="relationship.following"> <template v-if="relationship.following">
<button <button
@ -71,7 +71,7 @@
</button> </button>
</div> </div>
</template> </template>
<template v-slot:trigger> <template #trigger>
<button class="button-unstyled ellipsis-button"> <button class="button-unstyled ellipsis-button">
<FAIcon <FAIcon
class="icon" class="icon"
@ -93,10 +93,8 @@
keypath="user_card.block_confirm" keypath="user_card.block_confirm"
tag="span" tag="span"
> >
<template v-slot:user> <template #user>
<span <span v-text="user.screen_name_ui" />
v-text="user.screen_name_ui"
/>
</template> </template>
</i18n-t> </i18n-t>
</confirm-modal> </confirm-modal>

View file

@ -25,7 +25,7 @@ const Announcement = {
}, },
computed: { computed: {
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser
}), }),
content() { content() {
return this.announcement.content return this.announcement.content
@ -39,7 +39,10 @@ const Announcement = {
return return
} }
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale)
)
}, },
startsAt() { startsAt() {
const time = this.announcement['starts_at'] const time = this.announcement['starts_at']
@ -47,7 +50,10 @@ const Announcement = {
return return
} }
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale)
)
}, },
endsAt() { endsAt() {
const time = this.announcement['ends_at'] const time = this.announcement['ends_at']
@ -55,7 +61,10 @@ const Announcement = {
return return
} }
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale)
)
}, },
inactive() { inactive() {
return this.announcement.inactive return this.announcement.inactive
@ -64,7 +73,10 @@ const Announcement = {
methods: { methods: {
markAsRead() { markAsRead() {
if (!this.isRead) { if (!this.isRead) {
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id) return this.$store.dispatch(
'markAnnouncementAsRead',
this.announcement.id
)
} }
}, },
deleteAnnouncement() { deleteAnnouncement() {
@ -72,7 +84,9 @@ const Announcement = {
}, },
formatTimeOrDate(time, locale) { formatTimeOrDate(time, locale) {
const d = new Date(time) const d = new Date(time)
return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale) return this.announcement['all_day']
? d.toLocaleDateString(locale)
: d.toLocaleString(locale)
}, },
enterEditMode() { enterEditMode() {
this.editedAnnouncement.content = this.announcement.pleroma['raw_content'] this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
@ -82,14 +96,15 @@ const Announcement = {
this.editing = true this.editing = true
}, },
submitEdit() { submitEdit() {
this.$store.dispatch('editAnnouncement', { this.$store
.dispatch('editAnnouncement', {
id: this.announcement.id, id: this.announcement.id,
...this.editedAnnouncement ...this.editedAnnouncement
}) })
.then(() => { .then(() => {
this.editing = false this.editing = false
}) })
.catch(error => { .catch((error) => {
this.editError = error.error this.editError = error.error
}) })
}, },

View file

@ -21,7 +21,9 @@
class="times" class="times"
> >
<span v-if="publishedAt"> <span v-if="publishedAt">
{{ $t('announcements.published_time_display', { time: publishedAt }) }} {{
$t('announcements.published_time_display', { time: publishedAt })
}}
</span> </span>
<span v-if="startsAt"> <span v-if="startsAt">
{{ $t('announcements.start_time_display', { time: startsAt }) }} {{ $t('announcements.start_time_display', { time: startsAt }) }}
@ -99,7 +101,7 @@
<script src="./announcement.js"></script> <script src="./announcement.js"></script>
<style lang="scss"> <style lang="scss">
@import "../../variables"; @import '../../variables';
.announcement { .announcement {
border-bottom-width: 1px; border-bottom-width: 1px;
@ -108,7 +110,8 @@
border-radius: 0; border-radius: 0;
padding: var(--status-margin, $status-margin); padding: var(--status-margin, $status-margin);
.heading, .body { .heading,
.body {
margin-bottom: var(--status-margin, $status-margin); margin-bottom: var(--status-margin, $status-margin);
} }

View file

@ -10,22 +10,26 @@
:disabled="disabled" :disabled="disabled"
/> />
<span class="announcement-metadata"> <span class="announcement-metadata">
<label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label> <label for="announcement-start-time">{{
$t('announcements.start_time_prompt')
}}</label>
<input <input
id="announcement-start-time" id="announcement-start-time"
v-model="announcement.startsAt" v-model="announcement.startsAt"
:type="announcement.allDay ? 'date' : 'datetime-local'" :type="announcement.allDay ? 'date' : 'datetime-local'"
:disabled="disabled" :disabled="disabled"
> />
</span> </span>
<span class="announcement-metadata"> <span class="announcement-metadata">
<label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label> <label for="announcement-end-time">{{
$t('announcements.end_time_prompt')
}}</label>
<input <input
id="announcement-end-time" id="announcement-end-time"
v-model="announcement.endsAt" v-model="announcement.endsAt"
:type="announcement.allDay ? 'date' : 'datetime-local'" :type="announcement.allDay ? 'date' : 'datetime-local'"
:disabled="disabled" :disabled="disabled"
> />
</span> </span>
<span class="announcement-metadata"> <span class="announcement-metadata">
<Checkbox <Checkbox
@ -33,7 +37,9 @@
v-model="announcement.allDay" v-model="announcement.allDay"
:disabled="disabled" :disabled="disabled"
/> />
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label> <label for="announcement-all-day">{{
$t('announcements.all_day_prompt')
}}</label>
</span> </span>
</div> </div>
</template> </template>

View file

@ -24,7 +24,7 @@ const AnnouncementsPage = {
}, },
computed: { computed: {
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser
}), }),
announcements() { announcements() {
return this.$store.state.announcements.announcements return this.$store.state.announcements.announcements
@ -33,13 +33,14 @@ const AnnouncementsPage = {
methods: { methods: {
postAnnouncement() { postAnnouncement() {
this.posting = true this.posting = true
this.$store.dispatch('postAnnouncement', this.newAnnouncement) this.$store
.dispatch('postAnnouncement', this.newAnnouncement)
.then(() => { .then(() => {
this.newAnnouncement.content = '' this.newAnnouncement.content = ''
this.startsAt = undefined this.startsAt = undefined
this.endsAt = undefined this.endsAt = undefined
}) })
.catch(error => { .catch((error) => {
this.error = error.error this.error = error.error
}) })
.finally(() => { .finally(() => {

View file

@ -6,9 +6,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<section <section v-if="currentUser && currentUser.role === 'admin'">
v-if="currentUser && currentUser.role === 'admin'"
>
<div class="post-form"> <div class="post-form">
<div class="heading"> <div class="heading">
<h4>{{ $t('announcements.post_form_header') }}</h4> <h4>{{ $t('announcements.post_form_header') }}</h4>
@ -50,9 +48,7 @@
v-for="announcement in announcements" v-for="announcement in announcements"
:key="announcement.id" :key="announcement.id"
> >
<announcement <announcement :announcement="announcement" />
:announcement="announcement"
/>
</section> </section>
</div> </div>
</div> </div>
@ -61,13 +57,14 @@
<script src="./announcements_page.js"></script> <script src="./announcements_page.js"></script>
<style lang="scss"> <style lang="scss">
@import "../../variables"; @import '../../variables';
.announcements-page { .announcements-page {
.post-form { .post-form {
padding: var(--status-margin, $status-margin); padding: var(--status-margin, $status-margin);
.heading, .body { .heading,
.body {
margin-bottom: var(--status-margin, $status-margin); margin-bottom: var(--status-margin, $status-margin);
} }

View file

@ -35,8 +35,8 @@ export default {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.btn { .btn {
margin: .5em; margin: 0.5em;
padding: .5em 2em; padding: 0.5em 2em;
} }
} }
</style> </style>

View file

@ -53,7 +53,9 @@ const Attachment = {
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw, hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
preloadImage: this.$store.getters.mergedConfig.preloadImage, preloadImage: this.$store.getters.mergedConfig.preloadImage,
loading: false, loading: false,
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), img:
fileTypeService.fileType(this.attachment.mimetype) === 'image' &&
document.createElement('img'),
modalOpen: false, modalOpen: false,
showHidden: false, showHidden: false,
flashLoaded: false, flashLoaded: false,
@ -106,7 +108,7 @@ const Attachment = {
return this.nsfw && this.hideNsfwLocal && !this.showHidden return this.nsfw && this.hideNsfwLocal && !this.showHidden
}, },
isEmpty() { isEmpty() {
return (this.type === 'html' && !this.attachment.oembed) return this.type === 'html' && !this.attachment.oembed
}, },
useModal() { useModal() {
let modalTypes = [] let modalTypes = []
@ -180,7 +182,8 @@ const Attachment = {
}, },
toggleHidden(event) { toggleHidden(event) {
if ( if (
(this.mergedConfig.useOneClickNsfw && !this.showHidden) && this.mergedConfig.useOneClickNsfw &&
!this.showHidden &&
(this.type !== 'video' || this.mergedConfig.playVideosInModal) (this.type !== 'video' || this.mergedConfig.playVideosInModal)
) { ) {
this.openModal(event) this.openModal(event)
@ -208,7 +211,9 @@ const Attachment = {
}, },
resize(e) { resize(e) {
const target = e.target || e const target = e.target || e
if (!(target instanceof window.Element)) { return } if (!(target instanceof window.Element)) {
return
}
// Reset to default height for empty form, nothing else to do here. // Reset to default height for empty form, nothing else to do here.
if (target.value === '') { if (target.value === '') {
@ -219,7 +224,9 @@ const Attachment = {
const paddingString = getComputedStyle(target)['padding'] const paddingString = getComputedStyle(target)['padding']
// remove -px suffix // remove -px suffix
const padding = Number(paddingString.substring(0, paddingString.length - 2)) const padding = Number(
paddingString.substring(0, paddingString.length - 2)
)
target.style.height = 'auto' target.style.height = 'auto'
const newHeight = Math.floor(target.scrollHeight - padding * 2) const newHeight = Math.floor(target.scrollHeight - padding * 2)

View file

@ -15,7 +15,8 @@
@click.prevent @click.prevent
> >
<FAIcon :icon="placeholderIconClass" /> <FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }} <b>{{ nsfw ? 'NSFW / ' : '' }}</b
>{{ edit ? '' : placeholderName }}
</a> </a>
<div <div
v-if="edit || remove" v-if="edit || remove"
@ -30,7 +31,11 @@
</button> </button>
</div> </div>
<div <div
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)" v-if="
size !== 'hide' &&
!hideDescription &&
(edit || localDescription || showDescription)
"
class="description-container" class="description-container"
:class="{ '-static': !edit }" :class="{ '-static': !edit }"
> >
@ -41,7 +46,7 @@
class="description-field" class="description-field"
:placeholder="$t('post_status.media_description')" :placeholder="$t('post_status.media_description')"
@keydown.enter.prevent="" @keydown.enter.prevent=""
> />
<p v-else> <p v-else>
{{ localDescription }} {{ localDescription }}
</p> </p>
@ -68,7 +73,7 @@
:key="nsfwImage" :key="nsfwImage"
class="nsfw" class="nsfw"
:src="nsfwImage" :src="nsfwImage"
> />
<FAIcon <FAIcon
v-if="type === 'video'" v-if="type === 'video'"
class="play-icon" class="play-icon"
@ -88,7 +93,12 @@
<FAIcon icon="stop" /> <FAIcon icon="stop" />
</button> </button>
<button <button
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'" v-if="
attachment.description &&
size !== 'small' &&
!edit &&
type !== 'unknown'
"
class="button-unstyled attachment-button" class="button-unstyled attachment-button"
:title="$t('status.show_attachment_description')" :title="$t('status.show_attachment_description')"
@click.prevent="toggleDescription" @click.prevent="toggleDescription"
@ -218,11 +228,13 @@
v-if="attachment.thumb_url" v-if="attachment.thumb_url"
class="image" class="image"
> >
<img :src="attachment.thumb_url"> <img :src="attachment.thumb_url" />
</div> </div>
<div class="text"> <div class="text">
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1> <h1>
<a :href="attachment.url">{{ attachment.oembed.title }}</a>
</h1>
<div v-html="attachment.oembed.oembedHTML" /> <div v-html="attachment.oembed.oembedHTML" />
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
</div> </div>
@ -244,7 +256,11 @@
</span> </span>
</div> </div>
<div <div
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))" v-if="
size !== 'hide' &&
!hideDescription &&
(edit || (localDescription && showDescription))
"
class="description-container" class="description-container"
:class="{ '-static': !edit }" :class="{ '-static': !edit }"
> >

View file

@ -11,8 +11,12 @@ const AuthForm = {
}, },
computed: { computed: {
authForm() { authForm() {
if (this.requiredTOTP) { return 'MFATOTPForm' } if (this.requiredTOTP) {
if (this.requiredRecovery) { return 'MFARecoveryForm' } return 'MFATOTPForm'
}
if (this.requiredRecovery) {
return 'MFARecoveryForm'
}
return 'LoginForm' return 'LoginForm'
}, },
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery']) ...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])

View file

@ -2,11 +2,13 @@ const debounceMilliseconds = 500
export default { export default {
props: { props: {
query: { // function to query results and return a promise query: {
// function to query results and return a promise
type: Function, type: Function,
required: true required: true
}, },
filter: { // function to filter results in real time filter: {
// function to filter results in real time
type: Function type: Function
}, },
placeholder: { placeholder: {
@ -38,7 +40,9 @@ export default {
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
this.results = [] this.results = []
if (term) { if (term) {
this.query(term).then((results) => { this.results = results }) this.query(term).then((results) => {
this.results = results
})
} }
}, debounceMilliseconds) }, debounceMilliseconds)
}, },

View file

@ -8,7 +8,7 @@
:placeholder="placeholder" :placeholder="placeholder"
class="autosuggest-input" class="autosuggest-input"
@click="onInputClick" @click="onInputClick"
> />
<div <div
v-if="resultsVisible && filtered.length > 0" v-if="resultsVisible && filtered.length > 0"
class="autosuggest-results" class="autosuggest-results"

View file

@ -13,7 +13,11 @@ const AvatarList = {
}, },
methods: { methods: {
userProfileLink(user) { userProfileLink(user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(
user.id,
user.screen_name,
this.$store.state.instance.restrictedNicknames
)
} }
} }
} }

View file

@ -1,4 +1,3 @@
export default { export default {
computed: { computed: {}
}
} }

View file

@ -4,9 +4,7 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const BasicUserCard = { const BasicUserCard = {
props: [ props: ['user'],
'user'
],
data() { data() {
return { return {
userExpanded: false userExpanded: false
@ -22,7 +20,11 @@ const BasicUserCard = {
this.userExpanded = !this.userExpanded this.userExpanded = !this.userExpanded
}, },
userProfileLink(user) { userProfileLink(user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(
user.id,
user.screen_name,
this.$store.state.instance.restrictedNicknames
)
} }
} }
} }

View file

@ -4,7 +4,9 @@ const PublicTimeline = {
Timeline Timeline
}, },
computed: { computed: {
timeline () { return this.$store.state.statuses.timelines.bubble } timeline() {
return this.$store.state.statuses.timelines.bubble
}
}, },
created() { created() {
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
@ -12,7 +14,6 @@ const PublicTimeline = {
unmounted() { unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'bubble') this.$store.dispatch('stopFetchingTimeline', 'bubble')
} }
} }
export default PublicTimeline export default PublicTimeline

View file

@ -18,7 +18,10 @@ export default {
if (this.date.getTime() === today.getTime()) { if (this.date.getTime() === today.getTime()) {
return this.$t('display_date.today') return this.$t('display_date.today')
} else { } else {
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' }) return this.date.toLocaleDateString(
localeService.internalToBrowserLocale(this.$i18n.locale),
{ day: 'numeric', month: 'long' }
)
} }
} }
} }

View file

@ -9,7 +9,7 @@
:checked="modelValue" :checked="modelValue"
:indeterminate="indeterminate" :indeterminate="indeterminate"
@change="$emit('update:modelValue', $event.target.checked)" @change="$emit('update:modelValue', $event.target.checked)"
> />
<i class="checkbox-indicator" /> <i class="checkbox-indicator" />
<span <span
v-if="!!$slots.default" v-if="!!$slots.default"
@ -22,12 +22,8 @@
<script> <script>
export default { export default {
emits: ['update:modelValue'], props: ['modelValue', 'indeterminate', 'disabled'],
props: [ emits: ['update:modelValue']
'modelValue',
'indeterminate',
'disabled'
]
} }
</script> </script>
@ -71,7 +67,7 @@ export default {
&.disabled { &.disabled {
.checkbox-indicator::before, .checkbox-indicator::before,
.label { .label {
opacity: .5; opacity: 0.5;
} }
.label { .label {
color: $fallback--faint; color: $fallback--faint;
@ -79,7 +75,7 @@ export default {
} }
} }
input[type=checkbox] { input[type='checkbox'] {
display: none; display: none;
&:checked + .checkbox-indicator::before { &:checked + .checkbox-indicator::before {
@ -92,11 +88,10 @@ export default {
color: $fallback--text; color: $fallback--text;
color: var(--inputText, $fallback--text); color: var(--inputText, $fallback--text);
} }
} }
& > span { & > span {
margin-left: .5em; margin-left: 0.5em;
} }
} }
</style> </style>

View file

@ -14,7 +14,12 @@
:model-value="present" :model-value="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @update:modelValue="
$emit(
'update:modelValue',
typeof modelValue === 'undefined' ? fallback : undefined
)
"
/> />
<div class="input color-input-field"> <div class="input color-input-field">
<input <input
@ -24,7 +29,7 @@
:value="modelValue || fallback" :value="modelValue || fallback"
:disabled="!present || disabled" :disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)" @input="$emit('update:modelValue', $event.target.value)"
> />
<input <input
v-if="validColor" v-if="validColor"
:id="name" :id="name"
@ -33,7 +38,7 @@
:value="modelValue || fallback" :value="modelValue || fallback"
:disabled="!present || disabled" :disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)" @input="$emit('update:modelValue', $event.target.value)"
> />
<div <div
v-if="transparentColor" v-if="transparentColor"
class="transparentIndicator" class="transparentIndicator"
@ -46,7 +51,6 @@
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" src="./color_input.scss"></style>
<script> <script>
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js' import { hex2rgb } from '../../services/color_convert/color_convert.js'
@ -108,6 +112,7 @@ export default {
} }
} }
</script> </script>
<style lang="scss" src="./color_input.scss"></style>
<style lang="scss"> <style lang="scss">
.color-control { .color-control {

View file

@ -22,8 +22,7 @@ const ConfirmModal = {
type: String type: String
} }
}, },
computed: { computed: {},
},
methods: { methods: {
onCancel() { onCancel() {
this.$emit('cancelled') this.$emit('cancelled')

View file

@ -25,6 +25,8 @@
</dialog-modal> </dialog-modal>
</template> </template>
<script src="./confirm_modal.js"></script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../_variables'; @import '../../_variables';
@ -35,5 +37,3 @@
} }
} }
</style> </style>
<script src="./confirm_modal.js"></script>

View file

@ -43,11 +43,7 @@ import {
faThumbsUp faThumbsUp
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faAdjust, faExclamationTriangle, faThumbsUp)
faAdjust,
faExclamationTriangle,
faThumbsUp
)
export default { export default {
props: { props: {
@ -66,18 +62,34 @@ export default {
}, },
computed: { computed: {
hint() { hint() {
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad') const levelVal = this.contrast.aaa
? 'aaa'
: this.contrast.aa
? 'aa'
: 'bad'
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
const context = this.$t('settings.style.common.contrast.context.text') const context = this.$t('settings.style.common.contrast.context.text')
const ratio = this.contrast.text const ratio = this.contrast.text
return this.$t('settings.style.common.contrast.hint', { level, context, ratio }) return this.$t('settings.style.common.contrast.hint', {
level,
context,
ratio
})
}, },
hint_18pt() { hint_18pt() {
const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad') const levelVal = this.contrast.laaa
? 'aaa'
: this.contrast.laa
? 'aa'
: 'bad'
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
const context = this.$t('settings.style.common.contrast.context.18pt') const context = this.$t('settings.style.common.contrast.context.18pt')
const ratio = this.contrast.text const ratio = this.contrast.text
return this.$t('settings.style.common.contrast.hint', { level, context, ratio }) return this.$t('settings.style.common.contrast.hint', {
level,
context,
ratio
})
} }
} }
} }

View file

@ -11,11 +11,7 @@ import {
faChevronLeft faChevronLeft
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft)
faAngleDoubleDown,
faAngleDoubleLeft,
faChevronLeft
)
const sortById = (a, b) => { const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@ -39,12 +35,13 @@ const sortAndFilterConversation = (conversation, statusoid) => {
if (statusoid.type === 'retweet') { if (statusoid.type === 'retweet') {
conversation = filter( conversation = filter(
conversation, conversation,
(status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id) (status) =>
status.type === 'retweet' || status.id !== statusoid.retweeted_status.id
) )
} else { } else {
conversation = filter(conversation, (status) => status.type !== 'retweet') conversation = filter(conversation, (status) => status.type !== 'retweet')
} }
return conversation.filter(_ => _).sort(sortById) return conversation.filter((_) => _).sort(sortById)
} }
const conversation = { const conversation = {
@ -80,7 +77,10 @@ const conversation = {
return maxDepth >= 1 ? maxDepth : 1 return maxDepth >= 1 ? maxDepth : 1
}, },
streamingEnabled() { streamingEnabled() {
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED return (
this.mergedConfig.useStreamingApi &&
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
)
}, },
displayStyle() { displayStyle() {
return this.$store.getters.mergedConfig.conversationDisplay return this.$store.getters.mergedConfig.conversationDisplay
@ -108,11 +108,12 @@ const conversation = {
}, },
suspendable() { suspendable() {
if (this.isTreeView) { if (this.isTreeView) {
return Object.entries(this.statusContentProperties) return Object.entries(this.statusContentProperties).every(
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0) ([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0
)
} }
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
return this.$refs.statusComponent.every(s => s.suspendable) return this.$refs.statusComponent.every((s) => s.suspendable)
} else { } else {
return true return true
} }
@ -142,8 +143,12 @@ const conversation = {
return [this.status] return [this.status]
} }
const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId]) const conversation = clone(
const statusIndex = findIndex(conversation, { id: this.originalStatusId }) this.$store.state.statuses.conversationsObject[this.conversationId]
)
const statusIndex = findIndex(conversation, {
id: this.originalStatusId
})
if (statusIndex !== -1) { if (statusIndex !== -1) {
conversation[statusIndex] = this.status conversation[statusIndex] = this.status
} }
@ -157,42 +162,57 @@ const conversation = {
}, {}) }, {})
}, },
threadTree() { threadTree() {
const reverseLookupTable = this.conversation.reduce((table, status, index) => { const reverseLookupTable = this.conversation.reduce(
(table, status, index) => {
table[status.id] = index table[status.id] = index
return table return table
}, {}) },
{}
)
const threads = this.conversation.reduce((a, cur) => { const threads = this.conversation.reduce(
(a, cur) => {
const id = cur.id const id = cur.id
a.forest[id] = this.getReplies(id) a.forest[id] = this.getReplies(id).map((s) => s.id)
.map(s => s.id)
return a return a
}, { },
{
forest: {} forest: {}
}) }
)
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => { const walk = (forest, topLevel, depth = 0, processed = {}) =>
topLevel
.map((id) => {
if (processed[id]) { if (processed[id]) {
return [] return []
} }
processed[id] = true processed[id] = true
return [{ return [
{
status: this.conversation[reverseLookupTable[id]], status: this.conversation[reverseLookupTable[id]],
id, id,
depth depth
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), []) },
}).reduce((a, b) => a.concat(b), []) walk(forest, forest[id], depth + 1, processed)
].reduce((a, b) => a.concat(b), [])
})
.reduce((a, b) => a.concat(b), [])
const linearized = walk(threads.forest, this.topLevel.map(k => k.id)) const linearized = walk(
threads.forest,
this.topLevel.map((k) => k.id)
)
return linearized return linearized
}, },
replyIds() { replyIds() {
return this.conversation.map(k => k.id) return this.conversation
.map((k) => k.id)
.reduce((res, id) => { .reduce((res, id) => {
res[id] = (this.replies[id] || []).map(k => k.id) res[id] = (this.replies[id] || []).map((k) => k.id)
return res return res
}, {}) }, {})
}, },
@ -202,10 +222,14 @@ const conversation = {
if (sizes[id]) { if (sizes[id]) {
return sizes[id] return sizes[id]
} }
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0) sizes[id] =
1 +
this.replyIds[id]
.map((cid) => subTreeSizeFor(cid))
.reduce((a, b) => a + b, 0)
return sizes[id] return sizes[id]
} }
this.conversation.map(k => k.id).map(subTreeSizeFor) this.conversation.map((k) => k.id).map(subTreeSizeFor)
return Object.keys(sizes).reduce((res, id) => { return Object.keys(sizes).reduce((res, id) => {
res[id] = sizes[id] - 1 // exclude itself res[id] = sizes[id] - 1 // exclude itself
return res return res
@ -217,10 +241,14 @@ const conversation = {
if (depths[id]) { if (depths[id]) {
return depths[id] return depths[id]
} }
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0) depths[id] =
1 +
this.replyIds[id]
.map((cid) => subTreeDepthFor(cid))
.reduce((a, b) => (a > b ? a : b), 0)
return depths[id] return depths[id]
} }
this.conversation.map(k => k.id).map(subTreeDepthFor) this.conversation.map((k) => k.id).map(subTreeDepthFor)
return Object.keys(depths).reduce((res, id) => { return Object.keys(depths).reduce((res, id) => {
res[id] = depths[id] - 1 // exclude itself res[id] = depths[id] - 1 // exclude itself
return res return res
@ -233,8 +261,16 @@ const conversation = {
}, {}) }, {})
}, },
topLevel() { topLevel() {
const topLevel = this.conversation.reduce((tl, cur) => const topLevel = this.conversation.reduce(
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation) (tl, cur) =>
tl.filter(
(k) =>
this.getReplies(cur.id)
.map((v) => v.id)
.indexOf(k.id) === -1
),
this.conversation
)
return topLevel return topLevel
}, },
otherTopLevelCount() { otherTopLevelCount() {
@ -260,15 +296,26 @@ const conversation = {
shouldShowAllConversationButton() { shouldShowAllConversationButton() {
// The "show all conversation" button tells the user that there exist // The "show all conversation" button tells the user that there exist
// other toplevel statuses, so do not show it if there is only a single root // other toplevel statuses, so do not show it if there is only a single root
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1 return (
this.isTreeView &&
this.isExpanded &&
this.diveMode &&
this.topLevel.length > 1
)
}, },
shouldShowAncestors() { shouldShowAncestors() {
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length return (
this.isTreeView &&
this.isExpanded &&
this.ancestorsOf(this.diveRoot).length
)
}, },
replies() { replies() {
let i = 1 let i = 1
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => { return reduce(
this.conversation,
(result, { id, in_reply_to_status_id }) => {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
const irid = in_reply_to_status_id const irid = in_reply_to_status_id
/* eslint-enable camelcase */ /* eslint-enable camelcase */
@ -281,7 +328,9 @@ const conversation = {
} }
i++ i++
return result return result
}, {}) },
{}
)
}, },
isExpanded() { isExpanded() {
return !!(this.expanded || this.isPage) return !!(this.expanded || this.isPage)
@ -298,7 +347,7 @@ const conversation = {
if (this.threadDisplayStatusObject[id]) { if (this.threadDisplayStatusObject[id]) {
return this.threadDisplayStatusObject[id] return this.threadDisplayStatusObject[id]
} }
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) { if (depth - this.diveDepth <= this.maxDepthToShowByDefault) {
return 'showing' return 'showing'
} else { } else {
return 'hidden' return 'hidden'
@ -339,7 +388,7 @@ const conversation = {
}, },
focused() { focused() {
return (id) => { return (id) => {
return (this.isExpanded) && id === this.highlight return this.isExpanded && id === this.highlight
} }
}, },
maybeHighlight() { maybeHighlight() {
@ -347,7 +396,7 @@ const conversation = {
}, },
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState({ ...mapState({
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus
}) })
}, },
components: { components: {
@ -358,7 +407,11 @@ const conversation = {
statusId(newVal, oldVal) { statusId(newVal, oldVal) {
const newConversationId = this.getConversationId(newVal) const newConversationId = this.getConversationId(newVal)
const oldConversationId = this.getConversationId(oldVal) const oldConversationId = this.getConversationId(oldVal)
if (newConversationId && oldConversationId && newConversationId === oldConversationId) { if (
newConversationId &&
oldConversationId &&
newConversationId === oldConversationId
) {
this.setHighlight(this.originalStatusId) this.setHighlight(this.originalStatusId)
} else { } else {
this.fetchConversation() this.fetchConversation()
@ -372,23 +425,25 @@ const conversation = {
} }
}, },
virtualHidden(value) { virtualHidden(value) {
this.$store.dispatch( this.$store.dispatch('setVirtualHeight', {
'setVirtualHeight', statusId: this.statusId,
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` } height: `${this.$el.clientHeight}px`
) })
} }
}, },
methods: { methods: {
fetchConversation() { fetchConversation() {
if (this.status) { if (this.status) {
this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId }) this.$store.state.api.backendInteractor
.fetchConversation({ id: this.statusId })
.then(({ ancestors, descendants }) => { .then(({ ancestors, descendants }) => {
this.$store.dispatch('addNewStatuses', { statuses: ancestors }) this.$store.dispatch('addNewStatuses', { statuses: ancestors })
this.$store.dispatch('addNewStatuses', { statuses: descendants }) this.$store.dispatch('addNewStatuses', { statuses: descendants })
this.setHighlight(this.originalStatusId) this.setHighlight(this.originalStatusId)
}) })
} else { } else {
this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId }) this.$store.state.api.backendInteractor
.fetchStatus({ id: this.statusId })
.then((status) => { .then((status) => {
this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.$store.dispatch('addNewStatuses', { statuses: [status] })
this.fetchConversation() this.fetchConversation()
@ -417,7 +472,11 @@ const conversation = {
}, },
getConversationId(statusId) { getConversationId(statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId] const status = this.$store.state.statuses.allStatusesObject[statusId]
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id')) return get(
status,
'retweeted_status.statusnet_conversation_id',
get(status, 'statusnet_conversation_id')
)
}, },
setThreadDisplay(id, nextStatus) { setThreadDisplay(id, nextStatus) {
this.threadDisplayStatusObject = { this.threadDisplayStatusObject = {
@ -432,7 +491,9 @@ const conversation = {
}, },
setThreadDisplayRecursively(id, nextStatus) { setThreadDisplayRecursively(id, nextStatus) {
this.setThreadDisplay(id, nextStatus) this.setThreadDisplay(id, nextStatus)
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus)) this.getReplies(id)
.map((k) => k.id)
.map((id) => this.setThreadDisplayRecursively(id, nextStatus))
}, },
showThreadRecursively(id) { showThreadRecursively(id) {
this.setThreadDisplayRecursively(id, 'showing') this.setThreadDisplayRecursively(id, 'showing')
@ -447,7 +508,11 @@ const conversation = {
} }
}, },
toggleStatusContentProperty(id, name) { toggleStatusContentProperty(id, name) {
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name]) this.setStatusContentProperty(
id,
name,
!this.statusContentProperties[id][name]
)
}, },
leastVisibleAncestor(id) { leastVisibleAncestor(id) {
let cur = id let cur = id
@ -467,7 +532,9 @@ const conversation = {
this.tryScrollTo(id) this.tryScrollTo(id)
}, },
diveToTopLevel() { diveToTopLevel() {
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id) this.tryScrollTo(
this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id
)
}, },
// only used when we are not on a page // only used when we are not on a page
undive() { undive() {

View file

@ -3,7 +3,7 @@
v-if="!hideStatus" v-if="!hideStatus"
:style="hiddenStyle" :style="hiddenStyle"
class="Conversation" class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }" :class="{ '-expanded': isExpanded, panel: isExpanded }"
> >
<div <div
v-if="isExpanded" v-if="isExpanded"
@ -35,13 +35,15 @@
@click.prevent="diveToTopLevel" @click.prevent="diveToTopLevel"
> >
<template #icon> <template #icon>
<FAIcon <FAIcon icon="angle-double-left" />
icon="angle-double-left"
/>
</template> </template>
<template #text> <template #text>
<span> <span>
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }} {{
$tc('status.show_all_conversation', otherTopLevelCount, {
numStatus: otherTopLevelCount
})
}}
</span> </span>
</template> </template>
</i18n-t> </i18n-t>
@ -54,14 +56,20 @@
v-for="status in ancestorsOf(diveRoot)" v-for="status in ancestorsOf(diveRoot)"
:key="status.id" :key="status.id"
class="thread-ancestor" class="thread-ancestor"
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}" :class="{
'thread-ancestor-has-other-replies':
getReplies(status.id).length > 1,
'-faded': shouldFadeAncestors
}"
> >
<status <status
ref="statusComponent" ref="statusComponent"
:inline-expanded="collapsable && isExpanded" :inline-expanded="collapsable && isExpanded"
:statusoid="status" :statusoid="status"
:expandable="!isExpanded" :expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" :show-pinned="
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
"
:focused="focused(status.id)" :focused="focused(status.id)"
:in-conversation="isExpanded" :in-conversation="isExpanded"
:highlight="getHighlight()" :highlight="getHighlight()"
@ -69,7 +77,6 @@
:in-profile="inProfile" :in-profile="inProfile"
:profile-user-id="profileUserId" :profile-user-id="profileUserId"
class="conversation-status status-fadein panel-body" class="conversation-status status-fadein panel-body"
:simple-tree="treeViewIsSimple" :simple-tree="treeViewIsSimple"
:toggle-thread-display="toggleThreadDisplay" :toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus" :thread-display-status="threadDisplayStatus"
@ -78,28 +85,47 @@
:total-reply-depth="totalReplyDepth" :total-reply-depth="totalReplyDepth"
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus" :show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
:dive="() => diveIntoStatus(status.id)" :dive="() => diveIntoStatus(status.id)"
:controlled-showing-tall="
:controlled-showing-tall="statusContentProperties[status.id].showingTall" statusContentProperties[status.id].showingTall
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject" "
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject" :controlled-expanding-subject="
statusContentProperties[status.id].expandingSubject
"
:controlled-showing-long-subject="
statusContentProperties[status.id].showingLongSubject
"
:controlled-replying="statusContentProperties[status.id].replying" :controlled-replying="statusContentProperties[status.id].replying"
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying" :controlled-media-playing="
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')" statusContentProperties[status.id].mediaPlaying
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')" "
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')" :controlled-toggle-showing-tall="
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')" () => toggleStatusContentProperty(status.id, 'showingTall')
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)" "
:controlled-toggle-expanding-subject="
() => toggleStatusContentProperty(status.id, 'expandingSubject')
"
:controlled-toggle-showing-long-subject="
() =>
toggleStatusContentProperty(status.id, 'showingLongSubject')
"
:controlled-toggle-replying="
() => toggleStatusContentProperty(status.id, 'replying')
"
:controlled-set-media-playing="
(newVal) =>
toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)
"
@goto="setHighlight" @goto="setHighlight"
@toggleExpanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
<div <div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1" v-if="
showOtherRepliesButtonBelowStatus &&
getReplies(status.id).length > 1
"
class="thread-ancestor-dive-box" class="thread-ancestor-dive-box"
> >
<div <div class="thread-ancestor-dive-box-inner">
class="thread-ancestor-dive-box-inner"
>
<i18n-t <i18n-t
tag="button" tag="button"
scope="global" scope="global"
@ -108,13 +134,17 @@
@click.prevent="diveIntoStatus(status.id)" @click.prevent="diveIntoStatus(status.id)"
> >
<template #icon> <template #icon>
<FAIcon <FAIcon icon="angle-double-right" />
icon="angle-double-right"
/>
</template> </template>
<template #text> <template #text>
<span> <span>
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }} {{
$tc(
'status.ancestor_follow',
getReplies(status.id).length - 1,
{ numReplies: getReplies(status.id).length - 1 }
)
}}
</span> </span>
</template> </template>
</i18n-t> </i18n-t>
@ -127,7 +157,6 @@
:key="status.id" :key="status.id"
ref="statusComponent" ref="statusComponent"
:depth="0" :depth="0"
:status="status" :status="status"
:in-profile="inProfile" :in-profile="inProfile"
:conversation="conversation" :conversation="conversation"
@ -135,13 +164,11 @@
:is-expanded="isExpanded" :is-expanded="isExpanded"
:pinned-status-ids-object="pinnedStatusIdsObject" :pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId" :profile-user-id="profileUserId"
:focused="focused" :focused="focused"
:get-replies="getReplies" :get-replies="getReplies"
:highlight="maybeHighlight" :highlight="maybeHighlight"
:set-highlight="setHighlight" :set-highlight="setHighlight"
:toggle-expanded="toggleExpanded" :toggle-expanded="toggleExpanded"
:simple="treeViewIsSimple" :simple="treeViewIsSimple"
:toggle-thread-display="toggleThreadDisplay" :toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus" :thread-display-status="threadDisplayStatus"
@ -165,7 +192,9 @@
:inline-expanded="collapsable && isExpanded" :inline-expanded="collapsable && isExpanded"
:statusoid="status" :statusoid="status"
:expandable="!isExpanded" :expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" :show-pinned="
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
"
:focused="focused(status.id)" :focused="focused(status.id)"
:in-conversation="isExpanded" :in-conversation="isExpanded"
:highlight="getHighlight()" :highlight="getHighlight()"
@ -173,7 +202,6 @@
:in-profile="inProfile" :in-profile="inProfile"
:profile-user-id="profileUserId" :profile-user-id="profileUserId"
class="conversation-status status-fadein panel-body" class="conversation-status status-fadein panel-body"
:toggle-thread-display="toggleThreadDisplay" :toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus" :thread-display-status="threadDisplayStatus"
:show-thread-recursively="showThreadRecursively" :show-thread-recursively="showThreadRecursively"
@ -182,7 +210,6 @@
:status-content-properties="statusContentProperties" :status-content-properties="statusContentProperties"
:set-status-content-property="setStatusContentProperty" :set-status-content-property="setStatusContentProperty"
:toggle-status-content-property="toggleStatusContentProperty" :toggle-status-content-property="toggleStatusContentProperty"
@goto="setHighlight" @goto="setHighlight"
@toggleExpanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
@ -233,7 +260,8 @@
border-bottom-color: var(--border, $fallback--border); border-bottom-color: var(--border, $fallback--border);
border-radius: 0; border-radius: 0;
/* Make the button stretch along the whole row */ /* Make the button stretch along the whole row */
&, &-inner { &,
&-inner {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
@ -271,7 +299,8 @@
border-left-color: $fallback--cRed; border-left-color: $fallback--cRed;
border-left-color: var(--cRed, $fallback--cRed); border-left-color: var(--cRed, $fallback--cRed);
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); border-radius: 0 0 var(--panelRadius, $fallback--panelRadius)
var(--panelRadius, $fallback--panelRadius);
border-bottom: 1px solid var(--border, $fallback--border); border-bottom: 1px solid var(--border, $fallback--border);
} }

View file

@ -46,42 +46,56 @@ export default {
}, },
data: () => ({ data: () => ({
searchBarHidden: true, searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && ( supportsMask:
window.CSS.supports('mask-size', 'contain') || window.CSS &&
window.CSS.supports &&
(window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') || window.CSS.supports('-webkit-mask-size', 'contain') ||
window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain') window.CSS.supports('-o-mask-size', 'contain')),
),
showingConfirmLogout: false showingConfirmLogout: false
}), }),
computed: { computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, enableMask() {
return this.supportsMask && this.$store.state.instance.logoMask
},
logoStyle() { logoStyle() {
return { return {
'visibility': this.enableMask ? 'hidden' : 'visible' visibility: this.enableMask ? 'hidden' : 'visible'
} }
}, },
logoMaskStyle() { logoMaskStyle() {
return this.enableMask ? { return this.enableMask
? {
'mask-image': `url(${this.$store.state.instance.logo})` 'mask-image': `url(${this.$store.state.instance.logo})`
} : { }
: {
'background-color': this.enableMask ? '' : 'transparent' 'background-color': this.enableMask ? '' : 'transparent'
} }
}, },
logoBgStyle() { logoBgStyle() {
return Object.assign({ return Object.assign(
'margin': `${this.$store.state.instance.logoMargin} 0`, {
margin: `${this.$store.state.instance.logoMargin} 0`,
opacity: this.searchBarHidden ? 1 : 0 opacity: this.searchBarHidden ? 1 : 0
}, this.enableMask ? {} : {
'background-color': this.enableMask ? '' : 'transparent'
})
}, },
logo () { return this.$store.state.instance.logo }, this.enableMask
? {}
: {
'background-color': this.enableMask ? '' : 'transparent'
}
)
},
logo() {
return this.$store.state.instance.logo
},
mergedConfig() { mergedConfig() {
return this.$store.getters.mergedConfig return this.$store.getters.mergedConfig
}, },
sitename () { return this.$store.state.instance.name }, sitename() {
return this.$store.state.instance.name
},
showNavShortcuts() { showNavShortcuts() {
return this.mergedConfig.showNavShortcuts return this.mergedConfig.showNavShortcuts
}, },
@ -94,10 +108,18 @@ export default {
hideSiteName() { hideSiteName() {
return this.mergedConfig.hideSiteName return this.mergedConfig.hideSiteName
}, },
hideSitename () { return this.$store.state.instance.hideSitename }, hideSitename() {
logoLeft () { return this.$store.state.instance.logoLeft }, return this.$store.state.instance.hideSitename
currentUser () { return this.$store.state.users.currentUser }, },
privateMode () { return this.$store.state.instance.private }, logoLeft() {
return this.$store.state.instance.logoLeft
},
currentUser() {
return this.$store.state.users.currentUser
},
privateMode() {
return this.$store.state.instance.private
},
shouldConfirmLogout() { shouldConfirmLogout() {
return this.$store.getters.mergedConfig.modalOnLogout return this.$store.getters.mergedConfig.modalOnLogout
}, },

View file

@ -19,7 +19,7 @@
v-if="!hideSiteFavicon" v-if="!hideSiteFavicon"
class="favicon" class="favicon"
src="/favicon.png" src="/favicon.png"
> />
<span <span
v-if="!hideSiteName" v-if="!hideSiteName"
class="site-name" class="site-name"
@ -91,7 +91,7 @@
<img <img
:src="logo" :src="logo"
:style="logoStyle" :style="logoStyle"
> />
</router-link> </router-link>
<div class="item right actions"> <div class="item right actions">
<search-bar <search-bar
@ -106,7 +106,10 @@
<router-link <router-link
v-if="currentUser" v-if="currentUser"
class="nav-icon" class="nav-icon"
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }" :to="{
name: 'interactions',
params: { username: currentUser.screen_name }
}"
> >
<FAIcon <FAIcon
fixed-width fixed-width
@ -152,7 +155,10 @@
/> />
</button> </button>
<button <button
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'" v-if="
(currentUser && currentUser.role === 'admin') ||
currentUser.role === 'moderator'
"
class="button-unstyled nav-icon" class="button-unstyled nav-icon"
@click.stop="openModModal" @click.stop="openModModal"
> >

View file

@ -31,14 +31,14 @@
.dark-overlay { .dark-overlay {
&::before { &::before {
bottom: 0; bottom: 0;
content: " "; content: ' ';
display: block; display: block;
cursor: default; cursor: default;
left: 0; left: 0;
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
background: rgba(27,31,35,.5); background: rgba(27, 31, 35, 0.5);
z-index: 2000; z-index: 2000;
} }
} }
@ -74,7 +74,7 @@
.dialog-modal-footer { .dialog-modal-footer {
margin: 0; margin: 0;
padding: .5em .5em; padding: 0.5em 0.5em;
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg); background-color: var(--bg, $fallback--bg);
border-top: 1px solid $fallback--border; border-top: 1px solid $fallback--border;
@ -84,9 +84,8 @@
button { button {
width: auto; width: auto;
margin-left: .5rem; margin-left: 0.5rem;
} }
} }
} }
</style> </style>

View file

@ -9,7 +9,7 @@
class="btn button-default" class="btn button-default"
> >
{{ $t('domain_mute_card.unmute') }} {{ $t('domain_mute_card.unmute') }}
<template v-slot:progress> <template #progress>
{{ $t('domain_mute_card.unmute_progress') }} {{ $t('domain_mute_card.unmute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
@ -19,7 +19,7 @@
class="btn button-default" class="btn button-default"
> >
{{ $t('domain_mute_card.mute') }} {{ $t('domain_mute_card.mute') }}
<template v-slot:progress> <template #progress>
{{ $t('domain_mute_card.mute_progress') }} {{ $t('domain_mute_card.mute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>

View file

@ -38,7 +38,9 @@ const EditStatusModal = {
}, },
isFormVisible(val) { isFormVisible(val) {
if (val) { if (val) {
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus()) this.$nextTick(
() => this.$el && this.$el.querySelector('textarea').focus()
)
} }
} }
}, },
@ -55,7 +57,8 @@ const EditStatusModal = {
contentType contentType
} }
return statusPosterService.editStatus(params) return statusPosterService
.editStatus(params)
.then((data) => { .then((data) => {
return data return data
}) })

View file

@ -11,10 +11,10 @@
<PostStatusForm <PostStatusForm
class="panel-body" class="panel-body"
v-bind="params" v-bind="params"
@posted="closeModal" :disable-polls="true"
:disablePolls="true" :disable-visibility-selector="true"
:disableVisibilitySelector="true"
:post-handler="doEditStatus" :post-handler="doEditStatus"
@posted="closeModal"
/> />
</div> </div>
</Modal> </Modal>

View file

@ -4,13 +4,9 @@ import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
faSmileBeam
} from '@fortawesome/free-regular-svg-icons'
library.add( library.add(faSmileBeam)
faSmileBeam
)
/** /**
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs * EmojiInput - augmented inputs for emoji and autocomplete support in inputs
@ -127,25 +123,30 @@ const EmojiInput = {
return this.$store.getters.mergedConfig.padEmoji return this.$store.getters.mergedConfig.padEmoji
}, },
showSuggestions() { showSuggestions() {
return this.focused && return (
this.focused &&
this.suggestions && this.suggestions &&
this.suggestions.length > 0 && this.suggestions.length > 0 &&
!this.showPicker && !this.showPicker &&
!this.temporarilyHideSuggestions !this.temporarilyHideSuggestions
)
}, },
textAtCaret() { textAtCaret() {
return (this.wordAtCaret || {}).word || '' return (this.wordAtCaret || {}).word || ''
}, },
wordAtCaret() { wordAtCaret() {
if (this.modelValue && this.caret) { if (this.modelValue && this.caret) {
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {} const word =
Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
return word return word
} }
} }
}, },
mounted() { mounted() {
const { root } = this.$refs const { root } = this.$refs
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea') const input =
root.querySelector('.emoji-input > input') ||
root.querySelector('.emoji-input > textarea')
if (!input) return if (!input) return
this.input = input this.input = input
this.resize() this.resize()
@ -183,11 +184,12 @@ const EmojiInput = {
// Async: cancel if textAtCaret has changed during wait // Async: cancel if textAtCaret has changed during wait
if (this.textAtCaret !== newWord) return if (this.textAtCaret !== newWord) return
if (matchedSuggestions.length <= 0) return if (matchedSuggestions.length <= 0) return
this.suggestions = take(matchedSuggestions, 5) this.suggestions = take(matchedSuggestions, 5).map(
.map(({ imageUrl, ...rest }) => ({ ({ imageUrl, ...rest }) => ({
...rest, ...rest,
img: imageUrl || '' img: imageUrl || ''
})) })
)
}, },
suggestions: { suggestions: {
handler(newValue) { handler(newValue) {
@ -228,7 +230,11 @@ const EmojiInput = {
} }
}, },
replace(replacement) { replace(replacement) {
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement) const newValue = Completion.replaceWord(
this.modelValue,
this.wordAtCaret,
replacement
)
this.$emit('update:modelValue', newValue) this.$emit('update:modelValue', newValue)
this.caret = 0 this.caret = 0
}, },
@ -251,19 +257,25 @@ const EmojiInput = {
* them, masto seem to be rendering :emoji::emoji: correctly now so why not * them, masto seem to be rendering :emoji::emoji: correctly now so why not
*/ */
const isSpaceRegex = /\s/ const isSpaceRegex = /\s/
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : '' const spaceBefore =
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : '' surroundingSpace &&
!isSpaceRegex.exec(before.slice(-1)) &&
before.length &&
this.padEmoji > 0
? ' '
: ''
const spaceAfter =
surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji
? ' '
: ''
const newValue = [ const newValue = [before, spaceBefore, insertion, spaceAfter, after].join(
before, ''
spaceBefore, )
insertion,
spaceAfter,
after
].join('')
this.keepOpen = keepOpen this.keepOpen = keepOpen
this.$emit('update:modelValue', newValue) this.$emit('update:modelValue', newValue)
const position = this.caret + (insertion + spaceAfter + spaceBefore).length const position =
this.caret + (insertion + spaceAfter + spaceBefore).length
if (!keepOpen) { if (!keepOpen) {
this.input.focus() this.input.focus()
} }
@ -278,9 +290,14 @@ const EmojiInput = {
replaceText(e, suggestion) { replaceText(e, suggestion) {
const len = this.suggestions.length || 0 const len = this.suggestions.length || 0
if (len > 0 || suggestion) { if (len > 0 || suggestion) {
const chosenSuggestion = suggestion || this.suggestions[this.highlighted] const chosenSuggestion =
suggestion || this.suggestions[this.highlighted]
const replacement = chosenSuggestion.replacement const replacement = chosenSuggestion.replacement
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement) const newValue = Completion.replaceWord(
this.modelValue,
this.wordAtCaret,
replacement
)
this.$emit('update:modelValue', newValue) this.$emit('update:modelValue', newValue)
this.highlighted = 0 this.highlighted = 0
const position = this.wordAtCaret.start + replacement.length const position = this.wordAtCaret.start + replacement.length
@ -325,20 +342,22 @@ const EmojiInput = {
* replies in notifs) or mobile post form. Note that getting and setting * replies in notifs) or mobile post form. Note that getting and setting
* scroll is different for `Window` and `Element`s * scroll is different for `Window` and `Element`s
*/ */
const scrollerRef = this.$el.closest('.sidebar-scroller') || const scrollerRef =
this.$el.closest('.sidebar-scroller') ||
this.$el.closest('.post-form-modal-view') || this.$el.closest('.post-form-modal-view') ||
window window
const currentScroll = scrollerRef === window const currentScroll =
? scrollerRef.scrollY scrollerRef === window ? scrollerRef.scrollY : scrollerRef.scrollTop
: scrollerRef.scrollTop const scrollerHeight =
const scrollerHeight = scrollerRef === window scrollerRef === window
? scrollerRef.innerHeight ? scrollerRef.innerHeight
: scrollerRef.offsetHeight : scrollerRef.offsetHeight
const scrollerBottomBorder = currentScroll + scrollerHeight const scrollerBottomBorder = currentScroll + scrollerHeight
// We check where the bottom border of root element is, this uses findOffset // We check where the bottom border of root element is, this uses findOffset
// to find offset relative to scrollable container (scroller) // to find offset relative to scrollable container (scroller)
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top const rootBottomBorder =
rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder) const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
// could also check top delta but there's no case for it // could also check top delta but there's no case for it
@ -490,7 +509,10 @@ const EmojiInput = {
}, },
setPlacement(container, target, offsetBottom) { setPlacement(container, target, offsetBottom) {
if (!container || !target) return if (!container || !target) return
if (this.placement === 'bottom' || (this.placement === 'auto' && !this.overflowsBottom(container))) { if (
this.placement === 'bottom' ||
(this.placement === 'auto' && !this.overflowsBottom(container))
) {
target.style.top = offsetBottom + 'px' target.style.top = offsetBottom + 'px'
target.style.bottom = 'auto' target.style.bottom = 'auto'
} else { } else {

View file

@ -42,11 +42,14 @@
:class="{ highlighted: index === highlighted }" :class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)" @click.stop.prevent="onClick($event, suggestion)"
> >
<span v-if="!suggestion.mfm" class="image"> <span
v-if="!suggestion.mfm"
class="image"
>
<img <img
v-if="suggestion.img" v-if="suggestion.img"
:src="suggestion.img" :src="suggestion.img"
> />
<span v-else>{{ suggestion.replacement }}</span> <span v-else>{{ suggestion.replacement }}</span>
</span> </span>
<div class="label"> <div class="label">
@ -77,7 +80,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
margin: .2em .25em; margin: 0.2em 0.25em;
font-size: 1.3em; font-size: 1.3em;
cursor: pointer; cursor: pointer;
line-height: 24px; line-height: 24px;
@ -93,7 +96,7 @@
margin-top: 2px; margin-top: 2px;
&.hide { &.hide {
display: none display: none;
} }
} }
@ -104,7 +107,7 @@
margin-top: 2px; margin-top: 2px;
&.hide { &.hide {
display: none display: none;
} }
&-body { &-body {
@ -178,7 +181,8 @@
} }
} }
input, textarea { input,
textarea {
flex: 1 0 auto; flex: 1 0 auto;
} }
} }

View file

@ -1,5 +1,26 @@
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4'] const MFM_TAGS = [
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true })) 'blur',
'bounce',
'flip',
'font',
'jelly',
'jump',
'rainbow',
'rotate',
'shake',
'sparkle',
'spin',
'tada',
'twitch',
'x2',
'x3',
'x4'
].map((tag) => ({
displayText: tag,
detailText: '$[' + tag + ' ]',
replacement: '$[' + tag + ' ]',
mfm: true
}))
/** /**
* suggest - generates a suggestor function to be used by emoji-input * suggest - generates a suggestor function to be used by emoji-input
@ -13,10 +34,10 @@ const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow',
* doesn't support user linking you can just provide only emoji. * doesn't support user linking you can just provide only emoji.
*/ */
export default data => { export default (data) => {
const emojiCurry = suggestEmoji(data.emoji) const emojiCurry = suggestEmoji(data.emoji)
const usersCurry = data.store && suggestUsers(data.store) const usersCurry = data.store && suggestUsers(data.store)
return input => { return (input) => {
const firstChar = input[0] const firstChar = input[0]
if (firstChar === ':' && data.emoji) { if (firstChar === ':' && data.emoji) {
return emojiCurry(input) return emojiCurry(input)
@ -25,14 +46,15 @@ export default data => {
return usersCurry(input) return usersCurry(input)
} }
if (firstChar === '$') { if (firstChar === '$') {
return MFM_TAGS return MFM_TAGS.filter(
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1) ({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1
)
} }
return [] return []
} }
} }
export const suggestEmoji = emojis => input => { export const suggestEmoji = (emojis) => (input) => {
const noPrefix = input.toLowerCase().substr(1) const noPrefix = input.toLowerCase().substr(1)
return emojis return emojis
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix)) .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
@ -85,7 +107,7 @@ export const suggestUsers = ({ dispatch, state }) => {
}) })
} }
return async input => { return async (input) => {
const noPrefix = input.toLowerCase().substr(1) const noPrefix = input.toLowerCase().substr(1)
if (previousQuery === noPrefix) return suggestions if (previousQuery === noPrefix) return suggestions
@ -99,11 +121,14 @@ export const suggestUsers = ({ dispatch, state }) => {
await debounceUserSearch(noPrefix) await debounceUserSearch(noPrefix)
} }
const newSuggestions = state.users.users.filter( const newSuggestions = state.users.users
user => .filter(
(user) =>
user.screen_name.toLowerCase().startsWith(noPrefix) || user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix) user.name.toLowerCase().startsWith(noPrefix)
).slice(0, 20).sort((a, b) => { )
.slice(0, 20)
.sort((a, b) => {
let aScore = 0 let aScore = 0
let bScore = 0 let bScore = 0
@ -123,12 +148,20 @@ export const suggestUsers = ({ dispatch, state }) => {
return diff + nameAlphabetically + screenNameAlphabetically return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */ /* eslint-disable camelcase */
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({ })
.map(
({
screen_name,
screen_name_ui,
name,
profile_image_url_original
}) => ({
displayText: screen_name_ui, displayText: screen_name_ui,
detailText: name, detailText: name,
imageUrl: profile_image_url_original, imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' ' replacement: '@' + screen_name + ' '
})) })
)
/* eslint-enable camelcase */ /* eslint-enable camelcase */
suggestions = newSuggestions || [] suggestions = newSuggestions || []

View file

@ -8,11 +8,7 @@ import {
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { trim, escapeRegExp, startCase } from 'lodash' import { trim, escapeRegExp, startCase } from 'lodash'
library.add( library.add(faBoxOpen, faStickyNote, faSmileBeam)
faBoxOpen,
faStickyNote,
faSmileBeam
)
// At widest, approximately 20 emoji are visible in a row, // At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker // loading 3 rows, could be overkill for narrow picker
@ -42,7 +38,9 @@ const EmojiPicker = {
} }
}, },
components: { components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')), StickerPicker: defineAsyncComponent(() =>
import('../sticker_picker/sticker_picker.vue')
),
Checkbox Checkbox
}, },
methods: { methods: {
@ -53,7 +51,9 @@ const EmojiPicker = {
this.$emit('sticker-upload-failed', e) this.$emit('sticker-upload-failed', e)
}, },
onEmoji(emoji) { onEmoji(emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement const value = emoji.imageUrl
? `:${emoji.displayText}:`
: emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen }) this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
}, },
onScroll(e) { onScroll(e) {
@ -93,7 +93,8 @@ const EmojiPicker = {
// Always load when at the very top in case there's no scroll space yet // Always load when at the very top in case there's no scroll space yet
const atTop = scrollerTop < 5 const atTop = scrollerTop < 5
// Don't load when looking at unicode category or at the very bottom // Don't load when looking at unicode category or at the very bottom
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax const bottomAboveViewport =
bottom < scrollerTop || scrollerBottom === scrollerMax
if (!bottomAboveViewport && (approachingBottom || atTop)) { if (!bottomAboveViewport && (approachingBottom || atTop)) {
this.loadEmoji() this.loadEmoji()
} }
@ -101,7 +102,7 @@ const EmojiPicker = {
scrolledGroup(target) { scrolledGroup(target) {
const top = target.scrollTop + 5 const top = target.scrollTop + 5
this.$nextTick(() => { this.$nextTick(() => {
this.emojisView.forEach(group => { this.emojisView.forEach((group) => {
const ref = this.$refs['group-' + group.id] const ref = this.$refs['group-' + group.id]
if (ref.offsetTop <= top) { if (ref.offsetTop <= top) {
this.activeGroup = group.id this.activeGroup = group.id
@ -110,7 +111,8 @@ const EmojiPicker = {
}) })
}, },
loadEmoji() { loadEmoji() {
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length const allLoaded =
this.customEmojiBuffer.length === this.filteredEmoji.length
if (allLoaded) { if (allLoaded) {
return return
@ -141,8 +143,11 @@ const EmojiPicker = {
filterByKeyword(list) { filterByKeyword(list) {
if (this.keyword === '') return list if (this.keyword === '') return list
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i') const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
return list.filter(emoji => { return list.filter((emoji) => {
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword)) return (
regex.test(emoji.displayText) ||
(!emoji.imageUrl && emoji.replacement === this.keyword)
)
}) })
} }
}, },
@ -164,9 +169,7 @@ const EmojiPicker = {
return 0 return 0
}, },
filteredEmoji() { filteredEmoji() {
return this.filterByKeyword( return this.filterByKeyword(this.$store.state.instance.customEmoji || [])
this.$store.state.instance.customEmoji || []
)
}, },
customEmojiBuffer() { customEmojiBuffer() {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice) return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
@ -209,17 +212,20 @@ const EmojiPicker = {
}, },
emojisView() { emojisView() {
if (this.keyword === '') { if (this.keyword === '') {
return this.emojis.filter(pack => { return this.emojis.filter((pack) => {
return pack.id === this.activeGroup return pack.id === this.activeGroup
}) })
} else { } else {
return this.emojis.filter(pack => { return this.emojis.filter((pack) => {
return pack.emojis.length > 0 return pack.emojis.length > 0
}) })
} }
}, },
stickerPickerEnabled() { stickerPickerEnabled() {
return (this.$store.state.instance.stickers || []).length !== 0 && this.enableStickerPicker return (
(this.$store.state.instance.stickers || []).length !== 0 &&
this.enableStickerPicker
)
} }
} }
} }

View file

@ -2,9 +2,9 @@
<div class="emoji-picker panel panel-default panel-body"> <div class="emoji-picker panel panel-default panel-body">
<div class="heading"> <div class="heading">
<span <span
ref="emoji-tabs"
class="emoji-tabs" class="emoji-tabs"
@wheel="onWheel" @wheel="onWheel"
ref="emoji-tabs"
> >
<span <span
v-for="group in emojis" v-for="group in emojis"
@ -17,11 +17,13 @@
:title="group.text" :title="group.text"
@click.prevent="highlight(group.id)" @click.prevent="highlight(group.id)"
> >
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span> <span v-if="!group.first.imageUrl">{{
group.first.replacement
}}</span>
<img <img
v-else v-else
:src="group.first.imageUrl" :src="group.first.imageUrl"
> />
</span> </span>
<span <span
v-if="stickerPickerEnabled" v-if="stickerPickerEnabled"
@ -49,7 +51,7 @@
class="form-control" class="form-control"
:placeholder="$t('emoji.search_emoji')" :placeholder="$t('emoji.search_emoji')"
@input="$event.target.composing = false" @input="$event.target.composing = false"
> />
</div> </div>
<div <div
ref="emoji-groups" ref="emoji-groups"
@ -79,7 +81,7 @@
<img <img
v-else v-else
:src="emoji.imageUrl" :src="emoji.imageUrl"
> />
</span> </span>
<span :ref="'group-end-' + group.id" /> <span :ref="'group-end-' + group.id" />
</div> </div>

View file

@ -23,7 +23,9 @@ const EmojiReactions = {
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF) : this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
}, },
showMoreString() { showMoreString() {
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}` return `+${
this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF
}`
}, },
accountsForEmoji() { accountsForEmoji() {
return this.status.emoji_reactions.reduce((acc, reaction) => { return this.status.emoji_reactions.reduce((acc, reaction) => {
@ -44,10 +46,10 @@ const EmojiReactions = {
this.showAll = !this.showAll this.showAll = !this.showAll
}, },
reactedWith(emoji) { reactedWith(emoji) {
return this.status.emoji_reactions.find(r => r.name === emoji).me return this.status.emoji_reactions.find((r) => r.name === emoji).me
}, },
fetchEmojiReactionsByIfMissing() { fetchEmojiReactionsByIfMissing() {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts) const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
if (hasNoAccounts) { if (hasNoAccounts) {
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id) this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
} }

View file

@ -1,28 +1,35 @@
<template> <template>
<div class="emoji-reactions"> <div class="emoji-reactions">
<UserListPopover <UserListPopover
v-for="(reaction) in emojiReactions" v-for="reaction in emojiReactions"
:key="reaction.url || reaction.name" :key="reaction.url || reaction.name"
:users="accountsForEmoji[reaction.url || reaction.name]" :users="accountsForEmoji[reaction.url || reaction.name]"
> >
<button <button
class="emoji-reaction btn button-default" class="emoji-reaction btn button-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" :class="{
'picked-reaction': reactedWith(reaction.name),
'not-clickable': !loggedIn
}"
@click="emojiOnClick(reaction.name, $event)" @click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()" @mouseenter="fetchEmojiReactionsByIfMissing()"
> >
<span <span
v-if="reaction.url !== null" v-if="reaction.url !== null"
class="emoji-button-inner"
> >
<img <img
:src="reaction.url" :src="reaction.url"
:title="reaction.name" :title="reaction.name"
class="reaction-emoji" class="reaction-emoji"
width="2.55em" width="2.55em"
> />
{{ reaction.count }} {{ reaction.count }}
</span> </span>
<span v-else> <span
v-else
class="emoji-button-inner"
>
<span class="reaction-emoji unicode-emoji"> <span class="reaction-emoji unicode-emoji">
{{ reaction.name }} {{ reaction.name }}
</span> </span>
@ -52,7 +59,7 @@
} }
.unicode-emoji { .unicode-emoji {
font-size: 210%; font-size: 128%;
} }
.emoji-reaction { .emoji-reaction {
@ -60,13 +67,17 @@
margin-right: 0.5em; margin-right: 0.5em;
margin-top: 0.5em; margin-top: 0.5em;
display: flex; display: flex;
height: 28px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-sizing: border-box; box-sizing: border-box;
.reaction-emoji { .reaction-emoji {
width: 2.55em !important;
margin-right: 0.25em; margin-right: 0.25em;
} }
img.reaction-emoji {
width: 1.55em !important;
display: block;
}
&:focus { &:focus {
outline: none; outline: none;
} }
@ -93,9 +104,12 @@
} }
.button-default.picked-reaction { .button-default.picked-reaction {
border: 1px solid var(--accent, $fallback--link); background: none;
margin-left: -1px; // offset the border, can't use inset shadows either padding: 1px 0.5em;
margin-right: calc(0.5em - 1px);
}
.emoji-button-inner {
display: flex;
align-items: center;
}
}
</style> </style>

View file

@ -1,9 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faCircleNotch)
faCircleNotch
)
const Exporter = { const Exporter = {
props: { props: {
@ -26,17 +24,21 @@ const Exporter = {
methods: { methods: {
process() { process() {
this.processing = true this.processing = true
this.getContent() this.getContent().then((content) => {
.then((content) => {
const fileToDownload = document.createElement('a') const fileToDownload = document.createElement('a')
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content)) fileToDownload.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(content)
)
fileToDownload.setAttribute('download', this.filename) fileToDownload.setAttribute('download', this.filename)
fileToDownload.style.display = 'none' fileToDownload.style.display = 'none'
document.body.appendChild(fileToDownload) document.body.appendChild(fileToDownload)
fileToDownload.click() fileToDownload.click()
document.body.removeChild(fileToDownload) document.body.removeChild(fileToDownload)
// Add delay before hiding processing state since browser takes some time to handle file download // Add delay before hiding processing state since browser takes some time to handle file download
setTimeout(() => { this.processing = false }, 2000) setTimeout(() => {
this.processing = false
}, 2000)
}) })
} }
} }

View file

@ -62,54 +62,75 @@ const ExtraButtons = {
}, },
translateStatus() { translateStatus() {
if (this.noTranslationTargetSet) { if (this.noTranslationTargetSet) {
this.$store.dispatch('pushGlobalNotice', { messageKey: 'toast.no_translation_target_set', level: 'info' }) this.$store.dispatch('pushGlobalNotice', {
messageKey: 'toast.no_translation_target_set',
level: 'info'
})
} }
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage const translateTo =
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo }) this.$store.getters.mergedConfig.translationLanguage ||
this.$store.state.instance.interfaceLanguage
this.$store
.dispatch('translateStatus', {
id: this.status.id,
language: translateTo
})
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
pinStatus() { pinStatus() {
this.$store.dispatch('pinStatus', this.status.id) this.$store
.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
unpinStatus() { unpinStatus() {
this.$store.dispatch('unpinStatus', this.status.id) this.$store
.dispatch('unpinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
muteConversation() { muteConversation() {
this.$store.dispatch('muteConversation', this.status.id) this.$store
.dispatch('muteConversation', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
unmuteConversation() { unmuteConversation() {
this.$store.dispatch('unmuteConversation', this.status.id) this.$store
.dispatch('unmuteConversation', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
copyLink() { copyLink() {
navigator.clipboard.writeText(this.statusLink) navigator.clipboard
.writeText(this.statusLink)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
bookmarkStatus() { bookmarkStatus() {
this.$store.dispatch('bookmark', { id: this.status.id }) this.$store
.dispatch('bookmark', { id: this.status.id })
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
unbookmarkStatus() { unbookmarkStatus() {
this.$store.dispatch('unbookmark', { id: this.status.id }) this.$store
.dispatch('unbookmark', { id: this.status.id })
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
reportStatus() { reportStatus() {
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] }) this.$store.dispatch('openUserReportingModal', {
userId: this.status.user.id,
statusIds: [this.status.id]
})
}, },
editStatus() { editStatus() {
this.$store.dispatch('fetchStatusSource', { id: this.status.id }) this.$store
.then(data => this.$store.dispatch('openEditStatusModal', { .dispatch('fetchStatusSource', { id: this.status.id })
.then((data) =>
this.$store.dispatch('openEditStatusModal', {
statusId: this.status.id, statusId: this.status.id,
subject: data.spoiler_text, subject: data.spoiler_text,
statusText: data.text, statusText: data.text,
@ -118,12 +139,23 @@ const ExtraButtons = {
statusFiles: [...this.status.attachments], statusFiles: [...this.status.attachments],
visibility: this.status.visibility, visibility: this.status.visibility,
statusContentType: data.content_type statusContentType: data.content_type
})) })
)
}, },
showStatusHistory() { showStatusHistory() {
const originalStatus = { ...this.status } const originalStatus = { ...this.status }
const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html'] const stripFieldsList = [
stripFieldsList.forEach(p => delete originalStatus[p]) 'attachments',
'created_at',
'emojis',
'text',
'raw_html',
'nsfw',
'poll',
'summary',
'summary_raw_html'
]
stripFieldsList.forEach((p) => delete originalStatus[p])
this.$store.dispatch('openStatusHistoryModal', originalStatus) this.$store.dispatch('openStatusHistoryModal', originalStatus)
}, },
redraftStatus() { redraftStatus() {
@ -134,8 +166,10 @@ const ExtraButtons = {
} }
}, },
doRedraftStatus() { doRedraftStatus() {
this.$store.dispatch('fetchStatusSource', { id: this.status.id }) this.$store
.then(data => this.$store.dispatch('openPostStatusModal', { .dispatch('fetchStatusSource', { id: this.status.id })
.then((data) =>
this.$store.dispatch('openPostStatusModal', {
isRedraft: true, isRedraft: true,
statusId: this.status.id, statusId: this.status.id,
subject: data.spoiler_text, subject: data.spoiler_text,
@ -145,7 +179,8 @@ const ExtraButtons = {
statusFiles: [...this.status.attachments], statusFiles: [...this.status.attachments],
statusScope: this.status.visibility, statusScope: this.status.visibility,
statusContentType: data.content_type statusContentType: data.content_type
})) })
)
this.doDeleteStatus() this.doDeleteStatus()
}, },
showRedraftStatusConfirmDialog() { showRedraftStatusConfirmDialog() {
@ -156,17 +191,26 @@ const ExtraButtons = {
} }
}, },
computed: { computed: {
currentUser () { return this.$store.state.users.currentUser }, currentUser() {
return this.$store.state.users.currentUser
},
canDelete() { canDelete() {
if (!this.currentUser) { return } if (!this.currentUser) {
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin return
}
const superuser =
this.currentUser.rights.moderator || this.currentUser.rights.admin
return superuser || this.status.user.id === this.currentUser.id return superuser || this.status.user.id === this.currentUser.id
}, },
ownStatus() { ownStatus() {
return this.status.user.id === this.currentUser.id return this.status.user.id === this.currentUser.id
}, },
canPin() { canPin() {
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted') return (
this.ownStatus &&
(this.status.visibility === 'public' ||
this.status.visibility === 'unlisted')
)
}, },
canMute() { canMute() {
return !!this.currentUser return !!this.currentUser
@ -179,7 +223,12 @@ const ExtraButtons = {
}, },
statusLink() { statusLink() {
if (this.status.is_local) { if (this.status.is_local) {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` return `${this.$store.state.instance.server}${
this.$router.resolve({
name: 'conversation',
params: { id: this.status.id }
}).href
}`
} else { } else {
return this.status.external_url return this.status.external_url
} }
@ -190,7 +239,9 @@ const ExtraButtons = {
isEdited() { isEdited() {
return this.status.edited_at !== null return this.status.edited_at !== null
}, },
editingAvailable () { return this.$store.state.instance.editingAvailable } editingAvailable() {
return this.$store.state.instance.editingAvailable
}
} }
} }

View file

@ -7,7 +7,7 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template v-slot:content="{close}"> <template #content="{ close }">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
v-if="canMute && !status.thread_muted" v-if="canMute && !status.thread_muted"
@ -17,7 +17,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="eye-slash" icon="eye-slash"
/><span>{{ $t("status.mute_conversation") }}</span> /><span>{{ $t('status.mute_conversation') }}</span>
</button> </button>
<button <button
v-if="canMute && status.thread_muted" v-if="canMute && status.thread_muted"
@ -27,7 +27,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="eye-slash" icon="eye-slash"
/><span>{{ $t("status.unmute_conversation") }}</span> /><span>{{ $t('status.unmute_conversation') }}</span>
</button> </button>
<button <button
v-if="!status.pinned && canPin" v-if="!status.pinned && canPin"
@ -38,7 +38,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="thumbtack" icon="thumbtack"
/><span>{{ $t("status.pin") }}</span> /><span>{{ $t('status.pin') }}</span>
</button> </button>
<button <button
v-if="status.pinned && canPin" v-if="status.pinned && canPin"
@ -49,7 +49,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="thumbtack" icon="thumbtack"
/><span>{{ $t("status.unpin") }}</span> /><span>{{ $t('status.unpin') }}</span>
</button> </button>
<button <button
v-if="!status.bookmarked" v-if="!status.bookmarked"
@ -60,7 +60,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
:icon="['far', 'bookmark']" :icon="['far', 'bookmark']"
/><span>{{ $t("status.bookmark") }}</span> /><span>{{ $t('status.bookmark') }}</span>
</button> </button>
<button <button
v-if="status.bookmarked" v-if="status.bookmarked"
@ -71,7 +71,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="bookmark" icon="bookmark"
/><span>{{ $t("status.unbookmark") }}</span> /><span>{{ $t('status.unbookmark') }}</span>
</button> </button>
<button <button
v-if="ownStatus && editingAvailable" v-if="ownStatus && editingAvailable"
@ -82,7 +82,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="pen" icon="pen"
/><span>{{ $t("status.edit") }}</span> /><span>{{ $t('status.edit') }}</span>
</button> </button>
<button <button
v-if="isEdited && editingAvailable" v-if="isEdited && editingAvailable"
@ -93,7 +93,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="history" icon="history"
/><span>{{ $t("status.edit_history") }}</span> /><span>{{ $t('status.edit_history') }}</span>
</button> </button>
<button <button
v-if="ownStatus" v-if="ownStatus"
@ -104,7 +104,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="file-pen" icon="file-pen"
/><span>{{ $t("status.redraft") }}</span> /><span>{{ $t('status.redraft') }}</span>
</button> </button>
<button <button
v-if="canDelete" v-if="canDelete"
@ -115,7 +115,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="times" icon="times"
/><span>{{ $t("status.delete") }}</span> /><span>{{ $t('status.delete') }}</span>
</button> </button>
<button <button
class="button-default dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@ -125,7 +125,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="share-alt" icon="share-alt"
/><span>{{ $t("status.copy_link") }}</span> /><span>{{ $t('status.copy_link') }}</span>
</button> </button>
<a <a
v-if="!status.is_local" v-if="!status.is_local"
@ -137,7 +137,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="external-link-alt" icon="external-link-alt"
/><span>{{ $t("status.external_source") }}</span> /><span>{{ $t('status.external_source') }}</span>
</a> </a>
<button <button
class="button-default dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@ -147,7 +147,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
:icon="['far', 'flag']" :icon="['far', 'flag']"
/><span>{{ $t("user_card.report") }}</span> /><span>{{ $t('user_card.report') }}</span>
</button> </button>
<button <button
v-if="canTranslate" v-if="canTranslate"
@ -158,7 +158,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="globe" icon="globe"
/><span>{{ $t("status.translate") }}</span> /><span>{{ $t('status.translate') }}</span>
<template v-if="noTranslationTargetSet"> <template v-if="noTranslationTargetSet">
<span class="dropdown-item-icon__badge warning"> <span class="dropdown-item-icon__badge warning">
@ -172,7 +172,7 @@
</button> </button>
</div> </div>
</template> </template>
<template v-slot:trigger> <template #trigger>
<button class="button-unstyled popover-trigger"> <button class="button-unstyled popover-trigger">
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"

View file

@ -1,14 +1,9 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faStar } from '@fortawesome/free-solid-svg-icons' import { faStar } from '@fortawesome/free-solid-svg-icons'
import { import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons'
faStar as faStarRegular
} from '@fortawesome/free-regular-svg-icons'
library.add( library.add(faStar, faStarRegular)
faStar,
faStarRegular
)
const FavoriteButton = { const FavoriteButton = {
props: ['status', 'loggedIn'], props: ['status', 'loggedIn'],
@ -33,7 +28,9 @@ const FavoriteButton = {
computed: { computed: {
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
remoteInteractionLink() { remoteInteractionLink() {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id }) return this.$store.getters.remoteInteractionLink({
statusId: this.status.id
})
} }
} }
} }

View file

@ -56,6 +56,7 @@
.interactive { .interactive {
.svg-inline--fa { .svg-inline--fa {
animation-duration: 0.6s; animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
} }
&:hover .svg-inline--fa, &:hover .svg-inline--fa,

View file

@ -2,10 +2,20 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
const FeaturesPanel = { const FeaturesPanel = {
computed: { computed: {
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, whoToFollow: function () {
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable }, return this.$store.state.instance.suggestionsEnabled
textlimit: function () { return this.$store.state.instance.textlimit }, },
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) } mediaProxy: function () {
return this.$store.state.instance.mediaProxyAvailable
},
textlimit: function () {
return this.$store.state.instance.textlimit
},
uploadlimit: function () {
return fileSizeFormatService.fileSizeFormat(
this.$store.state.instance.uploadlimit
)
}
} }
} }

View file

@ -16,7 +16,10 @@
</li> </li>
<li>{{ $t('features_panel.scope_options') }}</li> <li>{{ $t('features_panel.scope_options') }}</li>
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li> <li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
<li>{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }} {{ $t('upload.file_size_units.' + uploadlimit.unit) }}</li> <li>
{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }}
{{ $t('upload.file_size_units.' + uploadlimit.unit) }}
</li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -5,10 +5,7 @@ import {
faExclamationTriangle faExclamationTriangle
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faStop, faExclamationTriangle)
faStop,
faExclamationTriangle
)
const Flash = { const Flash = {
props: ['src'], props: ['src'],
@ -32,9 +29,12 @@ const Flash = {
container.appendChild(player) container.appendChild(player)
player.style.width = '100%' player.style.width = '100%'
player.style.height = '100%' player.style.height = '100%'
player.load(this.src).then(() => { player
.load(this.src)
.then(() => {
this.player = true this.player = true
}).catch((e) => { })
.catch((e) => {
console.error('Error loading ruffle', e) console.error('Error loading ruffle', e)
this.player = 'error' this.player = 'error'
}) })

View file

@ -1,5 +1,8 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import {
requestFollow,
requestUnfollow
} from '../../services/follow_manipulate/follow_manipulate'
export default { export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
components: { components: {
@ -50,7 +53,9 @@ export default {
this.showingConfirmUnfollow = false this.showingConfirmUnfollow = false
}, },
onClick() { onClick() {
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() this.relationship.following || this.relationship.requested
? this.unfollow()
: this.follow()
}, },
follow() { follow() {
this.inProgress = true this.inProgress = true
@ -70,7 +75,10 @@ export default {
this.inProgress = true this.inProgress = true
requestUnfollow(this.relationship.id, store).then(() => { requestUnfollow(this.relationship.id, store).then(() => {
this.inProgress = false this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id }) store.commit('removeStatus', {
timeline: 'friends',
userId: this.relationship.id
})
}) })
this.hideConfirmUnfollow() this.hideConfirmUnfollow()

View file

@ -21,9 +21,7 @@
tag="span" tag="span"
> >
<template #user> <template #user>
<span <span v-text="user.screen_name_ui" />
v-text="user.screen_name_ui"
/>
</template> </template>
</i18n-t> </i18n-t>
</confirm-modal> </confirm-modal>

View file

@ -4,10 +4,7 @@ import FollowButton from '../follow_button/follow_button.vue'
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue' import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
const FollowCard = { const FollowCard = {
props: [ props: ['user', 'noFollowsYou'],
'user',
'noFollowsYou'
],
components: { components: {
BasicUserCard, BasicUserCard,
RemoteFollow, RemoteFollow,

View file

@ -17,7 +17,9 @@ const FollowRequestCard = {
methods: { methods: {
findFollowRequestNotificationId() { findFollowRequestNotificationId() {
const notif = notificationsFromStore(this.$store).find( const notif = notificationsFromStore(this.$store).find(
(notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request' (notif) =>
notif.from_profile.id === this.user.id &&
notif.type === 'follow_request'
) )
return notif && notif.id return notif && notif.id
}, },
@ -48,7 +50,7 @@ const FollowRequestCard = {
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId }) this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
this.$store.dispatch('updateNotification', { this.$store.dispatch('updateNotification', {
id: notifId, id: notifId,
updater: notification => { updater: (notification) => {
notification.type = 'follow' notification.type = 'follow'
} }
}) })
@ -63,7 +65,8 @@ const FollowRequestCard = {
}, },
doDeny() { doDeny() {
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) this.$store.state.api.backendInteractor
.denyUser({ id: this.user.id })
.then(() => { .then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId }) this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)

View file

@ -5,9 +5,7 @@ export default {
components: { components: {
Select Select
}, },
props: [ props: ['name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'],
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
],
emits: ['update:modelValue'], emits: ['update:modelValue'],
data() { data() {
return { return {
@ -19,7 +17,7 @@ export default {
'serif', 'serif',
'monospace', 'monospace',
'sans-serif' 'sans-serif'
].filter(_ => _) ].filter((_) => _)
} }
}, },
beforeUpdate() { beforeUpdate() {
@ -46,10 +44,12 @@ export default {
}, },
preset: { preset: {
get() { get() {
if (this.family === 'serif' || if (
this.family === 'serif' ||
this.family === 'sans-serif' || this.family === 'sans-serif' ||
this.family === 'monospace' || this.family === 'monospace' ||
this.family === 'inherit') { this.family === 'inherit'
) {
return this.family return this.family
} else { } else {
return 'custom' return 'custom'

View file

@ -15,8 +15,13 @@
class="opt exlcude-disabled" class="opt exlcude-disabled"
type="checkbox" type="checkbox"
:checked="present" :checked="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @change="
> $emit(
'update:modelValue',
typeof modelValue === 'undefined' ? fallback : undefined
)
"
/>
<label <label
v-if="typeof fallback !== 'undefined'" v-if="typeof fallback !== 'undefined'"
class="opt-l" class="opt-l"
@ -43,7 +48,7 @@
v-model="family" v-model="family"
class="custom-font" class="custom-font"
type="text" type="text"
> />
</div> </div>
</template> </template>

View file

@ -4,7 +4,9 @@ const FriendsTimeline = {
Timeline Timeline
}, },
computed: { computed: {
timeline () { return this.$store.state.statuses.timelines.friends } timeline() {
return this.$store.state.statuses.timelines.friends
}
} }
} }

View file

@ -29,35 +29,54 @@ const Gallery = {
if (!this.attachments) { if (!this.attachments) {
return [] return []
} }
const attachments = this.limit > 0 const attachments =
this.limit > 0
? this.attachments.slice(0, this.limit) ? this.attachments.slice(0, this.limit)
: this.attachments : this.attachments
if (this.size === 'hide') { if (this.size === 'hide') {
return attachments.map(item => ({ minimal: true, items: [item] })) return attachments.map((item) => ({ minimal: true, items: [item] }))
} }
const rows = this.grid const rows = this.grid
? [{ grid: true, items: attachments }] ? [{ grid: true, items: attachments }]
: attachments.reduce((acc, attachment, i) => { : attachments
.reduce(
(acc, attachment, i) => {
if (attachment.mimetype.includes('audio')) { if (attachment.mimetype.includes('audio')) {
return [...acc, { audio: true, items: [attachment] }, { items: [] }] return [
...acc,
{ audio: true, items: [attachment] },
{ items: [] }
]
} }
if (!( if (
!(
attachment.mimetype.includes('image') || attachment.mimetype.includes('image') ||
attachment.mimetype.includes('video') || attachment.mimetype.includes('video') ||
attachment.mimetype.includes('flash') attachment.mimetype.includes('flash')
)) { )
return [...acc, { minimal: true, items: [attachment] }, { items: [] }] ) {
return [
...acc,
{ minimal: true, items: [attachment] },
{ items: [] }
]
} }
const maxPerRow = 3 const maxPerRow = 3
const attachmentsRemaining = this.attachments.length - i + 1 const attachmentsRemaining = this.attachments.length - i + 1
const currentRow = acc[acc.length - 1].items const currentRow = acc[acc.length - 1].items
currentRow.push(attachment) currentRow.push(attachment)
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) { if (
currentRow.length >= maxPerRow &&
attachmentsRemaining > maxPerRow
) {
return [...acc, { items: [] }] return [...acc, { items: [] }]
} else { } else {
return acc return acc
} }
}, [{ items: [] }]).filter(_ => _.items.length > 0) },
[{ items: [] }]
)
.filter((_) => _.items.length > 0)
return rows return rows
}, },
attachmentsDimensionalScore() { attachmentsDimensionalScore() {
@ -91,11 +110,11 @@ const Gallery = {
if (row.audio) { if (row.audio) {
return { 'padding-bottom': '25%' } // fixed reduced height for audio return { 'padding-bottom': '25%' } // fixed reduced height for audio
} else if (!row.minimal && !row.grid) { } else if (!row.minimal && !row.grid) {
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` } return { 'padding-bottom': `${100 / (row.items.length + 0.6)}%` }
} }
}, },
itemStyle(id, row) { itemStyle(id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id)) const total = sumBy(row, (item) => this.getAspectRatio(item.id))
return { flex: `${this.getAspectRatio(id) / total} 1 0%` } return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
}, },
getAspectRatio(id) { getAspectRatio(id) {

View file

@ -25,11 +25,20 @@
:size="size" :size="size"
:editable="editable" :editable="editable"
:remove="removeAttachment" :remove="removeAttachment"
:shift-up="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment" :shift-up="
:shift-dn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment" !(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment
"
:shift-dn="
!(
attachmentIndex === row.items.length - 1 &&
rowIndex === rows.length - 1
) && shiftDnAttachment
"
:edit="editAttachment" :edit="editAttachment"
:description="descriptions && descriptions[attachment.id]" :description="descriptions && descriptions[attachment.id]"
:hide-description="size === 'small' || tooManyAttachments && hidingLong" :hide-description="
size === 'small' || (tooManyAttachments && hidingLong)
"
:style="itemStyle(attachment.id, row.items)" :style="itemStyle(attachment.id, row.items)"
@setMedia="onMedia" @setMedia="onMedia"
@naturalSizeLoad="onNaturalSizeLoad" @naturalSizeLoad="onNaturalSizeLoad"
@ -42,7 +51,7 @@
class="many-attachments" class="many-attachments"
> >
<div class="many-attachments-text"> <div class="many-attachments-text">
{{ $t("status.many_attachments", { number: attachments.length }) }} {{ $t('status.many_attachments', { number: attachments.length }) }}
</div> </div>
<div class="many-attachments-buttons"> <div class="many-attachments-buttons">
<span <span
@ -53,7 +62,7 @@
class="button-unstyled -link" class="button-unstyled -link"
@click="toggleHidingLong(true)" @click="toggleHidingLong(true)"
> >
{{ $t("status.collapse_attachments") }} {{ $t('status.collapse_attachments') }}
</button> </button>
</span> </span>
<span <span
@ -64,7 +73,7 @@
class="button-unstyled -link" class="button-unstyled -link"
@click="toggleHidingLong(false)" @click="toggleHidingLong(false)"
> >
{{ $t("status.show_all_attachments") }} {{ $t('status.show_all_attachments') }}
</button> </button>
</span> </span>
<span <span
@ -75,7 +84,7 @@
class="button-unstyled -link" class="button-unstyled -link"
@click="openGallery" @click="openGallery"
> >
{{ $t("status.open_gallery") }} {{ $t('status.open_gallery') }}
</button> </button>
</span> </span>
</div> </div>
@ -83,7 +92,7 @@
</div> </div>
</template> </template>
<script src='./gallery.js'></script> <script src="./gallery.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@ -109,8 +118,8 @@
.gallery-rows { .gallery-rows {
max-height: 25em; max-height: 25em;
overflow: hidden; overflow: hidden;
mask: mask: linear-gradient(to top, white, transparent) bottom/100% 70px
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, no-repeat,
linear-gradient(to top, white, white); linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */ /* Autoprefixed seem to ignore this one, and also syntax is different */

View file

@ -1,11 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faTimes } from '@fortawesome/free-solid-svg-icons'
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faTimes)
faTimes
)
const GlobalNoticeList = { const GlobalNoticeList = {
computed: { computed: {

View file

@ -1,7 +1,5 @@
<template> <template>
<span <span class="HashtagLink">
class="HashtagLink"
>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<a <a
:href="url" :href="url"

View file

@ -1,13 +1,9 @@
import Cropper from 'cropperjs' import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css' import 'cropperjs/dist/cropper.css'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
faCircleNotch
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faCircleNotch)
faCircleNotch
)
const ImageCropper = { const ImageCropper = {
props: { props: {
@ -59,7 +55,10 @@ const ImageCropper = {
return this.saveButtonLabel || this.$t('image_cropper.save') return this.saveButtonLabel || this.$t('image_cropper.save')
}, },
saveWithoutCroppingText() { saveWithoutCroppingText() {
return this.saveWithoutCroppingButtonlabel || this.$t('image_cropper.save_without_cropping') return (
this.saveWithoutCroppingButtonlabel ||
this.$t('image_cropper.save_without_cropping')
)
}, },
cancelText() { cancelText() {
return this.cancelButtonLabel || this.$t('image_cropper.cancel') return this.cancelButtonLabel || this.$t('image_cropper.cancel')
@ -89,7 +88,9 @@ const ImageCropper = {
this.cropper = new Cropper(this.$refs.img, this.cropperOptions) this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
}, },
getTriggerDOM() { getTriggerDOM() {
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger) return typeof this.trigger === 'object'
? this.trigger
: document.querySelector(this.trigger)
}, },
readFile() { readFile() {
const fileInput = this.$refs.input const fileInput = this.$refs.input

View file

@ -7,7 +7,7 @@
:src="dataUrl" :src="dataUrl"
alt="" alt=""
@load.stop="createCropper" @load.stop="createCropper"
> />
</div> </div>
<div class="image-cropper-buttons-wrapper"> <div class="image-cropper-buttons-wrapper">
<button <button
@ -43,7 +43,7 @@
type="file" type="file"
class="image-cropper-img-input" class="image-cropper-img-input"
:accept="mimes" :accept="mimes"
> />
</div> </div>
</template> </template>

View file

@ -1,13 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faCircleNotch, faTimes } from '@fortawesome/free-solid-svg-icons'
faCircleNotch,
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faCircleNotch, faTimes)
faCircleNotch,
faTimes
)
const Importer = { const Importer = {
props: { props: {
@ -35,9 +29,15 @@ const Importer = {
this.dismiss() this.dismiss()
this.submitting = true this.submitting = true
this.submitHandler(this.file) this.submitHandler(this.file)
.then(() => { this.success = true }) .then(() => {
.catch(() => { this.error = true }) this.success = true
.finally(() => { this.submitting = false }) })
.catch(() => {
this.error = true
})
.finally(() => {
this.submitting = false
})
}, },
dismiss() { dismiss() {
this.success = false this.success = false

View file

@ -5,7 +5,7 @@
ref="input" ref="input"
type="file" type="file"
@change="change" @change="change"
> />
</form> </form>
<FAIcon <FAIcon
v-if="submitting" v-if="submitting"
@ -25,9 +25,7 @@
class="button-unstyled" class="button-unstyled"
@click="dismiss" @click="dismiss"
> >
<FAIcon <FAIcon icon="times" />
icon="times"
/>
</button> </button>
{{ ' ' }} {{ ' ' }}
<span>{{ successMessage || $t('importer.success') }}</span> <span>{{ successMessage || $t('importer.success') }}</span>
@ -37,9 +35,7 @@
class="button-unstyled" class="button-unstyled"
@click="dismiss" @click="dismiss"
> >
<FAIcon <FAIcon icon="times" />
icon="times"
/>
</button> </button>
{{ ' ' }} {{ ' ' }}
<span>{{ errorMessage || $t('importer.error') }}</span> <span>{{ errorMessage || $t('importer.error') }}</span>

View file

@ -11,7 +11,8 @@ const tabModeDict = {
const Interactions = { const Interactions = {
data() { data() {
return { return {
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, allowFollowingMove:
this.$store.state.users.currentUser.allow_following_move,
filterMode: tabModeDict['mentions'] filterMode: tabModeDict['mentions']
} }
}, },

View file

@ -2,7 +2,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="title"> <div class="title">
{{ $t("nav.interactions") }} {{ $t('nav.interactions') }}
</div> </div>
</div> </div>
<tab-switcher <tab-switcher

View file

@ -56,7 +56,9 @@ export default {
}, },
controlledLanguage: { controlledLanguage: {
get: function () { return this.language }, get: function () {
return this.language
},
set: function (val) { set: function (val) {
this.setLanguage(val) this.setLanguage(val)
} }

View file

@ -2,11 +2,7 @@ import { mapGetters } from 'vuex'
const LinkPreview = { const LinkPreview = {
name: 'LinkPreview', name: 'LinkPreview',
props: [ props: ['card', 'size', 'nsfw'],
'card',
'size',
'nsfw'
],
data() { data() {
return { return {
imageLoaded: false imageLoaded: false
@ -28,9 +24,7 @@ const LinkPreview = {
hideNsfwConfig() { hideNsfwConfig() {
return this.mergedConfig.hideNsfw return this.mergedConfig.hideNsfw
}, },
...mapGetters([ ...mapGetters(['mergedConfig'])
'mergedConfig'
])
}, },
created() { created() {
if (this.useImage) { if (this.useImage) {

View file

@ -10,21 +10,24 @@
v-if="useImage && imageLoaded" v-if="useImage && imageLoaded"
class="card-image" class="card-image"
> >
<img :src="card.image"> <img :src="card.image" />
</div> </div>
<div class="card-content"> <div class="card-content">
<span class="card-host faint"> <span class="card-host faint">
<span <span
v-if="censored" v-if="censored"
class="nsfw-alert alert warning" class="nsfw-alert alert warning"
>{{ $t('status.nsfw') }}</span> >{{ $t('status.nsfw') }}</span
>
{{ card.provider_name }} {{ card.provider_name }}
</span> </span>
<h4 class="card-title">{{ card.title }}</h4> <h4 class="card-title">{{ card.title }}</h4>
<p <p
v-if="useDescription" v-if="useDescription"
class="card-description" class="card-description"
>{{ card.description }}</p> >
{{ card.description }}
</p>
</div> </div>
</a> </a>
</div> </div>

View file

@ -28,7 +28,7 @@ export default {
}, },
getKey: { getKey: {
type: Function, type: Function,
default: item => item.id default: (item) => item.id
} }
} }
} }

View file

@ -1,16 +1,10 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
faEllipsisH
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faEllipsisH)
faEllipsisH
)
const ListCard = { const ListCard = {
props: [ props: ['list']
'list'
]
} }
export default ListCard export default ListCard

View file

@ -3,15 +3,9 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import ListUserSearch from '../list_user_search/list_user_search.vue' import ListUserSearch from '../list_user_search/list_user_search.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faSearch, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
faSearch,
faChevronLeft
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faSearch, faChevronLeft)
faSearch,
faChevronLeft
)
const ListNew = { const ListNew = {
components: { components: {
@ -27,12 +21,12 @@ const ListNew = {
} }
}, },
created() { created() {
this.$store.dispatch('fetchList', { id: this.id }) this.$store.dispatch('fetchList', { id: this.id }).then(() => {
.then(() => { this.title = this.findListTitle(this.id) }) this.title = this.findListTitle(this.id)
this.$store.dispatch('fetchListAccounts', { id: this.id }) })
.then(() => { this.$store.dispatch('fetchListAccounts', { id: this.id }).then(() => {
this.selectedUserIds = this.findListAccounts(this.id) this.selectedUserIds = this.findListAccounts(this.id)
this.selectedUserIds.forEach(userId => { this.selectedUserIds.forEach((userId) => {
this.$store.dispatch('fetchUserIfMissing', userId) this.$store.dispatch('fetchUserIfMissing', userId)
}) })
}) })
@ -42,13 +36,15 @@ const ListNew = {
return this.$route.params.id return this.$route.params.id
}, },
users() { users() {
return this.userIds.map(userId => this.findUser(userId)) return this.userIds.map((userId) => this.findUser(userId))
}, },
selectedUsers() { selectedUsers() {
return this.selectedUserIds.map(userId => this.findUser(userId)).filter(user => user) return this.selectedUserIds
.map((userId) => this.findUser(userId))
.filter((user) => user)
}, },
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser
}), }),
...mapGetters(['findUser', 'findListTitle', 'findListAccounts']) ...mapGetters(['findUser', 'findListTitle', 'findListAccounts'])
}, },
@ -70,14 +66,17 @@ const ListNew = {
this.selectedUserIds.push(user.id) this.selectedUserIds.push(user.id)
}, },
removeUser(userId) { removeUser(userId) {
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) this.selectedUserIds = this.selectedUserIds.filter((id) => id !== userId)
}, },
onResults(results) { onResults(results) {
this.userIds = results this.userIds = results
}, },
updateList() { updateList() {
this.$store.dispatch('setList', { id: this.id, title: this.title }) this.$store.dispatch('setList', { id: this.id, title: this.title })
this.$store.dispatch('setListAccounts', { id: this.id, accountIds: this.selectedUserIds }) this.$store.dispatch('setListAccounts', {
id: this.id,
accountIds: this.selectedUserIds
})
this.$router.push({ name: 'list-timeline', params: { id: this.id } }) this.$router.push({ name: 'list-timeline', params: { id: this.id } })
}, },

View file

@ -19,7 +19,7 @@
ref="title" ref="title"
v-model="title" v-model="title"
:placeholder="$t('lists.title')" :placeholder="$t('lists.title')"
> />
</div> </div>
<div class="member-list"> <div class="member-list">
<div <div

View file

@ -3,15 +3,9 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import ListUserSearch from '../list_user_search/list_user_search.vue' import ListUserSearch from '../list_user_search/list_user_search.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faSearch, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
faSearch,
faChevronLeft
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faSearch, faChevronLeft)
faSearch,
faChevronLeft
)
const ListNew = { const ListNew = {
components: { components: {
@ -28,13 +22,13 @@ const ListNew = {
}, },
computed: { computed: {
users() { users() {
return this.userIds.map(userId => this.findUser(userId)) return this.userIds.map((userId) => this.findUser(userId))
}, },
selectedUsers() { selectedUsers() {
return this.selectedUserIds.map(userId => this.findUser(userId)) return this.selectedUserIds.map((userId) => this.findUser(userId))
}, },
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser
}), }),
...mapGetters(['findUser']) ...mapGetters(['findUser'])
}, },
@ -59,7 +53,7 @@ const ListNew = {
this.selectedUserIds.push(user.id) this.selectedUserIds.push(user.id)
}, },
removeUser(userId) { removeUser(userId) {
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) this.selectedUserIds = this.selectedUserIds.filter((id) => id !== userId)
}, },
onResults(results) { onResults(results) {
this.userIds = results this.userIds = results
@ -67,9 +61,11 @@ const ListNew = {
createList() { createList() {
// the API has two different endpoints for "creating a list with a name" // the API has two different endpoints for "creating a list with a name"
// and "updating the accounts on the list". // and "updating the accounts on the list".
this.$store.dispatch('createList', { title: this.title }) this.$store.dispatch('createList', { title: this.title }).then((list) => {
.then((list) => { this.$store.dispatch('setListAccounts', {
this.$store.dispatch('setListAccounts', { id: list.id, accountIds: this.selectedUserIds }) id: list.id,
accountIds: this.selectedUserIds
})
this.$router.push({ name: 'list-timeline', params: { id: list.id } }) this.$router.push({ name: 'list-timeline', params: { id: list.id } })
}) })
} }

View file

@ -19,7 +19,7 @@
ref="title" ref="title"
v-model="title" v-model="title"
:placeholder="$t('lists.title')" :placeholder="$t('lists.title')"
> />
</div> </div>
<div class="member-list"> <div class="member-list">
@ -35,9 +35,7 @@
/> />
</div> </div>
</div> </div>
<ListUserSearch <ListUserSearch @results="onResults" />
@results="onResults"
/>
<div <div
v-for="user in users" v-for="user in users"
:key="user.id" :key="user.id"

View file

@ -9,12 +9,17 @@ const ListTimeline = {
Timeline Timeline
}, },
computed: { computed: {
timeline () { return this.$store.state.statuses.timelines.list } timeline() {
return this.$store.state.statuses.timelines.list
}
}, },
created() { created() {
this.listId = this.$route.params.id this.listId = this.$route.params.id
this.$store.dispatch('fetchList', { id: this.listId }) this.$store.dispatch('fetchList', { id: this.listId })
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) this.$store.dispatch('startFetchingTimeline', {
timeline: 'list',
listId: this.listId
})
}, },
unmounted() { unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'list') this.$store.dispatch('stopFetchingTimeline', 'list')

View file

@ -1,15 +1,9 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faSearch, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
faSearch,
faChevronLeft
} from '@fortawesome/free-solid-svg-icons'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
library.add( library.add(faSearch, faChevronLeft)
faSearch,
faChevronLeft
)
const ListUserSearch = { const ListUserSearch = {
components: { components: {
@ -34,10 +28,19 @@ const ListUserSearch = {
this.loading = true this.loading = true
this.userIds = [] this.userIds = []
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly }) this.$store
.then(data => { .dispatch('search', {
q: query,
resolve: true,
type: 'accounts',
following: this.followingOnly
})
.then((data) => {
this.loading = false this.loading = false
this.$emit('results', data.accounts.map(a => a.id)) this.$emit(
'results',
data.accounts.map((a) => a.id)
)
}) })
} }
} }

View file

@ -12,7 +12,7 @@
v-model="query" v-model="query"
:placeholder="$t('lists.search')" :placeholder="$t('lists.search')"
@input="onInput" @input="onInput"
> />
</div> </div>
<div class="input-wrap"> <div class="input-wrap">
<Checkbox <Checkbox
@ -41,5 +41,4 @@
.search-icon { .search-icon {
margin-right: 0.3em; margin-right: 0.3em;
} }
</style> </style>

View file

@ -14,7 +14,7 @@
class="button-default" class="button-default"
@click="newList" @click="newList"
> >
{{ $t("lists.new") }} {{ $t('lists.new') }}
</button> </button>
</div> </div>
<div class="panel-body"> <div class="panel-body">

View file

@ -4,7 +4,7 @@ import { get } from 'lodash'
const LocalBubblePanel = { const LocalBubblePanel = {
computed: { computed: {
...mapState({ ...mapState({
bubbleInstances: state => get(state, 'instance.localBubbleInstances') bubbleInstances: (state) => get(state, 'instance.localBubbleInstances')
}) })
} }
} }

View file

@ -6,11 +6,11 @@
<div class="panel panel-default base01-background"> <div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background"> <div class="panel-heading timeline-heading base02-background">
<div class="title"> <div class="title">
{{ $t("about.bubble_instances") }} {{ $t('about.bubble_instances') }}
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p>{{ $t("about.bubble_instances_description")}}:</p> <p>{{ $t('about.bubble_instances_description') }}:</p>
<ul> <ul>
<li <li
v-for="instance in bubbleInstances" v-for="instance in bubbleInstances"

View file

@ -1,13 +1,9 @@
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
import oauthApi from '../../services/new_api/oauth.js' import oauthApi from '../../services/new_api/oauth.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faTimes } from '@fortawesome/free-solid-svg-icons'
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faTimes)
faTimes
)
const LoginForm = { const LoginForm = {
data: () => ({ data: () => ({
@ -15,17 +11,23 @@ const LoginForm = {
error: false error: false
}), }),
computed: { computed: {
isPasswordAuth () { return this.requiredPassword }, isPasswordAuth() {
isTokenAuth () { return this.requiredToken }, return this.requiredPassword
},
isTokenAuth() {
return this.requiredToken
},
...mapState({ ...mapState({
registrationOpen: state => state.instance.registrationOpen, registrationOpen: (state) => state.instance.registrationOpen,
instance: state => state.instance, instance: (state) => state.instance,
loggingIn: state => state.users.loggingIn, loggingIn: (state) => state.users.loggingIn,
oauth: state => state.oauth oauth: (state) => state.oauth
}), }),
...mapGetters( ...mapGetters('authFlow', [
'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA'] 'requiredPassword',
) 'requiredToken',
'requiredMFA'
])
}, },
methods: { methods: {
...mapMutations('authFlow', ['requireMFA']), ...mapMutations('authFlow', ['requireMFA']),
@ -42,8 +44,9 @@ const LoginForm = {
commit: this.$store.commit commit: this.$store.commit
} }
oauthApi.getOrCreateApp(data) oauthApi.getOrCreateApp(data).then((app) => {
.then((app) => { oauthApi.login({ ...app, ...data }) }) oauthApi.login({ ...app, ...data })
})
}, },
submitPassword() { submitPassword() {
const { clientId } = this.oauth const { clientId } = this.oauth
@ -56,19 +59,22 @@ const LoginForm = {
this.error = false this.error = false
oauthApi.getOrCreateApp(data).then((app) => { oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials( oauthApi
{ .getTokenWithCredentials({
...app, ...app,
instance: data.instance, instance: data.instance,
username: this.user.username, username: this.user.username,
password: this.user.password password: this.user.password
} })
).then((result) => { .then((result) => {
if (result.error) { if (result.error) {
if (result.error === 'mfa_required') { if (result.error === 'mfa_required') {
this.requireMFA({ settings: result }) this.requireMFA({ settings: result })
} else if (result.identifier === 'password_reset_required') { } else if (result.identifier === 'password_reset_required') {
this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } }) this.$router.push({
name: 'password-reset',
params: { passwordResetRequested: true }
})
} else { } else {
this.error = result.error this.error = result.error
this.focusOnPasswordInput() this.focusOnPasswordInput()
@ -81,7 +87,9 @@ const LoginForm = {
}) })
}) })
}, },
clearError () { this.error = false }, clearError() {
this.error = false
},
focusOnPasswordInput() { focusOnPasswordInput() {
let passwordInput = this.$refs.passwordInput let passwordInput = this.$refs.passwordInput
passwordInput.focus() passwordInput.focus()

View file

@ -20,7 +20,7 @@
:disabled="loggingIn" :disabled="loggingIn"
class="form-control" class="form-control"
:placeholder="$t('login.placeholder')" :placeholder="$t('login.placeholder')"
> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">{{ $t('login.password') }}</label> <label for="password">{{ $t('login.password') }}</label>
@ -31,7 +31,7 @@
:disabled="loggingIn" :disabled="loggingIn"
class="form-control" class="form-control"
type="password" type="password"
> />
</div> </div>
<div class="form-group"> <div class="form-group">
<router-link :to="{ name: 'password-reset' }"> <router-link :to="{ name: 'password-reset' }">
@ -110,7 +110,7 @@
} }
.login-bottom { .login-bottom {
margin-top: 1.0em; margin-top: 1em;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;

View file

@ -14,12 +14,7 @@ import {
faTimes faTimes
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faChevronLeft, faChevronRight, faCircleNotch, faTimes)
faChevronLeft,
faChevronRight,
faCircleNotch,
faTimes
)
const MediaModal = { const MediaModal = {
components: { components: {
@ -88,7 +83,10 @@ const MediaModal = {
}, },
goPrev() { goPrev() {
if (this.canNavigate) { if (this.canNavigate) {
const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1) const prevIndex =
this.currentIndex === 0
? this.media.length - 1
: this.currentIndex - 1
const newMedia = this.media[prevIndex] const newMedia = this.media[prevIndex]
if (this.getType(newMedia) === 'image') { if (this.getType(newMedia) === 'image') {
this.loading = true this.loading = true
@ -98,7 +96,10 @@ const MediaModal = {
}, },
goNext() { goNext() {
if (this.canNavigate) { if (this.canNavigate) {
const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1) const nextIndex =
this.currentIndex === this.media.length - 1
? 0
: this.currentIndex + 1
const newMedia = this.media[nextIndex] const newMedia = this.media[nextIndex]
if (this.getType(newMedia) === 'image') { if (this.getType(newMedia) === 'image') {
this.loading = true this.loading = true
@ -121,7 +122,8 @@ const MediaModal = {
} }
}, },
handleKeyupEvent(e) { handleKeyupEvent(e) {
if (this.showing && e.keyCode === 27) { // escape if (this.showing && e.keyCode === 27) {
// escape
this.hide() this.hide()
} }
}, },
@ -130,9 +132,11 @@ const MediaModal = {
return return
} }
if (e.keyCode === 39) { // arrow right if (e.keyCode === 39) {
// arrow right
this.goNext() this.goNext()
} else if (e.keyCode === 37) { // arrow left } else if (e.keyCode === 37) {
// arrow left
this.goPrev() this.goPrev()
} }
} }

View file

@ -31,7 +31,7 @@
:alt="currentMedia.description" :alt="currentMedia.description"
:title="currentMedia.description" :title="currentMedia.description"
@load="onImageLoaded" @load="onImageLoaded"
> />
</PinchZoom> </PinchZoom>
</SwipeClick> </SwipeClick>
<VideoAttachment <VideoAttachment
@ -94,10 +94,13 @@
> >
{{ description }} {{ description }}
</span> </span>
<span <span class="counter">
class="counter" {{
> $tc('media_modal.counter', currentIndex + 1, {
{{ $tc('media_modal.counter', currentIndex + 1, { current: currentIndex + 1, total: media.length }) }} current: currentIndex + 1,
total: media.length
})
}}
</span> </span>
<span <span
v-if="loading" v-if="loading"
@ -116,7 +119,9 @@
<style lang="scss"> <style lang="scss">
$modal-view-button-icon-height: 3em; $modal-view-button-icon-height: 3em;
$modal-view-button-icon-half-height: calc(#{$modal-view-button-icon-height} / 2); $modal-view-button-icon-half-height: calc(
#{$modal-view-button-icon-height} / 2
);
$modal-view-button-icon-width: 3em; $modal-view-button-icon-width: 3em;
$modal-view-button-icon-margin: 0.5em; $modal-view-button-icon-margin: 0.5em;
@ -227,7 +232,7 @@ $modal-view-button-icon-margin: 0.5em;
appearance: none; appearance: none;
overflow: visible; overflow: visible;
cursor: pointer; cursor: pointer;
transition: opacity 333ms cubic-bezier(.4,0,.22,1); transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
height: $modal-view-button-icon-height; height: $modal-view-button-icon-height;
width: $modal-view-button-icon-width; width: $modal-view-button-icon-width;
@ -237,9 +242,9 @@ $modal-view-button-icon-margin: 0.5em;
width: $modal-view-button-icon-width; width: $modal-view-button-icon-width;
font-size: 1rem; font-size: 1rem;
line-height: $modal-view-button-icon-height; line-height: $modal-view-button-icon-height;
color: #FFF; color: #fff;
text-align: center; text-align: center;
background-color: rgba(0,0,0,.3); background-color: rgba(0, 0, 0, 0.3);
} }
} }
@ -255,9 +260,9 @@ $modal-view-button-icon-margin: 0.5em;
position: absolute; position: absolute;
top: 0; top: 0;
line-height: $modal-view-button-icon-height; line-height: $modal-view-button-icon-height;
color: #FFF; color: #fff;
text-align: center; text-align: center;
background-color: rgba(0,0,0,.3); background-color: rgba(0, 0, 0, 0.3);
} }
&--prev { &--prev {

View file

@ -5,10 +5,7 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faUpload, faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faUpload, faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faUpload, faCircleNotch)
faUpload,
faCircleNotch
)
const mediaUpload = { const mediaUpload = {
data() { data() {
@ -28,8 +25,15 @@ const mediaUpload = {
const store = this.$store const store = this.$store
if (file.size > store.state.instance.uploadlimit) { if (file.size > store.state.instance.uploadlimit) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size) const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat(store.state.instance.uploadlimit) const allowedsize = fileSizeFormatService.fileSizeFormat(
self.$emit('upload-failed', 'file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit }) store.state.instance.uploadlimit
)
self.$emit('upload-failed', 'file_too_big', {
filesize: filesize.num,
filesizeunit: filesize.unit,
allowedsize: allowedsize.num,
allowedsizeunit: allowedsize.unit
})
return return
} }
const formData = new FormData() const formData = new FormData()
@ -38,14 +42,17 @@ const mediaUpload = {
self.$emit('uploading') self.$emit('uploading')
self.uploadCount++ self.uploadCount++
statusPosterService.uploadMedia({ store, formData }) statusPosterService.uploadMedia({ store, formData }).then(
.then((fileData) => { (fileData) => {
self.$emit('uploaded', fileData) self.$emit('uploaded', fileData)
self.decreaseUploadCount() self.decreaseUploadCount()
}, (error) => { // eslint-disable-line handle-callback-err },
(error) => {
// eslint-disable-line handle-callback-err
self.$emit('upload-failed', 'default') self.$emit('upload-failed', 'default')
self.decreaseUploadCount() self.decreaseUploadCount()
}) }
)
}, },
decreaseUploadCount() { decreaseUploadCount() {
this.uploadCount-- this.uploadCount--
@ -68,12 +75,9 @@ const mediaUpload = {
this.multiUpload(target.files) this.multiUpload(target.files)
} }
}, },
props: [ props: ['dropFiles', 'disabled'],
'dropFiles',
'disabled'
],
watch: { watch: {
'dropFiles': function (fileInfos) { dropFiles: function (fileInfos) {
if (!this.uploading) { if (!this.uploading) {
this.multiUpload(fileInfos) this.multiUpload(fileInfos)
} }

View file

@ -22,7 +22,7 @@
type="file" type="file"
multiple="true" multiple="true"
@change="change" @change="change"
> />
</label> </label>
</template> </template>

View file

@ -1,15 +1,14 @@
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import {
highlightClass,
highlightStyle
} from '../../services/user_highlighter/user_highlighter.js'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faAt } from '@fortawesome/free-solid-svg-icons'
faAt
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faAt)
faAt
)
const MentionLink = { const MentionLink = {
name: 'MentionLink', name: 'MentionLink',
@ -45,7 +44,9 @@ const MentionLink = {
}, },
computed: { computed: {
user() { user() {
return this.url && this.$store && this.$store.getters.findUserByUrl(this.url) return (
this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
)
}, },
isYou() { isYou() {
// FIXME why user !== currentUser??? // FIXME why user !== currentUser???
@ -56,7 +57,11 @@ const MentionLink = {
}, },
serverName() { serverName() {
// XXX assumed that domain does not contain @ // XXX assumed that domain does not contain @
return this.user && (this.userNameFullUi.split('@')[1] || this.$store.getters.instanceDomain) return (
this.user &&
(this.userNameFullUi.split('@')[1] ||
this.$store.getters.instanceDomain)
)
}, },
userNameFull() { userNameFull() {
return this.user && this.user.screen_name return this.user && this.user.screen_name
@ -68,7 +73,7 @@ const MentionLink = {
return this.user && this.mergedConfig.highlight[this.user.screen_name] return this.user && this.mergedConfig.highlight[this.user.screen_name]
}, },
highlightType() { highlightType() {
return this.highlight && ('-' + this.highlight.type) return this.highlight && '-' + this.highlight.type
}, },
highlightClass() { highlightClass() {
if (this.highlight) return highlightClass(this.user) if (this.highlight) return highlightClass(this.user)
@ -105,12 +110,17 @@ const MentionLink = {
return false return false
} else if (conf === 'full') { } else if (conf === 'full') {
return true return true
} else { // full_for_remote } else {
// full_for_remote
return this.isRemote return this.isRemote
} }
}, },
shouldShowTooltip() { shouldShowTooltip() {
return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote return (
this.mergedConfig.mentionLinkShowTooltip &&
this.mergedConfig.mentionLinkDisplay === 'short' &&
this.isRemote
)
}, },
shouldShowAvatar() { shouldShowAvatar() {
return this.mergedConfig.mentionLinkShowAvatar return this.mergedConfig.mentionLinkShowAvatar
@ -126,7 +136,7 @@ const MentionLink = {
}, },
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser
}) })
} }
} }

View file

@ -1,7 +1,5 @@
<template> <template>
<span <span class="MentionLink">
class="MentionLink"
>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<a <a
v-if="!user" v-if="!user"
@ -27,7 +25,8 @@
class="mention-avatar" class="mention-avatar"
:user="user" :user="user"
/> />
<span class="shortName">@<span <span class="shortName"
>@<span
class="userName" class="userName"
v-html="userName" v-html="userName"
/><span /><span
@ -40,16 +39,16 @@
<span <span
v-if="isYou && shouldShowYous" v-if="isYou && shouldShowYous"
:class="{ '-you': shouldBoldenYou }" :class="{ '-you': shouldBoldenYou }"
> {{ ' ' + $t('status.you') }}</span> >
<!-- eslint-enable vue/no-v-html --> {{ ' ' + $t('status.you') }}</span
</a><span >
<!-- eslint-enable vue/no-v-html --> </a
><span
v-if="shouldShowTooltip" v-if="shouldShowTooltip"
class="full popover-default" class="full popover-default"
:class="[highlightType]" :class="[highlightType]"
> >
<span <span class="userNameFull">
class="userNameFull"
>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
@<span @<span
class="userName" class="userName"

View file

@ -20,14 +20,14 @@
class="mention-link" class="mention-link"
:content="mention.content" :content="mention.content"
:url="mention.url" :url="mention.url"
/> /> </span
</span><button ><button
v-if="!expanded" v-if="!expanded"
class="button-unstyled showMoreLess" class="button-unstyled showMoreLess"
@click="toggleShowMore" @click="toggleShowMore"
> >
{{ $t('status.plus_more', { number: extraMentions.length }) }} {{ $t('status.plus_more', { number: extraMentions.length }) }}</button
</button><button ><button
v-if="expanded" v-if="expanded"
class="button-unstyled showMoreLess" class="button-unstyled showMoreLess"
@click="toggleShowMore" @click="toggleShowMore"

View file

@ -1,13 +1,9 @@
import mfaApi from '../../services/new_api/mfa.js' import mfaApi from '../../services/new_api/mfa.js'
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faTimes } from '@fortawesome/free-solid-svg-icons'
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faTimes)
faTimes
)
export default { export default {
data: () => ({ data: () => ({
@ -26,7 +22,9 @@ export default {
methods: { methods: {
...mapMutations('authFlow', ['requireTOTP', 'abortMFA']), ...mapMutations('authFlow', ['requireTOTP', 'abortMFA']),
...mapActions({ login: 'authFlow/login' }), ...mapActions({ login: 'authFlow/login' }),
clearError () { this.error = false }, clearError() {
this.error = false
},
submit() { submit() {
const { clientId, clientSecret } = this.oauth const { clientId, clientSecret } = this.oauth

Some files were not shown because too many files have changed in this diff Show more