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

* upstream/develop: (26 commits)
  added condition to check for logined user
  fix gradients and minor artifacts
  keep track of new instance options
  fix old MR
  oof
  get rid of slots
  added hide_network option, fixed properties naming
  Fix fetching new users, add storing local users in usersObjects with their screen_name as well as id, so that they could be fetched zero-state with screen-name link.
  Refactor arrays to individual options
  Reset enableFollowsExport to true after 2 sec when an export file is available to download
  Write a unit test for fileSizeFormatService
  I am dumb
  Handle errors from server
  Moved upload errors in user_settings to an array. Moved upload error strings to its separate section in i18n
  Avatar, background, banner filesize errors
  Count in binary bytes and remove i18 from file size format service
  Add a space between filesizes
  Lint
  Add file size formating
  Fix formating
  ...
This commit is contained in:
Henry Jameson 2018-12-14 01:16:54 +03:00
commit 8d345bbad9
22 changed files with 295 additions and 88 deletions

View file

@ -228,24 +228,23 @@ i[class*=icon-] {
padding: 0 10px 0 10px; padding: 0 10px 0 10px;
} }
.gaps {
margin: -1em 0 0 -1em;
}
.item { .item {
flex: 1; flex: 1;
line-height: 50px; line-height: 50px;
height: 50px; height: 50px;
overflow: hidden; overflow: hidden;
display: flex;
flex-wrap: wrap;
.nav-icon { .nav-icon {
font-size: 1.1em; font-size: 1.1em;
margin-left: 0.4em; margin-left: 0.4em;
} }
}
.gaps > .item { &.right {
padding: 1em 0 0 1em; justify-content: right;
padding-right: 20px;
}
} }
.auto-size { .auto-size {
@ -293,8 +292,6 @@ nav {
} }
.inner-nav { .inner-nav {
padding-left: 20px;
padding-right: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
flex-basis: 970px; flex-basis: 970px;
@ -452,6 +449,23 @@ nav {
color: var(--faint, $fallback--faint); color: var(--faint, $fallback--faint);
box-shadow: 0px 0px 4px rgba(0,0,0,.6); box-shadow: 0px 0px 4px rgba(0,0,0,.6);
box-shadow: var(--topBarShadow); box-shadow: var(--topBarShadow);
.back-button {
display: block;
max-width: 99px;
transition-property: opacity, max-width;
transition-duration: 300ms;
transition-timing-function: ease-out;
i {
margin: 0 1em;
}
&.hidden {
opacity: 0;
max-width: 20px;
}
}
} }
.fade-enter-active, .fade-leave-active { .fade-enter-active, .fade-leave-active {
@ -486,6 +500,7 @@ nav {
display: none; display: none;
width: 100%; width: 100%;
height: 46px; height: 46px;
button { button {
display: block; display: block;
flex: 1; flex: 1;
@ -499,6 +514,16 @@ nav {
body { body {
overflow-y: scroll; overflow-y: scroll;
} }
nav {
.back-button {
display: none;
}
.site-name {
padding-left: 20px;
}
}
.sidebar-bounds { .sidebar-bounds {
overflow: hidden; overflow: hidden;
max-height: 100vh; max-height: 100vh;
@ -591,11 +616,6 @@ nav {
} }
} }
.item.right {
text-align: right;
padding-right: 20px;
}
.visibility-tray { .visibility-tray {
font-size: 1.2em; font-size: 1.2em;
padding: 3px; padding: 3px;

View file

@ -7,7 +7,10 @@
</div> </div>
<div class='inner-nav'> <div class='inner-nav'>
<div class='item'> <div class='item'>
<router-link :to="{ name: 'root'}">{{sitename}}</router-link> <router-link class="back-button" @click.native="activatePanel('timeline')" :to="{ name: 'root' }" active-class="hidden">
<i class="icon-left-open" :title="$t('nav.back')"></i>
</router-link>
<router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
</div> </div>
<div class='item right'> <div class='item right'>
<user-finder class="nav-icon"></user-finder> <user-finder class="nav-icon"></user-finder>

View file

@ -21,13 +21,21 @@ const afterStoreSetup = ({ store, i18n }) => {
window.fetch('/api/statusnet/config.json') window.fetch('/api/statusnet/config.json')
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
const { name, closed: registrationClosed, textlimit, server, vapidPublicKey } = data.site const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey } = data.site
store.dispatch('setInstanceOption', { name: 'name', value: name }) store.dispatch('setInstanceOption', { name: 'name', value: name })
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) })
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) })
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) })
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
store.dispatch('setInstanceOption', { name: 'server', value: server }) store.dispatch('setInstanceOption', { name: 'server', value: server })
if (data.nsfwCensorImage) {
store.dispatch('setInstanceOption', { name: 'nsfwCensorImage', value: data.nsfwCensorImage })
}
if (vapidPublicKey) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
} }

View file

@ -11,7 +11,7 @@ const Attachment = {
], ],
data () { data () {
return { return {
nsfwImage, nsfwImage: this.$store.state.config.nsfwCensorImage || nsfwImage,
hideNsfwLocal: this.$store.state.config.hideNsfw, hideNsfwLocal: this.$store.state.config.hideNsfw,
preloadImage: this.$store.state.config.preloadImage, preloadImage: this.$store.state.config.preloadImage,
loopVideo: this.$store.state.config.loopVideo, loopVideo: this.$store.state.config.loopVideo,

View file

@ -1,5 +1,6 @@
/* eslint-env browser */ /* eslint-env browser */
import statusPosterService from '../../services/status_poster/status_poster.service.js' import statusPosterService from '../../services/status_poster/status_poster.service.js'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
const mediaUpload = { const mediaUpload = {
mounted () { mounted () {
@ -21,6 +22,12 @@ const mediaUpload = {
uploadFile (file) { uploadFile (file) {
const self = this const self = this
const store = this.$store const store = this.$store
if (file.size > store.state.instance.uploadlimit) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat(store.state.instance.uploadlimit)
self.$emit('upload-failed', 'file_too_big', {filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit})
return
}
const formData = new FormData() const formData = new FormData()
formData.append('media', file) formData.append('media', file)
@ -32,7 +39,7 @@ const mediaUpload = {
self.$emit('uploaded', fileData) self.$emit('uploaded', fileData)
self.uploading = false self.uploading = false
}, (error) => { // eslint-disable-line handle-callback-err }, (error) => { // eslint-disable-line handle-callback-err
self.$emit('upload-failed') self.$emit('upload-failed', 'default')
self.uploading = false self.uploading = false
}) })
}, },

View file

@ -259,6 +259,11 @@ const PostStatusForm = {
let index = this.newStatus.files.indexOf(fileInfo) let index = this.newStatus.files.indexOf(fileInfo)
this.newStatus.files.splice(index, 1) this.newStatus.files.splice(index, 1)
}, },
uploadFailed (errString, templateArgs) {
templateArgs = templateArgs || {}
this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
this.enableSubmit()
},
disableSubmit () { disableSubmit () {
this.submitDisabled = true this.submitDisabled = true
}, },

View file

@ -64,7 +64,7 @@
</div> </div>
</div> </div>
<div class='form-bottom'> <div class='form-bottom'>
<media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload> <media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
<p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p> <p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
<p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p> <p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>

View file

@ -2,6 +2,7 @@ import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue' import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import { throttle } from 'lodash'
const Timeline = { const Timeline = {
props: [ props: [
@ -88,7 +89,7 @@ const Timeline = {
this.paused = false this.paused = false
} }
}, },
fetchOlderStatuses () { fetchOlderStatuses: throttle(function () {
const store = this.$store const store = this.$store
const credentials = store.state.users.currentUser.credentials const credentials = store.state.users.currentUser.credentials
store.commit('setLoading', { timeline: this.timelineName, value: true }) store.commit('setLoading', { timeline: this.timelineName, value: true })
@ -101,7 +102,7 @@ const Timeline = {
userId: this.userId, userId: this.userId,
tag: this.tag tag: this.tag
}).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false })) }).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
}, }, 1000, this),
fetchFollowers () { fetchFollowers () {
const id = this.userId const id = this.userId
this.$store.state.api.backendInteractor.fetchFollowers({ id }) this.$store.state.api.backendInteractor.fetchFollowers({ id })

View file

@ -14,6 +14,9 @@ const UserCard = {
components: { components: {
UserCardContent UserCardContent
}, },
computed: {
currentUser () { return this.$store.state.users.currentUser }
},
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {
this.userExpanded = !this.userExpanded this.userExpanded = !this.userExpanded

View file

@ -10,13 +10,13 @@
<div :title="user.name" v-if="user.name_html" class="user-name"> <div :title="user.name" v-if="user.name_html" class="user-name">
<span v-html="user.name_html"></span> <span v-html="user.name_html"></span>
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you"> <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
{{ $t('user_card.follows_you') }} {{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span> </span>
</div> </div>
<div :title="user.name" v-else class="user-name"> <div :title="user.name" v-else class="user-name">
{{ user.name }} {{ user.name }}
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you"> <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
{{ $t('user_card.follows_you') }} {{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span> </span>
</div> </div>
<router-link class='user-screen-name' :to="{ name: 'user-profile', params: { id: user.id } }"> <router-link class='user-screen-name' :to="{ name: 'user-profile', params: { id: user.id } }">

View file

@ -20,10 +20,20 @@ export default {
if (color) { if (color) {
const rgb = (typeof color === 'string') ? hex2rgb(color) : color const rgb = (typeof color === 'string') ? hex2rgb(color) : color
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)` const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)`
const gradient = [
[tintColor, this.hideBio ? '60%' : ''],
this.hideBio ? [
color, '100%'
] : [
tintColor, ''
]
].map(_ => _.join(' ')).join(', ')
return { return {
backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`, backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,
backgroundImage: [ backgroundImage: [
`linear-gradient(to bottom, ${tintColor}, ${tintColor})`, `linear-gradient(to bottom, ${gradient})`,
`url(${this.user.cover_photo})` `url(${this.user.cover_photo})`
].join(', ') ].join(', ')
} }

View file

@ -90,7 +90,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="panel-body profile-panel-body" v-if="switcher"> <div class="panel-body profile-panel-body" v-if="!hideBio">
<div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}"> <div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}">
<div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}"> <div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}">
<h5>{{ $t('user_card.statuses') }}</h5> <h5>{{ $t('user_card.statuses') }}</h5>
@ -122,6 +122,9 @@
border-radius: var(--panelRadius, $fallback--panelRadius); border-radius: var(--panelRadius, $fallback--panelRadius);
overflow: hidden; overflow: hidden;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
.panel-heading { .panel-heading {
padding: 0.6em 0em; padding: 0.6em 0em;
text-align: center; text-align: center;

View file

@ -1,12 +1,12 @@
<template> <template>
<span class="user-finder-container"> <div class="user-finder-container">
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" /> <i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
<a href="#" v-if="hidden" :title="$t('finder.find_user')" ><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a> <a href="#" v-if="hidden" :title="$t('finder.find_user')" ><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
<span v-else> <span v-else>
<input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/> <input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
<i class="icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/> <i class="icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
</span> </span>
</span> </div>
</template> </template>
<script src="./user_finder.js"></script> <script src="./user_finder.js"></script>
@ -15,7 +15,6 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.user-finder-container { .user-finder-container {
height: 29px;
max-width: 100%; max-width: 100%;
} }

View file

@ -3,6 +3,16 @@
<div v-if="user" class="user-profile panel panel-default"> <div v-if="user" class="user-profile panel panel-default">
<user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content> <user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content>
</div> </div>
<div v-else class="panel user-profile-placeholder">
<div class="panel-heading">
<div class="title">
{{ $t('settings.profile_tab') }}
</div>
</div>
<div class="panel-body">
<i class="icon-spin3 animate-spin"></i>
</div>
</div>
<Timeline :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="userId"/> <Timeline :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="userId"/>
</div> </div>
</template> </template>
@ -21,4 +31,12 @@
align-items: stretch; align-items: stretch;
} }
} }
.user-profile-placeholder {
.panel-body {
display: flex;
justify-content: center;
align-items: middle;
padding: 7em;
}
}
</style> </style>

View file

@ -1,20 +1,30 @@
import TabSwitcher from '../tab_switcher/tab_switcher.jsx' import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
import StyleSwitcher from '../style_switcher/style_switcher.vue' import StyleSwitcher from '../style_switcher/style_switcher.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
const UserSettings = { const UserSettings = {
data () { data () {
return { return {
newname: this.$store.state.users.currentUser.name, newName: this.$store.state.users.currentUser.name,
newbio: this.$store.state.users.currentUser.description, newBio: this.$store.state.users.currentUser.description,
newlocked: this.$store.state.users.currentUser.locked, newLocked: this.$store.state.users.currentUser.locked,
newnorichtext: this.$store.state.users.currentUser.no_rich_text, newNoRichText: this.$store.state.users.currentUser.no_rich_text,
newdefaultScope: this.$store.state.users.currentUser.default_scope, newDefaultScope: this.$store.state.users.currentUser.default_scope,
newHideNetwork: this.$store.state.users.currentUser.hide_network,
followList: null, followList: null,
followImportError: false, followImportError: false,
followsImported: false, followsImported: false,
enableFollowsExport: true, enableFollowsExport: true,
uploading: [ false, false, false, false ], avatarUploading: false,
previews: [ null, null, null ], bannerUploading: false,
backgroundUploading: false,
followListUploading: false,
avatarPreview: null,
bannerPreview: null,
backgroundPreview: null,
avatarUploadError: null,
bannerUploadError: null,
backgroundUploadError: null,
deletingAccount: false, deletingAccount: false,
deleteAccountConfirmPasswordInput: '', deleteAccountConfirmPasswordInput: '',
deleteAccountError: false, deleteAccountError: false,
@ -40,48 +50,67 @@ const UserSettings = {
}, },
vis () { vis () {
return { return {
public: { selected: this.newdefaultScope === 'public' }, public: { selected: this.newDefaultScope === 'public' },
unlisted: { selected: this.newdefaultScope === 'unlisted' }, unlisted: { selected: this.newDefaultScope === 'unlisted' },
private: { selected: this.newdefaultScope === 'private' }, private: { selected: this.newDefaultScope === 'private' },
direct: { selected: this.newdefaultScope === 'direct' } direct: { selected: this.newDefaultScope === 'direct' }
} }
} }
}, },
methods: { methods: {
updateProfile () { updateProfile () {
const name = this.newname const name = this.newname
const description = this.newbio const description = this.newBio
const locked = this.newlocked const locked = this.newLocked
// Backend notation.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
const default_scope = this.newdefaultScope const default_scope = this.newDefaultScope
const no_rich_text = this.newnorichtext const no_rich_text = this.newNoRichText
this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked, default_scope, no_rich_text}}).then((user) => { const hide_network = this.newHideNetwork
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
}
})
/* eslint-enable camelcase */ /* eslint-enable camelcase */
this.$store.state.api.backendInteractor
.updateProfile({
params: {
name,
description,
locked,
// Backend notation.
/* eslint-disable camelcase */
default_scope,
no_rich_text,
hide_network
/* eslint-enable camelcase */
}}).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
}
})
}, },
changeVis (visibility) { changeVis (visibility) {
this.newdefaultScope = visibility this.newDefaultScope = visibility
}, },
uploadFile (slot, e) { uploadFile (slot, e) {
const file = e.target.files[0] const file = e.target.files[0]
if (!file) { return } if (!file) { return }
if (file.size > this.$store.state.instance[slot + 'limit']) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', {filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit})
return
}
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const reader = new FileReader() const reader = new FileReader()
reader.onload = ({target}) => { reader.onload = ({target}) => {
const img = target.result const img = target.result
this.previews[slot] = img this[slot + 'Preview'] = img
this.$forceUpdate() // just changing the array with the index doesn't update the view
} }
reader.readAsDataURL(file) reader.readAsDataURL(file)
}, },
submitAvatar () { submitAvatar () {
if (!this.previews[0]) { return } if (!this.avatarPreview) { return }
let img = this.previews[0] let img = this.avatarPreview
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
let imginfo = new Image() let imginfo = new Image()
let cropX, cropY, cropW, cropH let cropX, cropY, cropW, cropH
@ -97,20 +126,25 @@ const UserSettings = {
cropX = Math.floor((imginfo.width - imginfo.height) / 2) cropX = Math.floor((imginfo.width - imginfo.height) / 2)
cropW = imginfo.height cropW = imginfo.height
} }
this.uploading[0] = true this.avatarUploading = true
this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => { this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
if (!user.error) { if (!user.error) {
this.$store.commit('addNewUsers', [user]) this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user) this.$store.commit('setCurrentUser', user)
this.previews[0] = null this.avatarPreview = null
} else {
this.avatarUploadError = this.$t('upload.error.base') + user.error
} }
this.uploading[0] = false this.avatarUploading = false
}) })
}, },
clearUploadError (slot) {
this[slot + 'UploadError'] = null
},
submitBanner () { submitBanner () {
if (!this.previews[1]) { return } if (!this.bannerPreview) { return }
let banner = this.previews[1] let banner = this.bannerPreview
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
let imginfo = new Image() let imginfo = new Image()
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -120,22 +154,24 @@ const UserSettings = {
height = imginfo.height height = imginfo.height
offset_top = 0 offset_top = 0
offset_left = 0 offset_left = 0
this.uploading[1] = 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({params: {banner, offset_top, offset_left, width, height}}).then((data) => {
if (!data.error) { if (!data.error) {
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser)) let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
clone.cover_photo = data.url clone.cover_photo = data.url
this.$store.commit('addNewUsers', [clone]) this.$store.commit('addNewUsers', [clone])
this.$store.commit('setCurrentUser', clone) this.$store.commit('setCurrentUser', clone)
this.previews[1] = null this.bannerPreview = null
} else {
this.bannerUploadError = this.$t('upload.error.base') + data.error
} }
this.uploading[1] = false this.bannerUploading = false
}) })
/* eslint-enable camelcase */ /* eslint-enable camelcase */
}, },
submitBg () { submitBg () {
if (!this.previews[2]) { return } if (!this.backgroundPreview) { return }
let img = this.previews[2] let img = this.backgroundPreview
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
let imginfo = new Image() let imginfo = new Image()
let cropX, cropY, cropW, cropH let cropX, cropY, cropW, cropH
@ -144,20 +180,22 @@ const UserSettings = {
cropY = 0 cropY = 0
cropW = imginfo.width cropW = imginfo.width
cropH = imginfo.width cropH = imginfo.width
this.uploading[2] = true this.backgroundUploading = true
this.$store.state.api.backendInteractor.updateBg({params: {img, cropX, cropY, cropW, cropH}}).then((data) => { this.$store.state.api.backendInteractor.updateBg({params: {img, cropX, cropY, cropW, cropH}}).then((data) => {
if (!data.error) { if (!data.error) {
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser)) let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
clone.background_image = data.url clone.background_image = data.url
this.$store.commit('addNewUsers', [clone]) this.$store.commit('addNewUsers', [clone])
this.$store.commit('setCurrentUser', clone) this.$store.commit('setCurrentUser', clone)
this.previews[2] = null this.backgroundPreview = null
} else {
this.backgroundUploadError = this.$t('upload.error.base') + data.error
} }
this.uploading[2] = false this.backgroundUploading = false
}) })
}, },
importFollows () { importFollows () {
this.uploading[3] = true this.followListUploading = true
const followList = this.followList const followList = this.followList
this.$store.state.api.backendInteractor.followImport({params: followList}) this.$store.state.api.backendInteractor.followImport({params: followList})
.then((status) => { .then((status) => {
@ -166,7 +204,7 @@ const UserSettings = {
} else { } else {
this.followImportError = true this.followImportError = true
} }
this.uploading[3] = false this.followListUploading = false
}) })
}, },
/* This function takes an Array of Users /* This function takes an Array of Users
@ -198,6 +236,7 @@ const UserSettings = {
.fetchFriends({id: this.$store.state.users.currentUser.id}) .fetchFriends({id: this.$store.state.users.currentUser.id})
.then((friendList) => { .then((friendList) => {
this.exportPeople(friendList, 'friends.csv') this.exportPeople(friendList, 'friends.csv')
setTimeout(() => { this.enableFollowsExport = true }, 2000)
}) })
}, },
followListChange () { followListChange () {

View file

@ -9,11 +9,11 @@
<div class="setting-item" > <div class="setting-item" >
<h2>{{$t('settings.name_bio')}}</h2> <h2>{{$t('settings.name_bio')}}</h2>
<p>{{$t('settings.name')}}</p> <p>{{$t('settings.name')}}</p>
<input class='name-changer' id='username' v-model="newname"></input> <input class='name-changer' id='username' v-model="newName"></input>
<p>{{$t('settings.bio')}}</p> <p>{{$t('settings.bio')}}</p>
<textarea class="bio" v-model="newbio"></textarea> <textarea class="bio" v-model="newBio"></textarea>
<p> <p>
<input type="checkbox" v-model="newlocked" id="account-locked"> <input type="checkbox" v-model="newLocked" id="account-locked">
<label for="account-locked">{{$t('settings.lock_account_description')}}</label> <label for="account-locked">{{$t('settings.lock_account_description')}}</label>
</p> </p>
<div v-if="scopeOptionsEnabled"> <div v-if="scopeOptionsEnabled">
@ -26,47 +26,63 @@
</div> </div>
</div> </div>
<p> <p>
<input type="checkbox" v-model="newnorichtext" id="account-no-rich-text"> <input type="checkbox" v-model="newNoRichText" id="account-no-rich-text">
<label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label> <label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
</p> </p>
<button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button> <p>
<input type="checkbox" v-model="newHideNetwork" id="account-hide-network">
<label for="account-no-rich-text">{{$t('settings.hide_network_description')}}</label>
</p>
<button :disabled='newName.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{$t('settings.avatar')}}</h2> <h2>{{$t('settings.avatar')}}</h2>
<p>{{$t('settings.current_avatar')}}</p> <p>{{$t('settings.current_avatar')}}</p>
<img :src="user.profile_image_url_original" class="old-avatar"></img> <img :src="user.profile_image_url_original" class="old-avatar"></img>
<p>{{$t('settings.set_new_avatar')}}</p> <p>{{$t('settings.set_new_avatar')}}</p>
<img class="new-avatar" v-bind:src="previews[0]" v-if="previews[0]"> <img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
</img> </img>
<div> <div>
<input type="file" @change="uploadFile(0, $event)" ></input> <input type="file" @change="uploadFile('avatar', $event)" ></input>
</div>
<i class="icon-spin4 animate-spin" v-if="avatarUploading"></i>
<button class="btn btn-default" v-else-if="avatarPreview" @click="submitAvatar">{{$t('general.submit')}}</button>
<div class='alert error' v-if="avatarUploadError">
Error: {{ avatarUploadError }}
<i class="icon-cancel" @click="clearUploadError('avatar')"></i>
</div> </div>
<i class="icon-spin4 animate-spin" v-if="uploading[0]"></i>
<button class="btn btn-default" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{$t('settings.profile_banner')}}</h2> <h2>{{$t('settings.profile_banner')}}</h2>
<p>{{$t('settings.current_profile_banner')}}</p> <p>{{$t('settings.current_profile_banner')}}</p>
<img :src="user.cover_photo" class="banner"></img> <img :src="user.cover_photo" class="banner"></img>
<p>{{$t('settings.set_new_profile_banner')}}</p> <p>{{$t('settings.set_new_profile_banner')}}</p>
<img class="banner" v-bind:src="previews[1]" v-if="previews[1]"> <img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview">
</img> </img>
<div> <div>
<input type="file" @change="uploadFile(1, $event)" ></input> <input type="file" @change="uploadFile('banner', $event)" ></input>
</div>
<i class=" icon-spin4 animate-spin uploading" v-if="bannerUploading"></i>
<button class="btn btn-default" v-else-if="bannerPreview" @click="submitBanner">{{$t('general.submit')}}</button>
<div class='alert error' v-if="bannerUploadError">
Error: {{ bannerUploadError }}
<i class="icon-cancel" @click="clearUploadError('banner')"></i>
</div> </div>
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
<button class="btn btn-default" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{$t('settings.profile_background')}}</h2> <h2>{{$t('settings.profile_background')}}</h2>
<p>{{$t('settings.set_new_profile_background')}}</p> <p>{{$t('settings.set_new_profile_background')}}</p>
<img class="bg" v-bind:src="previews[2]" v-if="previews[2]"> <img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview">
</img> </img>
<div> <div>
<input type="file" @change="uploadFile(2, $event)" ></input> <input type="file" @change="uploadFile('background', $event)" ></input>
</div>
<i class=" icon-spin4 animate-spin uploading" v-if="backgroundUploading"></i>
<button class="btn btn-default" v-else-if="backgroundPreview" @click="submitBg">{{$t('general.submit')}}</button>
<div class='alert error' v-if="backgroundUploadError">
Error: {{ backgroundUploadError }}
<i class="icon-cancel" @click="clearUploadError('background')"></i>
</div> </div>
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
<button class="btn btn-default" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
</div> </div>
</div> </div>
@ -113,7 +129,7 @@
<form v-model="followImportForm"> <form v-model="followImportForm">
<input type="file" ref="followlist" v-on:change="followListChange"></input> <input type="file" ref="followlist" v-on:change="followListChange"></input>
</form> </form>
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[3]"></i> <i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button> <button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
<div v-if="followsImported"> <div v-if="followsImported">
<i class="icon-cross" @click="dismissImported"></i> <i class="icon-cross" @click="dismissImported"></i>

View file

@ -29,6 +29,7 @@
"username": "Username" "username": "Username"
}, },
"nav": { "nav": {
"back": "Back",
"chat": "Local Chat", "chat": "Local Chat",
"friend_requests": "Follow Requests", "friend_requests": "Follow Requests",
"mentions": "Mentions", "mentions": "Mentions",
@ -151,6 +152,7 @@
"notification_visibility_mentions": "Mentions", "notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats", "notification_visibility_repeats": "Repeats",
"no_rich_text_description": "Strip rich text formatting from all posts", "no_rich_text_description": "Strip rich text formatting from all posts",
"hide_network_description": "Don't show who I'm following and who's following me",
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
"panelRadius": "Panels", "panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused", "pause_on_unfocused": "Pause streaming when tab is not focused",
@ -322,6 +324,7 @@
"followers": "Followers", "followers": "Followers",
"following": "Following!", "following": "Following!",
"follows_you": "Follows you!", "follows_you": "Follows you!",
"its_you": "It's you!",
"mute": "Mute", "mute": "Mute",
"muted": "Muted", "muted": "Muted",
"per_day": "per day", "per_day": "per day",
@ -341,5 +344,19 @@
"reply": "Reply", "reply": "Reply",
"favorite": "Favorite", "favorite": "Favorite",
"user_settings": "User Settings" "user_settings": "User Settings"
},
"upload":{
"error": {
"base": "Upload failed.",
"file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Try again later"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
} }
} }

View file

@ -19,6 +19,7 @@
"username": "Имя пользователя" "username": "Имя пользователя"
}, },
"nav": { "nav": {
"back": "Назад",
"chat": "Локальный чат", "chat": "Локальный чат",
"mentions": "Упоминания", "mentions": "Упоминания",
"public_tl": "Публичная лента", "public_tl": "Публичная лента",
@ -126,6 +127,7 @@
"notification_visibility_mentions": "Упоминания", "notification_visibility_mentions": "Упоминания",
"notification_visibility_repeats": "Повторы", "notification_visibility_repeats": "Повторы",
"no_rich_text_description": "Убрать форматирование из всех постов", "no_rich_text_description": "Убрать форматирование из всех постов",
"hide_network_description": "Не показывать кого я читаю и кто меня читает",
"nsfw_clickthrough": "Включить скрытие NSFW вложений", "nsfw_clickthrough": "Включить скрытие NSFW вложений",
"panelRadius": "Панели", "panelRadius": "Панели",
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе", "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",

View file

@ -25,6 +25,8 @@ const defaultState = {
scopeCopy: true, scopeCopy: true,
subjectLineBehavior: 'email', subjectLineBehavior: 'email',
loginMethod: 'password', loginMethod: 'password',
nsfwCensorImage: undefined,
vapidPublicKey: undefined,
// Nasty stuff // Nasty stuff
pleromaBackend: true, pleromaBackend: true,

View file

@ -17,6 +17,9 @@ export const mergeOrAdd = (arr, obj, item) => {
// This is a new item, prepare it // This is a new item, prepare it
arr.push(item) arr.push(item)
obj[item.id] = item obj[item.id] = item
if (item.screen_name && !item.screen_name.includes('@')) {
obj[item.screen_name] = item
}
return { item, new: true } return { item, new: true }
} }
} }
@ -87,7 +90,7 @@ const users = {
actions: { actions: {
fetchUser (store, id) { fetchUser (store, id) {
store.rootState.api.backendInteractor.fetchUser({ id }) store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => store.commit('addNewUsers', user)) .then((user) => store.commit('addNewUsers', [user]))
}, },
registerPushNotifications (store) { registerPushNotifications (store) {
const token = store.state.currentUser.credentials const token = store.state.currentUser.credentials

View file

@ -0,0 +1,17 @@
const fileSizeFormat = (num) => {
var exponent
var unit
var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
if (num < 1) {
return num + ' ' + units[0]
}
exponent = Math.min(Math.floor(Math.log(num) / Math.log(1024)), units.length - 1)
num = (num / Math.pow(1024, exponent)).toFixed(2) * 1
unit = units[exponent]
return {num: num, unit: unit}
}
const fileSizeFormatService = {
fileSizeFormat
}
export default fileSizeFormatService

View file

@ -0,0 +1,34 @@
import fileSizeFormatService from '../../../../../src/services/file_size_format/file_size_format.js'
describe('fileSizeFormat', () => {
it('Formats file size', () => {
const values = [1, 1024, 1048576, 1073741824, 1099511627776]
const expected = [
{
num: 1,
unit: 'B'
},
{
num: 1,
unit: 'KiB'
},
{
num: 1,
unit: 'MiB'
},
{
num: 1,
unit: 'GiB'
},
{
num: 1,
unit: 'TiB'
}
]
var res = []
for (var value in values) {
res.push(fileSizeFormatService.fileSizeFormat(values[value]))
}
expect(res).to.eql(expected)
})
})