commit
f8cf92a01f
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"presets": ["es2015", "stage-2", "env"],
|
"presets": ["@babel/preset-env"],
|
||||||
"plugins": ["transform-runtime", "lodash", "transform-vue-jsx"],
|
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"],
|
||||||
"comments": false
|
"comments": false
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div class="async-component-error">
|
||||||
|
<div>
|
||||||
|
<h4>
|
||||||
|
{{ $t('general.generic_error') }}
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
{{ $t('general.error_retry') }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
@click="retry"
|
||||||
|
>
|
||||||
|
{{ $t('general.retry') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
retry () {
|
||||||
|
this.$emit('resetAsyncComponent')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.async-component-error {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
.btn {
|
||||||
|
margin: .5em;
|
||||||
|
padding: .5em 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,68 @@
|
|||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.color-input {
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
&-field.input {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 0;
|
||||||
|
max-width: 9em;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: .2em 8px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
background: none;
|
||||||
|
color: $fallback--lightText;
|
||||||
|
color: var(--inputText, $fallback--lightText);
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&.textColor {
|
||||||
|
flex: 1 0 3em;
|
||||||
|
min-width: 3em;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nativeColor {
|
||||||
|
flex: 0 0 2em;
|
||||||
|
min-width: 2em;
|
||||||
|
align-self: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.computedIndicator,
|
||||||
|
.transparentIndicator {
|
||||||
|
flex: 0 0 2em;
|
||||||
|
min-width: 2em;
|
||||||
|
align-self: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.transparentIndicator {
|
||||||
|
// forgot to install counter-strike source, ooops
|
||||||
|
background-color: #FF00FF;
|
||||||
|
position: relative;
|
||||||
|
&::before, &::after {
|
||||||
|
display: block;
|
||||||
|
content: '';
|
||||||
|
background-color: #000000;
|
||||||
|
position: absolute;
|
||||||
|
height: 50%;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
|
|
||||||
|
const DomainMuteCard = {
|
||||||
|
props: ['domain'],
|
||||||
|
components: {
|
||||||
|
ProgressButton
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user () {
|
||||||
|
return this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
muted () {
|
||||||
|
return this.user.domainMutes.includes(this.domain)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
unmuteDomain () {
|
||||||
|
return this.$store.dispatch('unmuteDomain', this.domain)
|
||||||
|
},
|
||||||
|
muteDomain () {
|
||||||
|
return this.$store.dispatch('muteDomain', this.domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DomainMuteCard
|
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="domain-mute-card">
|
||||||
|
<div class="domain-mute-card-domain">
|
||||||
|
{{ domain }}
|
||||||
|
</div>
|
||||||
|
<ProgressButton
|
||||||
|
v-if="muted"
|
||||||
|
:click="unmuteDomain"
|
||||||
|
class="btn btn-default"
|
||||||
|
>
|
||||||
|
{{ $t('domain_mute_card.unmute') }}
|
||||||
|
<template slot="progress">
|
||||||
|
{{ $t('domain_mute_card.unmute_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
|
<ProgressButton
|
||||||
|
v-else
|
||||||
|
:click="muteDomain"
|
||||||
|
class="btn btn-default"
|
||||||
|
>
|
||||||
|
{{ $t('domain_mute_card.mute') }}
|
||||||
|
<template slot="progress">
|
||||||
|
{{ $t('domain_mute_card.mute_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./domain_mute_card.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.domain-mute-card {
|
||||||
|
flex: 1 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.6em 1em 0.6em 0;
|
||||||
|
|
||||||
|
&-domain {
|
||||||
|
margin-right: 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autosuggest-results & {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,69 @@
|
|||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import Popover from '../popover/popover.vue'
|
||||||
|
|
||||||
|
const EMOJI_REACTION_COUNT_CUTOFF = 12
|
||||||
|
|
||||||
|
const EmojiReactions = {
|
||||||
|
name: 'EmojiReactions',
|
||||||
|
components: {
|
||||||
|
UserAvatar,
|
||||||
|
Popover
|
||||||
|
},
|
||||||
|
props: ['status'],
|
||||||
|
data: () => ({
|
||||||
|
showAll: false
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
tooManyReactions () {
|
||||||
|
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
|
||||||
|
},
|
||||||
|
emojiReactions () {
|
||||||
|
return this.showAll
|
||||||
|
? this.status.emoji_reactions
|
||||||
|
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
|
||||||
|
},
|
||||||
|
showMoreString () {
|
||||||
|
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
|
||||||
|
},
|
||||||
|
accountsForEmoji () {
|
||||||
|
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
||||||
|
acc[reaction.name] = reaction.accounts || []
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
loggedIn () {
|
||||||
|
return !!this.$store.state.users.currentUser
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleShowAll () {
|
||||||
|
this.showAll = !this.showAll
|
||||||
|
},
|
||||||
|
reactedWith (emoji) {
|
||||||
|
return this.status.emoji_reactions.find(r => r.name === emoji).me
|
||||||
|
},
|
||||||
|
fetchEmojiReactionsByIfMissing () {
|
||||||
|
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
|
||||||
|
if (hasNoAccounts) {
|
||||||
|
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reactWith (emoji) {
|
||||||
|
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||||
|
},
|
||||||
|
unreact (emoji) {
|
||||||
|
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||||
|
},
|
||||||
|
emojiOnClick (emoji, event) {
|
||||||
|
if (!this.loggedIn) return
|
||||||
|
|
||||||
|
if (this.reactedWith(emoji)) {
|
||||||
|
this.unreact(emoji)
|
||||||
|
} else {
|
||||||
|
this.reactWith(emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmojiReactions
|
@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div class="emoji-reactions">
|
||||||
|
<Popover
|
||||||
|
v-for="(reaction) in emojiReactions"
|
||||||
|
:key="reaction.name"
|
||||||
|
trigger="hover"
|
||||||
|
placement="top"
|
||||||
|
:offset="{ y: 5 }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="content"
|
||||||
|
class="reacted-users"
|
||||||
|
>
|
||||||
|
<div v-if="accountsForEmoji[reaction.name].length">
|
||||||
|
<div
|
||||||
|
v-for="(account) in accountsForEmoji[reaction.name]"
|
||||||
|
:key="account.id"
|
||||||
|
class="reacted-user"
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
:user="account"
|
||||||
|
class="avatar-small"
|
||||||
|
:compact="true"
|
||||||
|
/>
|
||||||
|
<div class="reacted-user-names">
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<span
|
||||||
|
class="reacted-user-name"
|
||||||
|
v-html="account.name_html"
|
||||||
|
/>
|
||||||
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
|
<span class="reacted-user-screen-name">{{ account.screen_name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<i class="icon-spin4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
slot="trigger"
|
||||||
|
class="emoji-reaction btn btn-default"
|
||||||
|
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||||
|
@click="emojiOnClick(reaction.name, $event)"
|
||||||
|
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||||
|
>
|
||||||
|
<span class="reaction-emoji">{{ reaction.name }}</span>
|
||||||
|
<span>{{ reaction.count }}</span>
|
||||||
|
</button>
|
||||||
|
</Popover>
|
||||||
|
<a
|
||||||
|
v-if="tooManyReactions"
|
||||||
|
class="emoji-reaction-expand faint"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
@click="toggleShowAll"
|
||||||
|
>
|
||||||
|
{{ showAll ? $t('general.show_less') : showMoreString }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./emoji_reactions.js" ></script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.emoji-reactions {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reacted-users {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reacted-user {
|
||||||
|
padding: 0.25em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.reacted-user-names {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
min-width: 5em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reacted-user-screen-name {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-reaction {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
.reaction-emoji {
|
||||||
|
width: 1.25em;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.not-clickable {
|
||||||
|
cursor: default;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: $fallback--buttonShadow;
|
||||||
|
box-shadow: var(--buttonShadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-reaction-expand {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.picked-reaction {
|
||||||
|
border: 1px solid var(--accent, $fallback--link);
|
||||||
|
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||||
|
margin-right: calc(0.5em - 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -1,16 +1,35 @@
|
|||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
import { get } from 'lodash'
|
||||||
|
|
||||||
const MRFTransparencyPanel = {
|
const MRFTransparencyPanel = {
|
||||||
computed: mapState({
|
computed: {
|
||||||
federationPolicy: state => state.instance.federationPolicy,
|
...mapState({
|
||||||
mrfPolicies: state => state.instance.federationPolicy.mrf_policies,
|
federationPolicy: state => get(state, 'instance.federationPolicy'),
|
||||||
acceptInstances: state => state.instance.federationPolicy.mrf_simple.accept,
|
mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []),
|
||||||
rejectInstances: state => state.instance.federationPolicy.mrf_simple.reject,
|
quarantineInstances: state => get(state, 'instance.federationPolicy.quarantined_instances', []),
|
||||||
quarantineInstances: state => state.instance.federationPolicy.quarantined_instances,
|
acceptInstances: state => get(state, 'instance.federationPolicy.mrf_simple.accept', []),
|
||||||
ftlRemovalInstances: state => state.instance.federationPolicy.mrf_simple.federated_timeline_removal,
|
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
|
||||||
mediaNsfwInstances: state => state.instance.federationPolicy.mrf_simple.media_nsfw,
|
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
|
||||||
mediaRemovalInstances: state => state.instance.federationPolicy.mrf_simple.media_removal
|
mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
|
||||||
})
|
mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
|
||||||
|
keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []),
|
||||||
|
keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []),
|
||||||
|
keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', [])
|
||||||
|
}),
|
||||||
|
hasInstanceSpecificPolicies () {
|
||||||
|
return this.quarantineInstances.length ||
|
||||||
|
this.acceptInstances.length ||
|
||||||
|
this.rejectInstances.length ||
|
||||||
|
this.ftlRemovalInstances.length ||
|
||||||
|
this.mediaNsfwInstances.length ||
|
||||||
|
this.mediaRemovalInstances.length
|
||||||
|
},
|
||||||
|
hasKeywordPolicies () {
|
||||||
|
return this.keywordsFtlRemoval.length ||
|
||||||
|
this.keywordsReject.length ||
|
||||||
|
this.keywordsReplace.length
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MRFTransparencyPanel
|
export default MRFTransparencyPanel
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
const NavPanel = {
|
const NavPanel = {
|
||||||
created () {
|
created () {
|
||||||
if (this.currentUser && this.currentUser.locked) {
|
if (this.currentUser && this.currentUser.locked) {
|
||||||
this.$store.dispatch('startFetchingFollowRequest')
|
this.$store.dispatch('startFetchingFollowRequests')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: mapState({
|
||||||
currentUser () {
|
currentUser: state => state.users.currentUser,
|
||||||
return this.$store.state.users.currentUser
|
chat: state => state.chat.channel,
|
||||||
},
|
followRequestCount: state => state.api.followRequests.length,
|
||||||
chat () {
|
privateMode: state => state.instance.private,
|
||||||
return this.$store.state.chat.channel
|
federating: state => state.instance.federating
|
||||||
},
|
})
|
||||||
followRequestCount () {
|
|
||||||
return this.$store.state.api.followRequests.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NavPanel
|
export default NavPanel
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="panel-loading">
|
||||||
|
<span class="loading-text">
|
||||||
|
<i class="icon-spin4 animate-spin" />
|
||||||
|
{{ $t('general.loading') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import 'src/_variables.scss';
|
||||||
|
|
||||||
|
.panel-loading {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2em;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
.loading-text i {
|
||||||
|
font-size: 3em;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,156 @@
|
|||||||
|
|
||||||
|
const Popover = {
|
||||||
|
name: 'Popover',
|
||||||
|
props: {
|
||||||
|
// Action to trigger popover: either 'hover' or 'click'
|
||||||
|
trigger: String,
|
||||||
|
// Either 'top' or 'bottom'
|
||||||
|
placement: String,
|
||||||
|
// Takes object with properties 'x' and 'y', values of these can be
|
||||||
|
// 'container' for using offsetParent as boundaries for either axis
|
||||||
|
// or 'viewport'
|
||||||
|
boundTo: Object,
|
||||||
|
// Takes a top/bottom/left/right object, how much space to leave
|
||||||
|
// between boundary and popover element
|
||||||
|
margin: Object,
|
||||||
|
// Takes a x/y object and tells how many pixels to offset from
|
||||||
|
// anchor point on either axis
|
||||||
|
offset: Object,
|
||||||
|
// Additional styles you may want for the popover container
|
||||||
|
popoverClass: String
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
hidden: true,
|
||||||
|
styles: { opacity: 0 },
|
||||||
|
oldSize: { width: 0, height: 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateStyles () {
|
||||||
|
if (this.hidden) {
|
||||||
|
this.styles = {
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popover will be anchored around this element, trigger ref is the container, so
|
||||||
|
// its children are what are inside the slot. Expect only one slot="trigger".
|
||||||
|
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
|
||||||
|
const screenBox = anchorEl.getBoundingClientRect()
|
||||||
|
// Screen position of the origin point for popover
|
||||||
|
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
|
||||||
|
const content = this.$refs.content
|
||||||
|
// Minor optimization, don't call a slow reflow call if we don't have to
|
||||||
|
const parentBounds = this.boundTo &&
|
||||||
|
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
|
||||||
|
this.$el.offsetParent.getBoundingClientRect()
|
||||||
|
const margin = this.margin || {}
|
||||||
|
|
||||||
|
// What are the screen bounds for the popover? Viewport vs container
|
||||||
|
// when using viewport, using default margin values to dodge the navbar
|
||||||
|
const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
|
||||||
|
min: parentBounds.left + (margin.left || 0),
|
||||||
|
max: parentBounds.right - (margin.right || 0)
|
||||||
|
} : {
|
||||||
|
min: 0 + (margin.left || 10),
|
||||||
|
max: window.innerWidth - (margin.right || 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
|
||||||
|
min: parentBounds.top + (margin.top || 0),
|
||||||
|
max: parentBounds.bottom - (margin.bottom || 0)
|
||||||
|
} : {
|
||||||
|
min: 0 + (margin.top || 50),
|
||||||
|
max: window.innerHeight - (margin.bottom || 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
let horizOffset = 0
|
||||||
|
|
||||||
|
// If overflowing from left, move it so that it doesn't
|
||||||
|
if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {
|
||||||
|
horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min
|
||||||
|
}
|
||||||
|
|
||||||
|
// If overflowing from right, move it so that it doesn't
|
||||||
|
if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {
|
||||||
|
horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to whatever user wished with placement prop
|
||||||
|
let usingTop = this.placement !== 'bottom'
|
||||||
|
|
||||||
|
// Handle special cases, first force to displaying on top if there's not space on bottom,
|
||||||
|
// regardless of what placement value was. Then check if there's not space on top, and
|
||||||
|
// force to bottom, again regardless of what placement value was.
|
||||||
|
if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
|
||||||
|
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
|
||||||
|
|
||||||
|
const yOffset = (this.offset && this.offset.y) || 0
|
||||||
|
const translateY = usingTop
|
||||||
|
? -anchorEl.offsetHeight - yOffset - content.offsetHeight
|
||||||
|
: yOffset
|
||||||
|
|
||||||
|
const xOffset = (this.offset && this.offset.x) || 0
|
||||||
|
const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
|
||||||
|
|
||||||
|
// Note, separate translateX and translateY avoids blurry text on chromium,
|
||||||
|
// single translate or translate3d resulted in blurry text.
|
||||||
|
this.styles = {
|
||||||
|
opacity: 1,
|
||||||
|
transform: `translateX(${Math.floor(translateX)}px) translateY(${Math.floor(translateY)}px)`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showPopover () {
|
||||||
|
if (this.hidden) this.$emit('show')
|
||||||
|
this.hidden = false
|
||||||
|
this.$nextTick(this.updateStyles)
|
||||||
|
},
|
||||||
|
hidePopover () {
|
||||||
|
if (!this.hidden) this.$emit('close')
|
||||||
|
this.hidden = true
|
||||||
|
this.styles = { opacity: 0 }
|
||||||
|
},
|
||||||
|
onMouseenter (e) {
|
||||||
|
if (this.trigger === 'hover') this.showPopover()
|
||||||
|
},
|
||||||
|
onMouseleave (e) {
|
||||||
|
if (this.trigger === 'hover') this.hidePopover()
|
||||||
|
},
|
||||||
|
onClick (e) {
|
||||||
|
if (this.trigger === 'click') {
|
||||||
|
if (this.hidden) {
|
||||||
|
this.showPopover()
|
||||||
|
} else {
|
||||||
|
this.hidePopover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickOutside (e) {
|
||||||
|
if (this.hidden) return
|
||||||
|
if (this.$el.contains(e.target)) return
|
||||||
|
this.hidePopover()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updated () {
|
||||||
|
// Monitor changes to content size, update styles only when content sizes have changed,
|
||||||
|
// that should be the only time we need to move the popover box if we don't care about scroll
|
||||||
|
// or resize
|
||||||
|
const content = this.$refs.content
|
||||||
|
if (!content) return
|
||||||
|
if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
|
||||||
|
this.updateStyles()
|
||||||
|
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
document.addEventListener('click', this.onClickOutside)
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
document.removeEventListener('click', this.onClickOutside)
|
||||||
|
this.hidePopover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Popover
|
@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
@mouseenter="onMouseenter"
|
||||||
|
@mouseleave="onMouseleave"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="trigger"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
|
<slot name="trigger" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!hidden"
|
||||||
|
ref="content"
|
||||||
|
:style="styles"
|
||||||
|
class="popover"
|
||||||
|
:class="popoverClass"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
name="content"
|
||||||
|
class="popover-inner"
|
||||||
|
:close="hidePopover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./popover.js" />
|
||||||
|
|
||||||
|
<style lang=scss>
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
z-index: 8;
|
||||||
|
position: absolute;
|
||||||
|
min-width: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
||||||
|
box-shadow: var(--panelShadow);
|
||||||
|
border-radius: $fallback--btnRadius;
|
||||||
|
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||||
|
|
||||||
|
background-color: $fallback--bg;
|
||||||
|
background-color: var(--popover, $fallback--bg);
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--popoverText, $fallback--text);
|
||||||
|
--faint: var(--popoverFaintText, $fallback--faint);
|
||||||
|
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||||
|
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||||
|
--postLink: var(--popoverPostLink, $fallback--link);
|
||||||
|
--postFaintLink: var(--popoverPostFaintLink, $fallback--link);
|
||||||
|
--icon: var(--popoverIcon, $fallback--icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
padding: .5rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
list-style: none;
|
||||||
|
max-width: 100vw;
|
||||||
|
z-index: 10;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
height: 0;
|
||||||
|
margin: .5rem 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: 1px solid $fallback--border;
|
||||||
|
border-top: 1px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
line-height: 21px;
|
||||||
|
margin-right: 5px;
|
||||||
|
overflow: auto;
|
||||||
|
display: block;
|
||||||
|
padding: .25rem 1.0rem .25rem 1.5rem;
|
||||||
|
clear: both;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: inherit;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
--btnText: var(--popoverText, $fallback--text);
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
color: var(--menuPopoverIcon, $fallback--icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active, &:hover {
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--selectedMenuPopoverText, $fallback--link);
|
||||||
|
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||||
|
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||||
|
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||||
|
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||||
|
i {
|
||||||
|
color: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,147 +0,0 @@
|
|||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.tooltip.popover {
|
|
||||||
z-index: 8;
|
|
||||||
|
|
||||||
.popover-inner {
|
|
||||||
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
|
||||||
box-shadow: var(--panelShadow);
|
|
||||||
border-radius: $fallback--btnRadius;
|
|
||||||
border-radius: var(--btnRadius, $fallback--btnRadius);
|
|
||||||
background-color: $fallback--bg;
|
|
||||||
background-color: var(--bg, $fallback--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-style: solid;
|
|
||||||
position: absolute;
|
|
||||||
margin: 5px;
|
|
||||||
border-color: $fallback--bg;
|
|
||||||
border-color: var(--bg, $fallback--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="top"] {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
border-width: 5px 5px 0 5px;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
bottom: -4px;
|
|
||||||
left: calc(50% - 5px);
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="bottom"] {
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
border-width: 0 5px 5px 5px;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
top: -4px;
|
|
||||||
left: calc(50% - 5px);
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="right"] {
|
|
||||||
margin-left: 5px;
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
border-width: 5px 5px 5px 0;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
left: -4px;
|
|
||||||
top: calc(50% - 5px);
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="left"] {
|
|
||||||
margin-right: 5px;
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
border-width: 5px 0 5px 5px;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
right: -4px;
|
|
||||||
top: calc(50% - 5px);
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-hidden='true'] {
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity .15s, visibility .15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-hidden='false'] {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity .15s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
display: block;
|
|
||||||
padding: .5rem 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
text-align: left;
|
|
||||||
list-style: none;
|
|
||||||
max-width: 100vw;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
.dropdown-divider {
|
|
||||||
height: 0;
|
|
||||||
margin: .5rem 0;
|
|
||||||
overflow: hidden;
|
|
||||||
border-top: 1px solid $fallback--border;
|
|
||||||
border-top: 1px solid var(--border, $fallback--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
line-height: 21px;
|
|
||||||
margin-right: 5px;
|
|
||||||
overflow: auto;
|
|
||||||
display: block;
|
|
||||||
padding: .25rem 1.0rem .25rem 1.5rem;
|
|
||||||
clear: both;
|
|
||||||
font-weight: 400;
|
|
||||||
text-align: inherit;
|
|
||||||
white-space: normal;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0px;
|
|
||||||
background-color: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
&-icon {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
|
|
||||||
i {
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
// TODO: improve the look on breeze themes
|
|
||||||
background-color: $fallback--fg;
|
|
||||||
background-color: var(--btn, $fallback--fg);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,39 @@
|
|||||||
|
import Popover from '../popover/popover.vue'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
const ReactButton = {
|
||||||
|
props: ['status'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
filterWord: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Popover
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addReaction (event, emoji, close) {
|
||||||
|
const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
|
||||||
|
if (existingReaction && existingReaction.me) {
|
||||||
|
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||||
|
} else {
|
||||||
|
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||||
|
}
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
commonEmojis () {
|
||||||
|
return ['👍', '😠', '👀', '😂', '🔥']
|
||||||
|
},
|
||||||
|
emojis () {
|
||||||
|
if (this.filterWord !== '') {
|
||||||
|
return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord))
|
||||||
|
}
|
||||||
|
return this.$store.state.instance.emoji || []
|
||||||
|
},
|
||||||
|
...mapGetters(['mergedConfig'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReactButton
|
@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<Popover
|
||||||
|
trigger="click"
|
||||||
|
placement="top"
|
||||||
|
:offset="{ y: 5 }"
|
||||||
|
class="react-button-popover"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="content"
|
||||||
|
slot-scope="{close}"
|
||||||
|
>
|
||||||
|
<div class="reaction-picker-filter">
|
||||||
|
<input
|
||||||
|
v-model="filterWord"
|
||||||
|
:placeholder="$t('emoji.search_emoji')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="reaction-picker">
|
||||||
|
<span
|
||||||
|
v-for="emoji in commonEmojis"
|
||||||
|
:key="emoji"
|
||||||
|
class="emoji-button"
|
||||||
|
@click="addReaction($event, emoji, close)"
|
||||||
|
>
|
||||||
|
{{ emoji }}
|
||||||
|
</span>
|
||||||
|
<div class="reaction-picker-divider" />
|
||||||
|
<span
|
||||||
|
v-for="(emoji, key) in emojis"
|
||||||
|
:key="key"
|
||||||
|
class="emoji-button"
|
||||||
|
@click="addReaction($event, emoji.replacement, close)"
|
||||||
|
>
|
||||||
|
{{ emoji.replacement }}
|
||||||
|
</span>
|
||||||
|
<div class="reaction-bottom-fader" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
slot="trigger"
|
||||||
|
class="icon-smile button-icon add-reaction-button"
|
||||||
|
:title="$t('tool_tip.add_reaction')"
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./react_button.js" ></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.reaction-picker-filter {
|
||||||
|
padding: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-picker-divider {
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.5em;
|
||||||
|
background-color: var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-picker {
|
||||||
|
width: 10em;
|
||||||
|
height: 9em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
overflow-y: scroll;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.5em;
|
||||||
|
text-align: center;
|
||||||
|
align-content: flex-start;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
|
||||||
|
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
|
||||||
|
linear-gradient(to top, white, white);
|
||||||
|
transition: mask-size 150ms;
|
||||||
|
mask-size: 100% 20px, 100% 20px, auto;
|
||||||
|
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
|
||||||
|
.emoji-button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
flex-basis: 20%;
|
||||||
|
line-height: 1.5em;
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-reaction-button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -1,112 +0,0 @@
|
|||||||
/* eslint-env browser */
|
|
||||||
import { filter, trim } from 'lodash'
|
|
||||||
|
|
||||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
|
||||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
|
||||||
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
|
|
||||||
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 pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
|
|
||||||
|
|
||||||
const multiChoiceProperties = [
|
|
||||||
'postContentType',
|
|
||||||
'subjectLineBehavior'
|
|
||||||
]
|
|
||||||
|
|
||||||
const settings = {
|
|
||||||
data () {
|
|
||||||
const instance = this.$store.state.instance
|
|
||||||
|
|
||||||
return {
|
|
||||||
loopSilentAvailable:
|
|
||||||
// Firefox
|
|
||||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
|
||||||
// Chrome-likes
|
|
||||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
|
|
||||||
// Future spec, still not supported in Nightly 63 as of 08/2018
|
|
||||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
|
|
||||||
|
|
||||||
backendVersion: instance.backendVersion,
|
|
||||||
frontendVersion: instance.frontendVersion
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
TabSwitcher,
|
|
||||||
StyleSwitcher,
|
|
||||||
InterfaceLanguageSwitcher,
|
|
||||||
Checkbox
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
user () {
|
|
||||||
return this.$store.state.users.currentUser
|
|
||||||
},
|
|
||||||
currentSaveStateNotice () {
|
|
||||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
|
||||||
},
|
|
||||||
postFormats () {
|
|
||||||
return this.$store.state.instance.postFormats || []
|
|
||||||
},
|
|
||||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
|
||||||
frontendVersionLink () {
|
|
||||||
return pleromaFeCommitUrl + this.frontendVersion
|
|
||||||
},
|
|
||||||
backendVersionLink () {
|
|
||||||
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: {
|
|
||||||
notificationVisibility: {
|
|
||||||
handler (value) {
|
|
||||||
this.$store.dispatch('setOption', {
|
|
||||||
name: 'notificationVisibility',
|
|
||||||
value: this.$store.getters.mergedConfig.notificationVisibility
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default settings
|
|
@ -1,400 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="settings panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<div class="title">
|
|
||||||
{{ $t('settings.settings') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition name="fade">
|
|
||||||
<template v-if="currentSaveStateNotice">
|
|
||||||
<div
|
|
||||||
v-if="currentSaveStateNotice.error"
|
|
||||||
class="alert error"
|
|
||||||
@click.prevent
|
|
||||||
>
|
|
||||||
{{ $t('settings.saving_err') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="!currentSaveStateNotice.error"
|
|
||||||
class="alert transparent"
|
|
||||||
@click.prevent
|
|
||||||
>
|
|
||||||
{{ $t('settings.saving_ok') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<keep-alive>
|
|
||||||
<tab-switcher>
|
|
||||||
<div :label="$t('settings.general')">
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.interface') }}</h2>
|
|
||||||
<ul class="setting-list">
|
|
||||||
<li>
|
|
||||||
<interface-language-switcher />
|
|
||||||
</li>
|
|
||||||
<li v-if="instanceSpecificPanelPresent">
|
|
||||||
<Checkbox v-model="hideISP">
|
|
||||||
{{ $t('settings.hide_isp') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('nav.timeline') }}</h2>
|
|
||||||
<ul class="setting-list">
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="hideMutedPosts">
|
|
||||||
{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="collapseMessageWithSubject">
|
|
||||||
{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="streaming">
|
|
||||||
{{ $t('settings.streaming') }}
|
|
||||||
</Checkbox>
|
|
||||||
<ul
|
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !streaming}]"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<Checkbox
|
|
||||||
v-model="pauseOnUnfocused"
|
|
||||||
:disabled="!streaming"
|
|
||||||
>
|
|
||||||
{{ $t('settings.pause_on_unfocused') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="autoLoad">
|
|
||||||
{{ $t('settings.autoload') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="hoverPreview">
|
|
||||||
{{ $t('settings.reply_link_preview') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.composing') }}</h2>
|
|
||||||
<ul class="setting-list">
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="scopeCopy">
|
|
||||||
{{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="alwaysShowSubjectInput">
|
|
||||||
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
{{ $t('settings.subject_line_behavior') }}
|
|
||||||
<label
|
|
||||||
for="subjectLineBehavior"
|
|
||||||
class="select"
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
id="subjectLineBehavior"
|
|
||||||
v-model="subjectLineBehavior"
|
|
||||||
>
|
|
||||||
<option value="email">
|
|
||||||
{{ $t('settings.subject_line_email') }}
|
|
||||||
{{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
|
|
||||||
</option>
|
|
||||||
<option value="masto">
|
|
||||||
{{ $t('settings.subject_line_mastodon') }}
|
|
||||||
{{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
|
|
||||||
</option>
|
|
||||||
<option value="noop">
|
|
||||||
{{ $t('settings.subject_line_noop') }}
|
|
||||||
{{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<i class="icon-down-open" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li v-if="postFormats.length > 0">
|
|
||||||
<div>
|
|
||||||
{{ $t('settings.post_status_content_type') }}
|
|
||||||
<label
|
|
||||||
for="postContentType"
|
|
||||||
class="select"
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
id="postContentType"
|
|
||||||
v-model="postContentType"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="postFormat in postFormats"
|
|
||||||
:key="postFormat"
|
|
||||||
:value="postFormat"
|
|
||||||
>
|
|
||||||
{{ $t(`post_status.content_type["${postFormat}"]`) }}
|
|
||||||
{{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<i class="icon-down-open" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="minimalScopesMode">
|
|
||||||
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="autohideFloatingPostButton">
|
|
||||||
{{ $t('settings.autohide_floating_post_button') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="padEmoji">
|
|
||||||
{{ $t('settings.pad_emoji') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.attachments') }}</h2>
|
|
||||||
<ul class="setting-list">
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="hideAttachments">
|
|
||||||
{{ $t('settings.hide_attachments_in_tl') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="hideAttachmentsInConv">
|
|
||||||
{{ $t('settings.hide_attachments_in_convo') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<label for="maxThumbnails">
|
|
||||||
{{ $t('settings.max_thumbnails') }}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="maxThumbnails"
|
|
||||||
v-model.number="maxThumbnails"
|
|
||||||
class="number-input"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="hideNsfw">
|
|
||||||
{{ $t('settings.nsfw_clickthrough') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<ul class="setting-list suboptions">
|
|
||||||
<li>
|
|
||||||
<Checkbox
|
|
||||||
v-model="preloadImage"
|
|
||||||
:disabled="!hideNsfw"
|
|
||||||
>
|
|
||||||
{{ $t('settings.preload_images') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox
|
|
||||||
v-model="useOneClickNsfw"
|
|
||||||
:disabled="!hideNsfw"
|
|
||||||
>
|
|
||||||
{{ $t('settings.use_one_click_nsfw') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="stopGifs">
|
|
||||||
{{ $t('settings.stop_gifs') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="loopVideo">
|
|
||||||
{{ $t('settings.loop_video') }}
|
|
||||||
</Checkbox>
|
|
||||||
<ul
|
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !streaming}]"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<Checkbox
|
|
||||||
v-model="loopVideoSilentOnly"
|
|
||||||
:disabled="!loopVideo || !loopSilentAvailable"
|
|
||||||
>
|
|
||||||
{{ $t('settings.loop_video_silent_only') }}
|
|
||||||
</Checkbox>
|
|
||||||
<div
|
|
||||||
v-if="!loopSilentAvailable"
|
|
||||||
class="unavailable"
|
|
||||||
>
|
|
||||||
<i class="icon-globe" />! {{ $t('settings.limited_availability') }}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="playVideosInModal">
|
|
||||||
{{ $t('settings.play_videos_in_modal') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="useContainFit">
|
|
||||||
{{ $t('settings.use_contain_fit') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.notifications') }}</h2>
|
|
||||||
<ul class="setting-list">
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="webPushNotifications">
|
|
||||||
{{ $t('settings.enable_web_push_notifications') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.fun') }}</h2>
|
|
||||||
<ul class="setting-list">
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="greentext">
|
|
||||||
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.theme')">
|
|
||||||
<div class="setting-item">
|
|
||||||
<style-switcher />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.filtering')">
|
|
||||||
<div class="setting-item">
|
|
||||||
<div class="select-multiple">
|
|
||||||
<span class="label">{{ $t('settings.notification_visibility') }}</span>
|
|
||||||
<ul class="option-list">
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationVisibility.likes">
|
|
||||||
{{ $t('settings.notification_visibility_likes') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationVisibility.repeats">
|
|
||||||
{{ $t('settings.notification_visibility_repeats') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationVisibility.follows">
|
|
||||||
{{ $t('settings.notification_visibility_follows') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationVisibility.mentions">
|
|
||||||
{{ $t('settings.notification_visibility_mentions') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ $t('settings.replies_in_timeline') }}
|
|
||||||
<label
|
|
||||||
for="replyVisibility"
|
|
||||||
class="select"
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
id="replyVisibility"
|
|
||||||
v-model="replyVisibility"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
value="all"
|
|
||||||
selected
|
|
||||||
>{{ $t('settings.reply_visibility_all') }}</option>
|
|
||||||
<option value="following">{{ $t('settings.reply_visibility_following') }}</option>
|
|
||||||
<option value="self">{{ $t('settings.reply_visibility_self') }}</option>
|
|
||||||
</select>
|
|
||||||
<i class="icon-down-open" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Checkbox v-model="hidePostStats">
|
|
||||||
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Checkbox v-model="hideUserStats">
|
|
||||||
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.filtering_explanation') }}</p>
|
|
||||||
<textarea
|
|
||||||
id="muteWords"
|
|
||||||
v-model="muteWordsString"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Checkbox v-model="hideFilteredStatuses">
|
|
||||||
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :label="$t('settings.version.title')">
|
|
||||||
<div class="setting-item">
|
|
||||||
<ul class="setting-list">
|
|
||||||
<li>
|
|
||||||
<p>{{ $t('settings.version.backend_version') }}</p>
|
|
||||||
<ul class="option-list">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
:href="backendVersionLink"
|
|
||||||
target="_blank"
|
|
||||||
>{{ backendVersion }}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p>{{ $t('settings.version.frontend_version') }}</p>
|
|
||||||
<ul class="option-list">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
:href="frontendVersionLink"
|
|
||||||
target="_blank"
|
|
||||||
>{{ frontendVersion }}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</tab-switcher>
|
|
||||||
</keep-alive>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./settings.js">
|
|
||||||
</script>
|
|
@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
instanceDefaultProperties,
|
||||||
|
multiChoiceProperties,
|
||||||
|
defaultState as configDefaultState
|
||||||
|
} from 'src/modules/config.js'
|
||||||
|
|
||||||
|
const SharedComputedObject = () => ({
|
||||||
|
user () {
|
||||||
|
return this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
// 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 or perform actions first)
|
||||||
|
useStreamingApi: {
|
||||||
|
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
||||||
|
set (value) {
|
||||||
|
const promise = value
|
||||||
|
? this.$store.dispatch('enableMastoSockets')
|
||||||
|
: this.$store.dispatch('disableMastoSockets')
|
||||||
|
|
||||||
|
promise.then(() => {
|
||||||
|
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error('Failed starting MastoAPI Streaming socket', e)
|
||||||
|
this.$store.dispatch('disableMastoSockets')
|
||||||
|
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default SharedComputedObject
|
@ -0,0 +1,42 @@
|
|||||||
|
import Modal from 'src/components/modal/modal.vue'
|
||||||
|
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||||
|
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||||
|
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||||
|
|
||||||
|
const SettingsModal = {
|
||||||
|
components: {
|
||||||
|
Modal,
|
||||||
|
SettingsModalContent: getResettableAsyncComponent(
|
||||||
|
() => import('./settings_modal_content.vue'),
|
||||||
|
{
|
||||||
|
loading: PanelLoading,
|
||||||
|
error: AsyncComponentError,
|
||||||
|
delay: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeModal () {
|
||||||
|
this.$store.dispatch('closeSettingsModal')
|
||||||
|
},
|
||||||
|
peekModal () {
|
||||||
|
this.$store.dispatch('togglePeekSettingsModal')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentSaveStateNotice () {
|
||||||
|
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||||
|
},
|
||||||
|
modalActivated () {
|
||||||
|
return this.$store.state.interface.settingsModalState !== 'hidden'
|
||||||
|
},
|
||||||
|
modalOpenedOnce () {
|
||||||
|
return this.$store.state.interface.settingsModalLoaded
|
||||||
|
},
|
||||||
|
modalPeeked () {
|
||||||
|
return this.$store.state.interface.settingsModalState === 'minimized'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsModal
|
@ -0,0 +1,44 @@
|
|||||||
|
@import 'src/_variables.scss';
|
||||||
|
.settings-modal {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.peek {
|
||||||
|
.settings-modal-panel {
|
||||||
|
/* Explanation:
|
||||||
|
* Modal is positioned vertically centered.
|
||||||
|
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
|
||||||
|
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
|
||||||
|
* + 100% - we move modal completely off-screen, it's top boundary touches
|
||||||
|
* bottom of the screen
|
||||||
|
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
|
||||||
|
*/
|
||||||
|
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-modal-panel {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
transition-duration: 300ms;
|
||||||
|
width: 1000px;
|
||||||
|
max-width: 90vw;
|
||||||
|
height: 90vh;
|
||||||
|
|
||||||
|
@media all and (max-width: 800px) {
|
||||||
|
max-width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
min-height: 28px;
|
||||||
|
min-width: 10em;
|
||||||
|
padding: 0 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:is-open="modalActivated"
|
||||||
|
class="settings-modal"
|
||||||
|
:class="{ peek: modalPeeked }"
|
||||||
|
:no-background="modalPeeked"
|
||||||
|
>
|
||||||
|
<div class="settings-modal-panel panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<span class="title">
|
||||||
|
{{ $t('settings.settings') }}
|
||||||
|
</span>
|
||||||
|
<transition name="fade">
|
||||||
|
<template v-if="currentSaveStateNotice">
|
||||||
|
<div
|
||||||
|
v-if="currentSaveStateNotice.error"
|
||||||
|
class="alert error"
|
||||||
|
@click.prevent
|
||||||
|
>
|
||||||
|
{{ $t('settings.saving_err') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!currentSaveStateNotice.error"
|
||||||
|
class="alert transparent"
|
||||||
|
@click.prevent
|
||||||
|
>
|
||||||
|
{{ $t('settings.saving_ok') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</transition>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
@click="peekModal"
|
||||||
|
>
|
||||||
|
{{ $t('general.peek') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
@click="closeModal"
|
||||||
|
>
|
||||||
|
{{ $t('general.close') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<SettingsModalContent v-if="modalOpenedOnce" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./settings_modal.js"></script>
|
||||||
|
|
||||||
|
<style src="./settings_modal.scss" lang="scss"></style>
|
@ -0,0 +1,34 @@
|
|||||||
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||||
|
|
||||||
|
import DataImportExportTab from './tabs/data_import_export_tab.vue'
|
||||||
|
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
|
||||||
|
import NotificationsTab from './tabs/notifications_tab.vue'
|
||||||
|
import FilteringTab from './tabs/filtering_tab.vue'
|
||||||
|
import SecurityTab from './tabs/security_tab/security_tab.vue'
|
||||||
|
import ProfileTab from './tabs/profile_tab.vue'
|
||||||
|
import GeneralTab from './tabs/general_tab.vue'
|
||||||
|
import VersionTab from './tabs/version_tab.vue'
|
||||||
|
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
|
||||||
|
|
||||||
|
const SettingsModalContent = {
|
||||||
|
components: {
|
||||||
|
TabSwitcher,
|
||||||
|
|
||||||
|
DataImportExportTab,
|
||||||
|
MutesAndBlocksTab,
|
||||||
|
NotificationsTab,
|
||||||
|
FilteringTab,
|
||||||
|
SecurityTab,
|
||||||
|
ProfileTab,
|
||||||
|
GeneralTab,
|
||||||
|
VersionTab,
|
||||||
|
ThemeTab
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoggedIn () {
|
||||||
|
return !!this.$store.state.users.currentUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsModalContent
|
@ -0,0 +1,43 @@
|
|||||||
|
@import 'src/_variables.scss';
|
||||||
|
.settings_tab-switcher {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||||
|
margin: 1em 1em 1.4em;
|
||||||
|
padding-bottom: 1.4em;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-bottom: .5em;
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
min-width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailable,
|
||||||
|
.unavailable i {
|
||||||
|
color: var(--cRed, $fallback--cRed);
|
||||||
|
color: $fallback--cRed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-input {
|
||||||
|
max-width: 6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<tab-switcher
|
||||||
|
ref="tabSwitcher"
|
||||||
|
class="settings_tab-switcher"
|
||||||
|
:side-tab-bar="true"
|
||||||
|
:scrollable-tabs="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:label="$t('settings.general')"
|
||||||
|
icon="wrench"
|
||||||
|
>
|
||||||
|
<GeneralTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLoggedIn"
|
||||||
|
:label="$t('settings.profile_tab')"
|
||||||
|
icon="user"
|
||||||
|
>
|
||||||
|
<ProfileTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLoggedIn"
|
||||||
|
:label="$t('settings.security_tab')"
|
||||||
|
icon="lock"
|
||||||
|
>
|
||||||
|
<SecurityTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:label="$t('settings.filtering')"
|
||||||
|
icon="filter"
|
||||||
|
>
|
||||||
|
<FilteringTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:label="$t('settings.theme')"
|
||||||
|
icon="brush"
|
||||||
|
>
|
||||||
|
<ThemeTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLoggedIn"
|
||||||
|
:label="$t('settings.notifications')"
|
||||||
|
icon="bell-ringing-o"
|
||||||
|
>
|
||||||
|
<NotificationsTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLoggedIn"
|
||||||
|
:label="$t('settings.data_import_export_tab')"
|
||||||
|
icon="download"
|
||||||
|
>
|
||||||
|
<DataImportExportTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLoggedIn"
|
||||||
|
:label="$t('settings.mutes_and_blocks')"
|
||||||
|
:fullHeight="true"
|
||||||
|
icon="eye-off"
|
||||||
|
>
|
||||||
|
<MutesAndBlocksTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:label="$t('settings.version.title')"
|
||||||
|
icon="info-circled"
|
||||||
|
>
|
||||||
|
<VersionTab />
|
||||||
|
</div>
|
||||||
|
</tab-switcher>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./settings_modal_content.js"></script>
|
||||||
|
|
||||||
|
<style src="./settings_modal_content.scss" lang="scss"></style>
|
@ -0,0 +1,65 @@
|
|||||||
|
import Importer from 'src/components/importer/importer.vue'
|
||||||
|
import Exporter from 'src/components/exporter/exporter.vue'
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
const DataImportExportTab = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
activeTab: 'profile',
|
||||||
|
newDomainToMute: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$store.dispatch('fetchTokens')
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Importer,
|
||||||
|
Exporter,
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user () {
|
||||||
|
return this.$store.state.users.currentUser
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFollowsContent () {
|
||||||
|
return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
|
||||||
|
.then(this.generateExportableUsersContent)
|
||||||
|
},
|
||||||
|
getBlocksContent () {
|
||||||
|
return this.$store.state.api.backendInteractor.fetchBlocks()
|
||||||
|
.then(this.generateExportableUsersContent)
|
||||||
|
},
|
||||||
|
importFollows (file) {
|
||||||
|
return this.$store.state.api.backendInteractor.importFollows({ file })
|
||||||
|
.then((status) => {
|
||||||
|
if (!status) {
|
||||||
|
throw new Error('failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
importBlocks (file) {
|
||||||
|
return this.$store.state.api.backendInteractor.importBlocks({ file })
|
||||||
|
.then((status) => {
|
||||||
|
if (!status) {
|
||||||
|
throw new Error('failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
generateExportableUsersContent (users) {
|
||||||
|
// Get addresses
|
||||||
|
return users.map((user) => {
|
||||||
|
// check is it's a local user
|
||||||
|
if (user && user.is_local) {
|
||||||
|
// append the instance address
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
return user.screen_name + '@' + location.hostname
|
||||||
|
}
|
||||||
|
return user.screen_name
|
||||||
|
}).join('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DataImportExportTab
|
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:label="$t('settings.data_import_export_tab')"
|
||||||
|
>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.follow_import') }}</h2>
|
||||||
|
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
|
||||||
|
<Importer
|
||||||
|
:submit-handler="importFollows"
|
||||||
|
:success-message="$t('settings.follows_imported')"
|
||||||
|
:error-message="$t('settings.follow_import_error')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.follow_export') }}</h2>
|
||||||
|
<Exporter
|
||||||
|
:get-content="getFollowsContent"
|
||||||
|
filename="friends.csv"
|
||||||
|
:export-button-label="$t('settings.follow_export_button')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.block_import') }}</h2>
|
||||||
|
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
|
||||||
|
<Importer
|
||||||
|
:submit-handler="importBlocks"
|
||||||
|
:success-message="$t('settings.blocks_imported')"
|
||||||
|
:error-message="$t('settings.block_import_error')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.block_export') }}</h2>
|
||||||
|
<Exporter
|
||||||
|
:get-content="getBlocksContent"
|
||||||
|
filename="blocks.csv"
|
||||||
|
:export-button-label="$t('settings.block_export_button')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./data_import_export_tab.js"></script>
|
||||||
|
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
@ -0,0 +1,44 @@
|
|||||||
|
import { filter, trim } from 'lodash'
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
|
||||||
|
const FilteringTab = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...SharedComputedObject(),
|
||||||
|
muteWordsString: {
|
||||||
|
get () {
|
||||||
|
return this.muteWordsStringLocal
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
this.muteWordsStringLocal = value
|
||||||
|
this.$store.dispatch('setOption', {
|
||||||
|
name: 'muteWords',
|
||||||
|
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Updating nested properties
|
||||||
|
watch: {
|
||||||
|
notificationVisibility: {
|
||||||
|
handler (value) {
|
||||||
|
this.$store.dispatch('setOption', {
|
||||||
|
name: 'notificationVisibility',
|
||||||
|
value: this.$store.getters.mergedConfig.notificationVisibility
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilteringTab
|
@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div :label="$t('settings.filtering')">
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="select-multiple">
|
||||||
|
<span class="label">{{ $t('settings.notification_visibility') }}</span>
|
||||||
|
<ul class="option-list">
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationVisibility.likes">
|
||||||
|
{{ $t('settings.notification_visibility_likes') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationVisibility.repeats">
|
||||||
|
{{ $t('settings.notification_visibility_repeats') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationVisibility.follows">
|
||||||
|
{{ $t('settings.notification_visibility_follows') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationVisibility.mentions">
|
||||||
|
{{ $t('settings.notification_visibility_mentions') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationVisibility.moves">
|
||||||
|
{{ $t('settings.notification_visibility_moves') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationVisibility.emojiReactions">
|
||||||
|
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ $t('settings.replies_in_timeline') }}
|
||||||
|
<label
|
||||||
|
for="replyVisibility"
|
||||||
|
class="select"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="replyVisibility"
|
||||||
|
v-model="replyVisibility"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="all"
|
||||||
|
selected
|
||||||
|
>{{ $t('settings.reply_visibility_all') }}</option>
|
||||||
|
<option value="following">{{ $t('settings.reply_visibility_following') }}</option>
|
||||||
|
<option value="self">{{ $t('settings.reply_visibility_self') }}</option>
|
||||||
|
</select>
|
||||||
|
<i class="icon-down-open" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Checkbox v-model="hidePostStats">
|
||||||
|
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Checkbox v-model="hideUserStats">
|
||||||
|
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div>
|
||||||
|
<p>{{ $t('settings.filtering_explanation') }}</p>
|
||||||
|
<textarea
|
||||||
|
id="muteWords"
|
||||||
|
v-model="muteWordsString"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Checkbox v-model="hideFilteredStatuses">
|
||||||
|
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script src="./filtering_tab.js"></script>
|
@ -0,0 +1,31 @@
|
|||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||||
|
|
||||||
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
|
||||||
|
const GeneralTab = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loopSilentAvailable:
|
||||||
|
// Firefox
|
||||||
|
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||||
|
// Chrome-likes
|
||||||
|
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
|
||||||
|
// Future spec, still not supported in Nightly 63 as of 08/2018
|
||||||
|
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
InterfaceLanguageSwitcher
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
postFormats () {
|
||||||
|
return this.$store.state.instance.postFormats || []
|
||||||
|
},
|
||||||
|
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||||
|
...SharedComputedObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeneralTab
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue