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:
commit
90a82b7dec
33 changed files with 490 additions and 1214 deletions
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
11
src/App.scss
11
src/App.scss
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
128
src/components/image_cropper/image_cropper.js
Normal file
128
src/components/image_cropper/image_cropper.js
Normal 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
|
42
src/components/image_cropper/image_cropper.vue
Normal file
42
src/components/image_cropper/image_cropper.vue
Normal 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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -91,7 +91,8 @@ const settings = {
|
|||
},
|
||||
currentSaveStateNotice () {
|
||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||
}
|
||||
},
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }
|
||||
},
|
||||
watch: {
|
||||
hideAttachmentsLocal (value) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ const Timeline = {
|
|||
'title',
|
||||
'userId',
|
||||
'tag',
|
||||
'embedded'
|
||||
'embedded',
|
||||
'count'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
|
|
@ -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()'>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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!",
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
BIN
static/bg2.jpg
BIN
static/bg2.jpg
Binary file not shown.
Before Width: | Height: | Size: 224 KiB |
BIN
static/bgalt.jpg
BIN
static/bgalt.jpg
Binary file not shown.
Before Width: | Height: | Size: 323 KiB |
Loading…
Add table
Reference in a new issue