aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/App.js4
-rw-r--r--src/App.scss16
-rw-r--r--src/App.vue17
-rw-r--r--src/components/avatar_list/avatar_list.js21
-rw-r--r--src/components/avatar_list/avatar_list.vue38
-rw-r--r--src/components/basic_user_card/basic_user_card.vue21
-rw-r--r--src/components/conversation/conversation.js1
-rw-r--r--src/components/conversation/conversation.vue2
-rw-r--r--src/components/exporter/exporter.js48
-rw-r--r--src/components/exporter/exporter.vue20
-rw-r--r--src/components/image_cropper/image_cropper.js16
-rw-r--r--src/components/image_cropper/image_cropper.vue4
-rw-r--r--src/components/importer/importer.js53
-rw-r--r--src/components/importer/importer.vue28
-rw-r--r--src/components/media_modal/media_modal.vue2
-rw-r--r--src/components/mobile_nav/mobile_nav.js5
-rw-r--r--src/components/mobile_nav/mobile_nav.vue42
-rw-r--r--src/components/mobile_post_status_modal/mobile_post_status_modal.js59
-rw-r--r--src/components/mobile_post_status_modal/mobile_post_status_modal.vue2
-rw-r--r--src/components/notification/notification.vue2
-rw-r--r--src/components/notifications/notifications.js4
-rw-r--r--src/components/post_status_form/post_status_form.js6
-rw-r--r--src/components/post_status_form/post_status_form.vue20
-rw-r--r--src/components/selectable_list/selectable_list.vue4
-rw-r--r--src/components/settings/settings.js4
-rw-r--r--src/components/settings/settings.vue4
-rw-r--r--src/components/status/status.js23
-rw-r--r--src/components/status/status.vue83
-rw-r--r--src/components/timeline/timeline.vue2
-rw-r--r--src/components/user_avatar/user_avatar.js2
-rw-r--r--src/components/user_avatar/user_avatar.vue4
-rw-r--r--src/components/user_card/user_card.js3
-rw-r--r--src/components/user_card/user_card.vue12
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.js106
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.vue157
-rw-r--r--src/components/user_settings/user_settings.js169
-rw-r--r--src/components/user_settings/user_settings.vue28
-rw-r--r--src/i18n/en.json34
-rw-r--r--src/i18n/es.json118
-rw-r--r--src/i18n/fi.json2
-rw-r--r--src/i18n/oc.json20
-rw-r--r--src/i18n/pl.json2
-rw-r--r--src/i18n/ru.json8
-rw-r--r--src/main.js4
-rw-r--r--src/modules/config.js2
-rw-r--r--src/modules/reports.js30
-rw-r--r--src/modules/statuses.js64
-rw-r--r--src/services/api/api.service.js216
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js28
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js5
50 files changed, 1194 insertions, 371 deletions
diff --git a/src/App.js b/src/App.js
index 46145b16..e72c73e3 100644
--- a/src/App.js
+++ b/src/App.js
@@ -10,6 +10,7 @@ import MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.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'
export default {
@@ -26,7 +27,8 @@ export default {
MediaModal,
SideDrawer,
MobilePostStatusModal,
- MobileNav
+ MobileNav,
+ UserReportingModal
},
data: () => ({
mobileActivePanel: 'timeline',
diff --git a/src/App.scss b/src/App.scss
index b1c65ade..2729e0b0 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -379,6 +379,7 @@ main-router {
.panel-heading {
display: flex;
+ flex: none;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
background-size: cover;
@@ -647,6 +648,19 @@ nav {
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 {
from {
background-color: rgba(0, 0, 0, 0);
@@ -793,4 +807,4 @@ nav {
background-color: var(--lightBg, $fallback--fg);
}
}
-} \ No newline at end of file
+}
diff --git a/src/App.vue b/src/App.vue
index 3b8623ad..21abd694 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -18,17 +18,19 @@
</div>
</div>
</nav>
- <div v-if="" class="container" id="content">
- <div class="sidebar-flexer mobile-hidden" v-if="!isMobileLayout">
+ <div class="container" id="content">
+ <div class="sidebar-flexer mobile-hidden">
<div class="sidebar-bounds">
<div class="sidebar-scroller">
<div class="sidebar">
<user-panel></user-panel>
- <nav-panel></nav-panel>
- <instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
- <features-panel v-if="!currentUser && showFeaturesPanel"></features-panel>
- <who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>
- <notifications v-if="currentUser"></notifications>
+ <div v-if="!isMobileLayout">
+ <nav-panel></nav-panel>
+ <instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
+ <features-panel v-if="!currentUser && showFeaturesPanel"></features-panel>
+ <who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>
+ <notifications v-if="currentUser"></notifications>
+ </div>
</div>
</div>
</div>
@@ -46,6 +48,7 @@
<media-modal></media-modal>
</div>
<chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
+ <UserReportingModal />
</div>
</template>
diff --git a/src/components/avatar_list/avatar_list.js b/src/components/avatar_list/avatar_list.js
new file mode 100644
index 00000000..9b6301b2
--- /dev/null
+++ b/src/components/avatar_list/avatar_list.js
@@ -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
diff --git a/src/components/avatar_list/avatar_list.vue b/src/components/avatar_list/avatar_list.vue
new file mode 100644
index 00000000..c0238570
--- /dev/null
+++ b/src/components/avatar_list/avatar_list.vue
@@ -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>
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 48de6678..634d62b3 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -1,7 +1,11 @@
<template>
<div class="basic-user-card">
<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>
<div class="basic-user-card-expanded-content" v-if="userExpanded">
<UserCard :user="user" :rounded="true" :bordered="true"/>
@@ -44,14 +48,15 @@
width: 16px;
vertical-align: middle;
}
+ }
- &-value {
- display: inline-block;
- max-width: 100%;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
+ &-user-name-value,
+ &-screen-name {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
}
&-expanded-content {
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 30600f73..ffeb7244 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -139,6 +139,7 @@ const conversation = {
},
setHighlight (id) {
this.highlight = id
+ this.$store.dispatch('fetchFavsAndRepeats', id)
},
getHighlight () {
return this.isExpanded ? this.highlight : null
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index c3bbb597..d04ff722 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -11,7 +11,7 @@
@goto="setHighlight"
@toggleExpanded="toggleExpanded"
:key="status.id"
- :inlineExpanded="collapsable"
+ :inlineExpanded="collapsable && isExpanded"
:statusoid="status"
:expandable='!isExpanded'
:focused="focused(status.id)"
diff --git a/src/components/exporter/exporter.js b/src/components/exporter/exporter.js
new file mode 100644
index 00000000..8f507416
--- /dev/null
+++ b/src/components/exporter/exporter.js
@@ -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
diff --git a/src/components/exporter/exporter.vue b/src/components/exporter/exporter.vue
new file mode 100644
index 00000000..f22e579e
--- /dev/null
+++ b/src/components/exporter/exporter.vue
@@ -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>
diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js
index 5ba8f04e..01361e25 100644
--- a/src/components/image_cropper/image_cropper.js
+++ b/src/components/image_cropper/image_cropper.js
@@ -70,22 +70,10 @@ const ImageCropper = {
this.dataUrl = undefined
this.$emit('close')
},
- submit () {
+ submit (cropping = true) {
this.submitting = true
this.avatarUploadError = null
- this.submitHandler(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)
+ this.submitHandler(cropping && this.cropper, this.file)
.then(() => this.destroy())
.catch((err) => {
this.submitError = err
diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue
index 129e6f46..d2b86e9e 100644
--- a/src/components/image_cropper/image_cropper.vue
+++ b/src/components/image_cropper/image_cropper.vue
@@ -5,9 +5,9 @@
<img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
</div>
<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="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>
</div>
<div class="alert error" v-if="submitError">
diff --git a/src/components/importer/importer.js b/src/components/importer/importer.js
new file mode 100644
index 00000000..c5f9e4d2
--- /dev/null
+++ b/src/components/importer/importer.js
@@ -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
diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue
new file mode 100644
index 00000000..0c5aa93d
--- /dev/null
+++ b/src/components/importer/importer.vue
@@ -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>
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
index 7f666603..a4c12d74 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -33,6 +33,8 @@
@import '../../_variables.scss';
.media-modal-view {
+ z-index: 1001;
+
&:hover {
.modal-view-button-arrow {
opacity: 0.75;
diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js
index bc63d2ba..9b341a3b 100644
--- a/src/components/mobile_nav/mobile_nav.js
+++ b/src/components/mobile_nav/mobile_nav.js
@@ -63,6 +63,11 @@ const MobileNav = {
},
markNotificationsAsSeen () {
this.$refs.notifications.markAsSeen()
+ },
+ onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
+ if (this.$store.state.config.autoLoad && scrollTop + clientHeight >= scrollHeight) {
+ this.$refs.notifications.fetchOlderNotifications()
+ }
}
},
watch: {
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index 5fa41638..90707ce7 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -1,25 +1,26 @@
<template>
- <nav class='nav-bar container' id="nav">
- <div class='mobile-inner-nav' @click="scrollToTop()">
- <div class='item'>
- <a href="#" class="mobile-nav-button" @click.stop.prevent="toggleMobileSidebar()">
- <i class="button-icon icon-menu"></i>
- </a>
- <router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
- </div>
- <div class='item right'>
- <a class="mobile-nav-button" v-if="currentUser" href="#" @click.stop.prevent="openMobileNotifications()">
- <i class="button-icon icon-bell-alt"></i>
- <div class="alert-dot" v-if="unseenNotificationsCount"></div>
- </a>
+ <div>
+ <nav class='nav-bar container' id="nav">
+ <div class='mobile-inner-nav' @click="scrollToTop()">
+ <div class='item'>
+ <a href="#" class="mobile-nav-button" @click.stop.prevent="toggleMobileSidebar()">
+ <i class="button-icon icon-menu"></i>
+ </a>
+ <router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
+ </div>
+ <div class='item right'>
+ <a class="mobile-nav-button" v-if="currentUser" href="#" @click.stop.prevent="openMobileNotifications()">
+ <i class="button-icon icon-bell-alt"></i>
+ <div class="alert-dot" v-if="unseenNotificationsCount"></div>
+ </a>
+ </div>
</div>
- </div>
- <SideDrawer ref="sideDrawer" :logout="logout"/>
+ </nav>
<div v-if="currentUser"
class="mobile-notifications-drawer"
:class="{ 'closed': !notificationsOpen }"
- @touchstart="notificationsTouchStart"
- @touchmove="notificationsTouchMove"
+ @touchstart.stop="notificationsTouchStart"
+ @touchmove.stop="notificationsTouchMove"
>
<div class="mobile-notifications-header">
<span class="title">{{$t('notifications.notifications')}}</span>
@@ -27,12 +28,13 @@
<i class="button-icon icon-cancel"/>
</a>
</div>
- <div v-if="currentUser" class="mobile-notifications">
+ <div class="mobile-notifications" @scroll="onScroll">
<Notifications ref="notifications" noHeading="true"/>
</div>
</div>
+ <SideDrawer ref="sideDrawer" :logout="logout"/>
<MobilePostStatusModal />
- </nav>
+ </div>
</template>
<script src="./mobile_nav.js"></script>
@@ -79,6 +81,8 @@
transition-property: transform;
transition-duration: 0.25s;
transform: translateX(0);
+ z-index: 1001;
+ -webkit-overflow-scrolling: touch;
&.closed {
transform: translateX(100%);
diff --git a/src/components/mobile_post_status_modal/mobile_post_status_modal.js b/src/components/mobile_post_status_modal/mobile_post_status_modal.js
index 2f24dd08..91b730e7 100644
--- a/src/components/mobile_post_status_modal/mobile_post_status_modal.js
+++ b/src/components/mobile_post_status_modal/mobile_post_status_modal.js
@@ -1,5 +1,5 @@
import PostStatusForm from '../post_status_form/post_status_form.vue'
-import { throttle } from 'lodash'
+import { debounce } from 'lodash'
const MobilePostStatusModal = {
components: {
@@ -16,11 +16,15 @@ const MobilePostStatusModal = {
}
},
created () {
- window.addEventListener('scroll', this.handleScroll)
+ if (this.autohideFloatingPostButton) {
+ this.activateFloatingPostButtonAutohide()
+ }
window.addEventListener('resize', this.handleOSK)
},
destroyed () {
- window.removeEventListener('scroll', this.handleScroll)
+ if (this.autohideFloatingPostButton) {
+ this.deactivateFloatingPostButtonAutohide()
+ }
window.removeEventListener('resize', this.handleOSK)
},
computed: {
@@ -28,10 +32,30 @@ const MobilePostStatusModal = {
return this.$store.state.users.currentUser
},
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: {
+ activateFloatingPostButtonAutohide () {
+ window.addEventListener('scroll', this.handleScrollStart)
+ window.addEventListener('scroll', this.handleScrollEnd)
+ },
+ deactivateFloatingPostButtonAutohide () {
+ window.removeEventListener('scroll', this.handleScrollStart)
+ window.removeEventListener('scroll', this.handleScrollEnd)
+ },
openPostForm () {
this.postFormOpen = true
this.hidden = true
@@ -65,26 +89,19 @@ const MobilePostStatusModal = {
this.inputActive = false
}
},
- handleScroll: throttle(function () {
- const scrollAmount = window.scrollY - this.oldScrollPos
- const scrollingDown = scrollAmount > 0
-
- if (scrollingDown !== this.scrollingDown) {
- this.amountScrolled = 0
- this.scrollingDown = scrollingDown
- if (!scrollingDown) {
- this.hidden = false
- }
- } else if (scrollingDown) {
- this.amountScrolled += scrollAmount
- if (this.amountScrolled > 100 && !this.hidden) {
- this.hidden = true
- }
+ handleScrollStart: debounce(function () {
+ if (window.scrollY > this.oldScrollPos) {
+ this.hidden = true
+ } else {
+ this.hidden = false
}
+ this.oldScrollPos = window.scrollY
+ }, 100, {leading: true, trailing: false}),
+ handleScrollEnd: debounce(function () {
+ this.hidden = false
this.oldScrollPos = window.scrollY
- this.scrollingDown = scrollingDown
- }, 100)
+ }, 100, {leading: false, trailing: true})
}
}
diff --git a/src/components/mobile_post_status_modal/mobile_post_status_modal.vue b/src/components/mobile_post_status_modal/mobile_post_status_modal.vue
index 0a451c28..c762705b 100644
--- a/src/components/mobile_post_status_modal/mobile_post_status_modal.vue
+++ b/src/components/mobile_post_status_modal/mobile_post_status_modal.vue
@@ -7,7 +7,7 @@
>
<div class="post-form-modal-panel panel" @click.stop="">
<div class="panel-heading">{{$t('post_status.new_status')}}</div>
- <PostStatusForm class="panel-body" @posted="closePostForm"/>
+ <PostStatusForm class="panel-body" @posted="closePostForm" />
</div>
</div>
<button
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index ae11d692..3427b9c5 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -7,7 +7,7 @@
</status>
<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">
- <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.from_profile.profile_image_url_original" />
+ <UserAvatar :compact="true" :betterShadow="betterShadow" :user="notification.from_profile"/>
</a>
<div class='notification-right'>
<UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded" />
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index e341212e..5b13b98e 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -52,6 +52,10 @@ const Notifications = {
this.$store.dispatch('markNotificationsAsSeen')
},
fetchOlderNotifications () {
+ if (this.loading) {
+ return
+ }
+
const store = this.$store
const credentials = store.state.users.currentUser.credentials
store.commit('setNotificationsLoading', { value: true })
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index c65c27e2..cbd2024a 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -182,6 +182,9 @@ const PostStatusForm = {
},
safeDMEnabled () {
return this.$store.state.instance.safeDM
+ },
+ hideScopeNotice () {
+ return this.$store.state.config.hideScopeNotice
}
},
methods: {
@@ -338,6 +341,9 @@ const PostStatusForm = {
},
changeVis (visibility) {
this.newStatus.visibility = visibility
+ },
+ dismissScopeNotice () {
+ this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })
}
}
}
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 1ce2b647..b8b93936 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -9,7 +9,25 @@
class="visibility-notice">
<router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
</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-else>{{ $t('post_status.direct_warning_to_all') }}</span>
</p>
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
index ba1e5266..3f16c921 100644
--- a/src/components/selectable_list/selectable_list.vue
+++ b/src/components/selectable_list/selectable_list.vue
@@ -31,6 +31,10 @@
&-item-inner {
display: flex;
align-items: center;
+
+ > * {
+ min-width: 0;
+ }
}
&-item-selected-inner {
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index a85ab674..c4aa45b2 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -46,6 +46,7 @@ const settings = {
streamingLocal: user.streaming,
pauseOnUnfocusedLocal: user.pauseOnUnfocused,
hoverPreviewLocal: user.hoverPreview,
+ autohideFloatingPostButtonLocal: user.autohideFloatingPostButton,
hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined'
? instance.hideMutedPosts
@@ -183,6 +184,9 @@ const settings = {
hoverPreviewLocal (value) {
this.$store.dispatch('setOption', { name: 'hoverPreview', value })
},
+ autohideFloatingPostButtonLocal (value) {
+ this.$store.dispatch('setOption', { name: 'autohideFloatingPostButton', value })
+ },
muteWordsString (value) {
value = filter(value.split('\n'), (word) => trim(word).length > 0)
this.$store.dispatch('setOption', { name: 'muteWords', value })
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 6890220f..920e6e12 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -122,6 +122,10 @@
{{$t('settings.minimal_scopes_mode')}} {{$t('settings.instance_default', { value: minimalScopesModeDefault })}}
</label>
</li>
+ <li>
+ <input type="checkbox" id="autohideFloatingPostButton" v-model="autohideFloatingPostButtonLocal">
+ <label for="autohideFloatingPostButton">{{$t('settings.autohide_floating_post_button')}}</label>
+ </li>
</ul>
</div>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 0295cd04..c01cfe79 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -7,11 +7,12 @@ import UserCard from '../user_card/user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import Gallery from '../gallery/gallery.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 fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.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 = {
name: 'Status',
@@ -30,7 +31,6 @@ const Status = {
data () {
return {
replying: false,
- expanded: false,
unmuted: false,
userExpanded: false,
preview: null,
@@ -97,6 +97,10 @@ const Status = {
return this.statusoid
}
},
+ statusFromGlobalRepository () {
+ // NOTE: Consider to replace status with statusFromGlobalRepository
+ return this.$store.state.statuses.allStatusesObject[this.status.id]
+ },
loggedIn () {
return !!this.$store.state.users.currentUser
},
@@ -156,7 +160,7 @@ const Status = {
if (this.$store.state.config.replyVisibility === 'all') {
return false
}
- if (this.inlineExpanded || this.expanded || this.inConversation || !this.isReply) {
+ if (this.inConversation || !this.isReply) {
return false
}
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) {
continue
}
- if (checkFollowing && this.status.attentions[i].following) {
+ if (checkFollowing && this.$store.getters.findUser(this.status.attentions[i].id).following) {
return false
}
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.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: {
@@ -268,7 +280,8 @@ const Status = {
UserCard,
UserAvatar,
Gallery,
- LinkPreview
+ LinkPreview,
+ AvatarList
},
methods: {
visibilityIcon (visibility) {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 34b17970..02715253 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -13,7 +13,7 @@
</template>
<template v-else>
<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">
<span class="user-name">
<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 v-if="!noHeading" class="media-left">
<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>
</div>
<div class="status-body">
@@ -91,8 +91,13 @@
</div>
<div v-if="showPreview" class="status-preview-container">
- <status class="status-preview" v-if="preview" :isPreview="true" :statusoid="preview" :compact=true></status>
- <div class="status-preview status-preview-loading" v-else>
+ <status class="status-preview"
+ 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>
</div>
</div>
@@ -133,6 +138,24 @@
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
</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="loggedIn">
<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;
}
+.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) {
.status-el {
.retweet-info {
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index e0a34bd1..e6a8d458 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -16,7 +16,7 @@
</div>
<div :class="classes.body">
<div class="timeline">
- <conversation
+ <conversation
v-for="status in timeline.visibleStatuses"
class="status-fadein"
:key="status.id"
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
index e6fed3b5..a42b9c71 100644
--- a/src/components/user_avatar/user_avatar.js
+++ b/src/components/user_avatar/user_avatar.js
@@ -2,7 +2,7 @@ import StillImage from '../still-image/still-image.vue'
const UserAvatar = {
props: [
- 'src',
+ 'user',
'betterShadow',
'compact'
],
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
index 6bf7123d..e5466fdf 100644
--- a/src/components/user_avatar/user_avatar.vue
+++ b/src/components/user_avatar/user_avatar.vue
@@ -1,8 +1,10 @@
<template>
<StillImage
class="avatar"
+ :alt="user.screen_name"
+ :title="user.screen_name"
+ :src="user.profile_image_url_original"
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
- :src="imgSrc"
:imageLoadError="imageLoadError"
/>
</template>
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 1a100de3..7c6ffa89 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -151,6 +151,9 @@ export default {
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
+ },
+ reportUser () {
+ this.$store.dispatch('openUserReportingModal', this.user.id)
}
}
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index e1d3ff57..2d02ca03 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -4,7 +4,7 @@
<div class='user-info'>
<div class='container'>
<router-link :to="userProfileLink(user)">
- <UserAvatar :betterShadow="betterShadow" :src="user.profile_image_url_original"/>
+ <UserAvatar :betterShadow="betterShadow" :user="user"/>
</router-link>
<div class="name-and-screen-name">
<div class="top-line">
@@ -99,8 +99,14 @@
</button>
</span>
</div>
- <ModerationTools :user='user' v-if='loggedIn.role === "admin"'>
- </ModerationTools>
+ <div class='block' v-if='isOtherUser && loggedIn'>
+ <span>
+ <button @click="reportUser">
+ {{ $t('user_card.report') }}
+ </button>
+ </span>
+ </div>
+ <ModerationTools :user='user' v-if='loggedIn.role === "admin"'/>
</div>
</div>
</div>
diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js
new file mode 100644
index 00000000..7c6ea409
--- /dev/null
+++ b/src/components/user_reporting_modal/user_reporting_modal.js
@@ -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
diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue
new file mode 100644
index 00000000..432dd14d
--- /dev/null
+++ b/src/components/user_reporting_modal/user_reporting_modal.vue
@@ -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>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index e88ee612..2418450c 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -13,6 +13,8 @@ import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue'
import EmojiInput from '../emoji-input/emoji-input.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 userSearchApi from '../../services/new_api/user_search.js'
@@ -40,14 +42,9 @@ const UserSettings = {
hideFollowers: this.$store.state.users.currentUser.hide_followers,
showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role,
- followList: null,
- followImportError: false,
- followsImported: false,
- enableFollowsExport: true,
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
- followListUploading: false,
bannerPreview: null,
backgroundPreview: null,
bannerUploadError: null,
@@ -75,7 +72,9 @@ const UserSettings = {
Autosuggest,
BlockCard,
MuteCard,
- ProgressButton
+ ProgressButton,
+ Importer,
+ Exporter
},
computed: {
user () {
@@ -110,37 +109,23 @@ const UserSettings = {
},
methods: {
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
.updateProfile({
params: {
- name,
- description,
- locked,
+ note: this.newBio,
+ locked: this.newLocked,
// Backend notation.
/* eslint-disable camelcase */
- default_scope,
- no_rich_text,
- hide_follows,
- hide_followers,
- show_role
+ display_name: this.newName,
+ default_scope: this.newDefaultScope,
+ no_rich_text: this.newNoRichText,
+ hide_follows: this.hideFollows,
+ hide_followers: this.hideFollowers,
+ show_role: this.showRole
/* eslint-enable camelcase */
}}).then((user) => {
- if (!user.error) {
- this.$store.commit('addNewUsers', [user])
- this.$store.commit('setCurrentUser', user)
- }
+ this.$store.commit('addNewUsers', [user])
+ this.$store.commit('setCurrentUser', user)
})
},
changeVis (visibility) {
@@ -160,23 +145,29 @@ const UserSettings = {
reader.onload = ({target}) => {
const img = target.result
this[slot + 'Preview'] = img
+ this[slot] = file
}
reader.readAsDataURL(file)
},
submitAvatar (cropper, file) {
- let img
- if (cropper) {
- img = cropper.getCroppedCanvas().toDataURL(file.type)
- } else {
- img = file
- }
+ const that = this
+ return new Promise((resolve, reject) => {
+ function updateAvatar (avatar) {
+ that.$store.state.api.backendInteractor.updateAvatar({ avatar })
+ .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 (!user.error) {
- this.$store.commit('addNewUsers', [user])
- this.$store.commit('setCurrentUser', user)
+ if (cropper) {
+ cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
} else {
- throw new Error(this.$t('upload.error.base') + user.error)
+ updateAvatar(file)
}
})
},
@@ -186,30 +177,17 @@ const UserSettings = {
submitBanner () {
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.$store.state.api.backendInteractor.updateBanner({params: {banner, offset_top, offset_left, width, height}}).then((data) => {
- if (!data.error) {
- let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
- clone.cover_photo = data.url
- this.$store.commit('addNewUsers', [clone])
- this.$store.commit('setCurrentUser', clone)
+ this.$store.state.api.backendInteractor.updateBanner({banner: this.banner})
+ .then((user) => {
+ this.$store.commit('addNewUsers', [user])
+ this.$store.commit('setCurrentUser', user)
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 () {
if (!this.backgroundPreview) { return }
@@ -236,62 +214,41 @@ const UserSettings = {
this.backgroundUploading = false
})
},
- importFollows () {
- this.followListUploading = true
- const followList = this.followList
- this.$store.state.api.backendInteractor.followImport({params: followList})
+ importFollows (file) {
+ return this.$store.state.api.backendInteractor.importFollows(file)
.then((status) => {
- if (status) {
- this.followsImported = true
- } else {
- this.followImportError = true
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ importBlocks (file) {
+ return this.$store.state.api.backendInteractor.importBlocks(file)
+ .then((status) => {
+ if (!status) {
+ throw new Error('failed')
}
- this.followListUploading = false
})
},
- /* This function takes an Array of Users
- * and outputs a file with all the addresses for the user to download
- */
- exportPeople (users, filename) {
- // Get all the friends addresses
- var UserAddresses = users.map(function (user) {
+ generateExportableUsersContent (users) {
+ // Get addresses
+ return users.map((user) => {
// check is it's a local user
if (user && user.is_local) {
// append the instance address
// eslint-disable-next-line no-undef
- user.screen_name += '@' + location.hostname
+ return user.screen_name + '@' + location.hostname
}
return user.screen_name
}).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 () {
- this.enableFollowsExport = false
- this.$store.state.api.backendInteractor
- .exportFriends({
- id: this.$store.state.users.currentUser.id
- })
- .then((friendList) => {
- this.exportPeople(friendList, 'friends.csv')
- setTimeout(() => { this.enableFollowsExport = true }, 2000)
- })
- },
- followListChange () {
- // eslint-disable-next-line no-undef
- let formData = new FormData()
- formData.append('list', this.$refs.followlist.files[0])
- this.followList = formData
+ getFollowsContent () {
+ return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
+ .then(this.generateExportableUsersContent)
},
- dismissImported () {
- this.followsImported = false
- this.followImportError = false
+ getBlocksContent () {
+ return this.$store.state.api.backendInteractor.fetchBlocks()
+ .then(this.generateExportableUsersContent)
},
confirmDelete () {
this.deletingAccount = true
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index d68e68fa..8a94f0b8 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -171,26 +171,20 @@
<div class="setting-item">
<h2>{{$t('settings.follow_import')}}</h2>
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
- <form>
- <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 v-else-if="followImportError">
- <i class="icon-cross" @click="dismissImported"></i>
- <p>{{$t('settings.follow_import_error')}}</p>
- </div>
+ <Importer :submitHandler="importFollows" :successMessage="$t('settings.follows_imported')" :errorMessage="$t('settings.follow_import_error')" />
</div>
- <div class="setting-item" v-if="enableFollowsExport">
+ <div class="setting-item">
<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 class="setting-item" v-else>
- <h2>{{$t('settings.follow_export_processing')}}</h2>
+ <div class="setting-item">
+ <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>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 711e8d31..b4f0deb2 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -2,6 +2,10 @@
"chat": {
"title": "Chat"
},
+ "exporter": {
+ "export": "Export",
+ "processing": "Processing, you'll soon be asked to download your file"
+ },
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
@@ -31,6 +35,11 @@
"save_without_cropping": "Save without cropping",
"cancel": "Cancel"
},
+ "importer": {
+ "submit": "Submit",
+ "success": "Imported successfully.",
+ "error": "An error occured while importing this file."
+ },
"login": {
"login": "Log in",
"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_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
"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": {
"direct": "Direct - Post to mentioned users only",
"private": "Followers-only - Post to followers only",
@@ -126,6 +140,11 @@
"avatarRadius": "Avatars",
"background": "Background",
"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",
"btnRadius": "Buttons",
"cBlue": "Blue (Reply, follow)",
@@ -153,7 +172,6 @@
"filtering_explanation": "All statuses containing these words will be muted, one per line",
"follow_export": "Follow export",
"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_error": "Error importing followers",
"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_user_stats": "Hide user statistics (e.g. the number of followers)",
"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_theme": "Load preset",
"inputRadius": "Input fields",
@@ -219,6 +238,7 @@
"reply_visibility_all": "Show all replies",
"reply_visibility_following": "Only show replies directed at me or users I'm following",
"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_ok": "Settings saved",
"search_user_to_block": "Search whom you want to block",
@@ -380,6 +400,8 @@
"no_statuses": "No statuses"
},
"status": {
+ "favorites": "Favorites",
+ "repeats": "Repeats",
"reply_to": "Reply to",
"replies_list": "Replies:"
},
@@ -404,6 +426,7 @@
"muted": "Muted",
"per_day": "per day",
"remote_follow": "Remote follow",
+ "report": "Report",
"statuses": "Statuses",
"unblock": "Unblock",
"unblock_progress": "Unblocking...",
@@ -436,6 +459,15 @@
"profile_does_not_exist": "Sorry, this profile does not exist.",
"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": {
"more": "More",
"who_to_follow": "Who to follow"
diff --git a/src/i18n/es.json b/src/i18n/es.json
index a692eef9..2e38f859 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -2,6 +2,10 @@
"chat": {
"title": "Chat"
},
+ "exporter": {
+ "export": "Exportar",
+ "processing": "Procesando. Pronto se te pedirá que descargues tu archivo"
+ },
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
@@ -19,7 +23,22 @@
"apply": "Aplicar",
"submit": "Enviar",
"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": "Identificación",
@@ -31,6 +50,10 @@
"username": "Usuario",
"hint": "Inicia sesión para unirte a la discusión"
},
+ "media_modal": {
+ "previous": "Anterior",
+ "next": "Siguiente"
+ },
"nav": {
"about": "Sobre",
"back": "Volver",
@@ -61,15 +84,19 @@
"account_not_locked_warning_link": "bloqueada",
"attachments_sensitive": "Contenido sensible",
"content_type": {
- "text/plain": "Texto Plano"
+ "text/plain": "Texto Plano",
+ "text/html": "HTML",
+ "text/markdown": "Markdown",
+ "text/bbcode": "BBCode"
},
"content_warning": "Tema (opcional)",
"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",
"scope": {
"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",
"unlisted": "Sin Listar - Entradas no visibles en las Líneas Temporales Públicas"
}
@@ -83,6 +110,9 @@
"token": "Token de invitación",
"captcha": "CAPTCHA",
"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": {
"username_required": "no puede estar vacío",
"fullname_required": "no puede estar vacío",
@@ -92,7 +122,11 @@
"password_confirmation_match": "la contraseña no coincide"
}
},
+ "selectable_list": {
+ "select_all": "Seleccionarlo todo"
+ },
"settings": {
+ "app_name": "Nombre de la aplicación",
"attachmentRadius": "Adjuntos",
"attachments": "Adjuntos",
"autoload": "Activar carga automática al llegar al final de la página",
@@ -101,6 +135,12 @@
"avatarRadius": "Avatares",
"background": "Fondo",
"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",
"cBlue": "Azul (Responder, seguir)",
"cGreen": "Verde (Retweet)",
@@ -127,7 +167,6 @@
"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_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_error": "Error al importal el archivo",
"follows_imported": "¡Importado! Procesarlos llevará tiempo.",
@@ -135,12 +174,15 @@
"general": "General",
"hide_attachments_in_convo": "Ocultar adjuntos en las conversaciones",
"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",
"preload_images": "Precargar las imágenes",
"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_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
"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_theme": "Importar tema",
"inputRadius": "Campos de entrada",
@@ -155,6 +197,7 @@
"lock_account_description": "Restringir el acceso a tu cuenta solo a seguidores admitidos",
"loop_video": "Vídeos en bucle",
"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",
"use_contain_fit": "No recortar los adjuntos en miniaturas",
"name": "Nombre",
@@ -166,6 +209,8 @@
"notification_visibility_mentions": "Menciones",
"notification_visibility_repeats": "Repeticiones (Repeats)",
"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_followers_description": "No mostrar quién me sigue",
"show_admin_badge": "Mostrar la placa de administrador en mi perfil",
@@ -190,8 +235,11 @@
"reply_visibility_self": "Solo mostrar réplicas para mí",
"saving_err": "Error al guardar los ajustes",
"saving_ok": "Ajustes guardados",
+ "search_user_to_block": "Buscar usuarios a bloquear",
+ "search_user_to_mute": "Buscar usuarios a silenciar",
"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_profile_background": "Cambiar fondo 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_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",
+ "upload_a_photo": "Subir una foto",
"user_settings": "Ajustes de Usuario",
"values": {
"false": "no",
@@ -325,6 +374,11 @@
"checkbox": "He revisado los términos y condiciones",
"link": "un bonito enlace"
}
+ },
+ "version": {
+ "title": "Versión",
+ "backend_version": "Versión del Backend",
+ "frontend_version": "Versión del Frontend"
}
},
"timeline": {
@@ -336,7 +390,14 @@
"repeated": "repetida",
"show_new": "Mostrar lo nuevo",
"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": {
"approve": "Aprovar",
@@ -359,10 +420,47 @@
"muted": "Silenciado",
"per_day": "por día",
"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": {
- "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": {
"more": "Más",
@@ -389,4 +487,4 @@
"TiB": "TiB"
}
}
-}
+} \ No newline at end of file
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index fbe676cf..62cbecb8 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -222,6 +222,8 @@
"no_more_statuses": "Ei enempää viestejä"
},
"status": {
+ "favorites": "Tykkäykset",
+ "repeats": "Toistot",
"reply_to": "Vastaus",
"replies_list": "Vastaukset:"
},
diff --git a/src/i18n/oc.json b/src/i18n/oc.json
index a5826239..5f8d153f 100644
--- a/src/i18n/oc.json
+++ b/src/i18n/oc.json
@@ -2,6 +2,10 @@
"chat": {
"title": "Messatjariá"
},
+ "exporter": {
+ "export": "Exportar",
+ "processing": "Tractament, vos demandarem lèu de telecargar lo fichièr"
+ },
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
@@ -31,6 +35,11 @@
"save_without_cropping": "Salvar sens talhada",
"cancel": "Anullar"
},
+ "importer": {
+ "submit": "Mandar",
+ "success": "Corrèctament importat.",
+ "error": "Una error s’es producha pendent l’importacion d’aqueste fichièr."
+ },
"login": {
"login": "Connexion",
"description": "Connexion via OAuth",
@@ -126,6 +135,11 @@
"avatarRadius": "Avatars",
"background": "Rèire plan",
"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",
"btnRadius": "Botons",
"cBlue": "Blau (Respondre, seguir)",
@@ -153,7 +167,6 @@
"filtering_explanation": "Totes los estatuts amb aqueles mots seràn en silenci, un mot per linha",
"follow_export": "Exportar los abonaments",
"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_error": "Error en important los seguidors",
"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",
"name": "Nom",
"name_bio": "Nom & Bio",
+
"new_password": "Nòu senhal",
"notification_visibility_follows": "Abonaments",
"notification_visibility_likes": "Aimar",
@@ -380,6 +394,8 @@
"no_statuses": "Cap d’estatuts"
},
"status": {
+ "favorites": "Li a agradat",
+ "repeats": "A repetit",
"reply_to": "Respond a",
"replies_list": "Responsas :"
},
@@ -461,4 +477,4 @@
"TiB": "Tio"
}
}
-}
+} \ No newline at end of file
diff --git a/src/i18n/pl.json b/src/i18n/pl.json
index 715e5d6e..51cadfb6 100644
--- a/src/i18n/pl.json
+++ b/src/i18n/pl.json
@@ -363,7 +363,7 @@
"error_fetching": "Błąd pobierania",
"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",
- "repeated": "powtórzono",
+ "repeated": "powtórzył(-a)",
"show_new": "Pokaż nowe",
"up_to_date": "Na bieżąco",
"no_more_statuses": "Brak kolejnych statusów",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 5450f154..b3ab322d 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -42,8 +42,13 @@
"attachments_sensitive": "Вложения содержат чувствительный контент",
"content_warning": "Тема (не обязательно)",
"default": "Что нового?",
- "direct_warning": "Этот пост будет видет только упомянутым пользователям",
+ "direct_warning": "Этот пост будет виден только упомянутым пользователям",
"posting": "Отправляется",
+ "scope_notice": {
+ "public": "Этот пост будет виден всем",
+ "private": "Этот пост будет виден только вашим подписчикам",
+ "unlisted": "Этот пост не будет виден в публичной и федеративной ленте"
+ },
"scope": {
"direct": "Личное - этот пост видят только те кто в нём упомянут",
"private": "Для подписчиков - этот пост видят только подписчики",
@@ -152,6 +157,7 @@
"reply_visibility_all": "Показывать все ответы",
"reply_visibility_following": "Показывать только ответы мне и тех на кого я подписан",
"reply_visibility_self": "Показывать только ответы мне",
+ "autohide_floating_post_button": "Автоматически скрывать кнопку постинга (в мобильной версии)",
"saving_err": "Не удалось сохранить настройки",
"saving_ok": "Сохранено",
"security_tab": "Безопасность",
diff --git a/src/main.js b/src/main.js
index 725f5806..92f843b1 100644
--- a/src/main.js
+++ b/src/main.js
@@ -12,6 +12,7 @@ import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js'
import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js'
+import reportsModule from './modules/reports.js'
import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n'
@@ -75,7 +76,8 @@ const persistedStateOptions = {
chat: chatModule,
oauth: oauthModule,
mediaViewer: mediaViewerModule,
- oauthTokens: oauthTokensModule
+ oauthTokens: oauthTokensModule,
+ reports: reportsModule
},
plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now.
diff --git a/src/modules/config.js b/src/modules/config.js
index 1666a2c5..a8da525a 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -17,6 +17,7 @@ const defaultState = {
autoLoad: true,
streaming: false,
hoverPreview: true,
+ autohideFloatingPostButton: false,
pauseOnUnfocused: true,
stopGifs: false,
replyVisibility: 'all',
@@ -30,6 +31,7 @@ const defaultState = {
muteWords: [],
highlight: {},
interfaceLanguage: browserLocale,
+ hideScopeNotice: false,
scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default
diff --git a/src/modules/reports.js b/src/modules/reports.js
new file mode 100644
index 00000000..904022f1
--- /dev/null
+++ b/src/modules/reports.js
@@ -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
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index e70c2400..4c92d4e1 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -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 apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
@@ -402,12 +402,27 @@ export const mutations = {
},
setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
+
+ if (newStatus.favorited !== value) {
+ if (value) {
+ newStatus.fave_num++
+ } else {
+ newStatus.fave_num--
+ }
+ }
+
newStatus.favorited = value
},
- setFavoritedConfirm (state, { status }) {
+ setFavoritedConfirm (state, { status, user }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = status.favorited
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 }) {
const newStatus = state.allStatusesObject[status.id]
@@ -422,6 +437,17 @@ export const mutations = {
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 }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.deleted = true
@@ -459,6 +485,11 @@ export const mutations = {
},
queueFlush (state, { timeline, 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) {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
- apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
- .then(status => {
- commit('setFavoritedConfirm', { status })
- })
+ rootState.api.backendInteractor.favorite(status.id)
+ .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
},
unfavorite ({ rootState, commit }, status) {
- // Optimistic favoriting...
+ // Optimistic unfavoriting...
commit('setFavorited', { status, value: false })
- apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
- .then(status => {
- commit('setFavoritedConfirm', { status })
- })
+ rootState.api.backendInteractor.unfavorite(status.id)
+ .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
},
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...
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) {
+ // Optimistic unretweeting...
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 }) {
commit('queueFlush', { timeline, id })
@@ -524,6 +554,14 @@ const statuses = {
id: rootState.statuses.notifications.maxId,
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
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 6b255e9f..b7a602b8 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -3,12 +3,10 @@ const LOGIN_URL = '/api/account/verify_credentials.json'
const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing'
const MENTIONS_URL = '/api/statuses/mentions.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 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 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 DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
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_POST_STATUS_URL = '/api/v1/statuses'
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 { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
@@ -65,7 +67,24 @@ let fetch = (url, 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)
.then((response) => {
return new Promise((resolve, reject) => response.json()
@@ -78,28 +97,16 @@ const promisedRequest = (url, options) => {
})
}
-// Params
-// cropH
-// cropW
-// cropX
-// cropY
-// img (base 64 encodend data url)
-const updateAvatar = ({credentials, params}) => {
- let url = AVATAR_UPDATE_URL
-
+const updateAvatar = ({credentials, avatar}) => {
const form = new FormData()
-
- each(params, (value, key) => {
- if (value) {
- form.append(key, value)
- }
- })
-
- return fetch(url, {
+ form.append('avatar', avatar)
+ return fetch(MASTODON_PROFILE_UPDATE_URL, {
headers: authHeaders(credentials),
- method: 'POST',
+ method: 'PATCH',
body: form
- }).then((data) => data.json())
+ })
+ .then((data) => data.json())
+ .then((data) => parseUser(data))
}
const updateBg = ({credentials, params}) => {
@@ -120,52 +127,26 @@ const updateBg = ({credentials, params}) => {
}).then((data) => data.json())
}
-// Params
-// height
-// width
-// offset_left
-// offset_top
-// banner (base 64 encodend data url)
-const updateBanner = ({credentials, params}) => {
- let url = BANNER_UPDATE_URL
-
+const updateBanner = ({credentials, banner}) => {
const form = new FormData()
-
- each(params, (value, key) => {
- if (value) {
- form.append(key, value)
- }
- })
-
- return fetch(url, {
+ form.append('header', banner)
+ return fetch(MASTODON_PROFILE_UPDATE_URL, {
headers: authHeaders(credentials),
- method: 'POST',
+ method: 'PATCH',
body: form
- }).then((data) => data.json())
+ })
+ .then((data) => data.json())
+ .then((data) => parseUser(data))
}
-// Params
-// name
-// url
-// location
-// description
const updateProfile = ({credentials, params}) => {
- // Always include these fields, because they might be empty or false
- const fields = ['description', 'locked', 'no_rich_text', 'hide_follows', 'hide_followers', 'show_role']
- let url = PROFILE_UPDATE_URL
-
- const form = new FormData()
-
- each(params, (value, key) => {
- if (fields.includes(key) || value) {
- form.append(key, value)
- }
+ return promisedRequest({
+ url: MASTODON_PROFILE_UPDATE_URL,
+ method: 'PATCH',
+ payload: params,
+ credentials
})
- return fetch(url, {
- headers: authHeaders(credentials),
- method: 'POST',
- body: form
- }).then((data) => data.json())
+ .then((data) => parseUser(data))
}
// Params needed:
@@ -261,7 +242,7 @@ const denyUser = ({id, credentials}) => {
const fetchUser = ({id, credentials}) => {
let url = `${MASTODON_USER_URL}/${id}`
- return promisedRequest(url, { headers: authHeaders(credentials) })
+ return promisedRequest({ url, credentials })
.then((data) => parseUser(data))
}
@@ -525,62 +506,22 @@ const verifyCredentials = (user) => {
}
const favorite = ({ id, credentials }) => {
- return fetch(MASTODON_FAVORITE_URL(id), {
- headers: authHeaders(credentials),
- method: 'POST'
- })
- .then(response => {
- if (response.ok) {
- return response.json()
- } else {
- throw new Error('Error favoriting post')
- }
- })
+ return promisedRequest({ url: MASTODON_FAVORITE_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const unfavorite = ({ id, credentials }) => {
- return fetch(MASTODON_UNFAVORITE_URL(id), {
- headers: authHeaders(credentials),
- method: 'POST'
- })
- .then(response => {
- if (response.ok) {
- return response.json()
- } else {
- throw new Error('Error removing favorite')
- }
- })
+ return promisedRequest({ url: MASTODON_UNFAVORITE_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const retweet = ({ id, credentials }) => {
- return fetch(MASTODON_RETWEET_URL(id), {
- headers: authHeaders(credentials),
- method: 'POST'
- })
- .then(response => {
- if (response.ok) {
- return response.json()
- } else {
- throw new Error('Error repeating post')
- }
- })
+ return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const unretweet = ({ id, credentials }) => {
- return fetch(MASTODON_UNRETWEET_URL(id), {
- headers: authHeaders(credentials),
- method: 'POST'
- })
- .then(response => {
- if (response.ok) {
- return response.json()
- } else {
- throw new Error('Error removing repeat')
- }
- })
+ return promisedRequest({ url: MASTODON_UNRETWEET_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
@@ -634,9 +575,22 @@ const uploadMedia = ({formData, credentials}) => {
.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, {
- body: params,
+ body: formData,
method: 'POST',
headers: authHeaders(credentials)
})
@@ -672,26 +626,20 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma
}
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))
}
const muteUser = ({id, credentials}) => {
- return promisedRequest(MASTODON_MUTE_USER_URL(id), {
- headers: authHeaders(credentials),
- method: 'POST'
- })
+ return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST' })
}
const unmuteUser = ({id, credentials}) => {
- return promisedRequest(MASTODON_UNMUTE_USER_URL(id), {
- headers: authHeaders(credentials),
- method: 'POST'
- })
+ return promisedRequest({ url: MASTODON_UNMUTE_USER_URL(id), credentials, method: 'POST' })
}
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))
}
@@ -735,6 +683,28 @@ const markNotificationsAsSeen = ({id, credentials}) => {
}).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 = {
verifyCredentials,
fetchTimeline,
@@ -776,14 +746,18 @@ const apiService = {
updateProfile,
updateBanner,
externalProfile,
- followImport,
+ importBlocks,
+ importFollows,
deleteAccount,
changePassword,
fetchFollowRequests,
approveUser,
denyUser,
suggestions,
- markNotificationsAsSeen
+ markNotificationsAsSeen,
+ fetchFavoritedByUsers,
+ fetchRebloggedByUsers,
+ reportUser
}
export default apiService
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 75bba92b..c2b93de4 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -101,17 +101,27 @@ const backendInteractorService = (credentials) => {
const getCaptcha = () => apiService.getCaptcha()
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 updateBanner = ({params}) => apiService.updateBanner({credentials, params})
+ const updateBanner = ({banner}) => apiService.updateBanner({credentials, banner})
const updateProfile = ({params}) => apiService.updateProfile({credentials, params})
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 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 = {
fetchStatus,
fetchConversation,
@@ -147,12 +157,20 @@ const backendInteractorService = (credentials) => {
updateBanner,
updateProfile,
externalProfile,
- followImport,
+ importBlocks,
+ importFollows,
deleteAccount,
changePassword,
fetchFollowRequests,
approveUser,
- denyUser
+ denyUser,
+ fetchFavoritedByUsers,
+ fetchRebloggedByUsers,
+ reportUser,
+ favorite,
+ unfavorite,
+ retweet,
+ unretweet
}
return backendInteractorServiceInstance
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index e706e7d9..7a8708d5 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -275,6 +275,9 @@ export const parseStatus = (data) => {
output.retweeted_status = parseStatus(retweetedStatus)
}
+ output.favoritedBy = []
+ output.rebloggedBy = []
+
return output
}
@@ -306,7 +309,7 @@ export const parseNotification = (data) => {
}
output.created_at = new Date(data.created_at)
- output.id = data.id
+ output.id = parseInt(data.id)
return output
}