Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma-fe into develop

This commit is contained in:
Tarteka 2019-10-30 22:17:19 +01:00
commit fe4845a7c1
74 changed files with 1082 additions and 1107 deletions

View file

@ -5,6 +5,8 @@ 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
### Changed ### Changed

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);
@ -705,31 +708,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,3 +1,4 @@
import Checkbox from '../checkbox/checkbox.vue'
const filterByKeyword = (list, keyword = '') => { const filterByKeyword = (list, keyword = '') => {
return list.filter(x => x.displayText.includes(keyword)) return list.filter(x => x.displayText.includes(keyword))
@ -13,7 +14,6 @@ const EmojiPicker = {
}, },
data () { data () {
return { return {
labelKey: String(Math.random() * 100000),
keyword: '', keyword: '',
activeGroup: 'custom', activeGroup: 'custom',
showingStickers: false, showingStickers: false,
@ -22,7 +22,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

@ -14,10 +14,6 @@
padding: 7px; padding: 7px;
line-height: normal; line-height: normal;
} }
.keep-open-label {
padding: 0 7px;
display: flex;
}
.heading { .heading {
display: flex; display: flex;

View file

@ -75,22 +75,10 @@
</span> </span>
</div> </div>
</div> </div>
<div <div class="keep-open">
class="keep-open" <Checkbox v-model="keepOpen">
>
<input
: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') }} {{ $t('emoji.keep_open') }}
</div> </Checkbox>
</label>
</div> </div>
</div> </div>
<div <div

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,20 +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
}
},
components: { components: {
BasicUserCard, BasicUserCard,
RemoteFollow RemoteFollow,
FollowButton
}, },
computed: { computed: {
isMe () { isMe () {
@ -23,20 +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(() => {
this.inProgress = false
})
},
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="user.requested ? $t('user_card.follow_again') : ''" />
@click="followUser"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="user.requested">
{{ $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,9 +7,10 @@
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 }"
> >
<div class="gallery-row-inner">
<attachment <attachment
v-for="attachment in row" v-for="attachment in row"
:key="attachment.id" :key="attachment.id"
@ -17,9 +18,12 @@
:nsfw="nsfw" :nsfw="nsfw"
:attachment="attachment" :attachment="attachment"
:allow-play="false" :allow-play="false"
:natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
:style="itemStyle(attachment.id, row)"
/> />
</div> </div>
</div> </div>
</div>
</template> </template>
<script src='./gallery.js'></script> <script src='./gallery.js'></script>
@ -28,14 +32,23 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.gallery-row { .gallery-row {
height: 200px; position: relative;
height: 0;
width: 100%; width: 100%;
flex-grow: 1;
margin-top: 0.5em;
.gallery-row-inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
align-content: stretch; align-content: stretch;
flex-grow: 1; }
margin-top: 0.5em;
// 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

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,13 @@
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'
const MediaModal = { const MediaModal = {
components: { components: {
StillImage, StillImage,
VideoAttachment VideoAttachment,
Modal
}, },
computed: { computed: {
showing () { showing () {

View file

@ -1,9 +1,8 @@
<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'"
@ -33,21 +32,15 @@
> >
<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) & {
display: none;
}
&:hover { &:hover {
.modal-view-button-arrow { .modal-view-button-arrow {
opacity: 0.75; opacity: 0.75;
@ -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

@ -261,12 +261,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
class="post-form-modal-panel panel"
@click.stop=""
> >
<div class="post-form-modal-panel panel">
<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', {
name: 'notificationVisibility',
value: this.$store.getters.mergedConfig.notificationVisibility
})
}, },
padEmojiLocal (value) { deep: true
this.$store.dispatch('setOption', { name: 'padEmoji', value })
},
hideAttachmentsInConvLocal (value) {
this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value })
},
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

@ -173,21 +173,27 @@
<div <div
v-if="isReply" v-if="isReply"
class="reply-to-and-accountname" class="reply-to-and-accountname"
>
<StatusPopover
v-if="!isPreview"
:status-id="status.in_reply_to_status_id"
> >
<a <a
class="reply-to" class="reply-to"
href="#" href="#"
:aria-label="$t('tool_tip.reply')" :aria-label="$t('tool_tip.reply')"
@click.prevent="gotoOriginal(status.in_reply_to_status_id)" @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 <i class="button-icon icon-reply" />
v-if="!isPreview"
class="button-icon icon-reply"
/>
<span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span> <span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span>
</a> </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"
>{{ $t('status.replies_list') }}</span>
<template v-if="replies">
<span
v-for="reply in replies" v-for="reply in replies"
:key="reply.id" :key="reply.id"
class="reply-link faint" :status-id="reply.id"
> >
<a <a
href="#" href="#"
class="reply-link"
@click.prevent="gotoOriginal(reply.id)" @click.prevent="gotoOriginal(reply.id)"
@mouseenter="replyEnter(reply.id, $event)"
@mouseout="replyLeave()"
>{{ reply.name }}</a> >{{ reply.name }}</a>
</span> </StatusPopover>
</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: {},
@ -111,7 +112,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]
} }
@ -338,7 +339,8 @@ export default {
FontControl, FontControl,
TabSwitcher, TabSwitcher,
Preview, Preview,
ExportImport ExportImport,
Checkbox
}, },
methods: { methods: {
setCustomTheme () { setCustomTheme () {
@ -365,9 +367,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,45 +138,9 @@
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"
:title="user.requested ? $t('user_card.follow_again') : ''"
@click="followUser"
>
<template v-if="followRequestInProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="user.requested">
{{ $t('user_card.follow_sent') }}
</template>
<template v-else>
{{ $t('user_card.follow') }}
</template>
</button>
</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 <ProgressButton
v-if="!user.subscribed" v-if="!user.subscribed"
class="btn btn-default" class="btn btn-default"
@ -190,17 +157,8 @@
> >
<i class="icon-bell-ringing-o" /> <i class="icon-bell-ringing-o" />
</ProgressButton> </ProgressButton>
</template>
</div> </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
class="user-reporting-panel panel"
@click.stop=""
> >
<div class="user-reporting-panel panel">
<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({
@ -82,7 +83,8 @@ const UserSettings = {
ProgressButton, ProgressButton,
Importer, Importer,
Exporter, Exporter,
Mfa Mfa,
Checkbox
}, },
computed: { computed: {
user () { user () {

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"
@ -367,44 +347,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,13 +2,16 @@ 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 (lockerEls.size <= 1) {
// If previousNavPaddingRight is already set, don't set it again. // If previousNavPaddingRight is already set, don't set it again.
if (previousNavPaddingRight === undefined) { if (previousNavPaddingRight === undefined) {
const navEl = document.getElementById('nav') const navEl = document.getElementById('nav')
@ -22,11 +25,14 @@ const disableBodyScroll = (el) => {
appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
} }
document.body.classList.add('scroll-locked') document.body.classList.add('scroll-locked')
}
}) })
} }
const enableBodyScroll = (el) => { const enableBodyScroll = (el) => {
lockerEls.delete(el)
setTimeout(() => { setTimeout(() => {
if (lockerEls.size === 0) {
if (previousNavPaddingRight !== undefined) { if (previousNavPaddingRight !== undefined) {
document.getElementById('nav').style.paddingRight = previousNavPaddingRight document.getElementById('nav').style.paddingRight = previousNavPaddingRight
// Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again. // Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again.
@ -38,6 +44,7 @@ const enableBodyScroll = (el) => {
previousAppBgWrapperRight = undefined previousAppBgWrapperRight = undefined
} }
document.body.classList.remove('scroll-locked') document.body.classList.remove('scroll-locked')
}
}) })
bodyScrollLock.enableBodyScroll(el) bodyScrollLock.enableBodyScroll(el)
} }

View file

@ -555,6 +555,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",
@ -630,6 +632,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",

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

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
@ -72,6 +73,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

@ -219,10 +219,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())
} }

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

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

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