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

* upstream/develop: (33 commits)
  #383: content type error
  #376: update status timeline when it's empty
  #377: no exteral profile link for local users
  #371: show notification when user setting's saved
  Clean up CSS a bit
  #364: update ap_id error with username
  Rename: instanceSpecificPanelPresent
  Hide isp option if instance has panel disabled
  Take over branch and fix some issues
  Fix lint errors
  Better error handling
  Remove cropped image size restriction
  Remove modal component
  Make embedded image cropper
  Revert eslintrc changes
  Check if variable exists before using
  Remove event listeners when destory ImageCropper
  Localization of ImageCropper component
  Remove event listener when modal is destroyed
  Crop avatar image using minWidth/minHeight
  ...
This commit is contained in:
Henry Jameson 2019-02-21 19:32:49 +02:00
commit 90a82b7dec
33 changed files with 490 additions and 1214 deletions

View file

@ -11,7 +11,7 @@ module.exports = {
'html'
],
// add your custom rules here
'rules': {
rules: {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await

View file

@ -17,6 +17,7 @@
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-lodash": "^3.2.11",
"chromatism": "^3.0.0",
"cropperjs": "^1.4.3",
"diff": "^3.0.1",
"karma-mocha-reporter": "^2.2.1",
"localforage": "^1.5.0",

View file

@ -181,8 +181,7 @@ input, textarea, .select {
color: $fallback--text;
color: var(--text, $fallback--text);
}
&:disabled,
{
&:disabled {
&,
& + label,
& + label::before {
@ -649,10 +648,6 @@ nav {
color: var(--lightText, $fallback--lightText);
}
.text-format {
float: right;
}
div {
padding-top: 5px;
}
@ -739,3 +734,7 @@ nav {
width: 100%;
}
}
.btn.btn-default {
min-height: 28px;
}

View file

@ -3,8 +3,8 @@
<div class="panel panel-default">
<div class="panel-heading timeline-heading" :class="{ 'chat-heading': floating }" @click.stop.prevent="togglePanel">
<div class="title">
{{$t('chat.title')}}
<i class="icon-cancel" style="float: right;" v-if="floating"></i>
<span>{{$t('chat.title')}}</span>
<i class="icon-cancel" v-if="floating"></i>
</div>
</div>
<div class="chat-window" v-chat-scroll>
@ -98,4 +98,11 @@
resize: none;
}
}
.chat-panel {
.title {
display: flex;
justify-content: space-between;
}
}
</style>

View file

@ -26,7 +26,9 @@ const FollowList = {
entries () {
return this.showFollowers ? this.user.followers : this.user.friends
},
showActions () { return this.$store.state.users.currentUser.id === this.userId }
showFollowsYou () {
return !this.showFollowers || (this.showFollowers && this.userId !== this.$store.state.users.currentUser.id)
}
},
methods: {
fetchEntries () {
@ -55,6 +57,9 @@ const FollowList = {
}
}
},
watch: {
'user': 'fetchEntries'
},
components: {
UserCard
}

View file

@ -3,8 +3,7 @@
<user-card
v-for="entry in entries"
:key="entry.id" :user="entry"
:showFollows="!showFollowers"
:showActions="showActions"
:noFollowsYou="!showFollowsYou"
/>
<div class="text-center panel-footer">
<a v-if="error" @click="fetchEntries" class="alert error">

View file

@ -0,0 +1,128 @@
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
const ImageCropper = {
props: {
trigger: {
type: [String, window.Element],
required: true
},
submitHandler: {
type: Function,
required: true
},
cropperOptions: {
type: Object,
default () {
return {
aspectRatio: 1,
autoCropArea: 1,
viewMode: 1,
movable: false,
zoomable: false,
guides: false
}
}
},
mimes: {
type: String,
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
},
saveButtonLabel: {
type: String
},
cancelButtonLabel: {
type: String
}
},
data () {
return {
cropper: undefined,
dataUrl: undefined,
filename: undefined,
submitting: false,
submitError: null
}
},
computed: {
saveText () {
return this.saveButtonLabel || this.$t('image_cropper.save')
},
cancelText () {
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
},
submitErrorMsg () {
return this.submitError && this.submitError instanceof Error ? this.submitError.toString() : this.submitError
}
},
methods: {
destroy () {
if (this.cropper) {
this.cropper.destroy()
}
this.$refs.input.value = ''
this.dataUrl = undefined
this.$emit('close')
},
submit () {
this.submitting = true
this.avatarUploadError = null
this.submitHandler(this.cropper, this.filename)
.then(() => this.destroy())
.catch((err) => {
this.submitError = err
})
.finally(() => {
this.submitting = false
})
},
pickImage () {
this.$refs.input.click()
},
createCropper () {
this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
},
getTriggerDOM () {
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
},
readFile () {
const fileInput = this.$refs.input
if (fileInput.files != null && fileInput.files[0] != null) {
let reader = new window.FileReader()
reader.onload = (e) => {
this.dataUrl = e.target.result
this.$emit('open')
}
reader.readAsDataURL(fileInput.files[0])
this.filename = fileInput.files[0].name || 'unknown'
this.$emit('changed', fileInput.files[0], reader)
}
},
clearError () {
this.submitError = null
}
},
mounted () {
// listen for click event on trigger
const trigger = this.getTriggerDOM()
if (!trigger) {
this.$emit('error', 'No image make trigger found.', 'user')
} else {
trigger.addEventListener('click', this.pickImage)
}
// listen for input file changes
const fileInput = this.$refs.input
fileInput.addEventListener('change', this.readFile)
},
beforeDestroy: function () {
// remove the event listeners
const trigger = this.getTriggerDOM()
if (trigger) {
trigger.removeEventListener('click', this.pickImage)
}
const fileInput = this.$refs.input
fileInput.removeEventListener('change', this.readFile)
}
}
export default ImageCropper

View file

@ -0,0 +1,42 @@
<template>
<div class="image-cropper">
<div v-if="dataUrl">
<div class="image-cropper-image-container">
<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="destroy" v-text="cancelText"></button>
<i class="icon-spin4 animate-spin" v-if="submitting"></i>
</div>
<div class="alert error" v-if="submitError">
{{submitErrorMsg}}
<i class="button-icon icon-cancel" @click="clearError"></i>
</div>
</div>
<input ref="input" type="file" class="image-cropper-img-input" :accept="mimes">
</div>
</template>
<script src="./image_cropper.js"></script>
<style lang="scss">
.image-cropper {
&-img-input {
display: none;
}
&-image-container {
position: relative;
img {
display: block;
max-width: 100%;
}
}
&-buttons-wrapper {
margin-top: 15px;
}
}
</style>

View file

@ -11,27 +11,62 @@ const MediaModal = {
showing () {
return this.$store.state.mediaViewer.activated
},
media () {
return this.$store.state.mediaViewer.media
},
currentIndex () {
return this.$store.state.mediaViewer.currentIndex
},
currentMedia () {
return this.$store.state.mediaViewer.media[this.currentIndex]
return this.media[this.currentIndex]
},
canNavigate () {
return this.media.length > 1
},
type () {
return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
}
},
created () {
document.addEventListener('keyup', e => {
if (e.keyCode === 27 && this.showing) { // escape
this.hide()
}
})
},
methods: {
hide () {
this.$store.dispatch('closeMediaViewer')
},
goPrev () {
if (this.canNavigate) {
const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1)
this.$store.dispatch('setCurrent', this.media[prevIndex])
}
},
goNext () {
if (this.canNavigate) {
const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1)
this.$store.dispatch('setCurrent', this.media[nextIndex])
}
},
handleKeyupEvent (e) {
if (this.showing && e.keyCode === 27) { // escape
this.hide()
}
},
handleKeydownEvent (e) {
if (!this.showing) {
return
}
if (e.keyCode === 39) { // arrow right
this.goNext()
} else if (e.keyCode === 37) { // arrow left
this.goPrev()
}
}
},
mounted () {
document.addEventListener('keyup', this.handleKeyupEvent)
document.addEventListener('keydown', this.handleKeydownEvent)
},
destroyed () {
document.removeEventListener('keyup', this.handleKeyupEvent)
document.removeEventListener('keydown', this.handleKeydownEvent)
}
}

View file

@ -8,6 +8,22 @@
:controls="true"
@click.stop.native="">
</VideoAttachment>
<button
:title="$t('media_modal.previous')"
class="modal-view-button-arrow modal-view-button-arrow--prev"
v-if="canNavigate"
@click.stop.prevent="goPrev"
>
<i class="icon-left-open arrow-icon" />
</button>
<button
:title="$t('media_modal.next')"
class="modal-view-button-arrow modal-view-button-arrow--next"
v-if="canNavigate"
@click.stop.prevent="goNext"
>
<i class="icon-right-open arrow-icon" />
</button>
</div>
</template>
@ -19,15 +35,29 @@
.modal-view {
z-index: 1000;
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
cursor: pointer;
&:hover {
.modal-view-button-arrow {
opacity: 0.75;
&:focus,
&:hover {
outline: none;
box-shadow: none;
}
&:hover {
opacity: 1;
}
}
}
}
.modal-image {
@ -35,4 +65,49 @@
max-height: 90%;
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
}
.modal-view-button-arrow {
position: absolute;
display: block;
top: 50%;
margin-top: -50px;
width: 70px;
height: 100px;
border: 0;
padding: 0;
opacity: 0;
box-shadow: none;
background: none;
appearance: none;
overflow: visible;
cursor: pointer;
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
.arrow-icon {
position: absolute;
top: 35px;
height: 30px;
width: 32px;
font-size: 14px;
line-height: 30px;
color: #FFF;
text-align: center;
background-color: rgba(0,0,0,.3);
}
&--prev {
left: 0;
.arrow-icon {
left: 6px;
}
}
&--next {
right: 0;
.arrow-icon {
right: 6px;
}
}
}
</style>

View file

@ -19,7 +19,10 @@
</li>
<li v-if='currentUser && currentUser.locked'>
<router-link :to="{ name: 'friend-requests' }">
{{ $t("nav.friend_requests") }}
{{ $t("nav.friend_requests")}}
<span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count">
{{currentUser.follow_request_count}}
</span>
</router-link>
</li>
<li>
@ -52,6 +55,12 @@
padding: 0;
}
.follow-request-count {
margin: -6px 10px;
background-color: $fallback--bg;
background-color: var(--input, $fallback--faint);
}
.nav-panel li {
border-bottom: 1px solid;
border-color: $fallback--border;

View file

@ -103,6 +103,7 @@
flex: 1 1 0;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
.name-and-action {
flex: 1;
@ -123,8 +124,8 @@
object-fit: contain
}
}
.timeago {
float: right;
font-size: 12px;
}

View file

@ -56,6 +56,10 @@ const PostStatusForm = {
? this.copyMessageScope
: this.$store.state.users.currentUser.default_scope
const contentType = typeof this.$store.state.config.postContentType === 'undefined'
? this.$store.state.instance.postContentType
: this.$store.state.config.postContentType
return {
dropFiles: [],
submitDisabled: false,
@ -67,7 +71,8 @@ const PostStatusForm = {
status: statusText,
nsfw: false,
files: [],
visibility: scope
visibility: scope,
contentType
},
caret: 0
}
@ -166,11 +171,6 @@ const PostStatusForm = {
},
formattingOptionsEnabled () {
return this.$store.state.instance.formattingOptionsEnabled
},
defaultPostContentType () {
return typeof this.$store.state.config.postContentType === 'undefined'
? this.$store.state.instance.postContentType
: this.$store.state.config.postContentType
}
},
methods: {

View file

@ -35,7 +35,7 @@
<div class="visibility-tray">
<span class="text-format" v-if="formattingOptionsEnabled">
<label for="post-content-type" class="select">
<select id="post-content-type" v-model="defaultPostContentType" class="form-control">
<select id="post-content-type" v-model="newStatus.contentType" class="form-control">
<option value="text/plain">{{$t('post_status.content_type.plain_text')}}</option>
<option value="text/html">HTML</option>
<option value="text/markdown">Markdown</option>
@ -118,6 +118,14 @@
}
}
.post-status-form {
.visibility-tray {
display: flex;
justify-content: space-between;
flex-direction: row-reverse;
}
}
.post-status-form, .login {
.form-bottom {
display: flex;

View file

@ -91,7 +91,8 @@ const settings = {
},
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
}
},
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }
},
watch: {
hideAttachmentsLocal (value) {

View file

@ -27,7 +27,7 @@
<li>
<interface-language-switcher />
</li>
<li>
<li v-if="instanceSpecificPanelPresent">
<input type="checkbox" id="hideISP" v-model="hideISPLocal">
<label for="hideISP">{{$t('settings.hide_isp')}}</label>
</li>
@ -311,20 +311,6 @@
color: $fallback--cRed;
}
.old-avatar {
width: 128px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
.new-avatar {
object-fit: cover;
width: 128px;
height: 128px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
.btn {
min-height: 28px;
min-width: 10em;

View file

@ -45,6 +45,10 @@
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
<router-link to='/friend-requests'>
{{ $t("nav.friend_requests") }}
<span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count">
{{currentUser.follow_request_count}}
</span>
</router-link>
</li>
<li @click="toggleDrawer">

View file

@ -554,7 +554,7 @@ a.unmute {
.timeline > {
.status-el:last-child {
border-bottom-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
border-bottom: none;
}

View file

@ -11,7 +11,8 @@ const Timeline = {
'title',
'userId',
'tag',
'embedded'
'embedded',
'count'
],
data () {
return {

View file

@ -20,7 +20,10 @@
</div>
</div>
<div :class="classes.footer">
<div v-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
<div v-if="count===0" class="new-status-notification text-center panel-footer faint">
{{$t('timeline.no_statuses')}}
</div>
<div v-else-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
{{$t('timeline.no_more_statuses')}}
</div>
<a v-else-if="!timeline.loading" href="#" v-on:click.prevent='fetchOlderStatuses()'>

View file

@ -6,9 +6,8 @@ import { requestFollow, requestUnfollow } from '../../services/follow_manipulate
const UserCard = {
props: [
'user',
'showFollows',
'showApproval',
'showActions'
'noFollowsYou',
'showApproval'
],
data () {
return {
@ -26,7 +25,7 @@ const UserCard = {
currentUser () { return this.$store.state.users.currentUser },
following () { return this.updated ? this.updated.following : this.user.following },
showFollow () {
return this.showActions && (!this.showFollows && !this.following || this.updated && !this.updated.following)
return !this.showApproval && (!this.following || this.updated && !this.updated.following)
}
},
methods: {

View file

@ -1,27 +1,31 @@
<template>
<div class="card">
<router-link :to="userProfileLink(user)">
<UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
</router-link>
<div class="usercard" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="name-and-screen-name" v-else>
<div :title="user.name" class="user-name">
<span v-if="user.name_html" v-html="user.name_html"></span>
<span v-else>{{ user.name }}</span>
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
<div class="user-card-main-content">
<div class="usercard" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="name-and-screen-name" v-if="!userExpanded">
<div :title="user.name" class="user-name">
<span v-if="user.name_html" v-html="user.name_html"></span>
<span v-else>{{ user.name }}</span>
</div>
<div class="user-link-action">
<router-link class='user-screen-name' :to="userProfileLink(user)">
@{{user.screen_name}}
</router-link>
</div>
</div>
<div class="follow-box" v-if="!userExpanded">
<span class="faint" v-if="!noFollowsYou && user.follows_you">
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
</div>
<div class="user-link-action">
<router-link class='user-screen-name' :to="userProfileLink(user)">
@{{user.screen_name}}
</router-link>
<button
v-if="showFollow"
class="btn btn-default"
@click="followUser"
<button
v-if="showFollow"
class="btn btn-default"
@click="followUser"
:disabled="followRequestInProgress"
:title="followRequestSent ? $t('user_card.follow_again') : ''"
>
@ -35,7 +39,7 @@
{{ $t('user_card.follow') }}
</template>
</button>
<button v-if="showActions && showFollows && following" class="btn btn-default" @click="unfollowUser" :disabled="followRequestInProgress">
<button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="followRequestInProgress">
<template v-if="followRequestInProgress">
{{ $t('user_card.follow_progress') }}
</template>
@ -44,10 +48,10 @@
</template>
</button>
</div>
</div>
<div class="approval" v-if="showApproval">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
<button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
<div class="approval" v-if="showApproval">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
<button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
</div>
</div>
</div>
</template>
@ -57,15 +61,19 @@
<style lang="scss">
@import '../../_variables.scss';
.name-and-screen-name {
.user-card-main-content {
display: flex;
flex-direction: column;
flex: 1 1 100%;
margin-left: 0.7em;
margin-top:0.0em;
min-width: 0;
}
.name-and-screen-name {
text-align: left;
width: 100%;
.user-name {
display: flex;
justify-content: space-between;
.user-name {
img {
object-fit: contain;
height: 16px;
@ -73,21 +81,14 @@
vertical-align: middle;
}
}
.user-link-action {
display: flex;
align-items: flex-start;
justify-content: space-between;
button {
margin-top: 3px;
}
}
}
.follows-you {
margin-left: 2em;
}
.card {
display: flex;
@ -99,16 +100,31 @@
border-bottom: 1px solid;
margin: 0;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
border-bottom-color: var(--border, $fallback--border);
.avatar {
padding: 0;
}
.follow-box {
text-align: center;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
line-height: 1.5em;
.btn {
margin-top: 0.5em;
margin-left: auto;
width: 10em;
}
}
}
.usercard {
width: fill-available;
margin: 0.2em 0 0 0.7em;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
@ -129,9 +145,15 @@
}
.approval {
display: flex;
flex-direction: row;
flex-wrap: wrap;
button {
width: 100%;
margin-bottom: 0.5em;
margin-top: 0.5em;
margin-right: 0.5em;
flex: 1 1;
max-width: 12em;
min-width: 8em;
}
}
</style>

View file

@ -13,7 +13,7 @@
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
</router-link>
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser">
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
<i class="icon-link-ext usersettings"></i>
</a>
</div>
@ -386,6 +386,4 @@
}
}
.floater {
}
</style>

View file

@ -10,6 +10,7 @@
<Timeline
:label="$t('user_card.statuses')"
:disabled="!user.statuses_count"
:count="user.statuses_count"
:embedded="true"
:title="$t('user_profile.timeline_title')"
:timeline="timeline"

View file

@ -1,6 +1,7 @@
import { unescape } from 'lodash'
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import ImageCropper from '../image_cropper/image_cropper.vue'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
@ -20,14 +21,12 @@ const UserSettings = {
followImportError: false,
followsImported: false,
enableFollowsExport: true,
avatarUploading: false,
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
followListUploading: false,
avatarPreview: null,
bannerPreview: null,
backgroundPreview: null,
avatarUploadError: null,
bannerUploadError: null,
backgroundUploadError: null,
deletingAccount: false,
@ -41,7 +40,8 @@ const UserSettings = {
},
components: {
StyleSwitcher,
TabSwitcher
TabSwitcher,
ImageCropper
},
computed: {
user () {
@ -60,6 +60,9 @@ const UserSettings = {
private: { selected: this.newDefaultScope === 'private' },
direct: { selected: this.newDefaultScope === 'direct' }
}
},
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
}
},
methods: {
@ -117,35 +120,15 @@ const UserSettings = {
}
reader.readAsDataURL(file)
},
submitAvatar () {
if (!this.avatarPreview) { return }
let img = this.avatarPreview
// eslint-disable-next-line no-undef
let imginfo = new Image()
let cropX, cropY, cropW, cropH
imginfo.src = img
if (imginfo.height > imginfo.width) {
cropX = 0
cropW = imginfo.width
cropY = Math.floor((imginfo.height - imginfo.width) / 2)
cropH = imginfo.width
} else {
cropY = 0
cropH = imginfo.height
cropX = Math.floor((imginfo.width - imginfo.height) / 2)
cropW = imginfo.height
}
this.avatarUploading = true
this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
submitAvatar (cropper) {
const img = cropper.getCroppedCanvas().toDataURL('image/jpeg')
return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
this.avatarPreview = null
} else {
this.avatarUploadError = this.$t('upload.error.base') + user.error
throw new Error(this.$t('upload.error.base') + user.error)
}
this.avatarUploading = false
})
},
clearUploadError (slot) {

View file

@ -1,7 +1,20 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
{{$t('settings.user_settings')}}
<div class="title">
{{$t('settings.user_settings')}}
</div>
<transition name="fade">
<template v-if="currentSaveStateNotice">
<div @click.prevent class="alert error" v-if="currentSaveStateNotice.error">
{{ $t('settings.saving_err') }}
</div>
<div @click.prevent class="alert transparent" v-if="!currentSaveStateNotice.error">
{{ $t('settings.saving_ok') }}
</div>
</template>
</transition>
</div>
<div class="panel-body profile-edit">
<tab-switcher>
@ -48,19 +61,10 @@
<h2>{{$t('settings.avatar')}}</h2>
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</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="current-avatar"></img>
<p>{{$t('settings.set_new_avatar')}}</p>
<img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
</img>
<div>
<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="button-icon icon-cancel" @click="clearUploadError('avatar')"></i>
</div>
<button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
<image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
</div>
<div class="setting-item">
<h2>{{$t('settings.profile_banner')}}</h2>
@ -167,6 +171,8 @@
</script>
<style lang="scss">
@import '../../_variables.scss';
.profile-edit {
.bio {
margin: 0;
@ -193,5 +199,13 @@
.bg {
max-width: 100%;
}
.current-avatar {
display: block;
width: 150px;
height: 150px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
}
</style>

View file

@ -21,6 +21,11 @@
"more": "More",
"generic_error": "An error occured"
},
"image_cropper": {
"crop_picture": "Crop picture",
"save": "Save",
"cancel": "Cancel"
},
"login": {
"login": "Log in",
"description": "Log in with OAuth",
@ -31,6 +36,10 @@
"username": "Username",
"hint": "Log in to join the discussion"
},
"media_modal": {
"previous": "Previous",
"next": "Next"
},
"nav": {
"about": "About",
"back": "Back",
@ -206,6 +215,7 @@
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
"tooltipRadius": "Tooltips/alerts",
"upload_a_photo": "Upload a photo",
"user_settings": "User Settings",
"values": {
"false": "no",
@ -332,7 +342,8 @@
"repeated": "repeated",
"show_new": "Show new",
"up_to_date": "Up-to-date",
"no_more_statuses": "No more statuses"
"no_more_statuses": "No more statuses",
"no_statuses": "No statuses"
},
"user_card": {
"approve": "Approve",
@ -344,7 +355,7 @@
"follow_sent": "Request sent!",
"follow_progress": "Requesting…",
"follow_again": "Send request again?",
"follow_unfollow": "Stop following",
"follow_unfollow": "Unfollow",
"followees": "Following",
"followers": "Followers",
"following": "Following!",

View file

@ -84,12 +84,12 @@ export default function createPersistedState ({
setState(key, reducer(state, paths), storage)
.then(success => {
if (typeof success !== 'undefined') {
if (mutation.type === 'setOption') {
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
store.dispatch('settingsSaved', { success })
}
}
}, error => {
if (mutation.type === 'setOption') {
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
store.dispatch('settingsSaved', { error })
}
})

View file

@ -231,8 +231,14 @@ const users = {
store.commit('setToken', result.access_token)
store.dispatch('loginUser', result.access_token)
} else {
let data = await response.json()
let errors = humanizeErrors(JSON.parse(data.error))
const data = await response.json()
let errors = JSON.parse(data.error)
// replace ap_id with username
if (errors.ap_id) {
errors.username = errors.ap_id
delete errors.ap_id
}
errors = humanizeErrors(errors)
store.commit('signUpFailure', errors)
throw Error(errors)
}

View file

@ -117,6 +117,9 @@ export const parseUser = (data) => {
output.statuses_count = data.statuses_count
output.friends = []
output.followers = []
if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count
}
return output
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

1073
yarn.lock

File diff suppressed because it is too large Load diff