Merge remote-tracking branch 'upstream/develop' into shigusegubu

* upstream/develop: (29 commits)
  add hideISP to defaultState of config module
  add changelog entry
  mrf transparency panel: refactor to use vuex mapState
  mrf transparency panel: remove unneeded components{}
  boot: cleanup resolveStaffAccounts
  lint
  about: add MRF transparency panel
  about: add staff panel
  about page: fix hiding of instance-specific panel, flow ToS and ISP better
  nav panel: add link to about page
  redirect /remote-users/:username@:hostname -> /users/:id, /remote-users/:hostname/:username -> /users/:id
  clear filter on reopen, fix error message in console
  reset position when reopening emoji picker
  eslint fix
  fix not being able to see unicode emojis when there few of them when searching on emoji-a-ton instances
  replace sanity button with loading on scroll
  fix search not working, use computer property instead of state
  fix eslint warnings
  Lightbox/modal multi image improvements - #381
  '/api/pleroma/profile/mfa' -> '/api/pleroma/accounts/mfa'
  ...
This commit is contained in:
Henry Jameson 2019-11-11 00:26:13 +02:00
commit 45a1d30bd6
31 changed files with 590 additions and 105 deletions

View file

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

View file

@ -184,6 +184,15 @@ const getAppSecret = async ({ store }) => {
})
}
const resolveStaffAccounts = async ({ store, accounts }) => {
const backendInteractor = store.state.api.backendInteractor
let nicknames = accounts.map(uri => uri.split('/').pop())
.map(id => backendInteractor.fetchUser({ id }))
nicknames = await Promise.all(nicknames)
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
}
const getNodeInfo = async ({ store }) => {
try {
const res = await window.fetch('/nodeinfo/2.0.json')
@ -212,6 +221,12 @@ const getNodeInfo = async ({ store }) => {
const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
const federation = metadata.federation
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
const accounts = metadata.staffAccounts
await resolveStaffAccounts({ store, accounts })
} else {
throw (res)
}

View file

@ -18,6 +18,7 @@ import AuthForm from 'components/auth_form/auth_form.js'
import ChatPanel from 'components/chat_panel/chat_panel.vue'
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
import About from 'components/about/about.vue'
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
export default (store) => {
const validateAuthenticatedRoute = (to, from, next) => {
@ -42,6 +43,16 @@ export default (store) => {
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'remote-user-profile-acct',
path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'remote-user-profile',
path: '/remote-users/:hostname/:username',
component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },

View file

@ -1,15 +1,24 @@
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from '../features_panel/features_panel.vue'
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
import StaffPanel from '../staff_panel/staff_panel.vue'
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
const About = {
components: {
InstanceSpecificPanel,
FeaturesPanel,
TermsOfServicePanel
TermsOfServicePanel,
StaffPanel,
MRFTransparencyPanel
},
computed: {
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent
}
}
}

View file

@ -1,8 +1,10 @@
<template>
<div class="sidebar">
<instance-specific-panel />
<features-panel v-if="showFeaturesPanel" />
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<staff-panel />
<terms-of-service-panel />
<MRFTransparencyPanel />
<features-panel v-if="showFeaturesPanel" />
</div>
</template>

View file

@ -2,7 +2,7 @@
<label
class="checkbox"
:class="{ disabled, indeterminate }"
>
>
<input
type="checkbox"
:disabled="disabled"
@ -12,9 +12,9 @@
>
<i class="checkbox-indicator" />
<span
class="label"
v-if="!!$slots.default"
>
class="label"
>
<slot />
</span>
</label>

View file

@ -1,9 +1,12 @@
import Checkbox from '../checkbox/checkbox.vue'
import { set } from 'vue'
const LOAD_EMOJI_BY = 50
const LOAD_EMOJI_INTERVAL = 100
const LOAD_EMOJI_SANE_AMOUNT = 500
// At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker
const LOAD_EMOJI_BY = 60
// When to start loading new batch emoji, in pixels
const LOAD_EMOJI_MARGIN = 64
const filterByKeyword = (list, keyword = '') => {
return list.filter(x => x.displayText.includes(keyword))
@ -24,10 +27,8 @@ const EmojiPicker = {
showingStickers: false,
groupsScrolledClass: 'scrolled-top',
keepOpen: false,
customEmojiBuffer: (this.$store.state.instance.customEmoji || [])
.slice(0, LOAD_EMOJI_BY),
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
customEmojiCounter: LOAD_EMOJI_BY,
customEmojiLoadAllConfirmed: false
}
},
@ -36,10 +37,22 @@ const EmojiPicker = {
Checkbox
},
methods: {
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
this.$emit('sticker-upload-failed', e)
},
onEmoji (emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
},
onScroll (e) {
const target = (e && e.target) || this.$refs['emoji-groups']
this.updateScrolledClass(target)
this.scrolledGroup(target)
this.triggerLoadMore(target)
},
highlight (key) {
const ref = this.$refs['group-' + key]
const top = ref[0].offsetTop
@ -49,9 +62,7 @@ const EmojiPicker = {
this.$refs['emoji-groups'].scrollTop = top + 1
})
},
scrolledGroup (e) {
const target = (e && e.target) || this.$refs['emoji-groups']
const top = target.scrollTop + 5
updateScrolledClass (target) {
if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) {
@ -59,6 +70,28 @@ const EmojiPicker = {
} else {
this.groupsScrolledClass = 'scrolled-middle'
}
},
triggerLoadMore (target) {
const ref = this.$refs['group-end-custom'][0]
if (!ref) return
const bottom = ref.offsetTop + ref.offsetHeight
const scrollerBottom = target.scrollTop + target.clientHeight
const scrollerTop = target.scrollTop
const scrollerMax = target.scrollHeight
// Loads more emoji when they come into view
const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
// Always load when at the very top in case there's no scroll space yet
const atTop = scrollerTop < 5
// Don't load when looking at unicode category or at the very bottom
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
if (!bottomAboveViewport && (approachingBottom || atTop)) {
this.loadEmoji()
}
},
scrolledGroup (target) {
const top = target.scrollTop + 5
this.$nextTick(() => {
this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id]
@ -68,67 +101,40 @@ const EmojiPicker = {
})
})
},
loadEmojiInsane () {
this.customEmojiLoadAllConfirmed = true
this.continueEmojiLoad()
},
loadEmoji () {
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
const saneLoaded = this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
!this.customEmojiLoadAllConfirmed
if (allLoaded || saneLoaded) {
if (allLoaded) {
return
}
this.customEmojiBuffer.push(
...this.filteredEmoji.slice(
this.customEmojiCounter,
this.customEmojiCounter + LOAD_EMOJI_BY
)
)
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
this.customEmojiCounter += LOAD_EMOJI_BY
this.customEmojiBufferSlice += LOAD_EMOJI_BY
},
startEmojiLoad (forceUpdate = false) {
if (!forceUpdate) {
this.keyword = ''
}
this.$nextTick(() => {
this.$refs['emoji-groups'].scrollTop = 0
})
const bufferSize = this.customEmojiBuffer.length
const bufferPrefilledSane = bufferSize === LOAD_EMOJI_SANE_AMOUNT && !this.customEmojiLoadAllConfirmed
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
if (forceUpdate || bufferPrefilledSane || bufferPrefilledAll) {
if (bufferPrefilledAll && !forceUpdate) {
return
}
if (this.customEmojiTimeout) {
window.clearTimeout(this.customEmojiTimeout)
}
set(
this,
'customEmojiBuffer',
this.filteredEmoji.slice(0, LOAD_EMOJI_BY)
)
this.customEmojiCounter = LOAD_EMOJI_BY
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
},
continueEmojiLoad () {
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
this.customEmojiBufferSlice = LOAD_EMOJI_BY
},
toggleStickers () {
this.showingStickers = !this.showingStickers
},
setShowStickers (value) {
this.showingStickers = value
},
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
this.$emit('sticker-upload-failed', e)
}
},
watch: {
keyword () {
this.customEmojiLoadAllConfirmed = false
this.scrolledGroup()
this.onScroll()
this.startEmojiLoad(true)
}
},
@ -142,19 +148,14 @@ const EmojiPicker = {
}
return 0
},
saneAmount () {
// for UI
return LOAD_EMOJI_SANE_AMOUNT
},
filteredEmoji () {
return filterByKeyword(
this.$store.state.instance.customEmoji || [],
this.keyword
)
},
askForSanity () {
return this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
!this.customEmojiLoadAllConfirmed
customEmojiBuffer () {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
},
emojis () {
const standardEmojis = this.$store.state.instance.emoji || []

View file

@ -47,7 +47,7 @@
ref="emoji-groups"
class="emoji-groups"
:class="groupsScrolledClass"
@scroll="scrolledGroup"
@scroll="onScroll"
>
<div
v-for="group in emojisView"
@ -73,6 +73,7 @@
:src="emoji.imageUrl"
>
</span>
<span :ref="'group-end-' + group.id" />
</div>
</div>
<div class="keep-open">
@ -80,20 +81,6 @@
{{ $t('emoji.keep_open') }}
</Checkbox>
</div>
<div
v-if="askForSanity"
class="too-many-emoji"
>
<div class="alert warning hint">
{{ $t('emoji.load_all_hint', { saneAmount } ) }}
</div>
<button
class="btn btn-default"
@click.prevent="loadEmojiInsane"
>
{{ $t('emoji.load_all', { emojiAmount: filteredEmoji.length } ) }}
</button>
</div>
</div>
<div
v-if="showingStickers"

View file

@ -2,6 +2,7 @@ import StillImage from '../still-image/still-image.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 GestureService from '../../services/gesture_service/gesture_service'
const MediaModal = {
components: {
@ -29,7 +30,27 @@ const MediaModal = {
return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
}
},
created () {
this.mediaSwipeGestureRight = GestureService.swipeGesture(
GestureService.DIRECTION_RIGHT,
this.goPrev,
50
)
this.mediaSwipeGestureLeft = GestureService.swipeGesture(
GestureService.DIRECTION_LEFT,
this.goNext,
50
)
},
methods: {
mediaTouchStart (e) {
GestureService.beginSwipe(e, this.mediaSwipeGestureRight)
GestureService.beginSwipe(e, this.mediaSwipeGestureLeft)
},
mediaTouchMove (e) {
GestureService.updateSwipe(e, this.mediaSwipeGestureRight)
GestureService.updateSwipe(e, this.mediaSwipeGestureLeft)
},
hide () {
this.$store.dispatch('closeMediaViewer')
},

View file

@ -8,6 +8,8 @@
v-if="type === 'image'"
class="modal-image"
:src="currentMedia.url"
@touchstart.stop="mediaTouchStart"
@touchmove.stop="mediaTouchMove"
>
<VideoAttachment
v-if="type === 'video'"
@ -41,18 +43,16 @@
.modal-view.media-modal-view {
z-index: 1001;
&:hover {
.modal-view-button-arrow {
opacity: 0.75;
.modal-view-button-arrow {
opacity: 0.75;
&:focus,
&:hover {
outline: none;
box-shadow: none;
}
&:hover {
opacity: 1;
}
&:focus,
&:hover {
outline: none;
box-shadow: none;
}
&:hover {
opacity: 1;
}
}
}

View file

@ -0,0 +1,16 @@
import { mapState } from 'vuex'
const MRFTransparencyPanel = {
computed: mapState({
federationPolicy: state => state.instance.federationPolicy,
mrfPolicies: state => state.instance.federationPolicy.mrf_policies,
acceptInstances: state => state.instance.federationPolicy.mrf_simple.accept,
rejectInstances: state => state.instance.federationPolicy.mrf_simple.reject,
quarantineInstances: state => state.instance.federationPolicy.quarantined_instances,
ftlRemovalInstances: state => state.instance.federationPolicy.mrf_simple.federated_timeline_removal,
mediaNsfwInstances: state => state.instance.federationPolicy.mrf_simple.media_nsfw,
mediaRemovalInstances: state => state.instance.federationPolicy.mrf_simple.media_removal
})
}
export default MRFTransparencyPanel

View file

@ -0,0 +1,122 @@
<template>
<div
v-if="federationPolicy"
class="mrf-transparency-panel"
>
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background">
<div class="title">
{{ $t("about.federation") }}
</div>
</div>
<div class="panel-body">
<div class="mrf-section">
<h2>{{ $t("about.mrf_policies") }}</h2>
<p>{{ $t("about.mrf_policies_desc") }}</p>
<ul>
<li
v-for="policy in mrfPolicies"
:key="policy"
v-text="policy"
/>
</ul>
<h2>{{ $t("about.mrf_policy_simple") }}</h2>
<div v-if="acceptInstances.length">
<h4>{{ $t("about.mrf_policy_simple_accept") }}</h4>
<p>{{ $t("about.mrf_policy_simple_accept_desc") }}</p>
<ul>
<li
v-for="instance in acceptInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
<div v-if="rejectInstances.length">
<h4>{{ $t("about.mrf_policy_simple_reject") }}</h4>
<p>{{ $t("about.mrf_policy_simple_reject_desc") }}</p>
<ul>
<li
v-for="instance in rejectInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
<div v-if="quarantineInstances.length">
<h4>{{ $t("about.mrf_policy_simple_quarantine") }}</h4>
<p>{{ $t("about.mrf_policy_simple_quarantine_desc") }}</p>
<ul>
<li
v-for="instance in quarantineInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
<div v-if="ftlRemovalInstances.length">
<h4>{{ $t("about.mrf_policy_simple_ftl_removal") }}</h4>
<p>{{ $t("about.mrf_policy_simple_ftl_removal_desc") }}</p>
<ul>
<li
v-for="instance in ftlRemovalInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
<div v-if="mediaNsfwInstances.length">
<h4>{{ $t("about.mrf_policy_simple_media_nsfw") }}</h4>
<p>{{ $t("about.mrf_policy_simple_media_nsfw_desc") }}</p>
<ul>
<li
v-for="instance in mediaNsfwInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
<div v-if="mediaRemovalInstances.length">
<h4>{{ $t("about.mrf_policy_simple_media_removal") }}</h4>
<p>{{ $t("about.mrf_policy_simple_media_removal_desc") }}</p>
<ul>
<li
v-for="instance in mediaRemovalInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script src="./mrf_transparency_panel.js"></script>
<style lang="scss">
.mrf-section {
margin: 1em;
}
</style>

View file

@ -38,6 +38,11 @@
{{ $t("nav.twkn") }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'about' }">
{{ $t("nav.about") }}
</router-link>
</li>
</ul>
</div>
</div>

View file

@ -163,7 +163,7 @@
<div
ref="bottom"
class="form-bottom"
>
>
<div class="form-bottom-left">
<media-upload
ref="mediaUpload"

View file

@ -0,0 +1,31 @@
const RemoteUserResolver = {
data: () => ({
error: false
}),
mounted () {
this.redirect()
},
methods: {
redirect () {
const acct = this.$route.params.username + '@' + this.$route.params.hostname
this.$store.state.api.backendInteractor.fetchUser({ id: acct })
.then((externalUser) => {
if (externalUser.error) {
this.error = true
} else {
this.$store.commit('addNewUsers', [externalUser])
const id = externalUser.id
this.$router.replace({
name: 'external-user-profile',
params: { id }
})
}
})
.catch(() => {
this.error = true
})
}
}
}
export default RemoteUserResolver

View file

@ -0,0 +1,20 @@
<template>
<div class="panel panel-default">
<div class="panel-heading">
{{ $t('remote_user_resolver.remote_user_resolver') }}
</div>
<div class="panel-body">
<p>
{{ $t('remote_user_resolver.searching_for') }} @{{ $route.params.username }}@{{ $route.params.hostname }}
</p>
<p v-if="error">
{{ $t('remote_user_resolver.error') }}
</p>
</div>
</div>
</template>
<script src="./remote_user_resolver.js"></script>
<style lang="scss">
</style>

View file

@ -0,0 +1,14 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const StaffPanel = {
components: {
BasicUserCard
},
computed: {
staffAccounts () {
return this.$store.state.instance.staffAccounts
}
}
}
export default StaffPanel

View file

@ -0,0 +1,23 @@
<template>
<div class="staff-panel">
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background">
<div class="title">
{{ $t("about.staff") }}
</div>
</div>
<div class="panel-body">
<basic-user-card
v-for="user in staffAccounts"
:key="user.screen_name"
:user="user"
/>
</div>
</div>
</div>
</template>
<script src="./staff_panel.js" ></script>
<style lang="scss">
</style>

View file

@ -12,11 +12,13 @@ export default Vue.component('tab-switcher', {
},
onSwitch: {
required: false,
type: Function
type: Function,
default: undefined
},
activeTab: {
required: false,
type: String
type: String,
default: undefined
},
scrollableTabs: {
required: false,

View file

@ -276,6 +276,8 @@
mask-composite: exclude;
background-size: cover;
mask-size: 100% 60%;
border-top-left-radius: calc(var(--panelRadius) - 1px);
border-top-right-radius: calc(var(--panelRadius) - 1px);
&.hide-bio {
mask-size: 100% 40px;

View file

@ -35,6 +35,7 @@ const MuteList = withSubscription({
const UserSettings = {
data () {
return {
newEmail: '',
newName: this.$store.state.users.currentUser.name,
newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked,
@ -56,6 +57,9 @@ const UserSettings = {
backgroundPreview: null,
bannerUploadError: null,
backgroundUploadError: null,
changeEmailError: false,
changeEmailPassword: '',
changedEmail: false,
deletingAccount: false,
deleteAccountConfirmPasswordInput: '',
deleteAccountError: false,
@ -305,6 +309,22 @@ const UserSettings = {
}
})
},
changeEmail () {
const params = {
email: this.newEmail,
password: this.changeEmailPassword
}
this.$store.state.api.backendInteractor.changeEmail(params)
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
this.changeEmailError = false
} else {
this.changedEmail = false
this.changeEmailError = res.error
}
})
},
activateTab (tabName) {
this.activeTab = tabName
},

View file

@ -85,14 +85,14 @@
<Checkbox
v-model="hideFollowsCount"
:disabled="!hideFollows"
>
>
{{ $t('settings.hide_follows_count_description') }}
</Checkbox>
</p>
<p>
<Checkbox
v-model="hideFollowers"
>
>
{{ $t('settings.hide_followers_description') }}
</Checkbox>
</p>
@ -233,6 +233,39 @@
</div>
<div :label="$t('settings.security_tab')">
<div class="setting-item">
<h2>{{ $t('settings.change_email') }}</h2>
<div>
<p>{{ $t('settings.new_email') }}</p>
<input
v-model="newEmail"
type="email"
autocomplete="email"
>
</div>
<div>
<p>{{ $t('settings.current_password') }}</p>
<input
v-model="changeEmailPassword"
type="password"
autocomplete="current-password"
>
</div>
<button
class="btn btn-default"
@click="changeEmail"
>
{{ $t('general.submit') }}
</button>
<p v-if="changedEmail">
{{ $t('settings.changed_email') }}
</p>
<template v-if="changeEmailError !== false">
<p>{{ $t('settings.change_email_error') }}</p>
<p>{{ changeEmailError }}</p>
</template>
</div>
<div class="setting-item">
<h2>{{ $t('settings.change_password') }}</h2>
<div>

View file

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

View file

@ -1,4 +1,23 @@
{
"about": {
"staff": "Staff",
"federation": "Federation",
"mrf_policies": "Enabled MRF Policies",
"mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
"mrf_policy_simple": "Instance-specific Policies",
"mrf_policy_simple_accept": "Accept",
"mrf_policy_simple_accept_desc": "This instance only accepts messages from the following instances:",
"mrf_policy_simple_reject": "Reject",
"mrf_policy_simple_reject_desc": "This instance will not accept messages from the following instances:",
"mrf_policy_simple_quarantine": "Quarantine",
"mrf_policy_simple_quarantine_desc": "This instance will send only public posts to the following instances:",
"mrf_policy_simple_ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
"mrf_policy_simple_ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:",
"mrf_policy_simple_media_removal": "Media Removal",
"mrf_policy_simple_media_removal_desc": "This instance removes media from posts on the following instances:",
"mrf_policy_simple_media_nsfw": "Media Force-set As Sensitive",
"mrf_policy_simple_media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:"
},
"chat": {
"title": "Chat"
},
@ -172,6 +191,11 @@
"password_confirmation_match": "should be the same as password"
}
},
"remote_user_resolver": {
"remote_user_resolver": "Remote user resolver",
"searching_for": "Searching for",
"error": "Not found."
},
"selectable_list": {
"select_all": "Select all"
},
@ -219,6 +243,9 @@
"cGreen": "Green (Retweet)",
"cOrange": "Orange (Favorite)",
"cRed": "Red (Cancel)",
"change_email": "Change Email",
"change_email_error": "There was an issue changing your email.",
"changed_email": "Email changed successfully!",
"change_password": "Change Password",
"change_password_error": "There was an issue changing your password.",
"changed_password": "Password changed successfully!",
@ -277,6 +304,7 @@
"use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name",
"name_bio": "Name & Bio",
"new_email": "New Email",
"new_password": "New password",
"notification_visibility": "Types of notifications to show",
"notification_visibility_follows": "Follows",

View file

@ -558,6 +558,8 @@
"unmute": "Isiltasuna kendu",
"unmute_progress": "Isiltasuna kentzen...",
"mute_progress": "Isiltzen...",
"hide_repeats": "Ezkutatu errepikapenak",
"show_repeats": "Erakutsi errpekiapenak",
"admin_menu": {
"moderation": "Moderazioa",
"grant_admin": "Administratzaile baimena",
@ -633,6 +635,8 @@
"return_home": "Itzuli hasierara",
"not_found": "Ezin izan dugu helbide elektroniko edo erabiltzaile hori aurkitu.",
"too_many_requests": "Saiakera gehiegi burutu ditzu, saiatu berriro geroxeago.",
"password_reset_disabled": "Pasahitza berrezartzea debekatuta dago. Mesedez, jarri harremanetan instantzia administratzailearekin."
"password_reset_disabled": "Pasahitza berrezartzea debekatuta dago. Mesedez, jarri harremanetan instantzia administratzailearekin.",
"password_reset_required": "Pasahitza berrezarri behar duzu saioa hasteko.",
"password_reset_required_but_mailer_is_disabled": "Pasahitza berrezarri behar duzu, baina pasahitza berrezartzeko aukera desgaituta dago. Mesedez, jarri harremanetan instantziaren administratzailearekin."
}
}

View file

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

View file

@ -5,6 +5,7 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
export const defaultState = {
colors: {},
hideISP: false,
// bad name: actually hides posts of muted USERS
hideMutedPosts: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default

View file

@ -8,6 +8,7 @@ const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
@ -16,12 +17,12 @@ const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const MFA_SETTINGS_URL = '/api/pleroma/profile/mfa'
const MFA_BACKUP_CODES_URL = '/api/pleroma/profile/mfa/backup_codes'
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
const MFA_SETUP_OTP_URL = '/api/pleroma/profile/mfa/setup/totp'
const MFA_CONFIRM_OTP_URL = '/api/pleroma/profile/mfa/confirm/totp'
const MFA_DISABLE_OTP_URL = '/api/pleroma/profile/mfa/totp'
const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp'
const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp'
const MFA_DISABLE_OTP_URL = '/api/pleroma/account/mfa/totp'
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
@ -691,6 +692,20 @@ const deleteAccount = ({ credentials, password }) => {
.then((response) => response.json())
}
const changeEmail = ({ credentials, email, password }) => {
const form = new FormData()
form.append('email', email)
form.append('password', password)
return fetch(CHANGE_EMAIL_URL, {
body: form,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.json())
}
const changePassword = ({ credentials, password, newPassword, newPasswordConfirmation }) => {
const form = new FormData()
@ -966,6 +981,7 @@ const apiService = {
importBlocks,
importFollows,
deleteAccount,
changeEmail,
changePassword,
settingsMFA,
mfaDisableOTP,

View file

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

View file

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

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

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