Merge remote-tracking branch 'upstream/develop' into webpack-4-dart-sass

* upstream/develop: (116 commits)
  Fix small mistake in Polish translation
  link interaction avatars to the user profile
  Use more clear explanation in the scope notice, make sure the hide button doesn't overlap with text in notice.
  use backendInteractor
  refactor api service functions using new helper
  clean up
  update favorite number earlier
  update status interaction upon retweet action response
  sync up favoritedBy with favorite/unfavorite action
  do not regenerate status object
  reduce needless calculation
  Move scope visibility notice to the status form, make it dismissible
  Revert "eliminate expandable prop in favor of inConversation"
  status attention doesn’t have relationship entities
  make it short
  fix wrong inlineExpanded
  expanded is always false, eliminate it
  eliminate expandable prop in favor of inConversation
  fix conversationId comparision bug using integer format
  Display additional scope description above the status form for mobile users.
  ...
This commit is contained in:
Henry Jameson 2019-05-13 20:38:54 +03:00
commit 750dca4a10
50 changed files with 1195 additions and 372 deletions

View file

@ -10,6 +10,7 @@ import MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue' import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue' import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue' import MobileNav from './components/mobile_nav/mobile_nav.vue'
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
import { windowWidth } from './services/window_utils/window_utils' import { windowWidth } from './services/window_utils/window_utils'
export default { export default {
@ -26,7 +27,8 @@ export default {
MediaModal, MediaModal,
SideDrawer, SideDrawer,
MobilePostStatusModal, MobilePostStatusModal,
MobileNav MobileNav,
UserReportingModal
}, },
data: () => ({ data: () => ({
mobileActivePanel: 'timeline', mobileActivePanel: 'timeline',

View file

@ -379,6 +379,7 @@ main-router {
.panel-heading { .panel-heading {
display: flex; display: flex;
flex: none;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
background-size: cover; background-size: cover;
@ -647,6 +648,19 @@ nav {
border-radius: var(--inputRadius, $fallback--inputRadius); border-radius: var(--inputRadius, $fallback--inputRadius);
} }
.notice-dismissible {
padding-right: 4rem;
position: relative;
.dismiss {
position: absolute;
top: 0;
right: 0;
padding: .5em;
color: inherit;
}
}
@keyframes modal-background-fadein { @keyframes modal-background-fadein {
from { from {
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);

View file

@ -18,12 +18,13 @@
</div> </div>
</div> </div>
</nav> </nav>
<div v-if="" class="container" id="content"> <div class="container" id="content">
<div class="sidebar-flexer mobile-hidden" v-if="!isMobileLayout"> <div class="sidebar-flexer mobile-hidden">
<div class="sidebar-bounds"> <div class="sidebar-bounds">
<div class="sidebar-scroller"> <div class="sidebar-scroller">
<div class="sidebar"> <div class="sidebar">
<user-panel></user-panel> <user-panel></user-panel>
<div v-if="!isMobileLayout">
<nav-panel></nav-panel> <nav-panel></nav-panel>
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel> <instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
<features-panel v-if="!currentUser && showFeaturesPanel"></features-panel> <features-panel v-if="!currentUser && showFeaturesPanel"></features-panel>
@ -33,6 +34,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="main"> <div class="main">
<div v-if="!currentUser" class="login-hint panel panel-default"> <div v-if="!currentUser" class="login-hint panel panel-default">
<router-link :to="{ name: 'login' }" class="panel-body"> <router-link :to="{ name: 'login' }" class="panel-body">
@ -46,6 +48,7 @@
<media-modal></media-modal> <media-modal></media-modal>
</div> </div>
<chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel> <chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
<UserReportingModal />
</div> </div>
</template> </template>

View file

@ -0,0 +1,21 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const AvatarList = {
props: ['users'],
computed: {
slicedUsers () {
return this.users ? this.users.slice(0, 15) : []
}
},
components: {
UserAvatar
},
methods: {
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
}
}
}
export default AvatarList

View file

@ -0,0 +1,38 @@
<template>
<div class="avatars">
<router-link :to="userProfileLink(user)" class="avatars-item" v-for="user in slicedUsers">
<UserAvatar :user="user" class="avatar-small" />
</router-link>
</div>
</template>
<script src="./avatar_list.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.avatars {
display: flex;
margin: 0;
padding: 0;
// For hiding overflowing elements
flex-wrap: wrap;
height: 24px;
.avatars-item {
margin: 0 0 5px 5px;
&:first-child {
padding-left: 5px;
}
.avatar-small {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
height: 24px;
width: 24px;
}
}
}
</style>

View file

@ -1,7 +1,11 @@
<template> <template>
<div class="basic-user-card"> <div class="basic-user-card">
<router-link :to="userProfileLink(user)"> <router-link :to="userProfileLink(user)">
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/> <UserAvatar
class="avatar"
:user="user"
@click.prevent.native="toggleUserExpanded"
/>
</router-link> </router-link>
<div class="basic-user-card-expanded-content" v-if="userExpanded"> <div class="basic-user-card-expanded-content" v-if="userExpanded">
<UserCard :user="user" :rounded="true" :bordered="true"/> <UserCard :user="user" :rounded="true" :bordered="true"/>
@ -44,15 +48,16 @@
width: 16px; width: 16px;
vertical-align: middle; vertical-align: middle;
} }
}
&-value { &-user-name-value,
&-screen-name {
display: inline-block; display: inline-block;
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
}
&-expanded-content { &-expanded-content {
flex: 1; flex: 1;

View file

@ -139,6 +139,7 @@ const conversation = {
}, },
setHighlight (id) { setHighlight (id) {
this.highlight = id this.highlight = id
this.$store.dispatch('fetchFavsAndRepeats', id)
}, },
getHighlight () { getHighlight () {
return this.isExpanded ? this.highlight : null return this.isExpanded ? this.highlight : null

View file

@ -11,7 +11,7 @@
@goto="setHighlight" @goto="setHighlight"
@toggleExpanded="toggleExpanded" @toggleExpanded="toggleExpanded"
:key="status.id" :key="status.id"
:inlineExpanded="collapsable" :inlineExpanded="collapsable && isExpanded"
:statusoid="status" :statusoid="status"
:expandable='!isExpanded' :expandable='!isExpanded'
:focused="focused(status.id)" :focused="focused(status.id)"

View file

@ -0,0 +1,48 @@
const Exporter = {
props: {
getContent: {
type: Function,
required: true
},
filename: {
type: String,
default: 'export.csv'
},
exportButtonLabel: {
type: String,
default () {
return this.$t('exporter.export')
}
},
processingMessage: {
type: String,
default () {
return this.$t('exporter.processing')
}
}
},
data () {
return {
processing: false
}
},
methods: {
process () {
this.processing = true
this.getContent()
.then((content) => {
const fileToDownload = document.createElement('a')
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content))
fileToDownload.setAttribute('download', this.filename)
fileToDownload.style.display = 'none'
document.body.appendChild(fileToDownload)
fileToDownload.click()
document.body.removeChild(fileToDownload)
// Add delay before hiding processing state since browser takes some time to handle file download
setTimeout(() => { this.processing = false }, 2000)
})
}
}
}
export default Exporter

View file

@ -0,0 +1,20 @@
<template>
<div class="exporter">
<div v-if="processing">
<i class="icon-spin4 animate-spin exporter-processing"></i>
<span>{{processingMessage}}</span>
</div>
<button class="btn btn-default" @click="process" v-else>{{exportButtonLabel}}</button>
</div>
</template>
<script src="./exporter.js"></script>
<style lang="scss">
.exporter {
&-processing {
font-size: 1.5em;
margin: 0.25em;
}
}
</style>

View file

@ -70,22 +70,10 @@ const ImageCropper = {
this.dataUrl = undefined this.dataUrl = undefined
this.$emit('close') this.$emit('close')
}, },
submit () { submit (cropping = true) {
this.submitting = true this.submitting = true
this.avatarUploadError = null this.avatarUploadError = null
this.submitHandler(this.cropper, this.file) this.submitHandler(cropping && this.cropper, this.file)
.then(() => this.destroy())
.catch((err) => {
this.submitError = err
})
.finally(() => {
this.submitting = false
})
},
submitWithoutCropping () {
this.submitting = true
this.avatarUploadError = null
this.submitHandler(false, this.dataUrl)
.then(() => this.destroy()) .then(() => this.destroy())
.catch((err) => { .catch((err) => {
this.submitError = err this.submitError = err

View file

@ -5,9 +5,9 @@
<img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" /> <img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
</div> </div>
<div class="image-cropper-buttons-wrapper"> <div class="image-cropper-buttons-wrapper">
<button class="btn" type="button" :disabled="submitting" @click="submit" v-text="saveText"></button> <button class="btn" type="button" :disabled="submitting" @click="submit()" v-text="saveText"></button>
<button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button> <button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button>
<button class="btn" type="button" :disabled="submitting" @click="submitWithoutCropping" v-text="saveWithoutCroppingText"></button> <button class="btn" type="button" :disabled="submitting" @click="submit(false)" v-text="saveWithoutCroppingText"></button>
<i class="icon-spin4 animate-spin" v-if="submitting"></i> <i class="icon-spin4 animate-spin" v-if="submitting"></i>
</div> </div>
<div class="alert error" v-if="submitError"> <div class="alert error" v-if="submitError">

View file

@ -0,0 +1,53 @@
const Importer = {
props: {
submitHandler: {
type: Function,
required: true
},
submitButtonLabel: {
type: String,
default () {
return this.$t('importer.submit')
}
},
successMessage: {
type: String,
default () {
return this.$t('importer.success')
}
},
errorMessage: {
type: String,
default () {
return this.$t('importer.error')
}
}
},
data () {
return {
file: null,
error: false,
success: false,
submitting: false
}
},
methods: {
change () {
this.file = this.$refs.input.files[0]
},
submit () {
this.dismiss()
this.submitting = true
this.submitHandler(this.file)
.then(() => { this.success = true })
.catch(() => { this.error = true })
.finally(() => { this.submitting = false })
},
dismiss () {
this.success = false
this.error = false
}
}
}
export default Importer

View file

@ -0,0 +1,28 @@
<template>
<div class="importer">
<form>
<input type="file" ref="input" v-on:change="change" />
</form>
<i class="icon-spin4 animate-spin importer-uploading" v-if="submitting"></i>
<button class="btn btn-default" v-else @click="submit">{{submitButtonLabel}}</button>
<div v-if="success">
<i class="icon-cross" @click="dismiss"></i>
<p>{{successMessage}}</p>
</div>
<div v-else-if="error">
<i class="icon-cross" @click="dismiss"></i>
<p>{{errorMessage}}</p>
</div>
</div>
</template>
<script src="./importer.js"></script>
<style lang="scss">
.importer {
&-uploading {
font-size: 1.5em;
margin: 0.25em;
}
}
</style>

View file

@ -33,6 +33,8 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.media-modal-view { .media-modal-view {
z-index: 1001;
&:hover { &:hover {
.modal-view-button-arrow { .modal-view-button-arrow {
opacity: 0.75; opacity: 0.75;

View file

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

View file

@ -1,4 +1,5 @@
<template> <template>
<div>
<nav class='nav-bar container' id="nav"> <nav class='nav-bar container' id="nav">
<div class='mobile-inner-nav' @click="scrollToTop()"> <div class='mobile-inner-nav' @click="scrollToTop()">
<div class='item'> <div class='item'>
@ -14,12 +15,12 @@
</a> </a>
</div> </div>
</div> </div>
<SideDrawer ref="sideDrawer" :logout="logout"/> </nav>
<div v-if="currentUser" <div v-if="currentUser"
class="mobile-notifications-drawer" class="mobile-notifications-drawer"
:class="{ 'closed': !notificationsOpen }" :class="{ 'closed': !notificationsOpen }"
@touchstart="notificationsTouchStart" @touchstart.stop="notificationsTouchStart"
@touchmove="notificationsTouchMove" @touchmove.stop="notificationsTouchMove"
> >
<div class="mobile-notifications-header"> <div class="mobile-notifications-header">
<span class="title">{{$t('notifications.notifications')}}</span> <span class="title">{{$t('notifications.notifications')}}</span>
@ -27,12 +28,13 @@
<i class="button-icon icon-cancel"/> <i class="button-icon icon-cancel"/>
</a> </a>
</div> </div>
<div v-if="currentUser" class="mobile-notifications"> <div class="mobile-notifications" @scroll="onScroll">
<Notifications ref="notifications" noHeading="true"/> <Notifications ref="notifications" noHeading="true"/>
</div> </div>
</div> </div>
<SideDrawer ref="sideDrawer" :logout="logout"/>
<MobilePostStatusModal /> <MobilePostStatusModal />
</nav> </div>
</template> </template>
<script src="./mobile_nav.js"></script> <script src="./mobile_nav.js"></script>
@ -79,6 +81,8 @@
transition-property: transform; transition-property: transform;
transition-duration: 0.25s; transition-duration: 0.25s;
transform: translateX(0); transform: translateX(0);
z-index: 1001;
-webkit-overflow-scrolling: touch;
&.closed { &.closed {
transform: translateX(100%); transform: translateX(100%);

View file

@ -1,5 +1,5 @@
import PostStatusForm from '../post_status_form/post_status_form.vue' import PostStatusForm from '../post_status_form/post_status_form.vue'
import { throttle } from 'lodash' import { debounce } from 'lodash'
const MobilePostStatusModal = { const MobilePostStatusModal = {
components: { components: {
@ -16,11 +16,15 @@ const MobilePostStatusModal = {
} }
}, },
created () { created () {
window.addEventListener('scroll', this.handleScroll) if (this.autohideFloatingPostButton) {
this.activateFloatingPostButtonAutohide()
}
window.addEventListener('resize', this.handleOSK) window.addEventListener('resize', this.handleOSK)
}, },
destroyed () { destroyed () {
window.removeEventListener('scroll', this.handleScroll) if (this.autohideFloatingPostButton) {
this.deactivateFloatingPostButtonAutohide()
}
window.removeEventListener('resize', this.handleOSK) window.removeEventListener('resize', this.handleOSK)
}, },
computed: { computed: {
@ -28,10 +32,30 @@ const MobilePostStatusModal = {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
isHidden () { isHidden () {
return this.hidden || this.inputActive return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
},
autohideFloatingPostButton () {
return !!this.$store.state.config.autohideFloatingPostButton
}
},
watch: {
autohideFloatingPostButton: function (isEnabled) {
if (isEnabled) {
this.activateFloatingPostButtonAutohide()
} else {
this.deactivateFloatingPostButtonAutohide()
}
} }
}, },
methods: { methods: {
activateFloatingPostButtonAutohide () {
window.addEventListener('scroll', this.handleScrollStart)
window.addEventListener('scroll', this.handleScrollEnd)
},
deactivateFloatingPostButtonAutohide () {
window.removeEventListener('scroll', this.handleScrollStart)
window.removeEventListener('scroll', this.handleScrollEnd)
},
openPostForm () { openPostForm () {
this.postFormOpen = true this.postFormOpen = true
this.hidden = true this.hidden = true
@ -65,26 +89,19 @@ const MobilePostStatusModal = {
this.inputActive = false this.inputActive = false
} }
}, },
handleScroll: throttle(function () { handleScrollStart: debounce(function () {
const scrollAmount = window.scrollY - this.oldScrollPos if (window.scrollY > this.oldScrollPos) {
const scrollingDown = scrollAmount > 0 this.hidden = true
} else {
if (scrollingDown !== this.scrollingDown) {
this.amountScrolled = 0
this.scrollingDown = scrollingDown
if (!scrollingDown) {
this.hidden = false this.hidden = false
} }
} else if (scrollingDown) {
this.amountScrolled += scrollAmount
if (this.amountScrolled > 100 && !this.hidden) {
this.hidden = true
}
}
this.oldScrollPos = window.scrollY this.oldScrollPos = window.scrollY
this.scrollingDown = scrollingDown }, 100, {leading: true, trailing: false}),
}, 100)
handleScrollEnd: debounce(function () {
this.hidden = false
this.oldScrollPos = window.scrollY
}, 100, {leading: false, trailing: true})
} }
} }

View file

@ -7,7 +7,7 @@
> >
<div class="post-form-modal-panel panel" @click.stop=""> <div class="post-form-modal-panel panel" @click.stop="">
<div class="panel-heading">{{$t('post_status.new_status')}}</div> <div class="panel-heading">{{$t('post_status.new_status')}}</div>
<PostStatusForm class="panel-body" @posted="closePostForm"/> <PostStatusForm class="panel-body" @posted="closePostForm" />
</div> </div>
</div> </div>
<button <button

View file

@ -7,7 +7,7 @@
</status> </status>
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]" v-else> <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]" v-else>
<a class='avatar-container' :href="notification.from_profile.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded"> <a class='avatar-container' :href="notification.from_profile.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.from_profile.profile_image_url_original" /> <UserAvatar :compact="true" :betterShadow="betterShadow" :user="notification.from_profile"/>
</a> </a>
<div class='notification-right'> <div class='notification-right'>
<UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded" /> <UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded" />

View file

@ -52,6 +52,10 @@ const Notifications = {
this.$store.dispatch('markNotificationsAsSeen') this.$store.dispatch('markNotificationsAsSeen')
}, },
fetchOlderNotifications () { fetchOlderNotifications () {
if (this.loading) {
return
}
const store = this.$store const store = this.$store
const credentials = store.state.users.currentUser.credentials const credentials = store.state.users.currentUser.credentials
store.commit('setNotificationsLoading', { value: true }) store.commit('setNotificationsLoading', { value: true })

View file

@ -182,6 +182,9 @@ const PostStatusForm = {
}, },
safeDMEnabled () { safeDMEnabled () {
return this.$store.state.instance.safeDM return this.$store.state.instance.safeDM
},
hideScopeNotice () {
return this.$store.state.config.hideScopeNotice
} }
}, },
methods: { methods: {
@ -338,6 +341,9 @@ const PostStatusForm = {
}, },
changeVis (visibility) { changeVis (visibility) {
this.newStatus.visibility = visibility this.newStatus.visibility = visibility
},
dismissScopeNotice () {
this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })
} }
} }
} }

View file

@ -9,7 +9,25 @@
class="visibility-notice"> class="visibility-notice">
<router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link> <router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
</i18n> </i18n>
<p v-if="newStatus.visibility === 'direct'" class="visibility-notice"> <p v-if="!hideScopeNotice && newStatus.visibility === 'public'" class="visibility-notice notice-dismissible">
<span>{{ $t('post_status.scope_notice.public') }}</span>
<a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss">
<i class='icon-cancel'></i>
</a>
</p>
<p v-else-if="!hideScopeNotice && newStatus.visibility === 'unlisted'" class="visibility-notice notice-dismissible">
<span>{{ $t('post_status.scope_notice.unlisted') }}</span>
<a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss">
<i class='icon-cancel'></i>
</a>
</p>
<p v-else-if="!hideScopeNotice && newStatus.visibility === 'private' && $store.state.users.currentUser.locked" class="visibility-notice notice-dismissible">
<span>{{ $t('post_status.scope_notice.private') }}</span>
<a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss">
<i class='icon-cancel'></i>
</a>
</p>
<p v-else-if="newStatus.visibility === 'direct'" class="visibility-notice">
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span> <span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span> <span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
</p> </p>

View file

@ -31,6 +31,10 @@
&-item-inner { &-item-inner {
display: flex; display: flex;
align-items: center; align-items: center;
> * {
min-width: 0;
}
} }
&-item-selected-inner { &-item-selected-inner {

View file

@ -46,6 +46,7 @@ const settings = {
streamingLocal: user.streaming, streamingLocal: user.streaming,
pauseOnUnfocusedLocal: user.pauseOnUnfocused, pauseOnUnfocusedLocal: user.pauseOnUnfocused,
hoverPreviewLocal: user.hoverPreview, hoverPreviewLocal: user.hoverPreview,
autohideFloatingPostButtonLocal: user.autohideFloatingPostButton,
hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined' hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined'
? instance.hideMutedPosts ? instance.hideMutedPosts
@ -183,6 +184,9 @@ const settings = {
hoverPreviewLocal (value) { hoverPreviewLocal (value) {
this.$store.dispatch('setOption', { name: 'hoverPreview', value }) this.$store.dispatch('setOption', { name: 'hoverPreview', value })
}, },
autohideFloatingPostButtonLocal (value) {
this.$store.dispatch('setOption', { name: 'autohideFloatingPostButton', value })
},
muteWordsString (value) { muteWordsString (value) {
value = filter(value.split('\n'), (word) => trim(word).length > 0) value = filter(value.split('\n'), (word) => trim(word).length > 0)
this.$store.dispatch('setOption', { name: 'muteWords', value }) this.$store.dispatch('setOption', { name: 'muteWords', value })

View file

@ -122,6 +122,10 @@
{{$t('settings.minimal_scopes_mode')}} {{$t('settings.instance_default', { value: minimalScopesModeDefault })}} {{$t('settings.minimal_scopes_mode')}} {{$t('settings.instance_default', { value: minimalScopesModeDefault })}}
</label> </label>
</li> </li>
<li>
<input type="checkbox" id="autohideFloatingPostButton" v-model="autohideFloatingPostButtonLocal">
<label for="autohideFloatingPostButton">{{$t('settings.autohide_floating_post_button')}}</label>
</li>
</ul> </ul>
</div> </div>

View file

@ -7,11 +7,12 @@ import UserCard from '../user_card/user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import Gallery from '../gallery/gallery.vue' import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue' import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service' import fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { filter, find, unescape } from 'lodash' import { filter, find, unescape, uniqBy } from 'lodash'
const Status = { const Status = {
name: 'Status', name: 'Status',
@ -30,7 +31,6 @@ const Status = {
data () { data () {
return { return {
replying: false, replying: false,
expanded: false,
unmuted: false, unmuted: false,
userExpanded: false, userExpanded: false,
preview: null, preview: null,
@ -97,6 +97,10 @@ const Status = {
return this.statusoid return this.statusoid
} }
}, },
statusFromGlobalRepository () {
// NOTE: Consider to replace status with statusFromGlobalRepository
return this.$store.state.statuses.allStatusesObject[this.status.id]
},
loggedIn () { loggedIn () {
return !!this.$store.state.users.currentUser return !!this.$store.state.users.currentUser
}, },
@ -156,7 +160,7 @@ const Status = {
if (this.$store.state.config.replyVisibility === 'all') { if (this.$store.state.config.replyVisibility === 'all') {
return false return false
} }
if (this.inlineExpanded || this.expanded || this.inConversation || !this.isReply) { if (this.inConversation || !this.isReply) {
return false return false
} }
if (this.status.user.id === this.$store.state.users.currentUser.id) { if (this.status.user.id === this.$store.state.users.currentUser.id) {
@ -170,7 +174,7 @@ const Status = {
if (this.status.user.id === this.status.attentions[i].id) { if (this.status.user.id === this.status.attentions[i].id) {
continue continue
} }
if (checkFollowing && this.status.attentions[i].following) { if (checkFollowing && this.$store.getters.findUser(this.status.attentions[i].id).following) {
return false return false
} }
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) { if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
@ -257,6 +261,14 @@ const Status = {
return this.status.statusnet_html return this.status.statusnet_html
} }
return this.status.summary_html + '<br />' + this.status.statusnet_html return this.status.summary_html + '<br />' + this.status.statusnet_html
},
combinedFavsAndRepeatsUsers () {
// Use the status from the global status repository since favs and repeats are saved in it
const combinedUsers = [].concat(
this.statusFromGlobalRepository.favoritedBy,
this.statusFromGlobalRepository.rebloggedBy
)
return uniqBy(combinedUsers, 'id')
} }
}, },
components: { components: {
@ -268,7 +280,8 @@ const Status = {
UserCard, UserCard,
UserAvatar, UserAvatar,
Gallery, Gallery,
LinkPreview LinkPreview,
AvatarList
}, },
methods: { methods: {
visibilityIcon (visibility) { visibilityIcon (visibility) {

View file

@ -13,7 +13,7 @@
</template> </template>
<template v-else> <template v-else>
<div v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info"> <div v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/> <UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :user="statusoid.user"/>
<div class="media-body faint"> <div class="media-body faint">
<span class="user-name"> <span class="user-name">
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/> <router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
@ -27,7 +27,7 @@
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]" :style="[ userStyle ]" class="media status"> <div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]" :style="[ userStyle ]" class="media status">
<div v-if="!noHeading" class="media-left"> <div v-if="!noHeading" class="media-left">
<router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded"> <router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
<UserAvatar :compact="compact" :betterShadow="betterShadow" :src="status.user.profile_image_url_original"/> <UserAvatar :compact="compact" :betterShadow="betterShadow" :user="status.user"/>
</router-link> </router-link>
</div> </div>
<div class="status-body"> <div class="status-body">
@ -91,8 +91,13 @@
</div> </div>
<div v-if="showPreview" class="status-preview-container"> <div v-if="showPreview" class="status-preview-container">
<status class="status-preview" v-if="preview" :isPreview="true" :statusoid="preview" :compact=true></status> <status class="status-preview"
<div class="status-preview status-preview-loading" v-else> v-if="preview"
:isPreview="true"
:statusoid="preview"
:compact=true
/>
<div v-else class="status-preview status-preview-loading">
<i class="icon-spin4 animate-spin"></i> <i class="icon-spin4 animate-spin"></i>
</div> </div>
</div> </div>
@ -133,6 +138,24 @@
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" /> <link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
</div> </div>
<transition name="fade">
<div class="favs-repeated-users" v-if="isFocused && combinedFavsAndRepeatsUsers.length > 0">
<div class="stats">
<div class="stat-count" v-if="statusFromGlobalRepository.rebloggedBy && statusFromGlobalRepository.rebloggedBy.length > 0">
<a class="stat-title">{{ $t('status.repeats') }}</a>
<div class="stat-number">{{ statusFromGlobalRepository.rebloggedBy.length }}</div>
</div>
<div class="stat-count" v-if="statusFromGlobalRepository.favoritedBy && statusFromGlobalRepository.favoritedBy.length > 0">
<a class="stat-title">{{ $t('status.favorites') }}</a>
<div class="stat-number">{{ statusFromGlobalRepository.favoritedBy.length }}</div>
</div>
<div class="avatar-row">
<AvatarList :users="combinedFavsAndRepeatsUsers"></AvatarList>
</div>
</div>
</div>
</transition>
<div v-if="!noHeading && !isPreview" class='status-actions media-body'> <div v-if="!noHeading && !isPreview" class='status-actions media-body'>
<div v-if="loggedIn"> <div v-if="loggedIn">
<i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'icon-reply-active': replying}"></i> <i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'icon-reply-active': replying}"></i>
@ -604,6 +627,58 @@ a.unmute {
flex: 1; flex: 1;
} }
.timeline > {
.status-el:last-child {
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
border-bottom: none;
}
}
.favs-repeated-users {
margin-top: $status-margin;
.stats {
width: 100%;
display: flex;
line-height: 1em;
.stat-count {
margin-right: $status-margin;
.stat-title {
color: var(--faint, $fallback--faint);
font-size: 12px;
text-transform: uppercase;
position: relative;
}
.stat-number {
font-weight: bolder;
font-size: 16px;
line-height: 1em;
}
}
.avatar-row {
flex: 1;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
height: 100%;
width: 1px;
left: 0;
background-color: var(--faint, $fallback--faint);
}
}
}
}
@media all and (max-width: 800px) { @media all and (max-width: 800px) {
.status-el { .status-el {
.retweet-info { .retweet-info {

View file

@ -2,7 +2,7 @@ import StillImage from '../still-image/still-image.vue'
const UserAvatar = { const UserAvatar = {
props: [ props: [
'src', 'user',
'betterShadow', 'betterShadow',
'compact' 'compact'
], ],

View file

@ -1,8 +1,10 @@
<template> <template>
<StillImage <StillImage
class="avatar" class="avatar"
:alt="user.screen_name"
:title="user.screen_name"
:src="user.profile_image_url_original"
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }" :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
:src="imgSrc"
:imageLoadError="imageLoadError" :imageLoadError="imageLoadError"
/> />
</template> </template>

View file

@ -151,6 +151,9 @@ export default {
}, },
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
} }
} }
} }

View file

@ -4,7 +4,7 @@
<div class='user-info'> <div class='user-info'>
<div class='container'> <div class='container'>
<router-link :to="userProfileLink(user)"> <router-link :to="userProfileLink(user)">
<UserAvatar :betterShadow="betterShadow" :src="user.profile_image_url_original"/> <UserAvatar :betterShadow="betterShadow" :user="user"/>
</router-link> </router-link>
<div class="name-and-screen-name"> <div class="name-and-screen-name">
<div class="top-line"> <div class="top-line">
@ -99,8 +99,14 @@
</button> </button>
</span> </span>
</div> </div>
<ModerationTools :user='user' v-if='loggedIn.role === "admin"'> <div class='block' v-if='isOtherUser && loggedIn'>
</ModerationTools> <span>
<button @click="reportUser">
{{ $t('user_card.report') }}
</button>
</span>
</div>
<ModerationTools :user='user' v-if='loggedIn.role === "admin"'/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,106 @@
import Status from '../status/status.vue'
import List from '../list/list.vue'
import Checkbox from '../checkbox/checkbox.vue'
const UserReportingModal = {
components: {
Status,
List,
Checkbox
},
data () {
return {
comment: '',
forward: false,
statusIdsToReport: [],
processing: false,
error: false
}
},
computed: {
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
isOpen () {
return this.isLoggedIn && this.$store.state.reports.modalActivated
},
userId () {
return this.$store.state.reports.userId
},
user () {
return this.$store.getters.findUser(this.userId)
},
remoteInstance () {
return !this.user.is_local && this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1)
},
statuses () {
return this.$store.state.reports.statuses
}
},
watch: {
userId: 'resetState'
},
methods: {
resetState () {
// Reset state
this.comment = ''
this.forward = false
this.statusIdsToReport = []
this.processing = false
this.error = false
},
closeModal () {
this.$store.dispatch('closeUserReportingModal')
},
reportUser () {
this.processing = true
this.error = false
const params = {
userId: this.userId,
comment: this.comment,
forward: this.forward,
statusIds: this.statusIdsToReport
}
this.$store.state.api.backendInteractor.reportUser(params)
.then(() => {
this.processing = false
this.resetState()
this.closeModal()
})
.catch(() => {
this.processing = false
this.error = true
})
},
clearError () {
this.error = false
},
isChecked (statusId) {
return this.statusIdsToReport.indexOf(statusId) !== -1
},
toggleStatus (checked, statusId) {
if (checked === this.isChecked(statusId)) {
return
}
if (checked) {
this.statusIdsToReport.push(statusId)
} else {
this.statusIdsToReport.splice(this.statusIdsToReport.indexOf(statusId), 1)
}
},
resize (e) {
const target = e.target || e
if (!(target instanceof window.Element)) { return }
// Auto is needed to make textbox shrink when removing lines
target.style.height = 'auto'
target.style.height = `${target.scrollHeight}px`
if (target.value === '') {
target.style.height = null
}
}
}
}
export default UserReportingModal

View file

@ -0,0 +1,157 @@
<template>
<div class="modal-view" @click="closeModal" v-if="isOpen">
<div class="user-reporting-panel panel" @click.stop="">
<div class="panel-heading">
<div class="title">{{$t('user_reporting.title', [user.screen_name])}}</div>
</div>
<div class="panel-body">
<div class="user-reporting-panel-left">
<div>
<p>{{$t('user_reporting.add_comment_description')}}</p>
<textarea
v-model="comment"
class="form-control"
:placeholder="$t('user_reporting.additional_comments')"
rows="1"
@input="resize"
/>
</div>
<div v-if="!user.is_local">
<p>{{$t('user_reporting.forward_description')}}</p>
<Checkbox v-model="forward">{{$t('user_reporting.forward_to', [remoteInstance])}}</Checkbox>
</div>
<div>
<button class="btn btn-default" @click="reportUser" :disabled="processing">{{$t('user_reporting.submit')}}</button>
<div class="alert error" v-if="error">
{{$t('user_reporting.generic_error')}}
</div>
</div>
</div>
<div class="user-reporting-panel-right">
<List :items="statuses">
<template slot="item" slot-scope="{item}">
<div class="status-fadein user-reporting-panel-sitem">
<Status :inConversation="false" :focused="false" :statusoid="item" />
<Checkbox :checked="isChecked(item.id)" @change="checked => toggleStatus(checked, item.id)" />
</div>
</template>
</List>
</div>
</div>
</div>
</div>
</template>
<script src="./user_reporting_modal.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.user-reporting-panel {
width: 90vw;
max-width: 700px;
min-height: 20vh;
max-height: 80vh;
.panel-heading {
.title {
text-align: center;
// TODO: Consider making these as default of panel
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.panel-body {
display: flex;
flex-direction: column-reverse;
border-top: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
overflow: hidden;
}
&-left {
padding: 1.1em 0.7em 0.7em;
line-height: 1.4em;
box-sizing: border-box;
> div {
margin-bottom: 1em;
&:last-child {
margin-bottom: 0;
}
}
p {
margin-top: 0;
}
textarea.form-control {
line-height: 16px;
resize: none;
overflow: hidden;
transition: min-height 200ms 100ms;
min-height: 44px;
width: 100%;
}
.btn {
min-width: 10em;
padding: 0 2em;
}
.alert {
margin: 1em 0 0 0;
line-height: 1.3em;
}
}
&-right {
display: flex;
flex-direction: column;
overflow-y: auto;
}
&-sitem {
display: flex;
justify-content: space-between;
> .status-el {
flex: 1;
}
> .checkbox {
margin: 0.75em;
}
}
@media all and (min-width: 801px) {
.panel-body {
flex-direction: row;
}
&-left {
width: 50%;
max-width: 320px;
border-right: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
padding: 1.1em;
> div {
margin-bottom: 2em;
}
}
&-right {
width: 50%;
flex: 1 1 auto;
margin-bottom: 12px;
}
}
}
</style>

View file

@ -13,6 +13,8 @@ import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import EmojiInput from '../emoji-input/emoji-input.vue' import EmojiInput from '../emoji-input/emoji-input.vue'
import Autosuggest from '../autosuggest/autosuggest.vue' import Autosuggest from '../autosuggest/autosuggest.vue'
import Importer from '../importer/importer.vue'
import Exporter from '../exporter/exporter.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription' import withSubscription from '../../hocs/with_subscription/with_subscription'
import userSearchApi from '../../services/new_api/user_search.js' import userSearchApi from '../../services/new_api/user_search.js'
@ -40,14 +42,9 @@ const UserSettings = {
hideFollowers: this.$store.state.users.currentUser.hide_followers, hideFollowers: this.$store.state.users.currentUser.hide_followers,
showRole: this.$store.state.users.currentUser.show_role, showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role, role: this.$store.state.users.currentUser.role,
followList: null,
followImportError: false,
followsImported: false,
enableFollowsExport: true,
pickAvatarBtnVisible: true, pickAvatarBtnVisible: true,
bannerUploading: false, bannerUploading: false,
backgroundUploading: false, backgroundUploading: false,
followListUploading: false,
bannerPreview: null, bannerPreview: null,
backgroundPreview: null, backgroundPreview: null,
bannerUploadError: null, bannerUploadError: null,
@ -75,7 +72,9 @@ const UserSettings = {
Autosuggest, Autosuggest,
BlockCard, BlockCard,
MuteCard, MuteCard,
ProgressButton ProgressButton,
Importer,
Exporter
}, },
computed: { computed: {
user () { user () {
@ -110,37 +109,23 @@ const UserSettings = {
}, },
methods: { methods: {
updateProfile () { updateProfile () {
const name = this.newName
const description = this.newBio
const locked = this.newLocked
// Backend notation.
/* eslint-disable camelcase */
const default_scope = this.newDefaultScope
const no_rich_text = this.newNoRichText
const hide_follows = this.hideFollows
const hide_followers = this.hideFollowers
const show_role = this.showRole
/* eslint-enable camelcase */
this.$store.state.api.backendInteractor this.$store.state.api.backendInteractor
.updateProfile({ .updateProfile({
params: { params: {
name, note: this.newBio,
description, locked: this.newLocked,
locked,
// Backend notation. // Backend notation.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
default_scope, display_name: this.newName,
no_rich_text, default_scope: this.newDefaultScope,
hide_follows, no_rich_text: this.newNoRichText,
hide_followers, hide_follows: this.hideFollows,
show_role hide_followers: this.hideFollowers,
show_role: this.showRole
/* eslint-enable camelcase */ /* eslint-enable camelcase */
}}).then((user) => { }}).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user]) this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user) this.$store.commit('setCurrentUser', user)
}
}) })
}, },
changeVis (visibility) { changeVis (visibility) {
@ -160,23 +145,29 @@ const UserSettings = {
reader.onload = ({target}) => { reader.onload = ({target}) => {
const img = target.result const img = target.result
this[slot + 'Preview'] = img this[slot + 'Preview'] = img
this[slot] = file
} }
reader.readAsDataURL(file) reader.readAsDataURL(file)
}, },
submitAvatar (cropper, file) { submitAvatar (cropper, file) {
let img const that = this
if (cropper) { return new Promise((resolve, reject) => {
img = cropper.getCroppedCanvas().toDataURL(file.type) function updateAvatar (avatar) {
} else { that.$store.state.api.backendInteractor.updateAvatar({ avatar })
img = file .then((user) => {
that.$store.commit('addNewUsers', [user])
that.$store.commit('setCurrentUser', user)
resolve()
})
.catch((err) => {
reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
})
} }
return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => { if (cropper) {
if (!user.error) { cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
} else { } else {
throw new Error(this.$t('upload.error.base') + user.error) updateAvatar(file)
} }
}) })
}, },
@ -186,30 +177,17 @@ const UserSettings = {
submitBanner () { submitBanner () {
if (!this.bannerPreview) { return } if (!this.bannerPreview) { return }
let banner = this.bannerPreview
// eslint-disable-next-line no-undef
let imginfo = new Image()
/* eslint-disable camelcase */
let offset_top, offset_left, width, height
imginfo.src = banner
width = imginfo.width
height = imginfo.height
offset_top = 0
offset_left = 0
this.bannerUploading = true this.bannerUploading = true
this.$store.state.api.backendInteractor.updateBanner({params: {banner, offset_top, offset_left, width, height}}).then((data) => { this.$store.state.api.backendInteractor.updateBanner({banner: this.banner})
if (!data.error) { .then((user) => {
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser)) this.$store.commit('addNewUsers', [user])
clone.cover_photo = data.url this.$store.commit('setCurrentUser', user)
this.$store.commit('addNewUsers', [clone])
this.$store.commit('setCurrentUser', clone)
this.bannerPreview = null this.bannerPreview = null
} else {
this.bannerUploadError = this.$t('upload.error.base') + data.error
}
this.bannerUploading = false
}) })
/* eslint-enable camelcase */ .catch((err) => {
this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
})
.then(() => { this.bannerUploading = false })
}, },
submitBg () { submitBg () {
if (!this.backgroundPreview) { return } if (!this.backgroundPreview) { return }
@ -236,62 +214,41 @@ const UserSettings = {
this.backgroundUploading = false this.backgroundUploading = false
}) })
}, },
importFollows () { importFollows (file) {
this.followListUploading = true return this.$store.state.api.backendInteractor.importFollows(file)
const followList = this.followList
this.$store.state.api.backendInteractor.followImport({params: followList})
.then((status) => { .then((status) => {
if (status) { if (!status) {
this.followsImported = true throw new Error('failed')
} else {
this.followImportError = true
} }
this.followListUploading = false
}) })
}, },
/* This function takes an Array of Users importBlocks (file) {
* and outputs a file with all the addresses for the user to download return this.$store.state.api.backendInteractor.importBlocks(file)
*/ .then((status) => {
exportPeople (users, filename) { if (!status) {
// Get all the friends addresses throw new Error('failed')
var UserAddresses = users.map(function (user) { }
})
},
generateExportableUsersContent (users) {
// Get addresses
return users.map((user) => {
// check is it's a local user // check is it's a local user
if (user && user.is_local) { if (user && user.is_local) {
// append the instance address // append the instance address
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
user.screen_name += '@' + location.hostname return user.screen_name + '@' + location.hostname
} }
return user.screen_name return user.screen_name
}).join('\n') }).join('\n')
// Make the user download the file
var fileToDownload = document.createElement('a')
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(UserAddresses))
fileToDownload.setAttribute('download', filename)
fileToDownload.style.display = 'none'
document.body.appendChild(fileToDownload)
fileToDownload.click()
document.body.removeChild(fileToDownload)
}, },
exportFollows () { getFollowsContent () {
this.enableFollowsExport = false return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
this.$store.state.api.backendInteractor .then(this.generateExportableUsersContent)
.exportFriends({
id: this.$store.state.users.currentUser.id
})
.then((friendList) => {
this.exportPeople(friendList, 'friends.csv')
setTimeout(() => { this.enableFollowsExport = true }, 2000)
})
}, },
followListChange () { getBlocksContent () {
// eslint-disable-next-line no-undef return this.$store.state.api.backendInteractor.fetchBlocks()
let formData = new FormData() .then(this.generateExportableUsersContent)
formData.append('list', this.$refs.followlist.files[0])
this.followList = formData
},
dismissImported () {
this.followsImported = false
this.followImportError = false
}, },
confirmDelete () { confirmDelete () {
this.deletingAccount = true this.deletingAccount = true

View file

@ -171,26 +171,20 @@
<div class="setting-item"> <div class="setting-item">
<h2>{{$t('settings.follow_import')}}</h2> <h2>{{$t('settings.follow_import')}}</h2>
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p> <p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
<form> <Importer :submitHandler="importFollows" :successMessage="$t('settings.follows_imported')" :errorMessage="$t('settings.follow_import_error')" />
<input type="file" ref="followlist" v-on:change="followListChange" />
</form>
<i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
<div v-if="followsImported">
<i class="icon-cross" @click="dismissImported"></i>
<p>{{$t('settings.follows_imported')}}</p>
</div> </div>
<div v-else-if="followImportError"> <div class="setting-item">
<i class="icon-cross" @click="dismissImported"></i>
<p>{{$t('settings.follow_import_error')}}</p>
</div>
</div>
<div class="setting-item" v-if="enableFollowsExport">
<h2>{{$t('settings.follow_export')}}</h2> <h2>{{$t('settings.follow_export')}}</h2>
<button class="btn btn-default" @click="exportFollows">{{$t('settings.follow_export_button')}}</button> <Exporter :getContent="getFollowsContent" filename="friends.csv" :exportButtonLabel="$t('settings.follow_export_button')" />
</div> </div>
<div class="setting-item" v-else> <div class="setting-item">
<h2>{{$t('settings.follow_export_processing')}}</h2> <h2>{{$t('settings.block_import')}}</h2>
<p>{{$t('settings.import_blocks_from_a_csv_file')}}</p>
<Importer :submitHandler="importBlocks" :successMessage="$t('settings.blocks_imported')" :errorMessage="$t('settings.block_import_error')" />
</div>
<div class="setting-item">
<h2>{{$t('settings.block_export')}}</h2>
<Exporter :getContent="getBlocksContent" filename="blocks.csv" :exportButtonLabel="$t('settings.block_export_button')" />
</div> </div>
</div> </div>

View file

@ -2,6 +2,10 @@
"chat": { "chat": {
"title": "Chat" "title": "Chat"
}, },
"exporter": {
"export": "Export",
"processing": "Processing, you'll soon be asked to download your file"
},
"features_panel": { "features_panel": {
"chat": "Chat", "chat": "Chat",
"gopher": "Gopher", "gopher": "Gopher",
@ -31,6 +35,11 @@
"save_without_cropping": "Save without cropping", "save_without_cropping": "Save without cropping",
"cancel": "Cancel" "cancel": "Cancel"
}, },
"importer": {
"submit": "Submit",
"success": "Imported successfully.",
"error": "An error occured while importing this file."
},
"login": { "login": {
"login": "Log in", "login": "Log in",
"description": "Log in with OAuth", "description": "Log in with OAuth",
@ -85,6 +94,11 @@
"direct_warning_to_all": "This post will be visible to all the mentioned users.", "direct_warning_to_all": "This post will be visible to all the mentioned users.",
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.", "direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
"posting": "Posting", "posting": "Posting",
"scope_notice": {
"public": "This post will be visible to everyone",
"private": "This post will be visible to your followers only",
"unlisted": "This post will not be visible in Public Timeline and The Whole Known Network"
},
"scope": { "scope": {
"direct": "Direct - Post to mentioned users only", "direct": "Direct - Post to mentioned users only",
"private": "Followers-only - Post to followers only", "private": "Followers-only - Post to followers only",
@ -126,6 +140,11 @@
"avatarRadius": "Avatars", "avatarRadius": "Avatars",
"background": "Background", "background": "Background",
"bio": "Bio", "bio": "Bio",
"block_export": "Block export",
"block_export_button": "Export your blocks to a csv file",
"block_import": "Block import",
"block_import_error": "Error importing blocks",
"blocks_imported": "Blocks imported! Processing them will take a while.",
"blocks_tab": "Blocks", "blocks_tab": "Blocks",
"btnRadius": "Buttons", "btnRadius": "Buttons",
"cBlue": "Blue (Reply, follow)", "cBlue": "Blue (Reply, follow)",
@ -153,7 +172,6 @@
"filtering_explanation": "All statuses containing these words will be muted, one per line", "filtering_explanation": "All statuses containing these words will be muted, one per line",
"follow_export": "Follow export", "follow_export": "Follow export",
"follow_export_button": "Export your follows to a csv file", "follow_export_button": "Export your follows to a csv file",
"follow_export_processing": "Processing, you'll soon be asked to download your file",
"follow_import": "Follow import", "follow_import": "Follow import",
"follow_import_error": "Error importing followers", "follow_import_error": "Error importing followers",
"follows_imported": "Follows imported! Processing them will take a while.", "follows_imported": "Follows imported! Processing them will take a while.",
@ -169,6 +187,7 @@
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)", "hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
"hide_user_stats": "Hide user statistics (e.g. the number of followers)", "hide_user_stats": "Hide user statistics (e.g. the number of followers)",
"hide_filtered_statuses": "Hide filtered statuses", "hide_filtered_statuses": "Hide filtered statuses",
"import_blocks_from_a_csv_file": "Import blocks from a csv file",
"import_followers_from_a_csv_file": "Import follows from a csv file", "import_followers_from_a_csv_file": "Import follows from a csv file",
"import_theme": "Load preset", "import_theme": "Load preset",
"inputRadius": "Input fields", "inputRadius": "Input fields",
@ -219,6 +238,7 @@
"reply_visibility_all": "Show all replies", "reply_visibility_all": "Show all replies",
"reply_visibility_following": "Only show replies directed at me or users I'm following", "reply_visibility_following": "Only show replies directed at me or users I'm following",
"reply_visibility_self": "Only show replies directed at me", "reply_visibility_self": "Only show replies directed at me",
"autohide_floating_post_button": "Automatically hide New Post button (mobile)",
"saving_err": "Error saving settings", "saving_err": "Error saving settings",
"saving_ok": "Settings saved", "saving_ok": "Settings saved",
"search_user_to_block": "Search whom you want to block", "search_user_to_block": "Search whom you want to block",
@ -380,6 +400,8 @@
"no_statuses": "No statuses" "no_statuses": "No statuses"
}, },
"status": { "status": {
"favorites": "Favorites",
"repeats": "Repeats",
"reply_to": "Reply to", "reply_to": "Reply to",
"replies_list": "Replies:" "replies_list": "Replies:"
}, },
@ -404,6 +426,7 @@
"muted": "Muted", "muted": "Muted",
"per_day": "per day", "per_day": "per day",
"remote_follow": "Remote follow", "remote_follow": "Remote follow",
"report": "Report",
"statuses": "Statuses", "statuses": "Statuses",
"unblock": "Unblock", "unblock": "Unblock",
"unblock_progress": "Unblocking...", "unblock_progress": "Unblocking...",
@ -436,6 +459,15 @@
"profile_does_not_exist": "Sorry, this profile does not exist.", "profile_does_not_exist": "Sorry, this profile does not exist.",
"profile_loading_error": "Sorry, there was an error loading this profile." "profile_loading_error": "Sorry, there was an error loading this profile."
}, },
"user_reporting": {
"title": "Reporting {0}",
"add_comment_description": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"additional_comments": "Additional comments",
"forward_description": "The account is from another server. Send a copy of the report there as well?",
"forward_to": "Forward to {0}",
"submit": "Submit",
"generic_error": "An error occurred while processing your request."
},
"who_to_follow": { "who_to_follow": {
"more": "More", "more": "More",
"who_to_follow": "Who to follow" "who_to_follow": "Who to follow"

View file

@ -2,6 +2,10 @@
"chat": { "chat": {
"title": "Chat" "title": "Chat"
}, },
"exporter": {
"export": "Exportar",
"processing": "Procesando. Pronto se te pedirá que descargues tu archivo"
},
"features_panel": { "features_panel": {
"chat": "Chat", "chat": "Chat",
"gopher": "Gopher", "gopher": "Gopher",
@ -19,7 +23,22 @@
"apply": "Aplicar", "apply": "Aplicar",
"submit": "Enviar", "submit": "Enviar",
"more": "Más", "more": "Más",
"generic_error": "Ha ocurrido un error" "generic_error": "Ha ocurrido un error",
"optional": "opcional",
"show_more": "Mostrar más",
"show_less": "Mostrar menos",
"cancel": "Cancelar"
},
"image_cropper": {
"crop_picture": "Recortar la foto",
"save": "Guardar",
"save_without_cropping": "Guardar sin recortar",
"cancel": "Cancelar"
},
"importer": {
"submit": "Enviar",
"success": "Importado con éxito",
"error": "Se ha producido un error al importar el archivo."
}, },
"login": { "login": {
"login": "Identificación", "login": "Identificación",
@ -30,6 +49,10 @@
"register": "Registrar", "register": "Registrar",
"username": "Usuario", "username": "Usuario",
"hint": "Inicia sesión para unirte a la discusión" "hint": "Inicia sesión para unirte a la discusión"
},
"media_modal": {
"previous": "Anterior",
"next": "Siguiente"
}, },
"nav": { "nav": {
"about": "Sobre", "about": "Sobre",
@ -61,15 +84,19 @@
"account_not_locked_warning_link": "bloqueada", "account_not_locked_warning_link": "bloqueada",
"attachments_sensitive": "Contenido sensible", "attachments_sensitive": "Contenido sensible",
"content_type": { "content_type": {
"text/plain": "Texto Plano" "text/plain": "Texto Plano",
"text/html": "HTML",
"text/markdown": "Markdown",
"text/bbcode": "BBCode"
}, },
"content_warning": "Tema (opcional)", "content_warning": "Tema (opcional)",
"default": "Acabo de aterrizar en L.A.", "default": "Acabo de aterrizar en L.A.",
"direct_warning": "Esta entrada solo será visible para los usuarios mencionados.", "direct_warning": "Esta publicación solo será visible para los usuarios mencionados.",
"direct_warning_to_first_only": "Esta publicación solo será visible para los usuarios mencionados al comienzo del mensaje.",
"posting": "Publicando", "posting": "Publicando",
"scope": { "scope": {
"direct": "Directo - Solo para los usuarios mencionados.", "direct": "Directo - Solo para los usuarios mencionados.",
"private": "Solo-Seguidores - Solo tus seguidores leeran la entrada", "private": "Solo-Seguidores - Solo tus seguidores leeran la publicación",
"public": "Público - Entradas visibles en las Líneas Temporales Públicas", "public": "Público - Entradas visibles en las Líneas Temporales Públicas",
"unlisted": "Sin Listar - Entradas no visibles en las Líneas Temporales Públicas" "unlisted": "Sin Listar - Entradas no visibles en las Líneas Temporales Públicas"
} }
@ -83,6 +110,9 @@
"token": "Token de invitación", "token": "Token de invitación",
"captcha": "CAPTCHA", "captcha": "CAPTCHA",
"new_captcha": "Click en la imagen para obtener un nuevo captca", "new_captcha": "Click en la imagen para obtener un nuevo captca",
"username_placeholder": "p.ej. lain",
"fullname_placeholder": "p.ej. Lain Iwakura",
"bio_placeholder": "e.g.\nHola, soy un ejemplo.\nAquí puedes poner algo representativo tuyo... o no.",
"validations": { "validations": {
"username_required": "no puede estar vacío", "username_required": "no puede estar vacío",
"fullname_required": "no puede estar vacío", "fullname_required": "no puede estar vacío",
@ -91,8 +121,12 @@
"password_confirmation_required": "no puede estar vacío", "password_confirmation_required": "no puede estar vacío",
"password_confirmation_match": "la contraseña no coincide" "password_confirmation_match": "la contraseña no coincide"
} }
},
"selectable_list": {
"select_all": "Seleccionarlo todo"
}, },
"settings": { "settings": {
"app_name": "Nombre de la aplicación",
"attachmentRadius": "Adjuntos", "attachmentRadius": "Adjuntos",
"attachments": "Adjuntos", "attachments": "Adjuntos",
"autoload": "Activar carga automática al llegar al final de la página", "autoload": "Activar carga automática al llegar al final de la página",
@ -101,6 +135,12 @@
"avatarRadius": "Avatares", "avatarRadius": "Avatares",
"background": "Fondo", "background": "Fondo",
"bio": "Biografía", "bio": "Biografía",
"block_export": "Exportar usuarios bloqueados",
"block_export_button": "Exporta la lista de tus usarios bloqueados a un archivo csv",
"block_import": "Importar usuarios bloqueados",
"block_import_error": "Error importando la lista de usuarios bloqueados",
"blocks_imported": "¡Lista de usuarios bloqueados importada! El procesado puede tardar un poco.",
"blocks_tab": "Bloqueados",
"btnRadius": "Botones", "btnRadius": "Botones",
"cBlue": "Azul (Responder, seguir)", "cBlue": "Azul (Responder, seguir)",
"cGreen": "Verde (Retweet)", "cGreen": "Verde (Retweet)",
@ -127,7 +167,6 @@
"filtering_explanation": "Todos los estados que contengan estas palabras serán silenciados, una por línea", "filtering_explanation": "Todos los estados que contengan estas palabras serán silenciados, una por línea",
"follow_export": "Exportar personas que tú sigues", "follow_export": "Exportar personas que tú sigues",
"follow_export_button": "Exporta tus seguidores a un archivo csv", "follow_export_button": "Exporta tus seguidores a un archivo csv",
"follow_export_processing": "Procesando, en breve se te preguntará para guardar el archivo",
"follow_import": "Importar personas que tú sigues", "follow_import": "Importar personas que tú sigues",
"follow_import_error": "Error al importal el archivo", "follow_import_error": "Error al importal el archivo",
"follows_imported": "¡Importado! Procesarlos llevará tiempo.", "follows_imported": "¡Importado! Procesarlos llevará tiempo.",
@ -135,12 +174,15 @@
"general": "General", "general": "General",
"hide_attachments_in_convo": "Ocultar adjuntos en las conversaciones", "hide_attachments_in_convo": "Ocultar adjuntos en las conversaciones",
"hide_attachments_in_tl": "Ocultar adjuntos en la línea temporal", "hide_attachments_in_tl": "Ocultar adjuntos en la línea temporal",
"hide_muted_posts": "Ocultar las publicaciones de los usuarios silenciados",
"max_thumbnails": "Cantidad máxima de miniaturas por publicación",
"hide_isp": "Ocultar el panel específico de la instancia", "hide_isp": "Ocultar el panel específico de la instancia",
"preload_images": "Precargar las imágenes", "preload_images": "Precargar las imágenes",
"use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.", "use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.",
"hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)", "hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)",
"hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)", "hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
"hide_filtered_statuses": "Ocultar estados filtrados", "hide_filtered_statuses": "Ocultar estados filtrados",
"import_blocks_from_a_csv_file": "Importar lista de usuarios bloqueados dese un archivo csv",
"import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv", "import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv",
"import_theme": "Importar tema", "import_theme": "Importar tema",
"inputRadius": "Campos de entrada", "inputRadius": "Campos de entrada",
@ -155,6 +197,7 @@
"lock_account_description": "Restringir el acceso a tu cuenta solo a seguidores admitidos", "lock_account_description": "Restringir el acceso a tu cuenta solo a seguidores admitidos",
"loop_video": "Vídeos en bucle", "loop_video": "Vídeos en bucle",
"loop_video_silent_only": "Bucle solo en vídeos sin sonido (p.ej. \"gifs\" de Mastodon)", "loop_video_silent_only": "Bucle solo en vídeos sin sonido (p.ej. \"gifs\" de Mastodon)",
"mutes_tab": "Silenciados",
"play_videos_in_modal": "Reproducir los vídeos directamente en el visor de medios", "play_videos_in_modal": "Reproducir los vídeos directamente en el visor de medios",
"use_contain_fit": "No recortar los adjuntos en miniaturas", "use_contain_fit": "No recortar los adjuntos en miniaturas",
"name": "Nombre", "name": "Nombre",
@ -166,6 +209,8 @@
"notification_visibility_mentions": "Menciones", "notification_visibility_mentions": "Menciones",
"notification_visibility_repeats": "Repeticiones (Repeats)", "notification_visibility_repeats": "Repeticiones (Repeats)",
"no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas", "no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas",
"no_blocks": "No hay usuarios bloqueados",
"no_mutes": "No hay usuarios sinlenciados",
"hide_follows_description": "No mostrar a quién sigo", "hide_follows_description": "No mostrar a quién sigo",
"hide_followers_description": "No mostrar quién me sigue", "hide_followers_description": "No mostrar quién me sigue",
"show_admin_badge": "Mostrar la placa de administrador en mi perfil", "show_admin_badge": "Mostrar la placa de administrador en mi perfil",
@ -190,8 +235,11 @@
"reply_visibility_self": "Solo mostrar réplicas para mí", "reply_visibility_self": "Solo mostrar réplicas para mí",
"saving_err": "Error al guardar los ajustes", "saving_err": "Error al guardar los ajustes",
"saving_ok": "Ajustes guardados", "saving_ok": "Ajustes guardados",
"search_user_to_block": "Buscar usuarios a bloquear",
"search_user_to_mute": "Buscar usuarios a silenciar",
"security_tab": "Seguridad", "security_tab": "Seguridad",
"scope_copy": "Copiar la visibilidad cuando contestamos (En los mensajes directos (MDs) siempre se copia)", "scope_copy": "Copiar la visibilidad de la publicación cuando contestamos (En los mensajes directos (MDs) siempre se copia)",
"minimal_scopes_mode": "Minimizar las opciones de publicación",
"set_new_avatar": "Cambiar avatar", "set_new_avatar": "Cambiar avatar",
"set_new_profile_background": "Cambiar fondo del perfil", "set_new_profile_background": "Cambiar fondo del perfil",
"set_new_profile_banner": "Cambiar cabecera del perfil", "set_new_profile_banner": "Cambiar cabecera del perfil",
@ -210,6 +258,7 @@
"theme_help_v2_1": "También puede invalidar los colores y la opacidad de ciertos componentes si activa la casilla de verificación, use el botón \"Borrar todo\" para deshacer los cambios.", "theme_help_v2_1": "También puede invalidar los colores y la opacidad de ciertos componentes si activa la casilla de verificación, use el botón \"Borrar todo\" para deshacer los cambios.",
"theme_help_v2_2": "Los iconos debajo de algunas entradas son indicadores de contraste de fondo/texto, desplace el ratón para obtener información detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.", "theme_help_v2_2": "Los iconos debajo de algunas entradas son indicadores de contraste de fondo/texto, desplace el ratón para obtener información detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.",
"tooltipRadius": "Información/alertas", "tooltipRadius": "Información/alertas",
"upload_a_photo": "Subir una foto",
"user_settings": "Ajustes de Usuario", "user_settings": "Ajustes de Usuario",
"values": { "values": {
"false": "no", "false": "no",
@ -325,6 +374,11 @@
"checkbox": "He revisado los términos y condiciones", "checkbox": "He revisado los términos y condiciones",
"link": "un bonito enlace" "link": "un bonito enlace"
} }
},
"version": {
"title": "Versión",
"backend_version": "Versión del Backend",
"frontend_version": "Versión del Frontend"
} }
}, },
"timeline": { "timeline": {
@ -336,7 +390,14 @@
"repeated": "repetida", "repeated": "repetida",
"show_new": "Mostrar lo nuevo", "show_new": "Mostrar lo nuevo",
"up_to_date": "Actualizado", "up_to_date": "Actualizado",
"no_more_statuses": "No hay más estados" "no_more_statuses": "No hay más estados",
"no_statuses": "Sin estados"
},
"status": {
"favorites": "Favoritos",
"repeats": "Repetidos",
"reply_to": "Responder a",
"replies_list": "Respuestas:"
}, },
"user_card": { "user_card": {
"approve": "Aprovar", "approve": "Aprovar",
@ -359,10 +420,47 @@
"muted": "Silenciado", "muted": "Silenciado",
"per_day": "por día", "per_day": "por día",
"remote_follow": "Seguir", "remote_follow": "Seguir",
"statuses": "Estados" "report": "Reportar",
"statuses": "Estados",
"unblock": "Desbloquear",
"unblock_progress": "Desbloqueando...",
"block_progress": "Bloqueando...",
"unmute": "Desenmudecer",
"unmute_progress": "Sesenmudeciendo...",
"mute_progress": "Silenciando...",
"admin_menu": {
"moderation": "Moderación",
"grant_admin": "Conceder permisos de Administrador",
"revoke_admin": "Revocar permisos de Administrador",
"grant_moderator": "Conceder permisos de Moderador",
"revoke_moderator": "Revocar permisos de Moderador",
"activate_account": "Activar cuenta",
"deactivate_account": "Desactivar cuenta",
"delete_account": "Borrar cuenta",
"force_nsfw": "Marcar todas las publicaciones como NSFW (no es seguro/apropiado para el trabajo)",
"strip_media": "Eliminar archivos multimedia de las publicaciones",
"force_unlisted": "Forzar que se publique en el modo -Sin Listar-",
"sandbox": "Forzar que se publique solo para tus seguidores",
"disable_remote_subscription": "No permitir que usuarios de instancias remotas te siga.",
"disable_any_subscription": "No permitir que ningún usuario te siga",
"quarantine": "No permitir publicaciones de usuarios de instancias remotas",
"delete_user": "Borrar usuario",
"delete_user_confirmation": "¿Estás completamente seguro? Esta acción no se puede deshacer."
}
}, },
"user_profile": { "user_profile": {
"timeline_title": "Linea temporal del usuario" "timeline_title": "Linea temporal del usuario",
"profile_does_not_exist": "Lo sentimos, este perfil no existe.",
"profile_loading_error": "Lo sentimos, hubo un error al cargar este perfil."
},
"user_reporting": {
"title": "Reportando a {0}",
"add_comment_description": "El informe será enviado a los moderadores de su instancia. Puedes proporcionar una explicación de por qué estás reportando esta cuenta a continuación:",
"additional_comments": "Comentarios adicionales",
"forward_description": "La cuenta es de otro servidor. ¿Enviar una copia del informe allí también?",
"forward_to": "Reenviar a {0}",
"submit": "Enviar",
"generic_error": "Se produjo un error al procesar la solicitud."
}, },
"who_to_follow": { "who_to_follow": {
"more": "Más", "more": "Más",

View file

@ -222,6 +222,8 @@
"no_more_statuses": "Ei enempää viestejä" "no_more_statuses": "Ei enempää viestejä"
}, },
"status": { "status": {
"favorites": "Tykkäykset",
"repeats": "Toistot",
"reply_to": "Vastaus", "reply_to": "Vastaus",
"replies_list": "Vastaukset:" "replies_list": "Vastaukset:"
}, },

View file

@ -1,6 +1,10 @@
{ {
"chat": { "chat": {
"title": "Messatjariá" "title": "Messatjariá"
},
"exporter": {
"export": "Exportar",
"processing": "Tractament, vos demandarem lèu de telecargar lo fichièr"
}, },
"features_panel": { "features_panel": {
"chat": "Chat", "chat": "Chat",
@ -30,6 +34,11 @@
"save": "Salvar", "save": "Salvar",
"save_without_cropping": "Salvar sens talhada", "save_without_cropping": "Salvar sens talhada",
"cancel": "Anullar" "cancel": "Anullar"
},
"importer": {
"submit": "Mandar",
"success": "Corrèctament importat.",
"error": "Una error ses producha pendent limportacion daqueste fichièr."
}, },
"login": { "login": {
"login": "Connexion", "login": "Connexion",
@ -126,6 +135,11 @@
"avatarRadius": "Avatars", "avatarRadius": "Avatars",
"background": "Rèire plan", "background": "Rèire plan",
"bio": "Biografia", "bio": "Biografia",
"block_export": "Exportar los blocatges",
"block_export_button": "Exportar los blocatges dins un fichièr csv",
"block_import": "Impòrt de blocatges",
"block_import_error": "Error en importar los blocatges",
"blocks_imported": "Blocatges importats ! Lo tractament tardarà un pauc.",
"blocks_tab": "Blocatges", "blocks_tab": "Blocatges",
"btnRadius": "Botons", "btnRadius": "Botons",
"cBlue": "Blau (Respondre, seguir)", "cBlue": "Blau (Respondre, seguir)",
@ -153,7 +167,6 @@
"filtering_explanation": "Totes los estatuts amb aqueles mots seràn en silenci, un mot per linha", "filtering_explanation": "Totes los estatuts amb aqueles mots seràn en silenci, un mot per linha",
"follow_export": "Exportar los abonaments", "follow_export": "Exportar los abonaments",
"follow_export_button": "Exportar vòstres abonaments dins un fichièr csv", "follow_export_button": "Exportar vòstres abonaments dins un fichièr csv",
"follow_export_processing": "Tractament, vos demandarem lèu de telecargar lo fichièr",
"follow_import": "Importar los abonaments", "follow_import": "Importar los abonaments",
"follow_import_error": "Error en important los seguidors", "follow_import_error": "Error en important los seguidors",
"follows_imported": "Seguidors importats. Lo tractament pòt trigar una estona.", "follows_imported": "Seguidors importats. Lo tractament pòt trigar una estona.",
@ -188,6 +201,7 @@
"use_contain_fit": "Talhar pas las pèças juntas per las vinhetas", "use_contain_fit": "Talhar pas las pèças juntas per las vinhetas",
"name": "Nom", "name": "Nom",
"name_bio": "Nom & Bio", "name_bio": "Nom & Bio",
"new_password": "Nòu senhal", "new_password": "Nòu senhal",
"notification_visibility_follows": "Abonaments", "notification_visibility_follows": "Abonaments",
"notification_visibility_likes": "Aimar", "notification_visibility_likes": "Aimar",
@ -380,6 +394,8 @@
"no_statuses": "Cap destatuts" "no_statuses": "Cap destatuts"
}, },
"status": { "status": {
"favorites": "Li a agradat",
"repeats": "A repetit",
"reply_to": "Respond a", "reply_to": "Respond a",
"replies_list": "Responsas:" "replies_list": "Responsas:"
}, },

View file

@ -363,7 +363,7 @@
"error_fetching": "Błąd pobierania", "error_fetching": "Błąd pobierania",
"load_older": "Załaduj starsze statusy", "load_older": "Załaduj starsze statusy",
"no_retweet_hint": "Wpis oznaczony jako tylko dla obserwujących lub bezpośredni nie może zostać powtórzony", "no_retweet_hint": "Wpis oznaczony jako tylko dla obserwujących lub bezpośredni nie może zostać powtórzony",
"repeated": "powtórzono", "repeated": "powtórzył(-a)",
"show_new": "Pokaż nowe", "show_new": "Pokaż nowe",
"up_to_date": "Na bieżąco", "up_to_date": "Na bieżąco",
"no_more_statuses": "Brak kolejnych statusów", "no_more_statuses": "Brak kolejnych statusów",

View file

@ -42,8 +42,13 @@
"attachments_sensitive": "Вложения содержат чувствительный контент", "attachments_sensitive": "Вложения содержат чувствительный контент",
"content_warning": "Тема (не обязательно)", "content_warning": "Тема (не обязательно)",
"default": "Что нового?", "default": "Что нового?",
"direct_warning": "Этот пост будет видет только упомянутым пользователям", "direct_warning": "Этот пост будет виден только упомянутым пользователям",
"posting": "Отправляется", "posting": "Отправляется",
"scope_notice": {
"public": "Этот пост будет виден всем",
"private": "Этот пост будет виден только вашим подписчикам",
"unlisted": "Этот пост не будет виден в публичной и федеративной ленте"
},
"scope": { "scope": {
"direct": "Личное - этот пост видят только те кто в нём упомянут", "direct": "Личное - этот пост видят только те кто в нём упомянут",
"private": "Для подписчиков - этот пост видят только подписчики", "private": "Для подписчиков - этот пост видят только подписчики",
@ -152,6 +157,7 @@
"reply_visibility_all": "Показывать все ответы", "reply_visibility_all": "Показывать все ответы",
"reply_visibility_following": "Показывать только ответы мне и тех на кого я подписан", "reply_visibility_following": "Показывать только ответы мне и тех на кого я подписан",
"reply_visibility_self": "Показывать только ответы мне", "reply_visibility_self": "Показывать только ответы мне",
"autohide_floating_post_button": "Автоматически скрывать кнопку постинга (в мобильной версии)",
"saving_err": "Не удалось сохранить настройки", "saving_err": "Не удалось сохранить настройки",
"saving_ok": "Сохранено", "saving_ok": "Сохранено",
"security_tab": "Безопасность", "security_tab": "Безопасность",

View file

@ -12,6 +12,7 @@ import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js' import oauthModule from './modules/oauth.js'
import mediaViewerModule from './modules/media_viewer.js' import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js' import oauthTokensModule from './modules/oauth_tokens.js'
import reportsModule from './modules/reports.js'
import VueTimeago from 'vue-timeago' import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
@ -75,7 +76,8 @@ const persistedStateOptions = {
chat: chatModule, chat: chatModule,
oauth: oauthModule, oauth: oauthModule,
mediaViewer: mediaViewerModule, mediaViewer: mediaViewerModule,
oauthTokens: oauthTokensModule oauthTokens: oauthTokensModule,
reports: reportsModule
}, },
plugins: [persistedState, pushNotifications], plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now. strict: false // Socket modifies itself, let's ignore this for now.

View file

@ -17,6 +17,7 @@ const defaultState = {
autoLoad: true, autoLoad: true,
streaming: false, streaming: false,
hoverPreview: true, hoverPreview: true,
autohideFloatingPostButton: false,
pauseOnUnfocused: true, pauseOnUnfocused: true,
stopGifs: false, stopGifs: false,
replyVisibility: 'all', replyVisibility: 'all',
@ -30,6 +31,7 @@ const defaultState = {
muteWords: [], muteWords: [],
highlight: {}, highlight: {},
interfaceLanguage: browserLocale, interfaceLanguage: browserLocale,
hideScopeNotice: false,
scopeCopy: undefined, // instance default scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default

30
src/modules/reports.js Normal file
View file

@ -0,0 +1,30 @@
import filter from 'lodash/filter'
const reports = {
state: {
userId: null,
statuses: [],
modalActivated: false
},
mutations: {
openUserReportingModal (state, { userId, statuses }) {
state.userId = userId
state.statuses = statuses
state.modalActivated = true
},
closeUserReportingModal (state) {
state.modalActivated = false
}
},
actions: {
openUserReportingModal ({ rootState, commit }, userId) {
const statuses = filter(rootState.statuses.allStatuses, status => status.user.id === userId)
commit('openUserReportingModal', { userId, statuses })
},
closeUserReportingModal ({ commit }) {
commit('closeUserReportingModal')
}
}
}
export default reports

View file

@ -1,4 +1,4 @@
import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash' import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
import { set } from 'vue' import { set } from 'vue'
import apiService from '../services/api/api.service.js' import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js' // import parse from '../services/status_parser/status_parser.js'
@ -402,12 +402,27 @@ export const mutations = {
}, },
setFavorited (state, { status, value }) { setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
if (newStatus.favorited !== value) {
if (value) {
newStatus.fave_num++
} else {
newStatus.fave_num--
}
}
newStatus.favorited = value newStatus.favorited = value
}, },
setFavoritedConfirm (state, { status }) { setFavoritedConfirm (state, { status, user }) {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = status.favorited newStatus.favorited = status.favorited
newStatus.fave_num = status.fave_num newStatus.fave_num = status.fave_num
const index = findIndex(newStatus.favoritedBy, { id: user.id })
if (index !== -1 && !newStatus.favorited) {
newStatus.favoritedBy.splice(index, 1)
} else if (index === -1 && newStatus.favorited) {
newStatus.favoritedBy.push(user)
}
}, },
setRetweeted (state, { status, value }) { setRetweeted (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
@ -422,6 +437,17 @@ export const mutations = {
newStatus.repeated = value newStatus.repeated = value
}, },
setRetweetedConfirm (state, { status, user }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.repeated = status.repeated
newStatus.repeat_num = status.repeat_num
const index = findIndex(newStatus.rebloggedBy, { id: user.id })
if (index !== -1 && !newStatus.repeated) {
newStatus.rebloggedBy.splice(index, 1)
} else if (index === -1 && newStatus.repeated) {
newStatus.rebloggedBy.push(user)
}
},
setDeleted (state, { status }) { setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
newStatus.deleted = true newStatus.deleted = true
@ -459,6 +485,11 @@ export const mutations = {
}, },
queueFlush (state, { timeline, id }) { queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = id state.timelines[timeline].flushMarker = id
},
addFavsAndRepeats (state, { id, favoritedByUsers, rebloggedByUsers }) {
const newStatus = state.allStatusesObject[id]
newStatus.favoritedBy = favoritedByUsers.filter(_ => _)
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
} }
} }
@ -493,27 +524,26 @@ const statuses = {
favorite ({ rootState, commit }, status) { favorite ({ rootState, commit }, status) {
// Optimistic favoriting... // Optimistic favoriting...
commit('setFavorited', { status, value: true }) commit('setFavorited', { status, value: true })
apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials }) rootState.api.backendInteractor.favorite(status.id)
.then(status => { .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
commit('setFavoritedConfirm', { status })
})
}, },
unfavorite ({ rootState, commit }, status) { unfavorite ({ rootState, commit }, status) {
// Optimistic favoriting... // Optimistic unfavoriting...
commit('setFavorited', { status, value: false }) commit('setFavorited', { status, value: false })
apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials }) rootState.api.backendInteractor.unfavorite(status.id)
.then(status => { .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
commit('setFavoritedConfirm', { status })
})
}, },
retweet ({ rootState, commit }, status) { retweet ({ rootState, commit }, status) {
// Optimistic retweeting... // Optimistic retweeting...
commit('setRetweeted', { status, value: true }) commit('setRetweeted', { status, value: true })
apiService.retweet({ id: status.id, credentials: rootState.users.currentUser.credentials }) rootState.api.backendInteractor.retweet(status.id)
.then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
}, },
unretweet ({ rootState, commit }, status) { unretweet ({ rootState, commit }, status) {
// Optimistic unretweeting...
commit('setRetweeted', { status, value: false }) commit('setRetweeted', { status, value: false })
apiService.unretweet({ id: status.id, credentials: rootState.users.currentUser.credentials }) rootState.api.backendInteractor.unretweet(status.id)
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
}, },
queueFlush ({ rootState, commit }, { timeline, id }) { queueFlush ({ rootState, commit }, { timeline, id }) {
commit('queueFlush', { timeline, id }) commit('queueFlush', { timeline, id })
@ -524,6 +554,14 @@ const statuses = {
id: rootState.statuses.notifications.maxId, id: rootState.statuses.notifications.maxId,
credentials: rootState.users.currentUser.credentials credentials: rootState.users.currentUser.credentials
}) })
},
fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers(id),
rootState.api.backendInteractor.fetchRebloggedByUsers(id)
]).then(([favoritedByUsers, rebloggedByUsers]) =>
commit('addFavsAndRepeats', { id, favoritedByUsers, rebloggedByUsers })
)
} }
}, },
mutations mutations

View file

@ -3,12 +3,10 @@ const LOGIN_URL = '/api/account/verify_credentials.json'
const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing' const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing'
const MENTIONS_URL = '/api/statuses/mentions.json' const MENTIONS_URL = '/api/statuses/mentions.json'
const REGISTRATION_URL = '/api/account/register.json' const REGISTRATION_URL = '/api/account/register.json'
const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json'
const BG_UPDATE_URL = '/api/qvitter/update_background_image.json' const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
const BANNER_UPDATE_URL = '/api/account/update_profile_banner.json'
const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json' const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
@ -49,6 +47,10 @@ const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute` const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
const MASTODON_POST_STATUS_URL = '/api/v1/statuses' const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
const MASTODON_STATUS_FAVORITEDBY_URL = id => `/api/v1/statuses/${id}/favourited_by`
const MASTODON_STATUS_REBLOGGEDBY_URL = id => `/api/v1/statuses/${id}/reblogged_by`
const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
const MASTODON_REPORT_USER_URL = '/api/v1/reports'
import { each, map, concat, last } from 'lodash' import { each, map, concat, last } from 'lodash'
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js' import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
@ -65,7 +67,24 @@ let fetch = (url, options) => {
return oldfetch(fullUrl, options) return oldfetch(fullUrl, options)
} }
const promisedRequest = (url, options) => { const promisedRequest = ({ method, url, payload, credentials, headers = {} }) => {
const options = {
method,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
...headers
}
}
if (payload) {
options.body = JSON.stringify(payload)
}
if (credentials) {
options.headers = {
...options.headers,
...authHeaders(credentials)
}
}
return fetch(url, options) return fetch(url, options)
.then((response) => { .then((response) => {
return new Promise((resolve, reject) => response.json() return new Promise((resolve, reject) => response.json()
@ -78,28 +97,16 @@ const promisedRequest = (url, options) => {
}) })
} }
// Params const updateAvatar = ({credentials, avatar}) => {
// cropH
// cropW
// cropX
// cropY
// img (base 64 encodend data url)
const updateAvatar = ({credentials, params}) => {
let url = AVATAR_UPDATE_URL
const form = new FormData() const form = new FormData()
form.append('avatar', avatar)
each(params, (value, key) => { return fetch(MASTODON_PROFILE_UPDATE_URL, {
if (value) {
form.append(key, value)
}
})
return fetch(url, {
headers: authHeaders(credentials), headers: authHeaders(credentials),
method: 'POST', method: 'PATCH',
body: form body: form
}).then((data) => data.json()) })
.then((data) => data.json())
.then((data) => parseUser(data))
} }
const updateBg = ({credentials, params}) => { const updateBg = ({credentials, params}) => {
@ -120,52 +127,26 @@ const updateBg = ({credentials, params}) => {
}).then((data) => data.json()) }).then((data) => data.json())
} }
// Params const updateBanner = ({credentials, banner}) => {
// height
// width
// offset_left
// offset_top
// banner (base 64 encodend data url)
const updateBanner = ({credentials, params}) => {
let url = BANNER_UPDATE_URL
const form = new FormData() const form = new FormData()
form.append('header', banner)
each(params, (value, key) => { return fetch(MASTODON_PROFILE_UPDATE_URL, {
if (value) {
form.append(key, value)
}
})
return fetch(url, {
headers: authHeaders(credentials), headers: authHeaders(credentials),
method: 'POST', method: 'PATCH',
body: form body: form
}).then((data) => data.json()) })
.then((data) => data.json())
.then((data) => parseUser(data))
} }
// Params
// name
// url
// location
// description
const updateProfile = ({credentials, params}) => { const updateProfile = ({credentials, params}) => {
// Always include these fields, because they might be empty or false return promisedRequest({
const fields = ['description', 'locked', 'no_rich_text', 'hide_follows', 'hide_followers', 'show_role'] url: MASTODON_PROFILE_UPDATE_URL,
let url = PROFILE_UPDATE_URL method: 'PATCH',
payload: params,
const form = new FormData() credentials
each(params, (value, key) => {
if (fields.includes(key) || value) {
form.append(key, value)
}
}) })
return fetch(url, { .then((data) => parseUser(data))
headers: authHeaders(credentials),
method: 'POST',
body: form
}).then((data) => data.json())
} }
// Params needed: // Params needed:
@ -261,7 +242,7 @@ const denyUser = ({id, credentials}) => {
const fetchUser = ({id, credentials}) => { const fetchUser = ({id, credentials}) => {
let url = `${MASTODON_USER_URL}/${id}` let url = `${MASTODON_USER_URL}/${id}`
return promisedRequest(url, { headers: authHeaders(credentials) }) return promisedRequest({ url, credentials })
.then((data) => parseUser(data)) .then((data) => parseUser(data))
} }
@ -525,62 +506,22 @@ const verifyCredentials = (user) => {
} }
const favorite = ({ id, credentials }) => { const favorite = ({ id, credentials }) => {
return fetch(MASTODON_FAVORITE_URL(id), { return promisedRequest({ url: MASTODON_FAVORITE_URL(id), method: 'POST', credentials })
headers: authHeaders(credentials),
method: 'POST'
})
.then(response => {
if (response.ok) {
return response.json()
} else {
throw new Error('Error favoriting post')
}
})
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
const unfavorite = ({ id, credentials }) => { const unfavorite = ({ id, credentials }) => {
return fetch(MASTODON_UNFAVORITE_URL(id), { return promisedRequest({ url: MASTODON_UNFAVORITE_URL(id), method: 'POST', credentials })
headers: authHeaders(credentials),
method: 'POST'
})
.then(response => {
if (response.ok) {
return response.json()
} else {
throw new Error('Error removing favorite')
}
})
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
const retweet = ({ id, credentials }) => { const retweet = ({ id, credentials }) => {
return fetch(MASTODON_RETWEET_URL(id), { return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', credentials })
headers: authHeaders(credentials),
method: 'POST'
})
.then(response => {
if (response.ok) {
return response.json()
} else {
throw new Error('Error repeating post')
}
})
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
const unretweet = ({ id, credentials }) => { const unretweet = ({ id, credentials }) => {
return fetch(MASTODON_UNRETWEET_URL(id), { return promisedRequest({ url: MASTODON_UNRETWEET_URL(id), method: 'POST', credentials })
headers: authHeaders(credentials),
method: 'POST'
})
.then(response => {
if (response.ok) {
return response.json()
} else {
throw new Error('Error removing repeat')
}
})
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
@ -634,9 +575,22 @@ const uploadMedia = ({formData, credentials}) => {
.then((data) => parseAttachment(data)) .then((data) => parseAttachment(data))
} }
const followImport = ({params, credentials}) => { const importBlocks = ({file, credentials}) => {
const formData = new FormData()
formData.append('list', file)
return fetch(BLOCKS_IMPORT_URL, {
body: formData,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.ok)
}
const importFollows = ({file, credentials}) => {
const formData = new FormData()
formData.append('list', file)
return fetch(FOLLOW_IMPORT_URL, { return fetch(FOLLOW_IMPORT_URL, {
body: params, body: formData,
method: 'POST', method: 'POST',
headers: authHeaders(credentials) headers: authHeaders(credentials)
}) })
@ -672,26 +626,20 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma
} }
const fetchMutes = ({credentials}) => { const fetchMutes = ({credentials}) => {
return promisedRequest(MASTODON_USER_MUTES_URL, { headers: authHeaders(credentials) }) return promisedRequest({ url: MASTODON_USER_MUTES_URL, credentials })
.then((users) => users.map(parseUser)) .then((users) => users.map(parseUser))
} }
const muteUser = ({id, credentials}) => { const muteUser = ({id, credentials}) => {
return promisedRequest(MASTODON_MUTE_USER_URL(id), { return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST' })
headers: authHeaders(credentials),
method: 'POST'
})
} }
const unmuteUser = ({id, credentials}) => { const unmuteUser = ({id, credentials}) => {
return promisedRequest(MASTODON_UNMUTE_USER_URL(id), { return promisedRequest({ url: MASTODON_UNMUTE_USER_URL(id), credentials, method: 'POST' })
headers: authHeaders(credentials),
method: 'POST'
})
} }
const fetchBlocks = ({credentials}) => { const fetchBlocks = ({credentials}) => {
return promisedRequest(MASTODON_USER_BLOCKS_URL, { headers: authHeaders(credentials) }) return promisedRequest({ url: MASTODON_USER_BLOCKS_URL, credentials })
.then((users) => users.map(parseUser)) .then((users) => users.map(parseUser))
} }
@ -735,6 +683,28 @@ const markNotificationsAsSeen = ({id, credentials}) => {
}).then((data) => data.json()) }).then((data) => data.json())
} }
const fetchFavoritedByUsers = ({id}) => {
return promisedRequest({ url: MASTODON_STATUS_FAVORITEDBY_URL(id) }).then((users) => users.map(parseUser))
}
const fetchRebloggedByUsers = ({id}) => {
return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
}
const reportUser = ({credentials, userId, statusIds, comment, forward}) => {
return promisedRequest({
url: MASTODON_REPORT_USER_URL,
method: 'POST',
payload: {
'account_id': userId,
'status_ids': statusIds,
comment,
forward
},
credentials
})
}
const apiService = { const apiService = {
verifyCredentials, verifyCredentials,
fetchTimeline, fetchTimeline,
@ -776,14 +746,18 @@ const apiService = {
updateProfile, updateProfile,
updateBanner, updateBanner,
externalProfile, externalProfile,
followImport, importBlocks,
importFollows,
deleteAccount, deleteAccount,
changePassword, changePassword,
fetchFollowRequests, fetchFollowRequests,
approveUser, approveUser,
denyUser, denyUser,
suggestions, suggestions,
markNotificationsAsSeen markNotificationsAsSeen,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser
} }
export default apiService export default apiService

View file

@ -101,17 +101,27 @@ const backendInteractorService = (credentials) => {
const getCaptcha = () => apiService.getCaptcha() const getCaptcha = () => apiService.getCaptcha()
const register = (params) => apiService.register(params) const register = (params) => apiService.register(params)
const updateAvatar = ({params}) => apiService.updateAvatar({credentials, params}) const updateAvatar = ({avatar}) => apiService.updateAvatar({credentials, avatar})
const updateBg = ({params}) => apiService.updateBg({credentials, params}) const updateBg = ({params}) => apiService.updateBg({credentials, params})
const updateBanner = ({params}) => apiService.updateBanner({credentials, params}) const updateBanner = ({banner}) => apiService.updateBanner({credentials, banner})
const updateProfile = ({params}) => apiService.updateProfile({credentials, params}) const updateProfile = ({params}) => apiService.updateProfile({credentials, params})
const externalProfile = (profileUrl) => apiService.externalProfile({profileUrl, credentials}) const externalProfile = (profileUrl) => apiService.externalProfile({profileUrl, credentials})
const followImport = ({params}) => apiService.followImport({params, credentials}) const importBlocks = (file) => apiService.importBlocks({file, credentials})
const importFollows = (file) => apiService.importFollows({file, credentials})
const deleteAccount = ({password}) => apiService.deleteAccount({credentials, password}) const deleteAccount = ({password}) => apiService.deleteAccount({credentials, password})
const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation}) const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation})
const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({id})
const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({id})
const reportUser = (params) => apiService.reportUser({credentials, ...params})
const favorite = (id) => apiService.favorite({id, credentials})
const unfavorite = (id) => apiService.unfavorite({id, credentials})
const retweet = (id) => apiService.retweet({id, credentials})
const unretweet = (id) => apiService.unretweet({id, credentials})
const backendInteractorServiceInstance = { const backendInteractorServiceInstance = {
fetchStatus, fetchStatus,
fetchConversation, fetchConversation,
@ -147,12 +157,20 @@ const backendInteractorService = (credentials) => {
updateBanner, updateBanner,
updateProfile, updateProfile,
externalProfile, externalProfile,
followImport, importBlocks,
importFollows,
deleteAccount, deleteAccount,
changePassword, changePassword,
fetchFollowRequests, fetchFollowRequests,
approveUser, approveUser,
denyUser denyUser,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser,
favorite,
unfavorite,
retweet,
unretweet
} }
return backendInteractorServiceInstance return backendInteractorServiceInstance

View file

@ -275,6 +275,9 @@ export const parseStatus = (data) => {
output.retweeted_status = parseStatus(retweetedStatus) output.retweeted_status = parseStatus(retweetedStatus)
} }
output.favoritedBy = []
output.rebloggedBy = []
return output return output
} }
@ -306,7 +309,7 @@ export const parseNotification = (data) => {
} }
output.created_at = new Date(data.created_at) output.created_at = new Date(data.created_at)
output.id = data.id output.id = parseInt(data.id)
return output return output
} }