Merge remote-tracking branch 'upstream/develop' into emoji-optimizations

* upstream/develop: (95 commits)
  Lightbox/modal multi image improvements - #381
  '/api/pleroma/profile/mfa' -> '/api/pleroma/accounts/mfa'
  Add ability to change user's email
  translations-de-batch-1
  eu-translate update
  profile-banner rounding css, fixes #690
  fix indentation
  remove needless ref
  show preview popover when hover numbered replies
  refactor conditions
  do not make too many nested div
  add fetchStatus action
  refactor status loading logic
  split status preview popover into a separate component
  uninstall mobile-detect library
  listen both events
  minor css fix
  restrict distance at top side only
  set different trigger event in desktop and mobile by default
  fix eslint warnings
  ...
This commit is contained in:
Henry Jameson 2019-11-08 19:48:31 +02:00
commit 0dcb696e26
77 changed files with 1318 additions and 1127 deletions

View file

@ -1,12 +1,15 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added ### Added
- Ability to hide/show repeats from user
- User profile button clutter organized into a menu
- Emoji picker - Emoji picker
- Started changelog anew - Started changelog anew
- Ability to change user's email
### Changed ### Changed
- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes - changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
### Fixed ### Fixed

View file

@ -45,7 +45,7 @@ export default {
}), }),
created () { created () {
// Load the locale from the storage // Load the locale from the storage
this.$i18n.locale = this.$store.state.config.interfaceLanguage this.$i18n.locale = this.$store.getters.mergedConfig.interfaceLanguage
window.addEventListener('resize', this.updateMobileState) window.addEventListener('resize', this.updateMobileState)
}, },
destroyed () { destroyed () {
@ -93,7 +93,7 @@ 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.state.config.hideISP && !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
}, },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },

View file

@ -39,10 +39,13 @@ h4 {
text-align: center; text-align: center;
} }
html {
font-size: 14px;
}
body { body {
font-family: sans-serif; font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif); font-family: var(--interfaceFont, sans-serif);
font-size: 14px;
margin: 0; margin: 0;
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
@ -717,31 +720,6 @@ nav {
} }
} }
@keyframes modal-background-fadein {
from {
background-color: rgba(0, 0, 0, 0);
}
to {
background-color: rgba(0, 0, 0, 0.5);
}
}
.modal-view {
z-index: 1000;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
animation-duration: 0.2s;
background-color: rgba(0, 0, 0, 0.5);
animation-name: modal-background-fadein;
}
.button-icon { .button-icon {
font-size: 1.2em; font-size: 1.2em;
} }

View file

@ -47,7 +47,7 @@ export default (store) => {
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings }, { name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
{ name: 'password-reset', path: '/password-reset', component: PasswordReset }, { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
{ name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute }, { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },

View file

@ -0,0 +1,35 @@
import ProgressButton from '../progress_button/progress_button.vue'
const AccountActions = {
props: [
'user'
],
data () {
return { }
},
components: {
ProgressButton
},
methods: {
showRepeats () {
this.$store.dispatch('showReblogs', this.user.id)
},
hideRepeats () {
this.$store.dispatch('hideReblogs', this.user.id)
},
blockUser () {
this.$store.dispatch('blockUser', this.user.id)
},
unblockUser () {
this.$store.dispatch('unblockUser', this.user.id)
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
}
}
}
export default AccountActions

View file

@ -0,0 +1,93 @@
<template>
<div class="account-actions">
<v-popover
trigger="click"
class="account-tools-popover"
:container="false"
placement="bottom-end"
:offset="5"
>
<div slot="popover">
<div class="dropdown-menu">
<button
class="btn btn-default btn-block dropdown-item"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
<template v-if="user.following">
<div
role="separator"
class="dropdown-divider"
/>
<button
v-if="user.showing_reblogs"
class="btn btn-default dropdown-item"
@click="hideRepeats"
>
{{ $t('user_card.hide_repeats') }}
</button>
<button
v-if="!user.showing_reblogs"
class="btn btn-default dropdown-item"
@click="showRepeats"
>
{{ $t('user_card.show_repeats') }}
</button>
</template>
<div
role="separator"
class="dropdown-divider"
/>
<button
v-if="user.statusnet_blocking"
class="btn btn-default btn-block dropdown-item"
@click="unblockUser"
>
{{ $t('user_card.unblock') }}
</button>
<button
v-else
class="btn btn-default btn-block dropdown-item"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
<button
class="btn btn-default btn-block dropdown-item"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
</div>
</div>
<div class="btn btn-default ellipsis-button">
<i class="icon-ellipsis trigger-button" />
</div>
</v-popover>
</div>
</template>
<script src="./account_actions.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import '../popper/popper.scss';
.account-actions {
margin: 0 .8em;
}
.account-actions button.dropdown-item {
margin-left: 0;
}
.account-actions .trigger-button {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
opacity: .8;
cursor: pointer;
&:hover {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
</style>

View file

@ -10,13 +10,14 @@ const Attachment = {
'statusId', 'statusId',
'size', 'size',
'allowPlay', 'allowPlay',
'setMedia' 'setMedia',
'naturalSizeLoad'
], ],
data () { data () {
return { return {
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage, nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
hideNsfwLocal: this.$store.state.config.hideNsfw, hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
preloadImage: this.$store.state.config.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,
@ -57,7 +58,7 @@ const Attachment = {
} }
}, },
openModal (event) { openModal (event) {
const modalTypes = this.$store.state.config.playVideosInModal const modalTypes = this.$store.getters.mergedConfig.playVideosInModal
? ['image', 'video'] ? ['image', 'video']
: ['image'] : ['image']
if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) || if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) ||
@ -70,7 +71,7 @@ const Attachment = {
} }
}, },
toggleHidden (event) { toggleHidden (event) {
if (this.$store.state.config.useOneClickNsfw && !this.showHidden) { if (this.$store.getters.mergedConfig.useOneClickNsfw && !this.showHidden) {
this.openModal(event) this.openModal(event)
return return
} }
@ -88,6 +89,11 @@ const Attachment = {
} else { } else {
this.showHidden = !this.showHidden this.showHidden = !this.showHidden
} }
},
onImageLoad (image) {
const width = image.naturalWidth
const height = image.naturalHeight
this.naturalSizeLoad && this.naturalSizeLoad({ width, height })
} }
} }
} }

View file

@ -58,6 +58,7 @@
:referrerpolicy="referrerpolicy" :referrerpolicy="referrerpolicy"
:mimetype="attachment.mimetype" :mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url" :src="attachment.large_thumb_url || attachment.url"
:image-load-handler="onImageLoad"
/> />
</a> </a>

View file

@ -1,13 +1,22 @@
<template> <template>
<label class="checkbox"> <label
class="checkbox"
:class="{ disabled, indeterminate }"
>
<input <input
type="checkbox" type="checkbox"
:disabled="disabled"
:checked="checked" :checked="checked"
:indeterminate.prop="indeterminate" :indeterminate.prop="indeterminate"
@change="$emit('change', $event.target.checked)" @change="$emit('change', $event.target.checked)"
> >
<i class="checkbox-indicator" /> <i class="checkbox-indicator" />
<span v-if="!!$slots.default"><slot /></span> <span
class="label"
v-if="!!$slots.default"
>
<slot />
</span>
</label> </label>
</template> </template>
@ -17,7 +26,11 @@ export default {
prop: 'checked', prop: 'checked',
event: 'change' event: 'change'
}, },
props: ['checked', 'indeterminate'] props: [
'checked',
'indeterminate',
'disabled'
]
} }
</script> </script>
@ -27,12 +40,16 @@ export default {
.checkbox { .checkbox {
position: relative; position: relative;
display: inline-block; display: inline-block;
padding-left: 1.2em;
min-height: 1.2em; min-height: 1.2em;
&-indicator {
position: relative;
padding-left: 1.2em;
}
&-indicator::before { &-indicator::before {
position: absolute; position: absolute;
left: 0; right: 0;
top: 0; top: 0;
display: block; display: block;
content: '✔'; content: '✔';
@ -54,6 +71,17 @@ export default {
box-sizing: border-box; box-sizing: border-box;
} }
&.disabled {
.checkbox-indicator::before,
.label {
opacity: .5;
}
.label {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
}
}
input[type=checkbox] { input[type=checkbox] {
display: none; display: none;
@ -68,9 +96,6 @@ export default {
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
} }
&:disabled + .checkbox-indicator::before {
opacity: .5;
}
} }
& > span { & > span {

View file

@ -99,7 +99,7 @@ const EmojiInput = {
}, },
computed: { computed: {
padEmoji () { padEmoji () {
return this.$store.state.config.padEmoji return this.$store.getters.mergedConfig.padEmoji
}, },
suggestions () { suggestions () {
const firstchar = this.textAtCaret.charAt(0) const firstchar = this.textAtCaret.charAt(0)

View file

@ -1,4 +1,5 @@
import { set } from 'vue' import { set } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
const LOAD_EMOJI_BY = 50 const LOAD_EMOJI_BY = 50
const LOAD_EMOJI_INTERVAL = 100 const LOAD_EMOJI_INTERVAL = 100
@ -18,7 +19,6 @@ const EmojiPicker = {
}, },
data () { data () {
return { return {
labelKey: String(Math.random() * 100000),
keyword: '', keyword: '',
activeGroup: 'custom', activeGroup: 'custom',
showingStickers: false, showingStickers: false,
@ -32,7 +32,8 @@ const EmojiPicker = {
} }
}, },
components: { components: {
StickerPicker: () => import('../sticker_picker/sticker_picker.vue') StickerPicker: () => import('../sticker_picker/sticker_picker.vue'),
Checkbox
}, },
methods: { methods: {
onEmoji (emoji) { onEmoji (emoji) {

View file

@ -75,22 +75,10 @@
</span> </span>
</div> </div>
</div> </div>
<div <div class="keep-open">
class="keep-open" <Checkbox v-model="keepOpen">
> {{ $t('emoji.keep_open') }}
<input </Checkbox>
:id="labelKey + 'keep-open'"
v-model="keepOpen"
type="checkbox"
>
<label
class="keep-open-label"
:for="labelKey + 'keep-open'"
>
<div class="keep-open-label-text">
{{ $t('emoji.keep_open') }}
</div>
</label>
</div> </div>
<div <div
v-if="askForSanity" v-if="askForSanity"

View file

@ -4,8 +4,6 @@
trigger="click" trigger="click"
placement="top" placement="top"
class="extra-button-popover" class="extra-button-popover"
:offset="5"
:container="false"
> >
<div slot="popover"> <div slot="popover">
<div class="dropdown-menu"> <div class="dropdown-menu">

View file

@ -1,10 +1,9 @@
import { mapGetters } from 'vuex'
const FavoriteButton = { const FavoriteButton = {
props: ['status', 'loggedIn'], props: ['status', 'loggedIn'],
data () { data () {
return { return {
hidePostStatsLocal: typeof this.$store.state.config.hidePostStats === 'undefined'
? this.$store.state.instance.hidePostStats
: this.$store.state.config.hidePostStats,
animated: false animated: false
} }
}, },
@ -28,7 +27,8 @@ const FavoriteButton = {
'icon-star': this.status.favorited, 'icon-star': this.status.favorited,
'animate-spin': this.animated 'animate-spin': this.animated
} }
} },
...mapGetters(['mergedConfig'])
} }
} }

View file

@ -6,7 +6,7 @@
:title="$t('tool_tip.favorite')" :title="$t('tool_tip.favorite')"
@click.prevent="favorite()" @click.prevent="favorite()"
/> />
<span v-if="!hidePostStatsLocal && status.fave_num > 0">{{ status.fave_num }}</span> <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
</div> </div>
<div v-else> <div v-else>
<i <i
@ -14,7 +14,7 @@
class="button-icon favorite-button" class="button-icon favorite-button"
:title="$t('tool_tip.favorite')" :title="$t('tool_tip.favorite')"
/> />
<span v-if="!hidePostStatsLocal && status.fave_num > 0">{{ status.fave_num }}</span> <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
</div> </div>
</template> </template>

View file

@ -0,0 +1,53 @@
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default {
props: ['user', 'labelFollowing', 'buttonClass'],
data () {
return {
inProgress: false
}
},
computed: {
isPressed () {
return this.inProgress || this.user.following
},
title () {
if (this.inProgress || this.user.following) {
return this.$t('user_card.follow_unfollow')
} else if (this.user.requested) {
return this.$t('user_card.follow_again')
} else {
return this.$t('user_card.follow')
}
},
label () {
if (this.inProgress) {
return this.$t('user_card.follow_progress')
} else if (this.user.following) {
return this.labelFollowing || this.$t('user_card.following')
} else if (this.user.requested) {
return this.$t('user_card.follow_sent')
} else {
return this.$t('user_card.follow')
}
}
},
methods: {
onClick () {
this.user.following ? this.unfollow() : this.follow()
},
follow () {
this.inProgress = true
requestFollow(this.user, this.$store).then(() => {
this.inProgress = false
})
},
unfollow () {
const store = this.$store
this.inProgress = true
requestUnfollow(this.user, store).then(() => {
this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
})
}
}
}

View file

@ -0,0 +1,13 @@
<template>
<button
class="btn btn-default follow-button"
:class="{ pressed: isPressed }"
:disabled="inProgress"
:title="title"
@click="onClick"
>
{{ label }}
</button>
</template>
<script src="./follow_button.js"></script>

View file

@ -1,21 +1,16 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import FollowButton from '../follow_button/follow_button.vue'
const FollowCard = { const FollowCard = {
props: [ props: [
'user', 'user',
'noFollowsYou' 'noFollowsYou'
], ],
data () {
return {
inProgress: false,
requestSent: false
}
},
components: { components: {
BasicUserCard, BasicUserCard,
RemoteFollow RemoteFollow,
FollowButton
}, },
computed: { computed: {
isMe () { isMe () {
@ -24,21 +19,6 @@ const FollowCard = {
loggedIn () { loggedIn () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
} }
},
methods: {
followUser () {
this.inProgress = true
requestFollow(this.user, this.$store).then(({ sent }) => {
this.inProgress = false
this.requestSent = sent
})
},
unfollowUser () {
this.inProgress = true
requestUnfollow(this.user, this.$store).then(() => {
this.inProgress = false
})
}
} }
} }

View file

@ -16,36 +16,11 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<button <FollowButton
v-if="!user.following" :user="user"
class="btn btn-default follow-card-follow-button" class="follow-card-follow-button"
:disabled="inProgress" :label-following="$t('user_card.follow_unfollow')"
:title="requestSent ? $t('user_card.follow_again') : ''" />
@click="followUser"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="requestSent">
{{ $t('user_card.follow_sent') }}
</template>
<template v-else>
{{ $t('user_card.follow') }}
</template>
</button>
<button
v-else
class="btn btn-default follow-card-follow-button pressed"
:disabled="inProgress"
@click="unfollowUser"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else>
{{ $t('user_card.follow_unfollow') }}
</template>
</button>
</template> </template>
</div> </div>
</basic-user-card> </basic-user-card>

View file

@ -1,23 +1,18 @@
import Attachment from '../attachment/attachment.vue' import Attachment from '../attachment/attachment.vue'
import { chunk, last, dropRight } from 'lodash' import { chunk, last, dropRight, sumBy } from 'lodash'
const Gallery = { const Gallery = {
data: () => ({
width: 500
}),
props: [ props: [
'attachments', 'attachments',
'nsfw', 'nsfw',
'setMedia' 'setMedia'
], ],
data () {
return {
sizes: {}
}
},
components: { Attachment }, components: { Attachment },
mounted () {
this.resize()
window.addEventListener('resize', this.resize)
},
destroyed () {
window.removeEventListener('resize', this.resize)
},
computed: { computed: {
rows () { rows () {
if (!this.attachments) { if (!this.attachments) {
@ -33,21 +28,24 @@ const Gallery = {
} }
return rows return rows
}, },
rowHeight () {
return itemsPerRow => ({ 'height': `${(this.width / (itemsPerRow + 0.6))}px` })
},
useContainFit () { useContainFit () {
return this.$store.state.config.useContainFit return this.$store.getters.mergedConfig.useContainFit
} }
}, },
methods: { methods: {
resize () { onNaturalSizeLoad (id, size) {
// Quick optimization to make resizing not always trigger state change, this.$set(this.sizes, id, size)
// only update attachment size in 10px steps },
const width = Math.floor(this.$el.getBoundingClientRect().width / 10) * 10 rowStyle (itemsPerRow) {
if (this.width !== width) { return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }
this.width = width },
} itemStyle (id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id))
return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
},
getAspectRatio (id) {
const size = this.sizes[id]
return size ? size.width / size.height : 1
} }
} }
} }

View file

@ -7,17 +7,21 @@
v-for="(row, index) in rows" v-for="(row, index) in rows"
:key="index" :key="index"
class="gallery-row" class="gallery-row"
:style="rowHeight(row.length)" :style="rowStyle(row.length)"
:class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }" :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
> >
<attachment <div class="gallery-row-inner">
v-for="attachment in row" <attachment
:key="attachment.id" v-for="attachment in row"
:set-media="setMedia" :key="attachment.id"
:nsfw="nsfw" :set-media="setMedia"
:attachment="attachment" :nsfw="nsfw"
:allow-play="false" :attachment="attachment"
/> :allow-play="false"
:natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
:style="itemStyle(attachment.id, row)"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -28,15 +32,24 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.gallery-row { .gallery-row {
height: 200px; position: relative;
height: 0;
width: 100%; width: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: stretch;
flex-grow: 1; flex-grow: 1;
margin-top: 0.5em; margin-top: 0.5em;
.gallery-row-inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: stretch;
}
// FIXME: specificity problem with this and .attachments.attachment // FIXME: specificity problem with this and .attachments.attachment
// we shouldn't have the need for .image here // we shouldn't have the need for .image here
.attachment.image { .attachment.image {

View file

@ -40,7 +40,7 @@ export default {
}, },
language: { language: {
get: function () { return this.$store.state.config.interfaceLanguage }, get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) { set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
this.$i18n.locale = val this.$i18n.locale = val

View file

@ -59,6 +59,8 @@ const LoginForm = {
if (result.error) { if (result.error) {
if (result.error === 'mfa_required') { if (result.error === 'mfa_required') {
this.requireMFA({ app: app, settings: result }) this.requireMFA({ app: app, settings: result })
} else if (result.identifier === 'password_reset_required') {
this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } })
} else { } else {
this.error = result.error this.error = result.error
this.focusOnPasswordInput() this.focusOnPasswordInput()

View file

@ -1,11 +1,14 @@
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue' import VideoAttachment from '../video_attachment/video_attachment.vue'
import Modal from '../modal/modal.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
import GestureService from '../../services/gesture_service/gesture_service'
const MediaModal = { const MediaModal = {
components: { components: {
StillImage, StillImage,
VideoAttachment VideoAttachment,
Modal
}, },
computed: { computed: {
showing () { showing () {
@ -27,7 +30,27 @@ const MediaModal = {
return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
} }
}, },
created () {
this.mediaSwipeGestureRight = GestureService.swipeGesture(
GestureService.DIRECTION_RIGHT,
this.goPrev,
50
)
this.mediaSwipeGestureLeft = GestureService.swipeGesture(
GestureService.DIRECTION_LEFT,
this.goNext,
50
)
},
methods: { methods: {
mediaTouchStart (e) {
GestureService.beginSwipe(e, this.mediaSwipeGestureRight)
GestureService.beginSwipe(e, this.mediaSwipeGestureLeft)
},
mediaTouchMove (e) {
GestureService.updateSwipe(e, this.mediaSwipeGestureRight)
GestureService.updateSwipe(e, this.mediaSwipeGestureLeft)
},
hide () { hide () {
this.$store.dispatch('closeMediaViewer') this.$store.dispatch('closeMediaViewer')
}, },

View file

@ -1,14 +1,15 @@
<template> <template>
<div <Modal
v-if="showing" v-if="showing"
v-body-scroll-lock="showing" class="media-modal-view"
class="modal-view media-modal-view" @backdropClicked="hide"
@click.prevent="hide"
> >
<img <img
v-if="type === 'image'" v-if="type === 'image'"
class="modal-image" class="modal-image"
:src="currentMedia.url" :src="currentMedia.url"
@touchstart.stop="mediaTouchStart"
@touchmove.stop="mediaTouchMove"
> >
<VideoAttachment <VideoAttachment
v-if="type === 'video'" v-if="type === 'video'"
@ -33,33 +34,25 @@
> >
<i class="icon-right-open arrow-icon" /> <i class="icon-right-open arrow-icon" />
</button> </button>
</div> </Modal>
</template> </template>
<script src="./media_modal.js"></script> <script src="./media_modal.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; .modal-view.media-modal-view {
.media-modal-view {
z-index: 1001; z-index: 1001;
body:not(.scroll-locked) & { .modal-view-button-arrow {
display: none; opacity: 0.75;
}
&:hover { &:focus,
.modal-view-button-arrow { &:hover {
opacity: 0.75; outline: none;
box-shadow: none;
&:focus, }
&:hover { &:hover {
outline: none; opacity: 1;
box-shadow: none;
}
&:hover {
opacity: 1;
}
} }
} }
} }
@ -114,5 +107,4 @@
} }
} }
} }
</style> </style>

View file

@ -63,7 +63,7 @@ const MobileNav = {
this.$refs.notifications.markAsSeen() this.$refs.notifications.markAsSeen()
}, },
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) { onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
if (this.$store.state.config.autoLoad && scrollTop + clientHeight >= scrollHeight) { if (this.$store.getters.mergedConfig.autoLoad && scrollTop + clientHeight >= scrollHeight) {
this.$refs.notifications.fetchOlderNotifications() this.$refs.notifications.fetchOlderNotifications()
} }
} }

View file

@ -30,7 +30,7 @@ const MobilePostStatusButton = {
return this.autohideFloatingPostButton && (this.hidden || this.inputActive) return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
}, },
autohideFloatingPostButton () { autohideFloatingPostButton () {
return !!this.$store.state.config.autohideFloatingPostButton return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
} }
}, },
watch: { watch: {

View file

@ -0,0 +1,52 @@
<template>
<div
v-show="isOpen"
v-body-scroll-lock="isOpen"
class="modal-view"
@click.self="$emit('backdropClicked')"
>
<slot />
</div>
</template>
<script>
export default {
props: {
isOpen: {
type: Boolean,
default: true
}
}
}
</script>
<style lang="scss">
.modal-view {
z-index: 1000;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
animation-duration: 0.2s;
background-color: rgba(0, 0, 0, 0.5);
animation-name: modal-background-fadein;
body:not(.scroll-locked) & {
opacity: 0;
}
}
@keyframes modal-background-fadein {
from {
background-color: rgba(0, 0, 0, 0);
}
to {
background-color: rgba(0, 0, 0, 0.5);
}
}
</style>

View file

@ -3,9 +3,7 @@
<v-popover <v-popover
trigger="click" trigger="click"
class="moderation-tools-popover" class="moderation-tools-popover"
:container="false"
placement="bottom-end" placement="bottom-end"
:offset="5"
@show="showDropDown = true" @show="showDropDown = true"
@hide="showDropDown = false" @hide="showDropDown = false"
> >

View file

@ -39,7 +39,7 @@ const Notification = {
return highlightClass(this.notification.from_profile) return highlightClass(this.notification.from_profile)
}, },
userStyle () { userStyle () {
const highlight = this.$store.state.config.highlight const highlight = this.$store.getters.mergedConfig.highlight
const user = this.notification.from_profile const user = this.notification.from_profile
return highlightStyle(highlight[user.screen_name]) return highlightStyle(highlight[user.screen_name])
}, },

View file

@ -25,6 +25,12 @@ const passwordReset = {
this.$router.push({ name: 'root' }) this.$router.push({ name: 'root' })
} }
}, },
props: {
passwordResetRequested: {
default: false,
type: Boolean
}
},
methods: { methods: {
dismissError () { dismissError () {
this.error = null this.error = null

View file

@ -10,7 +10,10 @@
> >
<div class="container"> <div class="container">
<div v-if="!mailerEnabled"> <div v-if="!mailerEnabled">
<p> <p v-if="passwordResetRequested">
{{ $t('password_reset.password_reset_required_but_mailer_is_disabled') }}
</p>
<p v-else>
{{ $t('password_reset.password_reset_disabled') }} {{ $t('password_reset.password_reset_disabled') }}
</p> </p>
</div> </div>
@ -25,6 +28,12 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<p
v-if="passwordResetRequested"
class="password-reset-required error"
>
{{ $t('password_reset.password_reset_required') }}
</p>
<p> <p>
{{ $t('password_reset.instruction') }} {{ $t('password_reset.instruction') }}
</p> </p>
@ -104,6 +113,11 @@
margin: 0.3em 0.0em 1em; margin: 0.3em 0.0em 1em;
} }
.password-reset-required {
background-color: var(--alertError, $fallback--alertError);
padding: 10px 0;
}
.notice-dismissible { .notice-dismissible {
padding-right: 2rem; padding-right: 2rem;
} }

View file

@ -20,7 +20,6 @@
margin: 5px; margin: 5px;
border-color: $fallback--bg; border-color: $fallback--bg;
border-color: var(--bg, $fallback--bg); border-color: var(--bg, $fallback--bg);
z-index: 1;
} }
&[x-placement^="top"] { &[x-placement^="top"] {
@ -31,7 +30,7 @@
border-left-color: transparent !important; border-left-color: transparent !important;
border-right-color: transparent !important; border-right-color: transparent !important;
border-bottom-color: transparent !important; border-bottom-color: transparent !important;
bottom: -5px; bottom: -4px;
left: calc(50% - 5px); left: calc(50% - 5px);
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
@ -46,7 +45,7 @@
border-left-color: transparent !important; border-left-color: transparent !important;
border-right-color: transparent !important; border-right-color: transparent !important;
border-top-color: transparent !important; border-top-color: transparent !important;
top: -5px; top: -4px;
left: calc(50% - 5px); left: calc(50% - 5px);
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
@ -61,7 +60,7 @@
border-left-color: transparent !important; border-left-color: transparent !important;
border-top-color: transparent !important; border-top-color: transparent !important;
border-bottom-color: transparent !important; border-bottom-color: transparent !important;
left: -5px; left: -4px;
top: calc(50% - 5px); top: calc(50% - 5px);
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
@ -76,7 +75,7 @@
border-top-color: transparent !important; border-top-color: transparent !important;
border-right-color: transparent !important; border-right-color: transparent !important;
border-bottom-color: transparent !important; border-bottom-color: transparent !important;
right: -5px; right: -4px;
top: calc(50% - 5px); top: calc(50% - 5px);
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;

View file

@ -7,6 +7,8 @@ import fileTypeService from '../../services/file_type/file_type.service.js'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { reject, map, uniqBy } from 'lodash' import { reject, map, uniqBy } from 'lodash'
import suggestor from '../emoji_input/suggestor.js' import suggestor from '../emoji_input/suggestor.js'
import { mapGetters } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue'
const buildMentionsString = ({ user, attentions = [] }, currentUser) => { const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
let allAttentions = [...attentions] let allAttentions = [...attentions]
@ -35,7 +37,8 @@ const PostStatusForm = {
MediaUpload, MediaUpload,
EmojiInput, EmojiInput,
PollForm, PollForm,
ScopeSelector ScopeSelector,
Checkbox
}, },
mounted () { mounted () {
this.resize(this.$refs.textarea) this.resize(this.$refs.textarea)
@ -50,9 +53,7 @@ const PostStatusForm = {
const preset = this.$route.query.message const preset = this.$route.query.message
let statusText = preset || '' let statusText = preset || ''
const scopeCopy = typeof this.$store.state.config.scopeCopy === 'undefined' const { scopeCopy } = this.$store.getters.mergedConfig
? this.$store.state.instance.scopeCopy
: this.$store.state.config.scopeCopy
if (this.replyTo) { if (this.replyTo) {
const currentUser = this.$store.state.users.currentUser const currentUser = this.$store.state.users.currentUser
@ -63,9 +64,7 @@ const PostStatusForm = {
? this.copyMessageScope ? this.copyMessageScope
: this.$store.state.users.currentUser.default_scope : this.$store.state.users.currentUser.default_scope
const contentType = typeof this.$store.state.config.postContentType === 'undefined' const { postContentType: contentType } = this.$store.getters.mergedConfig
? this.$store.state.instance.postContentType
: this.$store.state.config.postContentType
return { return {
dropFiles: [], dropFiles: [],
@ -94,10 +93,7 @@ const PostStatusForm = {
return this.$store.state.users.currentUser.default_scope return this.$store.state.users.currentUser.default_scope
}, },
showAllScopes () { showAllScopes () {
const minimalScopesMode = typeof this.$store.state.config.minimalScopesMode === 'undefined' return !this.mergedConfig.minimalScopesMode
? this.$store.state.instance.minimalScopesMode
: this.$store.state.config.minimalScopesMode
return !minimalScopesMode
}, },
emojiUserSuggestor () { emojiUserSuggestor () {
return suggestor({ return suggestor({
@ -145,13 +141,7 @@ const PostStatusForm = {
return this.$store.state.instance.minimalScopesMode return this.$store.state.instance.minimalScopesMode
}, },
alwaysShowSubject () { alwaysShowSubject () {
if (typeof this.$store.state.config.alwaysShowSubjectInput !== 'undefined') { return this.mergedConfig.alwaysShowSubjectInput
return this.$store.state.config.alwaysShowSubjectInput
} else if (typeof this.$store.state.instance.alwaysShowSubjectInput !== 'undefined') {
return this.$store.state.instance.alwaysShowSubjectInput
} else {
return true
}
}, },
postFormats () { postFormats () {
return this.$store.state.instance.postFormats || [] return this.$store.state.instance.postFormats || []
@ -164,13 +154,14 @@ const PostStatusForm = {
this.$store.state.instance.pollLimits.max_options >= 2 this.$store.state.instance.pollLimits.max_options >= 2
}, },
hideScopeNotice () { hideScopeNotice () {
return this.$store.state.config.hideScopeNotice return this.$store.getters.mergedConfig.hideScopeNotice
}, },
pollContentError () { pollContentError () {
return this.pollFormVisible && return this.pollFormVisible &&
this.newStatus.poll && this.newStatus.poll &&
this.newStatus.poll.error this.newStatus.poll.error
} },
...mapGetters(['mergedConfig'])
}, },
methods: { methods: {
postStatus (newStatus) { postStatus (newStatus) {

View file

@ -264,12 +264,9 @@
v-if="newStatus.files.length > 0" v-if="newStatus.files.length > 0"
class="upload_settings" class="upload_settings"
> >
<input <Checkbox v-model="newStatus.nsfw">
id="filesSensitive" {{ $t('post_status.attachments_sensitive') }}
v-model="newStatus.nsfw" </Checkbox>
type="checkbox"
>
<label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label>
</div> </div>
</form> </form>
</div> </div>

View file

@ -1,9 +1,11 @@
import PostStatusForm from '../post_status_form/post_status_form.vue' import PostStatusForm from '../post_status_form/post_status_form.vue'
import Modal from '../modal/modal.vue'
import get from 'lodash/get' import get from 'lodash/get'
const PostStatusModal = { const PostStatusModal = {
components: { components: {
PostStatusForm PostStatusForm,
Modal
}, },
data () { data () {
return { return {

View file

@ -1,14 +1,11 @@
<template> <template>
<div <Modal
v-if="isLoggedIn && !resettingForm" v-if="isLoggedIn && !resettingForm"
v-show="modalActivated" :is-open="modalActivated"
class="post-form-modal-view modal-view" class="post-form-modal-view"
@click="closeModal" @backdropClicked="closeModal"
> >
<div <div class="post-form-modal-panel panel">
class="post-form-modal-panel panel"
@click.stop=""
>
<div class="panel-heading"> <div class="panel-heading">
{{ $t('post_status.new_status') }} {{ $t('post_status.new_status') }}
</div> </div>
@ -18,15 +15,13 @@
@posted="closeModal" @posted="closeModal"
/> />
</div> </div>
</div> </Modal>
</template> </template>
<script src="./post_status_modal.js"></script> <script src="./post_status_modal.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; .modal-view.post-form-modal-view {
.post-form-modal-view {
align-items: flex-start; align-items: flex-start;
} }

View file

@ -1,10 +1,9 @@
import { mapGetters } from 'vuex'
const RetweetButton = { const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'], props: ['status', 'loggedIn', 'visibility'],
data () { data () {
return { return {
hidePostStatsLocal: typeof this.$store.state.config.hidePostStats === 'undefined'
? this.$store.state.instance.hidePostStats
: this.$store.state.config.hidePostStats,
animated: false animated: false
} }
}, },
@ -28,7 +27,8 @@ const RetweetButton = {
'retweeted-empty': !this.status.repeated, 'retweeted-empty': !this.status.repeated,
'animate-spin': this.animated 'animate-spin': this.animated
} }
} },
...mapGetters(['mergedConfig'])
} }
} }

View file

@ -7,7 +7,7 @@
:title="$t('tool_tip.repeat')" :title="$t('tool_tip.repeat')"
@click.prevent="retweet()" @click.prevent="retweet()"
/> />
<span v-if="!hidePostStatsLocal && status.repeat_num > 0">{{ status.repeat_num }}</span> <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
</template> </template>
<template v-else> <template v-else>
<i <i
@ -23,7 +23,7 @@
class="button-icon icon-retweet" class="button-icon icon-retweet"
:title="$t('tool_tip.repeat')" :title="$t('tool_tip.repeat')"
/> />
<span v-if="!hidePostStatsLocal && status.repeat_num > 0">{{ status.repeat_num }}</span> <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
</div> </div>
</template> </template>

View file

@ -5,88 +5,22 @@ import TabSwitcher from '../tab_switcher/tab_switcher.js'
import StyleSwitcher from '../style_switcher/style_switcher.vue' import StyleSwitcher from '../style_switcher/style_switcher.vue'
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue' import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
import { extractCommit } from '../../services/version/version.service' import { extractCommit } from '../../services/version/version.service'
import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js'
import Checkbox from '../checkbox/checkbox.vue'
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/' const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/' const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
const multiChoiceProperties = [
'postContentType',
'subjectLineBehavior'
]
const settings = { const settings = {
data () { data () {
const user = this.$store.state.config
const instance = this.$store.state.instance const instance = this.$store.state.instance
return { return {
hideAttachmentsLocal: user.hideAttachments,
padEmojiLocal: user.padEmoji,
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
maxThumbnails: user.maxThumbnails,
hideNsfwLocal: user.hideNsfw,
useOneClickNsfw: user.useOneClickNsfw,
hideISPLocal: user.hideISP,
preloadImage: user.preloadImage,
hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
? instance.hidePostStats
: user.hidePostStats,
hidePostStatsDefault: this.$t('settings.values.' + instance.hidePostStats),
hideUserStatsLocal: typeof user.hideUserStats === 'undefined'
? instance.hideUserStats
: user.hideUserStats,
hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats),
hideFilteredStatusesLocal: typeof user.hideFilteredStatuses === 'undefined'
? instance.hideFilteredStatuses
: user.hideFilteredStatuses,
hideFilteredStatusesDefault: this.$t('settings.values.' + instance.hideFilteredStatuses),
notificationVisibilityLocal: user.notificationVisibility,
replyVisibilityLocal: user.replyVisibility,
loopVideoLocal: user.loopVideo,
muteWordsString: user.muteWords.join('\n'),
autoLoadLocal: user.autoLoad,
streamingLocal: user.streaming,
pauseOnUnfocusedLocal: user.pauseOnUnfocused,
hoverPreviewLocal: user.hoverPreview,
autohideFloatingPostButtonLocal: user.autohideFloatingPostButton,
hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined'
? instance.hideMutedPosts
: user.hideMutedPosts,
hideMutedPostsDefault: this.$t('settings.values.' + instance.hideMutedPosts),
collapseMessageWithSubjectLocal: typeof user.collapseMessageWithSubject === 'undefined'
? instance.collapseMessageWithSubject
: user.collapseMessageWithSubject,
collapseMessageWithSubjectDefault: this.$t('settings.values.' + instance.collapseMessageWithSubject),
subjectLineBehaviorLocal: typeof user.subjectLineBehavior === 'undefined'
? instance.subjectLineBehavior
: user.subjectLineBehavior,
subjectLineBehaviorDefault: instance.subjectLineBehavior,
postContentTypeLocal: typeof user.postContentType === 'undefined'
? instance.postContentType
: user.postContentType,
postContentTypeDefault: instance.postContentType,
alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined'
? instance.alwaysShowSubjectInput
: user.alwaysShowSubjectInput,
alwaysShowSubjectInputDefault: this.$t('settings.values.' + instance.alwaysShowSubjectInput),
scopeCopyLocal: typeof user.scopeCopy === 'undefined'
? instance.scopeCopy
: user.scopeCopy,
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
minimalScopesModeLocal: typeof user.minimalScopesMode === 'undefined'
? instance.minimalScopesMode
: user.minimalScopesMode,
minimalScopesModeDefault: this.$t('settings.values.' + instance.minimalScopesMode),
stopGifs: user.stopGifs,
webPushNotificationsLocal: user.webPushNotifications,
loopVideoSilentOnlyLocal: user.loopVideosSilentOnly,
loopSilentAvailable: loopSilentAvailable:
// Firefox // Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
@ -94,8 +28,6 @@ const settings = {
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018 // Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'), Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
playVideosInModal: user.playVideosInModal,
useContainFit: user.useContainFit,
backendVersion: instance.backendVersion, backendVersion: instance.backendVersion,
frontendVersion: instance.frontendVersion frontendVersion: instance.frontendVersion
@ -104,7 +36,8 @@ const settings = {
components: { components: {
TabSwitcher, TabSwitcher,
StyleSwitcher, StyleSwitcher,
InterfaceLanguageSwitcher InterfaceLanguageSwitcher,
Checkbox
}, },
computed: { computed: {
user () { user () {
@ -122,116 +55,56 @@ const settings = {
}, },
backendVersionLink () { backendVersionLink () {
return pleromaBeCommitUrl + extractCommit(this.backendVersion) return pleromaBeCommitUrl + extractCommit(this.backendVersion)
},
// Getting localized values for instance-default properties
...instanceDefaultProperties
.filter(key => multiChoiceProperties.includes(key))
.map(key => [
key + 'DefaultValue',
function () {
return this.$store.getters.instanceDefaultConfig[key]
}
])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
...instanceDefaultProperties
.filter(key => !multiChoiceProperties.includes(key))
.map(key => [
key + 'LocalizedValue',
function () {
return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
}
])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Generating computed values for vuex properties
...Object.keys(configDefaultState)
.map(key => [key, {
get () { return this.$store.getters.mergedConfig[key] },
set (value) {
this.$store.dispatch('setOption', { name: key, value })
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Special cases (need to transform values)
muteWordsString: {
get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
set (value) {
this.$store.dispatch('setOption', {
name: 'muteWords',
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
}
} }
}, },
// Updating nested properties
watch: { watch: {
hideAttachmentsLocal (value) { notificationVisibility: {
this.$store.dispatch('setOption', { name: 'hideAttachments', value }) handler (value) {
}, this.$store.dispatch('setOption', {
padEmojiLocal (value) { name: 'notificationVisibility',
this.$store.dispatch('setOption', { name: 'padEmoji', value }) value: this.$store.getters.mergedConfig.notificationVisibility
}, })
hideAttachmentsInConvLocal (value) { },
this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value }) deep: true
},
hidePostStatsLocal (value) {
this.$store.dispatch('setOption', { name: 'hidePostStats', value })
},
hideUserStatsLocal (value) {
this.$store.dispatch('setOption', { name: 'hideUserStats', value })
},
hideFilteredStatusesLocal (value) {
this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
},
hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
},
useOneClickNsfw (value) {
this.$store.dispatch('setOption', { name: 'useOneClickNsfw', value })
},
preloadImage (value) {
this.$store.dispatch('setOption', { name: 'preloadImage', value })
},
hideISPLocal (value) {
this.$store.dispatch('setOption', { name: 'hideISP', value })
},
'notificationVisibilityLocal.likes' (value) {
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
},
'notificationVisibilityLocal.follows' (value) {
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
},
'notificationVisibilityLocal.repeats' (value) {
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
},
'notificationVisibilityLocal.mentions' (value) {
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
},
replyVisibilityLocal (value) {
this.$store.dispatch('setOption', { name: 'replyVisibility', value })
},
loopVideoLocal (value) {
this.$store.dispatch('setOption', { name: 'loopVideo', value })
},
loopVideoSilentOnlyLocal (value) {
this.$store.dispatch('setOption', { name: 'loopVideoSilentOnly', value })
},
autoLoadLocal (value) {
this.$store.dispatch('setOption', { name: 'autoLoad', value })
},
streamingLocal (value) {
this.$store.dispatch('setOption', { name: 'streaming', value })
},
pauseOnUnfocusedLocal (value) {
this.$store.dispatch('setOption', { name: 'pauseOnUnfocused', value })
},
hoverPreviewLocal (value) {
this.$store.dispatch('setOption', { name: 'hoverPreview', value })
},
autohideFloatingPostButtonLocal (value) {
this.$store.dispatch('setOption', { name: 'autohideFloatingPostButton', value })
},
muteWordsString (value) {
value = filter(value.split('\n'), (word) => trim(word).length > 0)
this.$store.dispatch('setOption', { name: 'muteWords', value })
},
hideMutedPostsLocal (value) {
this.$store.dispatch('setOption', { name: 'hideMutedPosts', value })
},
collapseMessageWithSubjectLocal (value) {
this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value })
},
scopeCopyLocal (value) {
this.$store.dispatch('setOption', { name: 'scopeCopy', value })
},
alwaysShowSubjectInputLocal (value) {
this.$store.dispatch('setOption', { name: 'alwaysShowSubjectInput', value })
},
subjectLineBehaviorLocal (value) {
this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value })
},
postContentTypeLocal (value) {
this.$store.dispatch('setOption', { name: 'postContentType', value })
},
minimalScopesModeLocal (value) {
this.$store.dispatch('setOption', { name: 'minimalScopesMode', value })
},
stopGifs (value) {
this.$store.dispatch('setOption', { name: 'stopGifs', value })
},
webPushNotificationsLocal (value) {
this.$store.dispatch('setOption', { name: 'webPushNotifications', value })
if (value) this.$store.dispatch('registerPushNotifications')
},
playVideosInModal (value) {
this.$store.dispatch('setOption', { name: 'playVideosInModal', value })
},
useContainFit (value) {
this.$store.dispatch('setOption', { name: 'useContainFit', value })
},
maxThumbnails (value) {
value = this.maxThumbnails = Math.floor(Math.max(value, 0))
this.$store.dispatch('setOption', { name: 'maxThumbnails', value })
} }
} }
} }

View file

@ -36,12 +36,9 @@
<interface-language-switcher /> <interface-language-switcher />
</li> </li>
<li v-if="instanceSpecificPanelPresent"> <li v-if="instanceSpecificPanelPresent">
<input <Checkbox v-model="hideISP">
id="hideISP" {{ $t('settings.hide_isp') }}
v-model="hideISPLocal" </Checkbox>
type="checkbox"
>
<label for="hideISP">{{ $t('settings.hide_isp') }}</label>
</li> </li>
</ul> </ul>
</div> </div>
@ -49,58 +46,42 @@
<h2>{{ $t('nav.timeline') }}</h2> <h2>{{ $t('nav.timeline') }}</h2>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<input <Checkbox v-model="hideMutedPosts">
id="hideMutedPosts" {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
v-model="hideMutedPostsLocal" </Checkbox>
type="checkbox"
>
<label for="hideMutedPosts">{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsDefault }) }}</label>
</li> </li>
<li> <li>
<input <Checkbox v-model="collapseMessageWithSubject">
id="collapseMessageWithSubject" {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
v-model="collapseMessageWithSubjectLocal" </Checkbox>
type="checkbox"
>
<label for="collapseMessageWithSubject">{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectDefault }) }}</label>
</li> </li>
<li> <li>
<input <Checkbox v-model="streaming">
id="streaming" {{ $t('settings.streaming') }}
v-model="streamingLocal" </Checkbox>
type="checkbox"
>
<label for="streaming">{{ $t('settings.streaming') }}</label>
<ul <ul
class="setting-list suboptions" class="setting-list suboptions"
:class="[{disabled: !streamingLocal}]" :class="[{disabled: !streaming}]"
> >
<li> <li>
<input <Checkbox
id="pauseOnUnfocused" v-model="pauseOnUnfocused"
v-model="pauseOnUnfocusedLocal" :disabled="!streaming"
:disabled="!streamingLocal"
type="checkbox"
> >
<label for="pauseOnUnfocused">{{ $t('settings.pause_on_unfocused') }}</label> {{ $t('settings.pause_on_unfocused') }}
</Checkbox>
</li> </li>
</ul> </ul>
</li> </li>
<li> <li>
<input <Checkbox v-model="autoLoad">
id="autoload" {{ $t('settings.autoload') }}
v-model="autoLoadLocal" </Checkbox>
type="checkbox"
>
<label for="autoload">{{ $t('settings.autoload') }}</label>
</li> </li>
<li> <li>
<input <Checkbox v-model="hoverPreview">
id="hoverPreview" {{ $t('settings.reply_link_preview') }}
v-model="hoverPreviewLocal" </Checkbox>
type="checkbox"
>
<label for="hoverPreview">{{ $t('settings.reply_link_preview') }}</label>
</li> </li>
</ul> </ul>
</div> </div>
@ -109,24 +90,14 @@
<h2>{{ $t('settings.composing') }}</h2> <h2>{{ $t('settings.composing') }}</h2>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<input <Checkbox v-model="scopeCopy">
id="scopeCopy" {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
v-model="scopeCopyLocal" </Checkbox>
type="checkbox"
>
<label for="scopeCopy">
{{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyDefault }) }}
</label>
</li> </li>
<li> <li>
<input <Checkbox v-model="alwaysShowSubjectInput">
id="subjectHide" {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
v-model="alwaysShowSubjectInputLocal" </Checkbox>
type="checkbox"
>
<label for="subjectHide">
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputDefault }) }}
</label>
</li> </li>
<li> <li>
<div> <div>
@ -137,19 +108,19 @@
> >
<select <select
id="subjectLineBehavior" id="subjectLineBehavior"
v-model="subjectLineBehaviorLocal" v-model="subjectLineBehavior"
> >
<option value="email"> <option value="email">
{{ $t('settings.subject_line_email') }} {{ $t('settings.subject_line_email') }}
{{ subjectLineBehaviorDefault == 'email' ? $t('settings.instance_default_simple') : '' }} {{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
</option> </option>
<option value="masto"> <option value="masto">
{{ $t('settings.subject_line_mastodon') }} {{ $t('settings.subject_line_mastodon') }}
{{ subjectLineBehaviorDefault == 'mastodon' ? $t('settings.instance_default_simple') : '' }} {{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
</option> </option>
<option value="noop"> <option value="noop">
{{ $t('settings.subject_line_noop') }} {{ $t('settings.subject_line_noop') }}
{{ subjectLineBehaviorDefault == 'noop' ? $t('settings.instance_default_simple') : '' }} {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
</option> </option>
</select> </select>
<i class="icon-down-open" /> <i class="icon-down-open" />
@ -165,7 +136,7 @@
> >
<select <select
id="postContentType" id="postContentType"
v-model="postContentTypeLocal" v-model="postContentType"
> >
<option <option
v-for="postFormat in postFormats" v-for="postFormat in postFormats"
@ -173,7 +144,7 @@
:value="postFormat" :value="postFormat"
> >
{{ $t(`post_status.content_type["${postFormat}"]`) }} {{ $t(`post_status.content_type["${postFormat}"]`) }}
{{ postContentTypeDefault === postFormat ? $t('settings.instance_default_simple') : '' }} {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
</option> </option>
</select> </select>
<i class="icon-down-open" /> <i class="icon-down-open" />
@ -181,30 +152,19 @@
</div> </div>
</li> </li>
<li> <li>
<input <Checkbox v-model="minimalScopesMode">
id="minimalScopesMode" {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
v-model="minimalScopesModeLocal" </Checkbox>
type="checkbox"
>
<label for="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeDefault }) }}
</label>
</li> </li>
<li> <li>
<input <Checkbox v-model="autohideFloatingPostButton">
id="autohideFloatingPostButton" {{ $t('settings.autohide_floating_post_button') }}
v-model="autohideFloatingPostButtonLocal" </Checkbox>
type="checkbox"
>
<label for="autohideFloatingPostButton">{{ $t('settings.autohide_floating_post_button') }}</label>
</li> </li>
<li> <li>
<input <Checkbox v-model="padEmoji">
id="padEmoji" {{ $t('settings.pad_emoji') }}
v-model="padEmojiLocal" </Checkbox>
type="checkbox"
>
<label for="padEmoji">{{ $t('settings.pad_emoji') }}</label>
</li> </li>
</ul> </ul>
</div> </div>
@ -213,23 +173,19 @@
<h2>{{ $t('settings.attachments') }}</h2> <h2>{{ $t('settings.attachments') }}</h2>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<input <Checkbox v-model="hideAttachments">
id="hideAttachments" {{ $t('settings.hide_attachments_in_tl') }}
v-model="hideAttachmentsLocal" </Checkbox>
type="checkbox"
>
<label for="hideAttachments">{{ $t('settings.hide_attachments_in_tl') }}</label>
</li> </li>
<li> <li>
<input <Checkbox v-model="hideAttachmentsInConv">
id="hideAttachmentsInConv" {{ $t('settings.hide_attachments_in_convo') }}
v-model="hideAttachmentsInConvLocal" </Checkbox>
type="checkbox"
>
<label for="hideAttachmentsInConv">{{ $t('settings.hide_attachments_in_convo') }}</label>
</li> </li>
<li> <li>
<label for="maxThumbnails">{{ $t('settings.max_thumbnails') }}</label> <label for="maxThumbnails">
{{ $t('settings.max_thumbnails') }}
</label>
<input <input
id="maxThumbnails" id="maxThumbnails"
v-model.number="maxThumbnails" v-model.number="maxThumbnails"
@ -240,60 +196,48 @@
> >
</li> </li>
<li> <li>
<input <Checkbox v-model="hideNsfw">
id="hideNsfw" {{ $t('settings.nsfw_clickthrough') }}
v-model="hideNsfwLocal" </Checkbox>
type="checkbox"
>
<label for="hideNsfw">{{ $t('settings.nsfw_clickthrough') }}</label>
</li> </li>
<ul class="setting-list suboptions"> <ul class="setting-list suboptions">
<li> <li>
<input <Checkbox
id="preloadImage"
v-model="preloadImage" v-model="preloadImage"
:disabled="!hideNsfwLocal" :disabled="!hideNsfw"
type="checkbox"
> >
<label for="preloadImage">{{ $t('settings.preload_images') }}</label> {{ $t('settings.preload_images') }}
</Checkbox>
</li> </li>
<li> <li>
<input <Checkbox
id="useOneClickNsfw"
v-model="useOneClickNsfw" v-model="useOneClickNsfw"
:disabled="!hideNsfwLocal" :disabled="!hideNsfw"
type="checkbox"
> >
<label for="useOneClickNsfw">{{ $t('settings.use_one_click_nsfw') }}</label> {{ $t('settings.use_one_click_nsfw') }}
</Checkbox>
</li> </li>
</ul> </ul>
<li> <li>
<input <Checkbox v-model="stopGifs">
id="stopGifs" {{ $t('settings.stop_gifs') }}
v-model="stopGifs" </Checkbox>
type="checkbox"
>
<label for="stopGifs">{{ $t('settings.stop_gifs') }}</label>
</li> </li>
<li> <li>
<input <Checkbox v-model="loopVideo">
id="loopVideo" {{ $t('settings.loop_video') }}
v-model="loopVideoLocal" </Checkbox>
type="checkbox"
>
<label for="loopVideo">{{ $t('settings.loop_video') }}</label>
<ul <ul
class="setting-list suboptions" class="setting-list suboptions"
:class="[{disabled: !streamingLocal}]" :class="[{disabled: !streaming}]"
> >
<li> <li>
<input <Checkbox
id="loopVideoSilentOnly" v-model="loopVideoSilentOnly"
v-model="loopVideoSilentOnlyLocal" :disabled="!loopVideo || !loopSilentAvailable"
:disabled="!loopVideoLocal || !loopSilentAvailable"
type="checkbox"
> >
<label for="loopVideoSilentOnly">{{ $t('settings.loop_video_silent_only') }}</label> {{ $t('settings.loop_video_silent_only') }}
</Checkbox>
<div <div
v-if="!loopSilentAvailable" v-if="!loopSilentAvailable"
class="unavailable" class="unavailable"
@ -304,20 +248,14 @@
</ul> </ul>
</li> </li>
<li> <li>
<input <Checkbox v-model="playVideosInModal">
id="playVideosInModal" {{ $t('settings.play_videos_in_modal') }}
v-model="playVideosInModal" </Checkbox>
type="checkbox"
>
<label for="playVideosInModal">{{ $t('settings.play_videos_in_modal') }}</label>
</li> </li>
<li> <li>
<input <Checkbox v-model="useContainFit">
id="useContainFit" {{ $t('settings.use_contain_fit') }}
v-model="useContainFit" </Checkbox>
type="checkbox"
>
<label for="useContainFit">{{ $t('settings.use_contain_fit') }}</label>
</li> </li>
</ul> </ul>
</div> </div>
@ -326,14 +264,9 @@
<h2>{{ $t('settings.notifications') }}</h2> <h2>{{ $t('settings.notifications') }}</h2>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<input <Checkbox v-model="webPushNotifications">
id="webPushNotifications"
v-model="webPushNotificationsLocal"
type="checkbox"
>
<label for="webPushNotifications">
{{ $t('settings.enable_web_push_notifications') }} {{ $t('settings.enable_web_push_notifications') }}
</label> </Checkbox>
</li> </li>
</ul> </ul>
</div> </div>
@ -351,44 +284,24 @@
<span class="label">{{ $t('settings.notification_visibility') }}</span> <span class="label">{{ $t('settings.notification_visibility') }}</span>
<ul class="option-list"> <ul class="option-list">
<li> <li>
<input <Checkbox v-model="notificationVisibility.likes">
id="notification-visibility-likes"
v-model="notificationVisibilityLocal.likes"
type="checkbox"
>
<label for="notification-visibility-likes">
{{ $t('settings.notification_visibility_likes') }} {{ $t('settings.notification_visibility_likes') }}
</label> </Checkbox>
</li> </li>
<li> <li>
<input <Checkbox v-model="notificationVisibility.repeats">
id="notification-visibility-repeats"
v-model="notificationVisibilityLocal.repeats"
type="checkbox"
>
<label for="notification-visibility-repeats">
{{ $t('settings.notification_visibility_repeats') }} {{ $t('settings.notification_visibility_repeats') }}
</label> </Checkbox>
</li> </li>
<li> <li>
<input <Checkbox v-model="notificationVisibility.follows">
id="notification-visibility-follows"
v-model="notificationVisibilityLocal.follows"
type="checkbox"
>
<label for="notification-visibility-follows">
{{ $t('settings.notification_visibility_follows') }} {{ $t('settings.notification_visibility_follows') }}
</label> </Checkbox>
</li> </li>
<li> <li>
<input <Checkbox v-model="notificationVisibility.mentions">
id="notification-visibility-mentions"
v-model="notificationVisibilityLocal.mentions"
type="checkbox"
>
<label for="notification-visibility-mentions">
{{ $t('settings.notification_visibility_mentions') }} {{ $t('settings.notification_visibility_mentions') }}
</label> </Checkbox>
</li> </li>
</ul> </ul>
</div> </div>
@ -400,7 +313,7 @@
> >
<select <select
id="replyVisibility" id="replyVisibility"
v-model="replyVisibilityLocal" v-model="replyVisibility"
> >
<option <option
value="all" value="all"
@ -413,24 +326,14 @@
</label> </label>
</div> </div>
<div> <div>
<input <Checkbox v-model="hidePostStats">
id="hidePostStats" {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
v-model="hidePostStatsLocal" </Checkbox>
type="checkbox"
>
<label for="hidePostStats">
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsDefault }) }}
</label>
</div> </div>
<div> <div>
<input <Checkbox v-model="hideUserStats">
id="hideUserStats" {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
v-model="hideUserStatsLocal" </Checkbox>
type="checkbox"
>
<label for="hideUserStats">
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsDefault }) }}
</label>
</div> </div>
</div> </div>
<div class="setting-item"> <div class="setting-item">
@ -442,14 +345,9 @@
/> />
</div> </div>
<div> <div>
<input <Checkbox v-model="hideFilteredStatuses">
id="hideFilteredStatuses" {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
v-model="hideFilteredStatusesLocal" </Checkbox>
type="checkbox"
>
<label for="hideFilteredStatuses">
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesDefault }) }}
</label>
</div> </div>
</div> </div>
</div> </div>

View file

@ -10,11 +10,13 @@ import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue' import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue' import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import StatusPopover from '../status_popover/status_popover.vue'
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 fileType from 'src/services/file_type/file_type.service' import fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { filter, find, unescape, uniqBy } from 'lodash' import { filter, unescape, uniqBy } from 'lodash'
import { mapGetters } from 'vuex'
const Status = { const Status = {
name: 'Status', name: 'Status',
@ -37,25 +39,19 @@ const Status = {
replying: false, replying: false,
unmuted: false, unmuted: false,
userExpanded: false, userExpanded: false,
preview: null,
showPreview: false,
showingTall: this.inConversation && this.focused, showingTall: this.inConversation && this.focused,
showingLongSubject: false, showingLongSubject: false,
error: null, error: null,
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject,
betterShadow: this.$store.state.interface.browserSupport.cssFilter betterShadow: this.$store.state.interface.browserSupport.cssFilter
} }
}, },
computed: { computed: {
localCollapseSubjectDefault () { localCollapseSubjectDefault () {
return typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' return this.mergedConfig.collapseMessageWithSubject
? this.$store.state.instance.collapseMessageWithSubject
: this.$store.state.config.collapseMessageWithSubject
}, },
muteWords () { muteWords () {
return this.$store.state.config.muteWords return this.mergedConfig.muteWords
}, },
repeaterClass () { repeaterClass () {
const user = this.statusoid.user const user = this.statusoid.user
@ -70,18 +66,18 @@ const Status = {
}, },
repeaterStyle () { repeaterStyle () {
const user = this.statusoid.user const user = this.statusoid.user
const highlight = this.$store.state.config.highlight const highlight = this.mergedConfig.highlight
return highlightStyle(highlight[user.screen_name]) return highlightStyle(highlight[user.screen_name])
}, },
userStyle () { userStyle () {
if (this.noHeading) return if (this.noHeading) return
const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
const highlight = this.$store.state.config.highlight const highlight = this.mergedConfig.highlight
return highlightStyle(highlight[user.screen_name]) return highlightStyle(highlight[user.screen_name])
}, },
hideAttachments () { hideAttachments () {
return (this.$store.state.config.hideAttachments && !this.inConversation) || return (this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation) (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
}, },
userProfileLink () { userProfileLink () {
return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name) return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
@ -120,9 +116,7 @@ const Status = {
}, },
muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) }, muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
hideFilteredStatuses () { hideFilteredStatuses () {
return typeof this.$store.state.config.hideFilteredStatuses === 'undefined' return this.mergedConfig.hideFilteredStatuses
? this.$store.state.instance.hideFilteredStatuses
: this.$store.state.config.hideFilteredStatuses
}, },
hideStatus () { hideStatus () {
return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses) return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)
@ -163,7 +157,7 @@ const Status = {
} }
}, },
hideReply () { hideReply () {
if (this.$store.state.config.replyVisibility === 'all') { if (this.mergedConfig.replyVisibility === 'all') {
return false return false
} }
if (this.inConversation || !this.isReply) { if (this.inConversation || !this.isReply) {
@ -175,7 +169,7 @@ const Status = {
if (this.status.type === 'retweet') { if (this.status.type === 'retweet') {
return false return false
} }
const checkFollowing = this.$store.state.config.replyVisibility === 'following' const checkFollowing = this.mergedConfig.replyVisibility === 'following'
for (var i = 0; i < this.status.attentions.length; ++i) { for (var i = 0; i < this.status.attentions.length; ++i) {
if (this.status.user.id === this.status.attentions[i].id) { if (this.status.user.id === this.status.attentions[i].id) {
continue continue
@ -220,9 +214,7 @@ const Status = {
replySubject () { replySubject () {
if (!this.status.summary) return '' if (!this.status.summary) return ''
const decodedSummary = unescape(this.status.summary) const decodedSummary = unescape(this.status.summary)
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined' const behavior = this.mergedConfig.subjectLineBehavior
? this.$store.state.instance.subjectLineBehavior
: this.$store.state.config.subjectLineBehavior
const startsWithRe = decodedSummary.match(/^re[: ]/i) const startsWithRe = decodedSummary.match(/^re[: ]/i)
if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') { if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') {
return decodedSummary return decodedSummary
@ -233,8 +225,8 @@ const Status = {
} }
}, },
attachmentSize () { attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) || if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation) || (this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
(this.status.attachments.length > this.maxThumbnails)) { (this.status.attachments.length > this.maxThumbnails)) {
return 'hide' return 'hide'
} else if (this.compact) { } else if (this.compact) {
@ -246,7 +238,7 @@ const Status = {
if (this.attachmentSize === 'hide') { if (this.attachmentSize === 'hide') {
return [] return []
} }
return this.$store.state.config.playVideosInModal return this.mergedConfig.playVideosInModal
? ['image', 'video'] ? ['image', 'video']
: ['image'] : ['image']
}, },
@ -261,7 +253,7 @@ const Status = {
) )
}, },
maxThumbnails () { maxThumbnails () {
return this.$store.state.config.maxThumbnails return this.mergedConfig.maxThumbnails
}, },
contentHtml () { contentHtml () {
if (!this.status.summary_html) { if (!this.status.summary_html) {
@ -284,10 +276,9 @@ const Status = {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ') return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
}, },
hidePostStats () { hidePostStats () {
return typeof this.$store.state.config.hidePostStats === 'undefined' return this.mergedConfig.hidePostStats
? this.$store.state.instance.hidePostStats },
: this.$store.state.config.hidePostStats ...mapGetters(['mergedConfig'])
}
}, },
components: { components: {
Attachment, Attachment,
@ -301,7 +292,8 @@ const Status = {
Gallery, Gallery,
LinkPreview, LinkPreview,
AvatarList, AvatarList,
Timeago Timeago,
StatusPopover
}, },
methods: { methods: {
visibilityIcon (visibility) { visibilityIcon (visibility) {
@ -376,27 +368,6 @@ const Status = {
this.expandingSubject = true this.expandingSubject = true
} }
}, },
replyEnter (id, event) {
this.showPreview = true
const targetId = id
const statuses = this.$store.state.statuses.allStatuses
if (!this.preview) {
// if we have the status somewhere already
this.preview = find(statuses, { 'id': targetId })
// or if we have to fetch it
if (!this.preview) {
this.$store.state.api.backendInteractor.fetchStatus({ id }).then((status) => {
this.preview = status
})
}
} else if (this.preview.id !== targetId) {
this.preview = find(statuses, { 'id': targetId })
}
},
replyLeave () {
this.showPreview = false
},
generateUserProfileLink (id, name) { generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
}, },

View file

@ -174,20 +174,26 @@
v-if="isReply" v-if="isReply"
class="reply-to-and-accountname" class="reply-to-and-accountname"
> >
<a <StatusPopover
class="reply-to" v-if="!isPreview"
href="#" :status-id="status.in_reply_to_status_id"
:aria-label="$t('tool_tip.reply')"
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
@mouseenter.prevent.stop="replyEnter(status.in_reply_to_status_id, $event)"
@mouseleave.prevent.stop="replyLeave()"
> >
<i <a
v-if="!isPreview" class="reply-to"
class="button-icon icon-reply" href="#"
/> :aria-label="$t('tool_tip.reply')"
<span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span> @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
</a> >
<i class="button-icon icon-reply" />
<span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span>
</a>
</StatusPopover>
<span
v-else
class="reply-to"
>
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
</span>
<router-link :to="replyProfileLink"> <router-link :to="replyProfileLink">
{{ replyToName }} {{ replyToName }}
</router-link> </router-link>
@ -199,50 +205,25 @@
</span> </span>
</div> </div>
<div <div
v-if="inConversation && !isPreview" v-if="inConversation && !isPreview && replies && replies.length"
class="replies" class="replies"
> >
<span <span class="faint">{{ $t('status.replies_list') }}</span>
v-if="replies && replies.length" <StatusPopover
class="faint" v-for="reply in replies"
>{{ $t('status.replies_list') }}</span> :key="reply.id"
<template v-if="replies"> :status-id="reply.id"
<span >
v-for="reply in replies" <a
:key="reply.id" href="#"
class="reply-link faint" class="reply-link"
> @click.prevent="gotoOriginal(reply.id)"
<a >{{ reply.name }}</a>
href="#" </StatusPopover>
@click.prevent="gotoOriginal(reply.id)"
@mouseenter="replyEnter(reply.id, $event)"
@mouseout="replyLeave()"
>{{ reply.name }}</a>
</span>
</template>
</div> </div>
</div> </div>
</div> </div>
<div
v-if="showPreview"
class="status-preview-container"
>
<status
v-if="preview"
class="status-preview"
:is-preview="true"
:statusoid="preview"
:compact="true"
/>
<div
v-else
class="status-preview status-preview-loading"
>
<i class="icon-spin4 animate-spin" />
</div>
</div>
<div <div
v-if="longSubject" v-if="longSubject"
class="status-content-wrapper" class="status-content-wrapper"
@ -439,18 +420,6 @@ $status-margin: 0.75em;
min-width: 0; min-width: 0;
} }
.status-preview.status-el {
border-style: solid;
border-width: 1px;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
.status-preview-container {
position: relative;
max-width: 100%;
}
.status-pin { .status-pin {
padding: $status-margin $status-margin 0; padding: $status-margin $status-margin 0;
display: flex; display: flex;
@ -458,44 +427,6 @@ $status-margin: 0.75em;
justify-content: flex-end; justify-content: flex-end;
} }
.status-preview {
position: absolute;
max-width: 95%;
display: flex;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-style: solid;
border-width: 1px;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
box-shadow: var(--popupShadow);
margin-top: 0.25em;
margin-left: 0.5em;
z-index: 50;
.status {
flex: 1;
border: 0;
min-width: 15em;
}
}
.status-preview-loading {
display: block;
min-width: 15em;
padding: 1em;
text-align: center;
border-width: 1px;
border-style: solid;
i {
font-size: 2em;
}
}
.media-left { .media-left {
margin-right: $status-margin; margin-right: $status-margin;
} }
@ -553,11 +484,6 @@ $status-margin: 0.75em;
flex-basis: 100%; flex-basis: 100%;
margin-bottom: 0.5em; margin-bottom: 0.5em;
a {
display: inline-block;
word-break: break-all;
}
small { small {
font-weight: lighter; font-weight: lighter;
} }
@ -568,6 +494,11 @@ $status-margin: 0.75em;
justify-content: space-between; justify-content: space-between;
line-height: 18px; line-height: 18px;
a {
display: inline-block;
word-break: break-all;
}
.name-and-account-name { .name-and-account-name {
display: flex; display: flex;
min-width: 0; min-width: 0;
@ -600,6 +531,7 @@ $status-margin: 0.75em;
} }
.heading-reply-row { .heading-reply-row {
position: relative;
align-content: baseline; align-content: baseline;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
@ -608,11 +540,13 @@ $status-margin: 0.75em;
flex-wrap: wrap; flex-wrap: wrap;
align-items: stretch; align-items: stretch;
a { > .reply-to-and-accountname > a {
max-width: 100%; max-width: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
display: inline-block;
word-break: break-all;
} }
} }
@ -639,6 +573,8 @@ $status-margin: 0.75em;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin: 0 0.4em 0 0.2em; margin: 0 0.4em 0 0.2em;
color: $fallback--faint;
color: var(--faint, $fallback--faint);
} }
.replies-separator { .replies-separator {
@ -840,6 +776,11 @@ $status-margin: 0.75em;
&.button-icon-active { &.button-icon-active {
color: $fallback--cBlue; color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue); color: var(--cBlue, $fallback--cBlue);
}
}
.button-icon.icon-reply {
&:not(.button-icon-disabled) {
cursor: pointer; cursor: pointer;
} }
} }

View file

@ -0,0 +1,34 @@
import { find } from 'lodash'
const StatusPopover = {
name: 'StatusPopover',
props: [
'statusId'
],
data () {
return {
popperOptions: {
modifiers: {
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
}
}
}
},
computed: {
status () {
return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
}
},
components: {
Status: () => import('../status/status.vue')
},
methods: {
enter () {
if (!this.status) {
this.$store.dispatch('fetchStatus', this.statusId)
}
}
}
}
export default StatusPopover

View file

@ -0,0 +1,85 @@
<template>
<v-popover
popover-class="status-popover"
placement="top-start"
:popper-options="popperOptions"
@show="enter()"
>
<template slot="popover">
<Status
v-if="status"
:is-preview="true"
:statusoid="status"
:compact="true"
/>
<div
v-else
class="status-preview-loading"
>
<i class="icon-spin4 animate-spin" />
</div>
</template>
<slot />
</v-popover>
</template>
<script src="./status_popover.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.tooltip.popover.status-popover {
font-size: 1rem;
min-width: 15em;
max-width: 95%;
margin-left: 0.5em;
.popover-inner {
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-style: solid;
border-width: 1px;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
box-shadow: var(--popupShadow);
}
.popover-arrow::before {
position: absolute;
content: '';
left: -7px;
border: solid 7px transparent;
z-index: -1;
}
&[x-placement^="bottom-start"] .popover-arrow::before {
top: -2px;
border-top-width: 0;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
}
&[x-placement^="top-start"] .popover-arrow::before {
bottom: -2px;
border-bottom-width: 0;
border-top-color: $fallback--border;
border-top-color: var(--border, $fallback--border);
}
.status-el.status-el {
border: none;
}
.status-preview-loading {
padding: 1em;
text-align: center;
i {
font-size: 2em;
}
}
}
</style>

View file

@ -3,11 +3,12 @@ const StillImage = {
'src', 'src',
'referrerpolicy', 'referrerpolicy',
'mimetype', 'mimetype',
'imageLoadError' 'imageLoadError',
'imageLoadHandler'
], ],
data () { data () {
return { return {
stopGifs: this.$store.state.config.stopGifs stopGifs: this.$store.getters.mergedConfig.stopGifs
} }
}, },
computed: { computed: {
@ -17,6 +18,7 @@ const StillImage = {
}, },
methods: { methods: {
onLoad () { onLoad () {
this.imageLoadHandler && this.imageLoadHandler(this.$refs.src)
const canvas = this.$refs.canvas const canvas = this.$refs.canvas
if (!canvas) return if (!canvas) return
const width = this.$refs.src.naturalWidth const width = this.$refs.src.naturalWidth

View file

@ -10,6 +10,7 @@ import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
import TabSwitcher from '../tab_switcher/tab_switcher.js' import TabSwitcher from '../tab_switcher/tab_switcher.js'
import Preview from './preview.vue' import Preview from './preview.vue'
import ExportImport from '../export_import/export_import.vue' import ExportImport from '../export_import/export_import.vue'
import Checkbox from '../checkbox/checkbox.vue'
// List of color values used in v1 // List of color values used in v1
const v1OnlyNames = [ const v1OnlyNames = [
@ -27,7 +28,7 @@ export default {
data () { data () {
return { return {
availableStyles: [], availableStyles: [],
selected: this.$store.state.config.theme, selected: this.$store.getters.mergedConfig.theme,
previewShadows: {}, previewShadows: {},
previewColors: {}, previewColors: {},
@ -112,7 +113,7 @@ export default {
}) })
}, },
mounted () { mounted () {
this.normalizeLocalState(this.$store.state.config.customTheme) this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme)
if (typeof this.shadowSelected === 'undefined') { if (typeof this.shadowSelected === 'undefined') {
this.shadowSelected = this.shadowsAvailable[0] this.shadowSelected = this.shadowsAvailable[0]
} }
@ -341,7 +342,8 @@ export default {
FontControl, FontControl,
TabSwitcher, TabSwitcher,
Preview, Preview,
ExportImport ExportImport,
Checkbox
}, },
methods: { methods: {
setCustomTheme () { setCustomTheme () {
@ -368,9 +370,9 @@ export default {
return version >= 1 || version <= 2 return version >= 1 || version <= 2
}, },
clearAll () { clearAll () {
const state = this.$store.state.config.customTheme const state = this.$store.getters.mergedConfig.customTheme
const version = state.colors ? 2 : 'l1' const version = state.colors ? 2 : 'l1'
this.normalizeLocalState(this.$store.state.config.customTheme, version) this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version)
}, },
// Clears all the extra stuff when loading V1 theme // Clears all the extra stuff when loading V1 theme

View file

@ -42,44 +42,29 @@
</div> </div>
<div class="save-load-options"> <div class="save-load-options">
<span class="keep-option"> <span class="keep-option">
<input <Checkbox v-model="keepColor">
id="keep-color" {{ $t('settings.style.switcher.keep_color') }}
v-model="keepColor" </Checkbox>
type="checkbox"
>
<label for="keep-color">{{ $t('settings.style.switcher.keep_color') }}</label>
</span> </span>
<span class="keep-option"> <span class="keep-option">
<input <Checkbox v-model="keepShadows">
id="keep-shadows" {{ $t('settings.style.switcher.keep_shadows') }}
v-model="keepShadows" </Checkbox>
type="checkbox"
>
<label for="keep-shadows">{{ $t('settings.style.switcher.keep_shadows') }}</label>
</span> </span>
<span class="keep-option"> <span class="keep-option">
<input <Checkbox v-model="keepOpacity">
id="keep-opacity" {{ $t('settings.style.switcher.keep_opacity') }}
v-model="keepOpacity" </Checkbox>
type="checkbox"
>
<label for="keep-opacity">{{ $t('settings.style.switcher.keep_opacity') }}</label>
</span> </span>
<span class="keep-option"> <span class="keep-option">
<input <Checkbox v-model="keepRoundness">
id="keep-roundness" {{ $t('settings.style.switcher.keep_roundness') }}
v-model="keepRoundness" </Checkbox>
type="checkbox"
>
<label for="keep-roundness">{{ $t('settings.style.switcher.keep_roundness') }}</label>
</span> </span>
<span class="keep-option"> <span class="keep-option">
<input <Checkbox v-model="keepFonts">
id="keep-fonts" {{ $t('settings.style.switcher.keep_fonts') }}
v-model="keepFonts" </Checkbox>
type="checkbox"
>
<label for="keep-fonts">{{ $t('settings.style.switcher.keep_fonts') }}</label>
</span> </span>
<p>{{ $t('settings.style.switcher.save_load_hint') }}</p> <p>{{ $t('settings.style.switcher.save_load_hint') }}</p>
</div> </div>

View file

@ -141,7 +141,7 @@ const Timeline = {
const bodyBRect = document.body.getBoundingClientRect() const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -(bodyBRect.y)) const height = Math.max(bodyBRect.height, -(bodyBRect.y))
if (this.timeline.loading === false && if (this.timeline.loading === false &&
this.$store.state.config.autoLoad && this.$store.getters.mergedConfig.autoLoad &&
this.$el.offsetHeight > 0 && this.$el.offsetHeight > 0 &&
(window.innerHeight + window.pageYOffset) >= (height - 750)) { (window.innerHeight + window.pageYOffset) >= (height - 750)) {
this.fetchOlderStatuses() this.fetchOlderStatuses()
@ -153,7 +153,7 @@ const Timeline = {
}, },
watch: { watch: {
newStatusCount (count) { newStatusCount (count) {
if (!this.$store.state.config.streaming) { if (!this.$store.getters.mergedConfig.streaming) {
return return
} }
if (count > 0) { if (count > 0) {
@ -162,7 +162,7 @@ const Timeline = {
const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
if (top < 15 && if (top < 15 &&
!this.paused && !this.paused &&
!(this.unfocused && this.$store.state.config.pauseOnUnfocused) !(this.unfocused && this.$store.getters.mergedConfig.pauseOnUnfocused)
) { ) {
this.showNewStatuses() this.showNewStatuses()
} else { } else {

View file

@ -1,19 +1,20 @@
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js' import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
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 } from 'vuex'
export default { export default {
props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' ], props: [
'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
],
data () { data () {
return { return {
followRequestInProgress: false, followRequestInProgress: false,
hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined'
? this.$store.state.instance.hideUserStats
: this.$store.state.config.hideUserStats,
betterShadow: this.$store.state.interface.browserSupport.cssFilter betterShadow: this.$store.state.interface.browserSupport.cssFilter
} }
}, },
@ -29,9 +30,9 @@ export default {
}] }]
}, },
style () { style () {
const color = this.$store.state.config.customTheme.colors const color = this.$store.getters.mergedConfig.customTheme.colors
? this.$store.state.config.customTheme.colors.bg // v2 ? this.$store.getters.mergedConfig.customTheme.colors.bg // v2
: this.$store.state.config.colors.bg // v1 : this.$store.getters.mergedConfig.colors.bg // v1
if (color) { if (color) {
const rgb = (typeof color === 'string') ? hex2rgb(color) : color const rgb = (typeof color === 'string') ? hex2rgb(color) : color
@ -63,21 +64,22 @@ export default {
}, },
userHighlightType: { userHighlightType: {
get () { get () {
const data = this.$store.state.config.highlight[this.user.screen_name] const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
return (data && data.type) || 'disabled' return (data && data.type) || 'disabled'
}, },
set (type) { set (type) {
const data = this.$store.state.config.highlight[this.user.screen_name] const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
if (type !== 'disabled') { if (type !== 'disabled') {
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: (data && data.color) || '#FFFFFF', type }) this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: (data && data.color) || '#FFFFFF', type })
} else { } else {
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined }) this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined })
} }
} },
...mapGetters(['mergedConfig'])
}, },
userHighlightColor: { userHighlightColor: {
get () { get () {
const data = this.$store.state.config.highlight[this.user.screen_name] const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
return data && data.color return data && data.color
}, },
set (color) { set (color) {
@ -90,36 +92,18 @@ export default {
const validRole = rights.admin || rights.moderator const validRole = rights.admin || rights.moderator
const roleTitle = rights.admin ? 'admin' : 'moderator' const roleTitle = rights.admin ? 'admin' : 'moderator'
return validRole && roleTitle return validRole && roleTitle
} },
...mapGetters(['mergedConfig'])
}, },
components: { components: {
UserAvatar, UserAvatar,
RemoteFollow, RemoteFollow,
ModerationTools, ModerationTools,
ProgressButton AccountActions,
ProgressButton,
FollowButton
}, },
methods: { methods: {
followUser () {
const store = this.$store
this.followRequestInProgress = true
requestFollow(this.user, store).then(() => {
this.followRequestInProgress = false
})
},
unfollowUser () {
const store = this.$store
this.followRequestInProgress = true
requestUnfollow(this.user, store).then(() => {
this.followRequestInProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
})
},
blockUser () {
this.$store.dispatch('blockUser', this.user.id)
},
unblockUser () {
this.$store.dispatch('unblockUser', this.user.id)
},
muteUser () { muteUser () {
this.$store.dispatch('muteUser', this.user.id) this.$store.dispatch('muteUser', this.user.id)
}, },
@ -147,10 +131,10 @@ export default {
} }
}, },
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(
}, user.id, user.screen_name,
reportUser () { this.$store.state.instance.restrictedNicknames
this.$store.dispatch('openUserReportingModal', this.user.id) )
}, },
zoomAvatar () { zoomAvatar () {
const attachment = { const attachment = {
@ -159,9 +143,6 @@ export default {
} }
this.$store.dispatch('setMedia', [attachment]) this.$store.dispatch('setMedia', [attachment])
this.$store.dispatch('setCurrent', attachment) this.$store.dispatch('setCurrent', attachment)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
} }
} }
} }

View file

@ -66,8 +66,11 @@
> >
<i class="icon-link-ext usersettings" /> <i class="icon-link-ext usersettings" />
</a> </a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
/>
</div> </div>
<div class="bottom-line"> <div class="bottom-line">
<router-link <router-link
class="user-screen-name" class="user-screen-name"
@ -81,7 +84,7 @@
>{{ visibleRole }}</span> >{{ visibleRole }}</span>
<span v-if="user.locked"><i class="icon icon-lock" /></span> <span v-if="user.locked"><i class="icon icon-lock" /></span>
<span <span
v-if="!hideUserStatsLocal && !hideBio" v-if="!mergedConfig.hideUserStats && !hideBio"
class="dailyAvg" class="dailyAvg"
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</span> >{{ dailyAvg }} {{ $t('user_card.per_day') }}</span>
</div> </div>
@ -135,72 +138,27 @@
v-if="loggedIn && isOtherUser" v-if="loggedIn && isOtherUser"
class="user-interactions" class="user-interactions"
> >
<div v-if="!user.following"> <div class="btn-group">
<button <FollowButton :user="user" />
class="btn btn-default btn-block" <template v-if="user.following">
:disabled="followRequestInProgress" <ProgressButton
:title="user.requested ? $t('user_card.follow_again') : ''" v-if="!user.subscribed"
@click="followUser" class="btn btn-default"
> :click="subscribeUser"
<template v-if="followRequestInProgress"> :title="$t('user_card.subscribe')"
{{ $t('user_card.follow_progress') }} >
</template> <i class="icon-bell-alt" />
<template v-else-if="user.requested"> </ProgressButton>
{{ $t('user_card.follow_sent') }} <ProgressButton
</template> v-else
<template v-else> class="btn btn-default pressed"
{{ $t('user_card.follow') }} :click="unsubscribeUser"
</template> :title="$t('user_card.unsubscribe')"
</button> >
<i class="icon-bell-ringing-o" />
</ProgressButton>
</template>
</div> </div>
<div v-else-if="followRequestInProgress">
<button
class="btn btn-default btn-block pressed"
disabled
:title="$t('user_card.follow_unfollow')"
@click="unfollowUser"
>
{{ $t('user_card.follow_progress') }}
</button>
</div>
<div
v-else
class="btn-group"
>
<button
class="btn btn-default pressed"
:title="$t('user_card.follow_unfollow')"
@click="unfollowUser"
>
{{ $t('user_card.following') }}
</button>
<ProgressButton
v-if="!user.subscribed"
class="btn btn-default"
:click="subscribeUser"
:title="$t('user_card.subscribe')"
>
<i class="icon-bell-alt" />
</ProgressButton>
<ProgressButton
v-else
class="btn btn-default pressed"
:click="unsubscribeUser"
:title="$t('user_card.unsubscribe')"
>
<i class="icon-bell-ringing-o" />
</ProgressButton>
</div>
<div>
<button
class="btn btn-default btn-block"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
</div>
<div> <div>
<button <button
v-if="user.muted" v-if="user.muted"
@ -217,33 +175,6 @@
{{ $t('user_card.mute') }} {{ $t('user_card.mute') }}
</button> </button>
</div> </div>
<div>
<button
v-if="user.statusnet_blocking"
class="btn btn-default btn-block pressed"
@click="unblockUser"
>
{{ $t('user_card.blocked') }}
</button>
<button
v-else
class="btn btn-default btn-block"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
</div>
<div>
<button
class="btn btn-default btn-block"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
</div>
<ModerationTools <ModerationTools
v-if="loggedIn.role === &quot;admin&quot;" v-if="loggedIn.role === &quot;admin&quot;"
:user="user" :user="user"
@ -262,7 +193,7 @@
class="panel-body" class="panel-body"
> >
<div <div
v-if="!hideUserStatsLocal && switcher" v-if="!mergedConfig.hideUserStats && switcher"
class="user-counts" class="user-counts"
> >
<div <div
@ -345,6 +276,8 @@
mask-composite: exclude; mask-composite: exclude;
background-size: cover; background-size: cover;
mask-size: 100% 60%; mask-size: 100% 60%;
border-top-left-radius: calc(var(--panelRadius) - 1px);
border-top-right-radius: calc(var(--panelRadius) - 1px);
&.hide-bio { &.hide-bio {
mask-size: 100% 40px; mask-size: 100% 40px;
@ -587,13 +520,12 @@
position: relative; position: relative;
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between;
margin-right: -.75em; margin-right: -.75em;
> * { > * {
flex: 1 0 0;
margin: 0 .75em .6em 0; margin: 0 .75em .6em 0;
white-space: nowrap; white-space: nowrap;
min-width: 95px;
} }
button { button {

View file

@ -2,12 +2,14 @@
import Status from '../status/status.vue' import Status from '../status/status.vue'
import List from '../list/list.vue' import List from '../list/list.vue'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import Modal from '../modal/modal.vue'
const UserReportingModal = { const UserReportingModal = {
components: { components: {
Status, Status,
List, List,
Checkbox Checkbox,
Modal
}, },
data () { data () {
return { return {

View file

@ -1,13 +1,9 @@
<template> <template>
<div <Modal
v-if="isOpen" v-if="isOpen"
class="modal-view" @backdropClicked="closeModal"
@click="closeModal"
> >
<div <div class="user-reporting-panel panel">
class="user-reporting-panel panel"
@click.stop=""
>
<div class="panel-heading"> <div class="panel-heading">
<div class="title"> <div class="title">
{{ $t('user_reporting.title', [user.screen_name]) }} {{ $t('user_reporting.title', [user.screen_name]) }}
@ -69,7 +65,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </Modal>
</template> </template>
<script src="./user_reporting_modal.js"></script> <script src="./user_reporting_modal.js"></script>

View file

@ -17,6 +17,7 @@ import Autosuggest from '../autosuggest/autosuggest.vue'
import Importer from '../importer/importer.vue' import Importer from '../importer/importer.vue'
import Exporter from '../exporter/exporter.vue' import Exporter from '../exporter/exporter.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription' import withSubscription from '../../hocs/with_subscription/with_subscription'
import Checkbox from '../checkbox/checkbox.vue'
import Mfa from './mfa.vue' import Mfa from './mfa.vue'
const BlockList = withSubscription({ const BlockList = withSubscription({
@ -34,6 +35,7 @@ const MuteList = withSubscription({
const UserSettings = { const UserSettings = {
data () { data () {
return { return {
newEmail: '',
newName: this.$store.state.users.currentUser.name, newName: this.$store.state.users.currentUser.name,
newBio: unescape(this.$store.state.users.currentUser.description), newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked, newLocked: this.$store.state.users.currentUser.locked,
@ -55,6 +57,9 @@ const UserSettings = {
backgroundPreview: null, backgroundPreview: null,
bannerUploadError: null, bannerUploadError: null,
backgroundUploadError: null, backgroundUploadError: null,
changeEmailError: false,
changeEmailPassword: '',
changedEmail: false,
deletingAccount: false, deletingAccount: false,
deleteAccountConfirmPasswordInput: '', deleteAccountConfirmPasswordInput: '',
deleteAccountError: false, deleteAccountError: false,
@ -82,7 +87,8 @@ const UserSettings = {
ProgressButton, ProgressButton,
Importer, Importer,
Exporter, Exporter,
Mfa Mfa,
Checkbox
}, },
computed: { computed: {
user () { user () {
@ -303,6 +309,22 @@ const UserSettings = {
} }
}) })
}, },
changeEmail () {
const params = {
email: this.newEmail,
password: this.changeEmailPassword
}
this.$store.state.api.backendInteractor.changeEmail(params)
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
this.changeEmailError = false
} else {
this.changedEmail = false
this.changeEmailError = res.error
}
})
},
activateTab (tabName) { activateTab (tabName) {
this.activeTab = tabName this.activeTab = tabName
}, },

View file

@ -53,12 +53,9 @@
/> />
</EmojiInput> </EmojiInput>
<p> <p>
<input <Checkbox v-model="newLocked">
id="account-locked" {{ $t('settings.lock_account_description') }}
v-model="newLocked" </Checkbox>
type="checkbox"
>
<label for="account-locked">{{ $t('settings.lock_account_description') }}</label>
</p> </p>
<div> <div>
<label for="default-vis">{{ $t('settings.default_vis') }}</label> <label for="default-vis">{{ $t('settings.default_vis') }}</label>
@ -75,69 +72,52 @@
</div> </div>
</div> </div>
<p> <p>
<input <Checkbox v-model="newNoRichText">
id="account-no-rich-text" {{ $t('settings.no_rich_text_description') }}
v-model="newNoRichText" </Checkbox>
type="checkbox"
>
<label for="account-no-rich-text">{{ $t('settings.no_rich_text_description') }}</label>
</p> </p>
<p> <p>
<input <Checkbox v-model="hideFollows">
id="account-hide-follows" {{ $t('settings.hide_follows_description') }}
v-model="hideFollows" </Checkbox>
type="checkbox"
>
<label for="account-hide-follows">{{ $t('settings.hide_follows_description') }}</label>
</p> </p>
<p class="setting-subitem"> <p class="setting-subitem">
<input <Checkbox
id="account-hide-follows-count"
v-model="hideFollowsCount" v-model="hideFollowsCount"
type="checkbox"
:disabled="!hideFollows" :disabled="!hideFollows"
> >
<label for="account-hide-follows-count">{{ $t('settings.hide_follows_count_description') }}</label> {{ $t('settings.hide_follows_count_description') }}
</Checkbox>
</p> </p>
<p> <p>
<input <Checkbox
id="account-hide-followers"
v-model="hideFollowers" v-model="hideFollowers"
type="checkbox"
> >
<label for="account-hide-followers">{{ $t('settings.hide_followers_description') }}</label> {{ $t('settings.hide_followers_description') }}
</Checkbox>
</p> </p>
<p class="setting-subitem"> <p class="setting-subitem">
<input <Checkbox
id="account-hide-followers-count"
v-model="hideFollowersCount" v-model="hideFollowersCount"
type="checkbox"
:disabled="!hideFollowers" :disabled="!hideFollowers"
> >
<label for="account-hide-followers-count">{{ $t('settings.hide_followers_count_description') }}</label> {{ $t('settings.hide_followers_count_description') }}
</Checkbox>
</p> </p>
<p> <p>
<input <Checkbox v-model="showRole">
id="account-show-role" <template v-if="role === 'admin'">
v-model="showRole" {{ $t('settings.show_admin_badge') }}
type="checkbox" </template>
> <template v-if="role === 'moderator'">
<label {{ $t('settings.show_moderator_badge') }}
v-if="role === 'admin'" </template>
for="account-show-role" </Checkbox>
>{{ $t('settings.show_admin_badge') }}</label>
<label
v-if="role === 'moderator'"
for="account-show-role"
>{{ $t('settings.show_moderator_badge') }}</label>
</p> </p>
<p> <p>
<input <Checkbox v-model="discoverable">
id="discoverable" {{ $t('settings.discoverable') }}
v-model="discoverable" </Checkbox>
type="checkbox"
>
<label for="discoverable">{{ $t('settings.discoverable') }}</label>
</p> </p>
<button <button
:disabled="newName && newName.length === 0" :disabled="newName && newName.length === 0"
@ -253,6 +233,39 @@
</div> </div>
<div :label="$t('settings.security_tab')"> <div :label="$t('settings.security_tab')">
<div class="setting-item">
<h2>{{ $t('settings.change_email') }}</h2>
<div>
<p>{{ $t('settings.new_email') }}</p>
<input
v-model="newEmail"
type="email"
autocomplete="email"
>
</div>
<div>
<p>{{ $t('settings.current_password') }}</p>
<input
v-model="changeEmailPassword"
type="password"
autocomplete="current-password"
>
</div>
<button
class="btn btn-default"
@click="changeEmail"
>
{{ $t('general.submit') }}
</button>
<p v-if="changedEmail">
{{ $t('settings.changed_email') }}
</p>
<template v-if="changeEmailError !== false">
<p>{{ $t('settings.change_email_error') }}</p>
<p>{{ changeEmailError }}</p>
</template>
</div>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.change_password') }}</h2> <h2>{{ $t('settings.change_password') }}</h2>
<div> <div>
@ -367,44 +380,24 @@
<span class="label">{{ $t('settings.notification_setting') }}</span> <span class="label">{{ $t('settings.notification_setting') }}</span>
<ul class="option-list"> <ul class="option-list">
<li> <li>
<input <Checkbox v-model="notificationSettings.follows">
id="notification-setting-follows"
v-model="notificationSettings.follows"
type="checkbox"
>
<label for="notification-setting-follows">
{{ $t('settings.notification_setting_follows') }} {{ $t('settings.notification_setting_follows') }}
</label> </Checkbox>
</li> </li>
<li> <li>
<input <Checkbox v-model="notificationSettings.followers">
id="notification-setting-followers"
v-model="notificationSettings.followers"
type="checkbox"
>
<label for="notification-setting-followers">
{{ $t('settings.notification_setting_followers') }} {{ $t('settings.notification_setting_followers') }}
</label> </Checkbox>
</li> </li>
<li> <li>
<input <Checkbox v-model="notificationSettings.non_follows">
id="notification-setting-non-follows"
v-model="notificationSettings.non_follows"
type="checkbox"
>
<label for="notification-setting-non-follows">
{{ $t('settings.notification_setting_non_follows') }} {{ $t('settings.notification_setting_non_follows') }}
</label> </Checkbox>
</li> </li>
<li> <li>
<input <Checkbox v-model="notificationSettings.non_followers">
id="notification-setting-non-followers"
v-model="notificationSettings.non_followers"
type="checkbox"
>
<label for="notification-setting-non-followers">
{{ $t('settings.notification_setting_non_followers') }} {{ $t('settings.notification_setting_non_followers') }}
</label> </Checkbox>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -3,7 +3,7 @@ const VideoAttachment = {
props: ['attachment', 'controls'], props: ['attachment', 'controls'],
data () { data () {
return { return {
loopVideo: this.$store.state.config.loopVideo loopVideo: this.$store.getters.mergedConfig.loopVideo
} }
}, },
methods: { methods: {
@ -12,16 +12,16 @@ const VideoAttachment = {
if (typeof target.webkitAudioDecodedByteCount !== 'undefined') { if (typeof target.webkitAudioDecodedByteCount !== 'undefined') {
// non-zero if video has audio track // non-zero if video has audio track
if (target.webkitAudioDecodedByteCount > 0) { if (target.webkitAudioDecodedByteCount > 0) {
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
} }
} else if (typeof target.mozHasAudio !== 'undefined') { } else if (typeof target.mozHasAudio !== 'undefined') {
// true if video has audio track // true if video has audio track
if (target.mozHasAudio) { if (target.mozHasAudio) {
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
} }
} else if (typeof target.audioTracks !== 'undefined') { } else if (typeof target.audioTracks !== 'undefined') {
if (target.audioTracks.length > 0) { if (target.audioTracks.length > 0) {
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
} }
} }
} }

View file

@ -2,42 +2,49 @@ import * as bodyScrollLock from 'body-scroll-lock'
let previousNavPaddingRight let previousNavPaddingRight
let previousAppBgWrapperRight let previousAppBgWrapperRight
const lockerEls = new Set([])
const disableBodyScroll = (el) => { const disableBodyScroll = (el) => {
const scrollBarGap = window.innerWidth - document.documentElement.clientWidth const scrollBarGap = window.innerWidth - document.documentElement.clientWidth
bodyScrollLock.disableBodyScroll(el, { bodyScrollLock.disableBodyScroll(el, {
reserveScrollBarGap: true reserveScrollBarGap: true
}) })
lockerEls.add(el)
setTimeout(() => { setTimeout(() => {
// If previousNavPaddingRight is already set, don't set it again. if (lockerEls.size <= 1) {
if (previousNavPaddingRight === undefined) { // If previousNavPaddingRight is already set, don't set it again.
const navEl = document.getElementById('nav') if (previousNavPaddingRight === undefined) {
previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right') const navEl = document.getElementById('nav')
navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right')
navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
}
// If previousAppBgWrapeprRight is already set, don't set it again.
if (previousAppBgWrapperRight === undefined) {
const appBgWrapperEl = document.getElementById('app_bg_wrapper')
previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right')
appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
}
document.body.classList.add('scroll-locked')
} }
// If previousAppBgWrapeprRight is already set, don't set it again.
if (previousAppBgWrapperRight === undefined) {
const appBgWrapperEl = document.getElementById('app_bg_wrapper')
previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right')
appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
}
document.body.classList.add('scroll-locked')
}) })
} }
const enableBodyScroll = (el) => { const enableBodyScroll = (el) => {
lockerEls.delete(el)
setTimeout(() => { setTimeout(() => {
if (previousNavPaddingRight !== undefined) { if (lockerEls.size === 0) {
document.getElementById('nav').style.paddingRight = previousNavPaddingRight if (previousNavPaddingRight !== undefined) {
// Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again. document.getElementById('nav').style.paddingRight = previousNavPaddingRight
previousNavPaddingRight = undefined // Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again.
previousNavPaddingRight = undefined
}
if (previousAppBgWrapperRight !== undefined) {
document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight
// Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again.
previousAppBgWrapperRight = undefined
}
document.body.classList.remove('scroll-locked')
} }
if (previousAppBgWrapperRight !== undefined) {
document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight
// Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again.
previousAppBgWrapperRight = undefined
}
document.body.classList.remove('scroll-locked')
}) })
bodyScrollLock.enableBodyScroll(el) bodyScrollLock.enableBodyScroll(el)
} }

View file

@ -5,11 +5,11 @@
"features_panel": { "features_panel": {
"chat": "Chat", "chat": "Chat",
"gopher": "Gopher", "gopher": "Gopher",
"media_proxy": "Media Proxy", "media_proxy": "Medienproxy",
"scope_options": "Reichweitenoptionen", "scope_options": "Reichweitenoptionen",
"text_limit": "Textlimit", "text_limit": "Textlimit",
"title": "Features", "title": "Features",
"who_to_follow": "Who to follow" "who_to_follow": "Wem folgen?"
}, },
"finder": { "finder": {
"error_fetching_user": "Fehler beim Suchen des Benutzers", "error_fetching_user": "Fehler beim Suchen des Benutzers",
@ -29,15 +29,18 @@
"username": "Benutzername" "username": "Benutzername"
}, },
"nav": { "nav": {
"about": "Über",
"back": "Zurück", "back": "Zurück",
"chat": "Lokaler Chat", "chat": "Lokaler Chat",
"friend_requests": "Followanfragen", "friend_requests": "Followanfragen",
"mentions": "Erwähnungen", "mentions": "Erwähnungen",
"interactions": "Interaktionen",
"dms": "Direktnachrichten", "dms": "Direktnachrichten",
"public_tl": "Öffentliche Zeitleiste", "public_tl": "Öffentliche Zeitleiste",
"timeline": "Zeitleiste", "timeline": "Zeitleiste",
"twkn": "Das gesamte bekannte Netzwerk", "twkn": "Das gesamte bekannte Netzwerk",
"user_search": "Benutzersuche", "user_search": "Benutzersuche",
"search": "Suche",
"preferences": "Voreinstellungen" "preferences": "Voreinstellungen"
}, },
"notifications": { "notifications": {
@ -115,6 +118,9 @@
"delete_account_description": "Lösche deinen Account und alle deine Nachrichten unwiderruflich.", "delete_account_description": "Lösche deinen Account und alle deine Nachrichten unwiderruflich.",
"delete_account_error": "Es ist ein Fehler beim Löschen deines Accounts aufgetreten. Tritt dies weiterhin auf, wende dich an den Administrator der Instanz.", "delete_account_error": "Es ist ein Fehler beim Löschen deines Accounts aufgetreten. Tritt dies weiterhin auf, wende dich an den Administrator der Instanz.",
"delete_account_instructions": "Tippe dein Passwort unten in das Feld ein, um die Löschung deines Accounts zu bestätigen.", "delete_account_instructions": "Tippe dein Passwort unten in das Feld ein, um die Löschung deines Accounts zu bestätigen.",
"discoverable": "Erlaubnis für automatisches Suchen nach diesem Account",
"avatar_size_instruction": "Die empfohlene minimale Größe für Avatare ist 150x150 Pixel.",
"pad_emoji": "Emojis mit Leerzeichen umrahmen",
"export_theme": "Farbschema speichern", "export_theme": "Farbschema speichern",
"filtering": "Filtern", "filtering": "Filtern",
"filtering_explanation": "Alle Beiträge die diese Wörter enthalten werden ausgeblendet. Ein Wort pro Zeile.", "filtering_explanation": "Alle Beiträge die diese Wörter enthalten werden ausgeblendet. Ein Wort pro Zeile.",
@ -128,8 +134,11 @@
"general": "Allgemein", "general": "Allgemein",
"hide_attachments_in_convo": "Anhänge in Unterhaltungen ausblenden", "hide_attachments_in_convo": "Anhänge in Unterhaltungen ausblenden",
"hide_attachments_in_tl": "Anhänge in der Zeitleiste ausblenden", "hide_attachments_in_tl": "Anhänge in der Zeitleiste ausblenden",
"hide_muted_posts": "Verberge Beiträge stummgeschalteter Nutzer",
"max_thumbnails": "Maximale Anzahl von Vorschaubildern pro Beitrag",
"hide_isp": "Instanz-spezifisches Panel ausblenden", "hide_isp": "Instanz-spezifisches Panel ausblenden",
"preload_images": "Bilder vorausladen", "preload_images": "Bilder vorausladen",
"use_one_click_nsfw": "Heikle Anhänge mit nur einem Klick öffnen",
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)", "hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
"hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)", "hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)",
"hide_filtered_statuses": "Gefilterte Beiträge verbergen", "hide_filtered_statuses": "Gefilterte Beiträge verbergen",
@ -147,6 +156,9 @@
"lock_account_description": "Sperre deinen Account, um neue Follower zu genehmigen oder abzulehnen", "lock_account_description": "Sperre deinen Account, um neue Follower zu genehmigen oder abzulehnen",
"loop_video": "Videos wiederholen", "loop_video": "Videos wiederholen",
"loop_video_silent_only": "Nur Videos ohne Ton wiederholen (z.B. Mastodons \"gifs\")", "loop_video_silent_only": "Nur Videos ohne Ton wiederholen (z.B. Mastodons \"gifs\")",
"mutes_tab": "Mutes",
"play_videos_in_modal": "Videos in größerem Medienfenster abspielen",
"use_contain_fit": "Vorschaubilder nicht zuschneiden",
"name": "Name", "name": "Name",
"name_bio": "Name & Bio", "name_bio": "Name & Bio",
"new_password": "Neues Passwort", "new_password": "Neues Passwort",
@ -158,6 +170,8 @@
"no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen", "no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen",
"hide_follows_description": "Zeige nicht, wem ich folge", "hide_follows_description": "Zeige nicht, wem ich folge",
"hide_followers_description": "Zeige nicht, wer mir folgt", "hide_followers_description": "Zeige nicht, wer mir folgt",
"hide_follows_count_description": "Verberge die Anzahl deiner Gefolgten",
"hide_followers_count_description": "Verberge die Anzahl deiner Folgenden",
"nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind", "nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind",
"oauth_tokens": "OAuth-Token", "oauth_tokens": "OAuth-Token",
"token": "Zeichen", "token": "Zeichen",
@ -176,10 +190,12 @@
"reply_visibility_all": "Alle Antworten zeigen", "reply_visibility_all": "Alle Antworten zeigen",
"reply_visibility_following": "Zeige nur Antworten an mich oder an Benutzer, denen ich folge", "reply_visibility_following": "Zeige nur Antworten an mich oder an Benutzer, denen ich folge",
"reply_visibility_self": "Nur Antworten an mich anzeigen", "reply_visibility_self": "Nur Antworten an mich anzeigen",
"autohide_floating_post_button": "Automatisches Verbergen des Knopfs für neue Beiträge (mobil)",
"saving_err": "Fehler beim Speichern der Einstellungen", "saving_err": "Fehler beim Speichern der Einstellungen",
"saving_ok": "Einstellungen gespeichert", "saving_ok": "Einstellungen gespeichert",
"security_tab": "Sicherheit", "security_tab": "Sicherheit",
"scope_copy": "Reichweite beim Antworten übernehmen (Direktnachrichten werden immer kopiert)", "scope_copy": "Reichweite beim Antworten übernehmen (Direktnachrichten werden immer kopiert)",
"minimal_scopes_mode": "Minimiere Reichweitenoptionen",
"set_new_avatar": "Setze einen neuen Avatar", "set_new_avatar": "Setze einen neuen Avatar",
"set_new_profile_background": "Setze einen neuen Hintergrund für dein Profil", "set_new_profile_background": "Setze einen neuen Hintergrund für dein Profil",
"set_new_profile_banner": "Setze einen neuen Banner für dein Profil", "set_new_profile_banner": "Setze einen neuen Banner für dein Profil",
@ -189,7 +205,8 @@
"subject_line_email": "Wie Email: \"re: Betreff\"", "subject_line_email": "Wie Email: \"re: Betreff\"",
"subject_line_mastodon": "Wie Mastodon: unverändert kopieren", "subject_line_mastodon": "Wie Mastodon: unverändert kopieren",
"subject_line_noop": "Nicht kopieren", "subject_line_noop": "Nicht kopieren",
"stop_gifs": "Play-on-hover GIFs", "post_status_content_type": "Beitragsart",
"stop_gifs": "Animationen nur beim Darüberfahren abspielen",
"streaming": "Aktiviere automatisches Laden (Streaming) von neuen Beiträgen", "streaming": "Aktiviere automatisches Laden (Streaming) von neuen Beiträgen",
"text": "Text", "text": "Text",
"theme": "Farbschema", "theme": "Farbschema",
@ -372,5 +389,25 @@
"GiB": "GiB", "GiB": "GiB",
"TiB": "TiB" "TiB": "TiB"
} }
},
"search": {
"people": "Leute",
"hashtags": "Hashtags",
"person_talking": "{count} Person spricht darüber",
"people_talking": "{count} Leute sprechen darüber",
"no_results": "Keine Ergebnisse"
},
"password_reset": {
"forgot_password": "Passwort vergessen?",
"password_reset": "Password zurücksetzen",
"instruction": "Wenn du hier deinen Benutznamen oder die zugehörige E-Mail-Adresse eingibst, kann dir der Server einen Link zum Passwortzurücksetzen zuschicken.",
"placeholder": "Dein Benutzername oder die zugehörige E-Mail-Adresse",
"check_email": "Im E-Mail-Posteingang des angebenen Kontos müsste sich jetzt (oder zumindest in Kürze) die E-Mail mit dem Link zum Passwortzurücksetzen befinden.",
"return_home": "Zurück zur Heimseite",
"not_found": "Benutzername/E-Mail-Adresse nicht gefunden. Vertippt?",
"too_many_requests": "Kurze Pause. Zu viele Versuche. Bitte, später nochmal probieren.",
"password_reset_disabled": "Passwortzurücksetzen deaktiviert. Bitte Administrator kontaktieren.",
"password_reset_required": "Passwortzurücksetzen erforderlich",
"password_reset_required_but_mailer_is_disabled": "Passwortzurücksetzen wäre erforderlich, ist aber deaktiviert. Bitte Administrator kontaktieren."
} }
} }

View file

@ -219,6 +219,9 @@
"cGreen": "Green (Retweet)", "cGreen": "Green (Retweet)",
"cOrange": "Orange (Favorite)", "cOrange": "Orange (Favorite)",
"cRed": "Red (Cancel)", "cRed": "Red (Cancel)",
"change_email": "Change Email",
"change_email_error": "There was an issue changing your email.",
"changed_email": "Email changed successfully!",
"change_password": "Change Password", "change_password": "Change Password",
"change_password_error": "There was an issue changing your password.", "change_password_error": "There was an issue changing your password.",
"changed_password": "Password changed successfully!", "changed_password": "Password changed successfully!",
@ -277,6 +280,7 @@
"use_contain_fit": "Don't crop the attachment in thumbnails", "use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name", "name": "Name",
"name_bio": "Name & Bio", "name_bio": "Name & Bio",
"new_email": "New Email",
"new_password": "New password", "new_password": "New password",
"notification_visibility": "Types of notifications to show", "notification_visibility": "Types of notifications to show",
"notification_visibility_follows": "Follows", "notification_visibility_follows": "Follows",
@ -558,6 +562,8 @@
"unmute": "Unmute", "unmute": "Unmute",
"unmute_progress": "Unmuting...", "unmute_progress": "Unmuting...",
"mute_progress": "Muting...", "mute_progress": "Muting...",
"hide_repeats": "Hide repeats",
"show_repeats": "Show repeats",
"admin_menu": { "admin_menu": {
"moderation": "Moderation", "moderation": "Moderation",
"grant_admin": "Grant Admin", "grant_admin": "Grant Admin",
@ -633,6 +639,8 @@
"return_home": "Return to the home page", "return_home": "Return to the home page",
"not_found": "We couldn't find that email or username.", "not_found": "We couldn't find that email or username.",
"too_many_requests": "You have reached the limit of attempts, try again later.", "too_many_requests": "You have reached the limit of attempts, try again later.",
"password_reset_disabled": "Password reset is disabled. Please contact your instance administrator." "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.",
"password_reset_required": "You must reset your password to log in.",
"password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator."
} }
} }

View file

@ -45,8 +45,8 @@
"error": "Se ha producido un error al importar el archivo." "error": "Se ha producido un error al importar el archivo."
}, },
"login": { "login": {
"login": "Identificación", "login": "Identificarse",
"description": "Identificación con OAuth", "description": "Identificarse con OAuth",
"logout": "Cerrar sesión", "logout": "Cerrar sesión",
"password": "Contraseña", "password": "Contraseña",
"placeholder": "p.ej. lain", "placeholder": "p.ej. lain",
@ -68,6 +68,7 @@
}, },
"nav": { "nav": {
"about": "Acerca de", "about": "Acerca de",
"administration": "Administración",
"back": "Volver", "back": "Volver",
"chat": "Chat Local", "chat": "Chat Local",
"friend_requests": "Solicitudes de seguimiento", "friend_requests": "Solicitudes de seguimiento",
@ -106,6 +107,15 @@
"expired": "La encuesta terminó hace {0}", "expired": "La encuesta terminó hace {0}",
"not_enough_options": "Muy pocas opciones únicas en la encuesta" "not_enough_options": "Muy pocas opciones únicas en la encuesta"
}, },
"emoji": {
"stickers": "Pegatinas",
"emoji": "Emoji",
"keep_open": "Mantener el selector abierto",
"search_emoji": "Buscar un emoji",
"add_emoji": "Insertar un emoji",
"custom": "Emojis personalizados",
"unicode": "Emojis unicode"
},
"stickers": { "stickers": {
"add_sticker": "Añadir Pegatina" "add_sticker": "Añadir Pegatina"
}, },
@ -222,7 +232,9 @@
"data_import_export_tab": "Importar / Exportar Datos", "data_import_export_tab": "Importar / Exportar Datos",
"default_vis": "Alcance de visibilidad por defecto", "default_vis": "Alcance de visibilidad por defecto",
"delete_account": "Eliminar la cuenta", "delete_account": "Eliminar la cuenta",
"discoverable": "Permitir la aparición de esta cuenta en los resultados de búsqueda y otros servicios",
"delete_account_description": "Eliminar para siempre la cuenta y todos los mensajes.", "delete_account_description": "Eliminar para siempre la cuenta y todos los mensajes.",
"pad_emoji": "Rellenar con espacios al agregar emojis desde el selector",
"delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.", "delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.",
"delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.", "delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.",
"avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.", "avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.",
@ -277,6 +289,8 @@
"no_mutes": "No hay usuarios sinlenciados", "no_mutes": "No hay usuarios sinlenciados",
"hide_follows_description": "No mostrar a quién sigo", "hide_follows_description": "No mostrar a quién sigo",
"hide_followers_description": "No mostrar quién me sigue", "hide_followers_description": "No mostrar quién me sigue",
"hide_follows_count_description": "No mostrar el número de cuentas que sigo",
"hide_followers_count_description": "No mostrar el número de cuentas que me siguen",
"show_admin_badge": "Mostrar la insignia de Administrador en mi perfil", "show_admin_badge": "Mostrar la insignia de Administrador en mi perfil",
"show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil", "show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil",
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW", "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
@ -529,6 +543,7 @@
"follows_you": "¡Te sigue!", "follows_you": "¡Te sigue!",
"its_you": "¡Eres tú!", "its_you": "¡Eres tú!",
"media": "Media", "media": "Media",
"mention": "Mencionar",
"mute": "Silenciar", "mute": "Silenciar",
"muted": "Silenciado", "muted": "Silenciado",
"per_day": "por día", "per_day": "por día",

View file

@ -68,6 +68,7 @@
}, },
"nav": { "nav": {
"about": "Honi buruz", "about": "Honi buruz",
"administration": "Administrazioa",
"back": "Atzera", "back": "Atzera",
"chat": "Txat lokala", "chat": "Txat lokala",
"friend_requests": "Jarraitzeko eskaerak", "friend_requests": "Jarraitzeko eskaerak",
@ -106,6 +107,15 @@
"expired": "Inkesta {0} bukatu zen", "expired": "Inkesta {0} bukatu zen",
"not_enough_options": "Aukera gutxiegi inkestan" "not_enough_options": "Aukera gutxiegi inkestan"
}, },
"emoji": {
"stickers": "Pegatinak",
"emoji": "Emoji",
"keep_open": "Mantendu hautatzailea zabalik",
"search_emoji": "Bilatu emoji bat",
"add_emoji": "Emoji bat gehitu",
"custom": "Ohiko emojiak",
"unicode": "Unicode emojiak"
},
"stickers": { "stickers": {
"add_sticker": "Pegatina gehitu" "add_sticker": "Pegatina gehitu"
}, },
@ -199,12 +209,12 @@
"avatarRadius": "Avatarrak", "avatarRadius": "Avatarrak",
"background": "Atzeko planoa", "background": "Atzeko planoa",
"bio": "Biografia", "bio": "Biografia",
"block_export": "Bloke esportatzea", "block_export": "Blokeatu dituzunak esportatu",
"block_export_button": "Esportatu zure blokeak csv fitxategi batera", "block_export_button": "Esportatu blokeatutakoak csv fitxategi batera",
"block_import": "Bloke inportazioa", "block_import": "Blokeatu dituzunak inportatu",
"block_import_error": "Errorea blokeak inportatzen", "block_import_error": "Errorea blokeatutakoak inportatzen",
"blocks_imported": "Blokeak inportaturik! Hauek prozesatzeak denbora hartuko du.", "blocks_imported": "Blokeatutakoak inportaturik! Hauek prozesatzeak denbora hartuko du.",
"blocks_tab": "Blokeak", "blocks_tab": "Blokeatutakoak",
"btnRadius": "Botoiak", "btnRadius": "Botoiak",
"cBlue": "Urdina (erantzun, jarraitu)", "cBlue": "Urdina (erantzun, jarraitu)",
"cGreen": "Berdea (Bertxiotu)", "cGreen": "Berdea (Bertxiotu)",
@ -222,7 +232,9 @@
"data_import_export_tab": "Datuak Inportatu / Esportatu", "data_import_export_tab": "Datuak Inportatu / Esportatu",
"default_vis": "Lehenetsitako ikusgaitasunak", "default_vis": "Lehenetsitako ikusgaitasunak",
"delete_account": "Ezabatu kontua", "delete_account": "Ezabatu kontua",
"discoverable": "Baimendu zure kontua kanpo bilaketa-emaitzetan eta bestelako zerbitzuetan agertzea",
"delete_account_description": "Betirako ezabatu zure kontua eta zure mezu guztiak", "delete_account_description": "Betirako ezabatu zure kontua eta zure mezu guztiak",
"pad_emoji": "Zuriuneak gehitu emoji bat aukeratzen denean",
"delete_account_error": "Arazo bat gertatu da zure kontua ezabatzerakoan. Arazoa jarraitu eskero, administratzailearekin harremanetan jarri.", "delete_account_error": "Arazo bat gertatu da zure kontua ezabatzerakoan. Arazoa jarraitu eskero, administratzailearekin harremanetan jarri.",
"delete_account_instructions": "Idatzi zure pasahitza kontua ezabatzeko.", "delete_account_instructions": "Idatzi zure pasahitza kontua ezabatzeko.",
"avatar_size_instruction": "Avatar irudien gomendatutako gutxieneko tamaina 150x150 pixel dira.", "avatar_size_instruction": "Avatar irudien gomendatutako gutxieneko tamaina 150x150 pixel dira.",
@ -254,7 +266,7 @@
"instance_default": "(lehenetsia: {value})", "instance_default": "(lehenetsia: {value})",
"instance_default_simple": "(lehenetsia)", "instance_default_simple": "(lehenetsia)",
"interface": "Interfazea", "interface": "Interfazea",
"interfaceLanguage": "Interfaze hizkuntza", "interfaceLanguage": "Interfazearen hizkuntza",
"invalid_theme_imported": "Hautatutako fitxategia ez da onartutako Pleroma gaia. Ez da zure gaian aldaketarik burutu.", "invalid_theme_imported": "Hautatutako fitxategia ez da onartutako Pleroma gaia. Ez da zure gaian aldaketarik burutu.",
"limited_availability": "Ez dago erabilgarri zure nabigatzailean", "limited_availability": "Ez dago erabilgarri zure nabigatzailean",
"links": "Estekak", "links": "Estekak",
@ -277,6 +289,8 @@
"no_mutes": "Ez daude erabiltzaile mututuak", "no_mutes": "Ez daude erabiltzaile mututuak",
"hide_follows_description": "Ez erakutsi nor jarraitzen ari naizen", "hide_follows_description": "Ez erakutsi nor jarraitzen ari naizen",
"hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen", "hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen",
"hide_follows_count_description": "Ez erakutsi jarraitzen ari naizen kontuen kopurua",
"hide_followers_count_description": "Ez erakutsi nire jarraitzaileen kontuen kopurua",
"show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan", "show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan",
"show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan", "show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan",
"nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko", "nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko",
@ -449,7 +463,7 @@
}, },
"version": { "version": {
"title": "Bertsioa", "title": "Bertsioa",
"backend_version": "Backend Bertsio", "backend_version": "Backend Bertsioa",
"frontend_version": "Frontend Bertsioa" "frontend_version": "Frontend Bertsioa"
} }
}, },
@ -529,6 +543,7 @@
"follows_you": "Jarraitzen dizu!", "follows_you": "Jarraitzen dizu!",
"its_you": "Zu zara!", "its_you": "Zu zara!",
"media": "Multimedia", "media": "Multimedia",
"mention": "Aipatu",
"mute": "Isilarazi", "mute": "Isilarazi",
"muted": "Isilduta", "muted": "Isilduta",
"per_day": "eguneko", "per_day": "eguneko",
@ -543,6 +558,8 @@
"unmute": "Isiltasuna kendu", "unmute": "Isiltasuna kendu",
"unmute_progress": "Isiltasuna kentzen...", "unmute_progress": "Isiltasuna kentzen...",
"mute_progress": "Isiltzen...", "mute_progress": "Isiltzen...",
"hide_repeats": "Ezkutatu errepikapenak",
"show_repeats": "Erakutsi errpekiapenak",
"admin_menu": { "admin_menu": {
"moderation": "Moderazioa", "moderation": "Moderazioa",
"grant_admin": "Administratzaile baimena", "grant_admin": "Administratzaile baimena",
@ -618,6 +635,8 @@
"return_home": "Itzuli hasierara", "return_home": "Itzuli hasierara",
"not_found": "Ezin izan dugu helbide elektroniko edo erabiltzaile hori aurkitu.", "not_found": "Ezin izan dugu helbide elektroniko edo erabiltzaile hori aurkitu.",
"too_many_requests": "Saiakera gehiegi burutu ditzu, saiatu berriro geroxeago.", "too_many_requests": "Saiakera gehiegi burutu ditzu, saiatu berriro geroxeago.",
"password_reset_disabled": "Pasahitza berrezartzea debekatuta dago. Mesedez, jarri harremanetan instantzia administratzailearekin." "password_reset_disabled": "Pasahitza berrezartzea debekatuta dago. Mesedez, jarri harremanetan instantzia administratzailearekin.",
"password_reset_required": "Pasahitza berrezarri behar duzu saioa hasteko.",
"password_reset_required_but_mailer_is_disabled": "Pasahitza berrezarri behar duzu, baina pasahitza berrezartzeko aukera desgaituta dago. Mesedez, jarri harremanetan instantziaren administratzailearekin."
} }
} }

View file

@ -127,6 +127,9 @@
"cGreen": "Повторить", "cGreen": "Повторить",
"cOrange": "Нравится", "cOrange": "Нравится",
"cRed": "Отменить", "cRed": "Отменить",
"change_email": "Сменить email",
"change_email_error": "Произошла ошибка при попытке изменить email.",
"changed_email": "Email изменён успешно.",
"change_password": "Сменить пароль", "change_password": "Сменить пароль",
"change_password_error": "Произошла ошибка при попытке изменить пароль.", "change_password_error": "Произошла ошибка при попытке изменить пароль.",
"changed_password": "Пароль изменён успешно.", "changed_password": "Пароль изменён успешно.",
@ -169,6 +172,7 @@
"loop_video_silent_only": "Зацикливать только беззвучные видео (т.е. \"гифки\" с Mastodon)", "loop_video_silent_only": "Зацикливать только беззвучные видео (т.е. \"гифки\" с Mastodon)",
"name": "Имя", "name": "Имя",
"name_bio": "Имя и описание", "name_bio": "Имя и описание",
"new_email": "Новый email",
"new_password": "Новый пароль", "new_password": "Новый пароль",
"notification_visibility": "Показывать уведомления", "notification_visibility": "Показывать уведомления",
"notification_visibility_follows": "Подписки", "notification_visibility_follows": "Подписки",

View file

@ -41,7 +41,13 @@ Vue.use(VueChatScroll)
Vue.use(VueClickOutside) Vue.use(VueClickOutside)
Vue.use(PortalVue) Vue.use(PortalVue)
Vue.use(VBodyScrollLock) Vue.use(VBodyScrollLock)
Vue.use(VTooltip) Vue.use(VTooltip, {
popover: {
defaultTrigger: 'hover click',
defaultContainer: false,
defaultOffset: 5
}
})
const i18n = new VueI18n({ const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary // By default, use the browser locale, we will update it if neccessary

View file

@ -3,8 +3,9 @@ import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
const browserLocale = (window.navigator.language || 'en').split('-')[0] const browserLocale = (window.navigator.language || 'en').split('-')[0]
const defaultState = { export const defaultState = {
colors: {}, colors: {},
// bad name: actually hides posts of muted USERS
hideMutedPosts: undefined, // instance default hideMutedPosts: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default collapseMessageWithSubject: undefined, // instance default
padEmoji: true, padEmoji: true,
@ -37,11 +38,37 @@ const defaultState = {
subjectLineBehavior: undefined, // instance default subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default
postContentType: undefined, // instance default postContentType: undefined, // instance default
minimalScopesMode: undefined // instance default minimalScopesMode: undefined, // instance default
// This hides statuses filtered via a word filter
hideFilteredStatuses: undefined, // instance default
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: false,
hidePostStats: undefined, // instance default
hideUserStats: undefined // instance default
} }
// caching the instance default properties
export const instanceDefaultProperties = Object.entries(defaultState)
.filter(([key, value]) => value === undefined)
.map(([key, value]) => key)
const config = { const config = {
state: defaultState, state: defaultState,
getters: {
mergedConfig (state, getters, rootState, rootGetters) {
const { instance } = rootState
return {
...state,
...instanceDefaultProperties
.map(key => [key, state[key] === undefined
? instance[key]
: state[key]
])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
}
}
},
mutations: { mutations: {
setOption (state, { name, value }) { setOption (state, { name, value }) {
set(state, name, value) set(state, name, value)

View file

@ -1,5 +1,6 @@
import { set } from 'vue' import { set } from 'vue'
import { setPreset } from '../services/style_setter/style_setter.js' import { setPreset } from '../services/style_setter/style_setter.js'
import { instanceDefaultProperties } from './config.js'
const defaultState = { const defaultState = {
// Stuff from static/config.json and apiConfig // Stuff from static/config.json and apiConfig
@ -74,6 +75,13 @@ const instance = {
} }
} }
}, },
getters: {
instanceDefaultConfig (state) {
return instanceDefaultProperties
.map(key => [key, state[key]])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
}
},
actions: { actions: {
setInstanceOption ({ commit, dispatch }, { name, value }) { setInstanceOption ({ commit, dispatch }, { name, value }) {
commit('setInstanceOption', { name, value }) commit('setInstanceOption', { name, value })

View file

@ -537,6 +537,10 @@ const statuses = {
setNotificationsSilence ({ rootState, commit }, { value }) { setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value }) commit('setNotificationsSilence', { value })
}, },
fetchStatus ({ rootState, dispatch }, id) {
rootState.api.backendInteractor.fetchStatus({ id })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
deleteStatus ({ rootState, commit }, status) { deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status }) commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })

View file

@ -60,6 +60,18 @@ const unmuteUser = (store, id) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship])) .then((relationship) => store.commit('updateUserRelationship', [relationship]))
} }
const hideReblogs = (store, userId) => {
return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: false })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
})
}
const showReblogs = (store, userId) => {
return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: true })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
export const mutations = { export const mutations = {
setMuted (state, { user: { id }, muted }) { setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id] const user = state.usersObject[id]
@ -135,6 +147,7 @@ export const mutations = {
user.muted = relationship.muting user.muted = relationship.muting
user.statusnet_blocking = relationship.blocking user.statusnet_blocking = relationship.blocking
user.subscribed = relationship.subscribing user.subscribed = relationship.subscribing
user.showing_reblogs = relationship.showing_reblogs
} }
}) })
}, },
@ -272,6 +285,12 @@ const users = {
unmuteUser (store, id) { unmuteUser (store, id) {
return unmuteUser(store, id) return unmuteUser(store, id)
}, },
hideReblogs (store, id) {
return hideReblogs(store, id)
},
showReblogs (store, id) {
return showReblogs(store, id)
},
muteUsers (store, ids = []) { muteUsers (store, ids = []) {
return Promise.all(ids.map(id => muteUser(store, id))) return Promise.all(ids.map(id => muteUser(store, id)))
}, },

View file

@ -8,6 +8,7 @@ const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import' const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const TAG_USER_URL = '/api/pleroma/admin/users/tag' const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}` const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
@ -16,12 +17,12 @@ const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions' const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const MFA_SETTINGS_URL = '/api/pleroma/profile/mfa' const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
const MFA_BACKUP_CODES_URL = '/api/pleroma/profile/mfa/backup_codes' const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
const MFA_SETUP_OTP_URL = '/api/pleroma/profile/mfa/setup/totp' const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp'
const MFA_CONFIRM_OTP_URL = '/api/pleroma/profile/mfa/confirm/totp' const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp'
const MFA_DISABLE_OTP_URL = '/api/pleroma/profile/mfa/totp' const MFA_DISABLE_OTP_URL = '/api/pleroma/account/mfa/totp'
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts' const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
@ -219,10 +220,16 @@ const authHeaders = (accessToken) => {
} }
} }
const followUser = ({ id, credentials }) => { const followUser = ({ id, credentials, ...options }) => {
let url = MASTODON_FOLLOW_URL(id) let url = MASTODON_FOLLOW_URL(id)
const form = {}
if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs }
return fetch(url, { return fetch(url, {
headers: authHeaders(credentials), body: JSON.stringify(form),
headers: {
...authHeaders(credentials),
'Content-Type': 'application/json'
},
method: 'POST' method: 'POST'
}).then((data) => data.json()) }).then((data) => data.json())
} }
@ -685,6 +692,20 @@ const deleteAccount = ({ credentials, password }) => {
.then((response) => response.json()) .then((response) => response.json())
} }
const changeEmail = ({ credentials, email, password }) => {
const form = new FormData()
form.append('email', email)
form.append('password', password)
return fetch(CHANGE_EMAIL_URL, {
body: form,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.json())
}
const changePassword = ({ credentials, password, newPassword, newPasswordConfirmation }) => { const changePassword = ({ credentials, password, newPassword, newPasswordConfirmation }) => {
const form = new FormData() const form = new FormData()
@ -960,6 +981,7 @@ const apiService = {
importBlocks, importBlocks,
importFollows, importFollows,
deleteAccount, deleteAccount,
changeEmail,
changePassword, changePassword,
settingsMFA, settingsMFA,
mfaDisableOTP, mfaDisableOTP,

View file

@ -31,8 +31,8 @@ const backendInteractorService = credentials => {
return apiService.fetchUserRelationship({ id, credentials }) return apiService.fetchUserRelationship({ id, credentials })
} }
const followUser = (id) => { const followUser = ({ id, reblogs }) => {
return apiService.followUser({ credentials, id }) return apiService.followUser({ credentials, id, reblogs })
} }
const unfollowUser = (id) => { const unfollowUser = (id) => {
@ -131,6 +131,7 @@ const backendInteractorService = credentials => {
const importFollows = (file) => apiService.importFollows({ file, credentials }) const importFollows = (file) => apiService.importFollows({ file, credentials })
const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password }) const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password })
const changeEmail = ({ email, password }) => apiService.changeEmail({ credentials, email, password })
const changePassword = ({ password, newPassword, newPasswordConfirmation }) => const changePassword = ({ password, newPassword, newPasswordConfirmation }) =>
apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation }) apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation })
@ -195,6 +196,7 @@ const backendInteractorService = credentials => {
importBlocks, importBlocks,
importFollows, importFollows,
deleteAccount, deleteAccount,
changeEmail,
changePassword, changePassword,
fetchSettingsMFA, fetchSettingsMFA,
generateMfaBackupCodes, generateMfaBackupCodes,

View file

@ -69,6 +69,7 @@ export const parseUser = (data) => {
output.following = relationship.following output.following = relationship.following
output.statusnet_blocking = relationship.blocking output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting output.muted = relationship.muting
output.showing_reblogs = relationship.showing_reblogs
output.subscribed = relationship.subscribing output.subscribed = relationship.subscribing
} }

View file

@ -14,7 +14,7 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
}) })
export const requestFollow = (user, store) => new Promise((resolve, reject) => { export const requestFollow = (user, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.followUser(user.id) store.state.api.backendInteractor.followUser({ id: user.id })
.then((updated) => { .then((updated) => {
store.commit('updateUserRelationship', [updated]) store.commit('updateUserRelationship', [updated])

View file

@ -8,11 +8,10 @@ const update = ({ store, notifications, older }) => {
const fetchAndUpdate = ({ store, credentials, older = false }) => { const fetchAndUpdate = ({ store, credentials, older = false }) => {
const args = { credentials } const args = { credentials }
const { getters } = store
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications const timelineData = rootState.statuses.notifications
const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined' const hideMutedPosts = getters.mergedConfig.hideMutedPosts
? rootState.instance.hideMutedPosts
: rootState.config.hideMutedPosts
args['withMuted'] = !hideMutedPosts args['withMuted'] = !hideMutedPosts

View file

@ -15,13 +15,21 @@ const update = ({ store, statuses, timeline, showImmediately, userId }) => {
}) })
} }
const fetchAndUpdate = ({ store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false, until }) => { const fetchAndUpdate = ({
store,
credentials,
timeline = 'friends',
older = false,
showImmediately = false,
userId = false,
tag = false,
until
}) => {
const args = { timeline, credentials } const args = { timeline, credentials }
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
const { getters } = store
const timelineData = rootState.statuses.timelines[camelCase(timeline)] const timelineData = rootState.statuses.timelines[camelCase(timeline)]
const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined' const hideMutedPosts = getters.mergedConfig.hideMutedPosts
? rootState.instance.hideMutedPosts
: rootState.config.hideMutedPosts
if (older) { if (older) {
args['until'] = until || timelineData.minId args['until'] = until || timelineData.minId

View file

@ -5,11 +5,11 @@
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"], "bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ], "ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
"monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ], "monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ],
"mammal": [ "Mammal", "#272c37", "#444b5d", "#f8f8f8", "#9bacc8", "#7f3142", "#2bd850", "#2b90d9", "#ca8f04" ],
"redmond-xx": "/static/themes/redmond-xx.json", "redmond-xx": "/static/themes/redmond-xx.json",
"redmond-xx-se": "/static/themes/redmond-xx-se.json", "redmond-xx-se": "/static/themes/redmond-xx-se.json",
"redmond-xxi": "/static/themes/redmond-xxi.json", "redmond-xxi": "/static/themes/redmond-xxi.json",
"breezy-dark": "/static/themes/breezy-dark.json", "breezy-dark": "/static/themes/breezy-dark.json",
"breezy-light": "/static/themes/breezy-light.json" "breezy-light": "/static/themes/breezy-light.json",
"mammal": "/static/themes/mammal.json"
} }

57
static/themes/mammal.json Normal file
View file

@ -0,0 +1,57 @@
{
"_pleroma_theme_version": 2,
"name": "Mammal",
"theme": {
"shadows": {
"button": [],
"buttonHover": [
{
"x": "0",
"y": "0",
"blur": "0",
"spread": 1024,
"color": "#56a7e1",
"alpha": "1",
"inset": true
}
],
"buttonPressed": [
{
"x": "0",
"y": "0",
"blur": "0",
"spread": 1024,
"color": "#56a7e1",
"alpha": "1",
"inset": true
}
],
"panel": [],
"panelHeader": [],
"topBar": []
},
"opacity": { "input": "1" },
"colors": {
"bg": "#282c37",
"text": "#f8f8f8",
"link": "#9bacc8",
"fg": "#444b5d",
"input": "#FFFFFF",
"inputText": "#282c37",
"btn": "#2b90d9",
"btnText": "#FFFFFF",
"cRed": "#7f3142",
"cBlue": "#2b90d9",
"cGreen": "#2bd850",
"cOrange": "#ca8f04"
},
"radii": {
"btn": 4,
"input": 4,
"panel": "0",
"avatar": "4",
"avatarAlt": "4",
"attachment": "4"
}
}
}

View file

@ -12,8 +12,8 @@ const generateInput = (value, padEmoji = true) => {
}, },
mocks: { mocks: {
$store: { $store: {
state: { getters: {
config: { mergedConfig: {
padEmoji padEmoji
} }
} }

View file

@ -18,7 +18,14 @@ const actions = {
} }
const testGetters = { const testGetters = {
findUser: state => getters.findUser(state.users) findUser: state => getters.findUser(state.users),
mergedConfig: state => ({
colors: '',
highlight: {},
customTheme: {
colors: []
}
})
} }
const localUser = { const localUser = {
@ -45,13 +52,6 @@ const externalProfileStore = new Vuex.Store({
interface: { interface: {
browserSupport: '' browserSupport: ''
}, },
config: {
colors: '',
highlight: {},
customTheme: {
colors: []
}
},
instance: { instance: {
hideUserStats: true hideUserStats: true
}, },