Merge branch 'profile-edit' into shigusegubu-themes3
This commit is contained in:
commit
23d53e9fd0
27 changed files with 1192 additions and 866 deletions
1
changelog.d/user_profile_edit.change
Normal file
1
changelog.d/user_profile_edit.change
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Profile editing change overhaul
|
||||||
|
|
@ -782,12 +782,6 @@ option {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.visibility-notice {
|
|
||||||
padding: 0.5em;
|
|
||||||
border: 1px solid var(--textFaint);
|
|
||||||
border-radius: var(--roundness);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-dismissible {
|
.notice-dismissible {
|
||||||
padding-right: 4rem;
|
padding-right: 4rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
|
|
@ -139,9 +139,9 @@
|
||||||
</confirm-modal>
|
</confirm-modal>
|
||||||
<UserTimedFilterModal
|
<UserTimedFilterModal
|
||||||
v-if="blockExpirationSupported"
|
v-if="blockExpirationSupported"
|
||||||
|
ref="timedBlockDialog"
|
||||||
:is-mute="false"
|
:is-mute="false"
|
||||||
:user="user"
|
:user="user"
|
||||||
ref="timedBlockDialog"
|
|
||||||
/>
|
/>
|
||||||
</teleport>
|
</teleport>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -205,12 +205,6 @@ const EmojiInput = {
|
||||||
return emoji.displayText
|
return emoji.displayText
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onInputScroll () {
|
|
||||||
this.$refs.hiddenOverlay.scrollTo({
|
|
||||||
top: this.input.scrollTop,
|
|
||||||
left: this.input.scrollLeft
|
|
||||||
})
|
|
||||||
},
|
|
||||||
suggestionListId () {
|
suggestionListId () {
|
||||||
return `suggestions-${this.randomSeed}`
|
return `suggestions-${this.randomSeed}`
|
||||||
},
|
},
|
||||||
|
|
@ -239,7 +233,6 @@ const EmojiInput = {
|
||||||
this.overlayStyle.fontSize = style.fontSize
|
this.overlayStyle.fontSize = style.fontSize
|
||||||
this.overlayStyle.wordWrap = style.wordWrap
|
this.overlayStyle.wordWrap = style.wordWrap
|
||||||
this.overlayStyle.whiteSpace = style.whiteSpace
|
this.overlayStyle.whiteSpace = style.whiteSpace
|
||||||
this.resize()
|
|
||||||
input.addEventListener('blur', this.onBlur)
|
input.addEventListener('blur', this.onBlur)
|
||||||
input.addEventListener('focus', this.onFocus)
|
input.addEventListener('focus', this.onFocus)
|
||||||
input.addEventListener('paste', this.onPaste)
|
input.addEventListener('paste', this.onPaste)
|
||||||
|
|
@ -302,6 +295,13 @@ const EmojiInput = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onInputScroll (e) {
|
||||||
|
this.$refs.hiddenOverlay.scrollTo({
|
||||||
|
top: this.input.scrollTop,
|
||||||
|
left: this.input.scrollLeft
|
||||||
|
})
|
||||||
|
this.setCaret(e)
|
||||||
|
},
|
||||||
triggerShowPicker () {
|
triggerShowPicker () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.picker.showPicker()
|
this.$refs.picker.showPicker()
|
||||||
|
|
@ -561,8 +561,6 @@ const EmojiInput = {
|
||||||
this.$refs.suggestorPopover.updateStyles()
|
this.$refs.suggestorPopover.updateStyles()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
resize () {
|
|
||||||
},
|
|
||||||
autoCompleteItemLabel (suggestion) {
|
autoCompleteItemLabel (suggestion) {
|
||||||
if (suggestion.user) {
|
if (suggestion.user) {
|
||||||
return suggestion.displayText + ' ' + suggestion.detailText
|
return suggestion.displayText + ' ' + suggestion.detailText
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
.emoji-picker-icon {
|
.emoji-picker-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -123,7 +124,7 @@
|
||||||
margin: 0.2em 0.25em;
|
margin: 0.2em 0.25em;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 24px;
|
line-height: 1.2em;
|
||||||
|
|
||||||
&:hover i {
|
&:hover i {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|
@ -133,7 +134,7 @@
|
||||||
.emoji-picker-panel {
|
.emoji-picker-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
margin-top: 2px;
|
margin-top: 0.2em;
|
||||||
|
|
||||||
&.hide {
|
&.hide {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -152,7 +153,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.with-picker input {
|
&.with-picker input {
|
||||||
padding-right: 30px;
|
padding-right: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden-overlay {
|
.hidden-overlay {
|
||||||
|
|
@ -215,8 +216,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailText {
|
.detailText {
|
||||||
font-size: 9px;
|
font-size: 0.6em;
|
||||||
line-height: 9px;
|
line-height: 0.6em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,6 @@ library.add(
|
||||||
|
|
||||||
const ImageCropper = {
|
const ImageCropper = {
|
||||||
props: {
|
props: {
|
||||||
trigger: {
|
|
||||||
type: [String, window.Element],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
submitHandler: {
|
|
||||||
type: Function,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
mimes: {
|
mimes: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
|
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
|
||||||
|
|
@ -30,6 +22,9 @@ const ImageCropper = {
|
||||||
},
|
},
|
||||||
cancelButtonLabel: {
|
cancelButtonLabel: {
|
||||||
type: String
|
type: String
|
||||||
|
},
|
||||||
|
aspectRatio: {
|
||||||
|
type: Number
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
|
@ -39,17 +34,7 @@ const ImageCropper = {
|
||||||
submitting: false
|
submitting: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
emits: ['submit'],
|
||||||
saveText () {
|
|
||||||
return this.saveButtonLabel || this.$t('image_cropper.save')
|
|
||||||
},
|
|
||||||
saveWithoutCroppingText () {
|
|
||||||
return this.saveWithoutCroppingButtonlabel || this.$t('image_cropper.save_without_cropping')
|
|
||||||
},
|
|
||||||
cancelText () {
|
|
||||||
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
destroy () {
|
destroy () {
|
||||||
this.$refs.input.value = ''
|
this.$refs.input.value = ''
|
||||||
|
|
@ -65,20 +50,15 @@ const ImageCropper = {
|
||||||
} else {
|
} else {
|
||||||
cropperPromise = Promise.resolve()
|
cropperPromise = Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
cropperPromise.then(canvas => {
|
cropperPromise.then(canvas => {
|
||||||
this.submitHandler(canvas, this.file)
|
this.$emit('submit', { canvas, file: this.file })
|
||||||
.then(() => this.destroy())
|
this.submitting = false
|
||||||
.finally(() => {
|
|
||||||
this.submitting = false
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
pickImage () {
|
pickImage () {
|
||||||
this.$refs.input.click()
|
this.$refs.input.click()
|
||||||
},
|
},
|
||||||
getTriggerDOM () {
|
|
||||||
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
|
|
||||||
},
|
|
||||||
readFile () {
|
readFile () {
|
||||||
const fileInput = this.$refs.input
|
const fileInput = this.$refs.input
|
||||||
if (fileInput.files != null && fileInput.files[0] != null) {
|
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||||
|
|
@ -117,23 +97,11 @@ const ImageCropper = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
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
|
// listen for input file changes
|
||||||
const fileInput = this.$refs.input
|
const fileInput = this.$refs.input
|
||||||
fileInput.addEventListener('change', this.readFile)
|
fileInput.addEventListener('change', this.readFile)
|
||||||
},
|
},
|
||||||
beforeUnmount: function () {
|
beforeUnmount: function () {
|
||||||
// remove the event listeners
|
|
||||||
const trigger = this.getTriggerDOM()
|
|
||||||
if (trigger) {
|
|
||||||
trigger.removeEventListener('click', this.pickImage)
|
|
||||||
}
|
|
||||||
const fileInput = this.$refs.input
|
const fileInput = this.$refs.input
|
||||||
fileInput.removeEventListener('change', this.readFile)
|
fileInput.removeEventListener('change', this.readFile)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="image-cropper">
|
<div class="image-cropper">
|
||||||
<div v-if="dataUrl">
|
<div class="image">
|
||||||
<cropper-canvas
|
<cropper-canvas
|
||||||
ref="cropperCanvas"
|
ref="cropperCanvas"
|
||||||
background
|
background
|
||||||
class="image-cropper-canvas"
|
class="image-cropper-canvas"
|
||||||
height="25em"
|
height="100%"
|
||||||
>
|
>
|
||||||
<cropper-image
|
<cropper-image
|
||||||
|
v-if="dataUrl"
|
||||||
ref="cropperImage"
|
ref="cropperImage"
|
||||||
:src="dataUrl"
|
:src="dataUrl"
|
||||||
alt="Picture"
|
alt="Picture"
|
||||||
|
|
@ -22,8 +23,8 @@
|
||||||
/>
|
/>
|
||||||
<cropper-selection
|
<cropper-selection
|
||||||
ref="cropperSelection"
|
ref="cropperSelection"
|
||||||
initial-coverage="1"
|
initial-coverage="0.9"
|
||||||
aspect-ratio="1"
|
:aspect-ratio="aspectRatio"
|
||||||
movable
|
movable
|
||||||
resizable
|
resizable
|
||||||
@change="onCropperSelectionChange"
|
@change="onCropperSelectionChange"
|
||||||
|
|
@ -47,41 +48,13 @@
|
||||||
<cropper-handle action="sw-resize" />
|
<cropper-handle action="sw-resize" />
|
||||||
</cropper-selection>
|
</cropper-selection>
|
||||||
</cropper-canvas>
|
</cropper-canvas>
|
||||||
<div class="image-cropper-buttons-wrapper">
|
|
||||||
<button
|
|
||||||
class="button-default btn"
|
|
||||||
type="button"
|
|
||||||
:disabled="submitting"
|
|
||||||
@click="submit()"
|
|
||||||
v-text="saveText"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="button-default btn"
|
|
||||||
type="button"
|
|
||||||
:disabled="submitting"
|
|
||||||
@click="destroy"
|
|
||||||
v-text="cancelText"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="button-default btn"
|
|
||||||
type="button"
|
|
||||||
:disabled="submitting"
|
|
||||||
@click="submit(false)"
|
|
||||||
v-text="saveWithoutCroppingText"
|
|
||||||
/>
|
|
||||||
<FAIcon
|
|
||||||
v-if="submitting"
|
|
||||||
spin
|
|
||||||
icon="circle-notch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
ref="input"
|
ref="input"
|
||||||
type="file"
|
type="file"
|
||||||
class="input image-cropper-img-input"
|
class="input image-cropper-img-input"
|
||||||
:accept="mimes"
|
:accept="mimes"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -89,13 +62,15 @@
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.image-cropper {
|
.image-cropper {
|
||||||
&-img-input {
|
display: flex;
|
||||||
display: none;
|
flex-direction: column;
|
||||||
|
|
||||||
|
&-canvas, .image {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-canvas {
|
& &-img-input {
|
||||||
height: 25em;
|
display: none;
|
||||||
width: 25em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-buttons-wrapper {
|
&-buttons-wrapper {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<div class="interface-language-switcher">
|
<div class="interface-language-switcher">
|
||||||
<label>
|
<label>
|
||||||
{{ promptText }}
|
{{ promptText }}
|
||||||
|
<ProfileSettingIndicator :is-profile="profile" />
|
||||||
</label>
|
</label>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li
|
<li
|
||||||
|
|
@ -46,12 +47,15 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import localeService from '../../services/locale/locale.service.js'
|
import localeService from '../../services/locale/locale.service.js'
|
||||||
|
|
||||||
import Select from '../select/select.vue'
|
import Select from '../select/select.vue'
|
||||||
|
import ProfileSettingIndicator from 'src/components/settings_modal/helpers/profile_setting_indicator.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
// eslint-disable-next-line vue/no-reserved-component-names
|
// eslint-disable-next-line vue/no-reserved-component-names
|
||||||
Select
|
Select,
|
||||||
|
ProfileSettingIndicator
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
promptText: {
|
promptText: {
|
||||||
|
|
@ -62,11 +66,12 @@ export default {
|
||||||
type: [Array, String],
|
type: [Array, String],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
setLanguage: {
|
profile: {
|
||||||
type: Function,
|
type: Boolean,
|
||||||
required: true
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['update'],
|
||||||
computed: {
|
computed: {
|
||||||
languages () {
|
languages () {
|
||||||
return localeService.languages
|
return localeService.languages
|
||||||
|
|
@ -77,7 +82,7 @@ export default {
|
||||||
return Array.isArray(this.language) ? this.language : [this.language]
|
return Array.isArray(this.language) ? this.language : [this.language]
|
||||||
},
|
},
|
||||||
set: function (val) {
|
set: function (val) {
|
||||||
this.setLanguage(val)
|
this.$emit('update', val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
} from 'src/services/theme_data/css_utils.js'
|
} from 'src/services/theme_data/css_utils.js'
|
||||||
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
||||||
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
|
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
|
||||||
|
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
||||||
|
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||||
|
|
@ -72,7 +73,10 @@ const AppearanceTab = {
|
||||||
key: mode,
|
key: mode,
|
||||||
value: mode,
|
value: mode,
|
||||||
label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
|
label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
|
||||||
}))
|
})),
|
||||||
|
backgroundUploading: false,
|
||||||
|
background: null,
|
||||||
|
backgroundPreview: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -420,7 +424,55 @@ const AppearanceTab = {
|
||||||
].join(''))
|
].join(''))
|
||||||
sheet.ready = true
|
sheet.ready = true
|
||||||
adoptStyleSheets()
|
adoptStyleSheets()
|
||||||
}
|
},
|
||||||
|
uploadFile (slot, e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
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'])
|
||||||
|
useInterfaceStore().pushGlobalNotice({
|
||||||
|
messageKey: 'upload.error.message',
|
||||||
|
messageArgs: [
|
||||||
|
this.$t('upload.error.file_too_big', {
|
||||||
|
filesize: filesize.num,
|
||||||
|
filesizeunit: filesize.unit,
|
||||||
|
allowedsize: allowedsize.num,
|
||||||
|
allowedsizeunit: allowedsize.unit
|
||||||
|
})
|
||||||
|
],
|
||||||
|
level: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = ({ target }) => {
|
||||||
|
const img = target.result
|
||||||
|
this[slot + 'Preview'] = img
|
||||||
|
this[slot] = file
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
},
|
||||||
|
resetBackground () {
|
||||||
|
const confirmed = window.confirm(this.$t('settings.reset_background_confirm'))
|
||||||
|
if (confirmed) {
|
||||||
|
this.submitBackground('')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitBackground (background) {
|
||||||
|
if (!this.backgroundPreview && background !== '') { return }
|
||||||
|
|
||||||
|
this.backgroundUploading = true
|
||||||
|
this.$store.state.api.backendInteractor.updateProfileImages({ background })
|
||||||
|
.then((data) => {
|
||||||
|
this.$store.commit('addNewUsers', [data])
|
||||||
|
this.$store.commit('setCurrentUser', data)
|
||||||
|
this.backgroundPreview = null
|
||||||
|
})
|
||||||
|
.catch(this.displayUploadError)
|
||||||
|
.finally(() => { this.backgroundUploading = false })
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,46 @@
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="file"] {
|
||||||
|
padding: 0.25em;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-background-preview {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 300px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.2em;
|
||||||
|
right: 0.2em;
|
||||||
|
border-radius: var(--roundness);
|
||||||
|
background-color: rgb(0 0 0 / 60%);
|
||||||
|
opacity: 0.7;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.palettes-container {
|
.palettes-container {
|
||||||
height: 15em;
|
height: 15em;
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,49 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.background') }}</h2>
|
||||||
|
<div class="banner-background-preview">
|
||||||
|
<img :src="user.background_image">
|
||||||
|
<button
|
||||||
|
v-if="!isDefaultBackground"
|
||||||
|
class="button-unstyled reset-button"
|
||||||
|
:title="$t('settings.reset_profile_background')"
|
||||||
|
@click="resetBackground"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
icon="times"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>{{ $t('settings.set_new_background') }}</p>
|
||||||
|
<img
|
||||||
|
v-if="backgroundPreview"
|
||||||
|
class="banner-background-preview"
|
||||||
|
:src="backgroundPreview"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="input"
|
||||||
|
@change="uploadFile('background', $event)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<FAIcon
|
||||||
|
v-if="backgroundUploading"
|
||||||
|
class="uploading"
|
||||||
|
spin
|
||||||
|
icon="circle-notch"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-else-if="backgroundPreview"
|
||||||
|
class="btn button-default"
|
||||||
|
@click="submitBackground(background)"
|
||||||
|
>
|
||||||
|
{{ $t('settings.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{ $t('settings.scale_and_layout') }}</h2>
|
<h2>{{ $t('settings.scale_and_layout') }}</h2>
|
||||||
<div class="alert neutral theme-notice">
|
<div class="alert neutral theme-notice">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||||
|
|
@ -8,8 +10,8 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche
|
||||||
import Select from 'src/components/select/select.vue'
|
import Select from 'src/components/select/select.vue'
|
||||||
|
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
|
||||||
|
|
||||||
|
import localeService from 'src/services/locale/locale.service.js'
|
||||||
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
|
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
|
@ -75,7 +77,6 @@ const GeneralTab = {
|
||||||
UnitSetting,
|
UnitSetting,
|
||||||
InterfaceLanguageSwitcher,
|
InterfaceLanguageSwitcher,
|
||||||
ScopeSelector,
|
ScopeSelector,
|
||||||
ProfileSettingIndicator,
|
|
||||||
Select
|
Select
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -97,7 +98,10 @@ const GeneralTab = {
|
||||||
},
|
},
|
||||||
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
|
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
|
||||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||||
...SharedComputedObject()
|
...SharedComputedObject(),
|
||||||
|
...mapState({
|
||||||
|
blockExpirationSupported: state => state.instance.blockExpiration,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeDefaultScope (value) {
|
changeDefaultScope (value) {
|
||||||
|
|
@ -117,7 +121,19 @@ const GeneralTab = {
|
||||||
},
|
},
|
||||||
clearEmojiCache () {
|
clearEmojiCache () {
|
||||||
this.clearCache(emojiCacheKey)
|
this.clearCache(emojiCacheKey)
|
||||||
}
|
},
|
||||||
|
updateProfile () {
|
||||||
|
const params = {
|
||||||
|
language: localeService.internalToBackendLocaleMulti(this.emailLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.state.api.backendInteractor
|
||||||
|
.updateProfile({ params })
|
||||||
|
.then((user) => {
|
||||||
|
this.$store.commit('addNewUsers', [user])
|
||||||
|
this.$store.commit('setCurrentUser', user)
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,15 @@
|
||||||
<interface-language-switcher
|
<interface-language-switcher
|
||||||
:prompt-text="$t('settings.interfaceLanguage')"
|
:prompt-text="$t('settings.interfaceLanguage')"
|
||||||
:language="language"
|
:language="language"
|
||||||
:set-language="val => language = val"
|
@update="val => language = val"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<interface-language-switcher
|
||||||
|
:prompt-text="$t('settings.email_language')"
|
||||||
|
:language="emailLanguage"
|
||||||
|
:profile="true"
|
||||||
|
@update="val => { emailLanguage = val; updateProfile() }"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="instanceSpecificPanelPresent">
|
<li v-if="instanceSpecificPanelPresent">
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
import unescape from 'lodash/unescape'
|
import UserCard from 'src/components/user_card/user_card.vue'
|
||||||
import merge from 'lodash/merge'
|
|
||||||
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
||||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||||
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
|
||||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||||
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||||
import suggestor from 'src/components/emoji_input/suggestor.js'
|
import suggestor from 'src/components/emoji_input/suggestor.js'
|
||||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||||
import Select from 'src/components/select/select.vue'
|
import Select from 'src/components/select/select.vue'
|
||||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import localeService from 'src/services/locale/locale.service.js'
|
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||||
|
|
||||||
import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js'
|
import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
@ -21,7 +19,6 @@ import {
|
||||||
faPlus,
|
faPlus,
|
||||||
faCircleNotch
|
faCircleNotch
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useInterfaceStore } from 'src/stores/interface'
|
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faTimes,
|
faTimes,
|
||||||
|
|
@ -32,35 +29,19 @@ library.add(
|
||||||
const ProfileTab = {
|
const ProfileTab = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
newName: this.$store.state.users.currentUser.name_unescaped,
|
locked: this.$store.state.users.currentUser.locked,
|
||||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
|
||||||
newLocked: this.$store.state.users.currentUser.locked,
|
|
||||||
newBirthday: this.$store.state.users.currentUser.birthday,
|
|
||||||
showBirthday: this.$store.state.users.currentUser.show_birthday,
|
|
||||||
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
|
||||||
showRole: this.$store.state.users.currentUser.show_role,
|
|
||||||
role: this.$store.state.users.currentUser.role,
|
|
||||||
bot: this.$store.state.users.currentUser.bot,
|
|
||||||
actorType: this.$store.state.users.currentUser.actor_type,
|
|
||||||
pickAvatarBtnVisible: true,
|
|
||||||
bannerUploading: false,
|
|
||||||
backgroundUploading: false,
|
|
||||||
banner: null,
|
|
||||||
bannerPreview: null,
|
|
||||||
background: null,
|
|
||||||
backgroundPreview: null,
|
|
||||||
emailLanguage: this.$store.state.users.currentUser.language || ['']
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
UserCard,
|
||||||
ScopeSelector,
|
ScopeSelector,
|
||||||
ImageCropper,
|
ImageCropper,
|
||||||
EmojiInput,
|
EmojiInput,
|
||||||
Autosuggest,
|
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
BooleanSetting,
|
BooleanSetting,
|
||||||
InterfaceLanguageSwitcher,
|
InterfaceLanguageSwitcher,
|
||||||
|
ProfileSettingIndicator,
|
||||||
Select
|
Select
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -88,193 +69,38 @@ const ProfileTab = {
|
||||||
userSuggestor () {
|
userSuggestor () {
|
||||||
return suggestor({ store: this.$store })
|
return suggestor({ store: this.$store })
|
||||||
},
|
},
|
||||||
fieldsLimits () {
|
|
||||||
return this.$store.state.instance.fieldsLimits
|
|
||||||
},
|
|
||||||
maxFields () {
|
|
||||||
return this.fieldsLimits ? this.fieldsLimits.maxFields : 0
|
|
||||||
},
|
|
||||||
defaultAvatar () {
|
|
||||||
return this.$store.state.instance.server + this.$store.state.instance.defaultAvatar
|
|
||||||
},
|
|
||||||
defaultBanner () {
|
|
||||||
return this.$store.state.instance.server + this.$store.state.instance.defaultBanner
|
|
||||||
},
|
|
||||||
isDefaultAvatar () {
|
|
||||||
const baseAvatar = this.$store.state.instance.defaultAvatar
|
|
||||||
return !(this.$store.state.users.currentUser.profile_image_url) ||
|
|
||||||
this.$store.state.users.currentUser.profile_image_url.includes(baseAvatar)
|
|
||||||
},
|
|
||||||
isDefaultBanner () {
|
|
||||||
const baseBanner = this.$store.state.instance.defaultBanner
|
|
||||||
return !(this.$store.state.users.currentUser.cover_photo) ||
|
|
||||||
this.$store.state.users.currentUser.cover_photo.includes(baseBanner)
|
|
||||||
},
|
|
||||||
isDefaultBackground () {
|
|
||||||
return !(this.$store.state.users.currentUser.background_image)
|
|
||||||
},
|
|
||||||
avatarImgSrc () {
|
|
||||||
const src = this.$store.state.users.currentUser.profile_image_url_original
|
|
||||||
return (!src) ? this.defaultAvatar : src
|
|
||||||
},
|
|
||||||
bannerImgSrc () {
|
bannerImgSrc () {
|
||||||
const src = this.$store.state.users.currentUser.cover_photo
|
const src = this.$store.state.users.currentUser.cover_photo
|
||||||
return (!src) ? this.defaultBanner : src
|
return (!src) ? this.defaultBanner : src
|
||||||
},
|
},
|
||||||
groupActorAvailable () {
|
|
||||||
return this.$store.state.instance.groupActorAvailable
|
|
||||||
},
|
|
||||||
availableActorTypes () {
|
|
||||||
return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service']
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
changeVis (visibility) {
|
||||||
|
this.newDefaultScope = visibility
|
||||||
|
},
|
||||||
updateProfile () {
|
updateProfile () {
|
||||||
const params = {
|
const params = {
|
||||||
note: this.newBio,
|
locked: this.locked
|
||||||
locked: this.newLocked,
|
|
||||||
|
|
||||||
// Backend notation.
|
|
||||||
display_name: this.newName,
|
|
||||||
fields_attributes: this.newFields.filter(el => el != null),
|
|
||||||
actor_type: this.actorType,
|
|
||||||
show_role: this.showRole,
|
|
||||||
birthday: this.newBirthday || '',
|
|
||||||
show_birthday: this.showBirthday
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.emailLanguage) {
|
|
||||||
params.language = localeService.internalToBackendLocaleMulti(this.emailLanguage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.state.api.backendInteractor
|
this.$store.state.api.backendInteractor
|
||||||
.updateProfile({ params })
|
.updateProfile({ params })
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
this.newFields.splice(user.fields.length)
|
|
||||||
merge(this.newFields, user.fields)
|
|
||||||
this.$store.commit('addNewUsers', [user])
|
this.$store.commit('addNewUsers', [user])
|
||||||
this.$store.commit('setCurrentUser', user)
|
this.$store.commit('setCurrentUser', user)
|
||||||
})
|
})
|
||||||
},
|
.catch((error) => {
|
||||||
changeVis (visibility) {
|
this.displayUploadError(error)
|
||||||
this.newDefaultScope = visibility
|
|
||||||
},
|
|
||||||
addField () {
|
|
||||||
if (this.newFields.length < this.maxFields) {
|
|
||||||
this.newFields.push({ name: '', value: '' })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
deleteField (index) {
|
|
||||||
this.newFields.splice(index, 1)
|
|
||||||
},
|
|
||||||
uploadFile (slot, e) {
|
|
||||||
const file = e.target.files[0]
|
|
||||||
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'])
|
|
||||||
useInterfaceStore().pushGlobalNotice({
|
|
||||||
messageKey: 'upload.error.message',
|
|
||||||
messageArgs: [
|
|
||||||
this.$t('upload.error.file_too_big', {
|
|
||||||
filesize: filesize.num,
|
|
||||||
filesizeunit: filesize.unit,
|
|
||||||
allowedsize: allowedsize.num,
|
|
||||||
allowedsizeunit: allowedsize.unit
|
|
||||||
})
|
|
||||||
],
|
|
||||||
level: 'error'
|
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = ({ target }) => {
|
|
||||||
const img = target.result
|
|
||||||
this[slot + 'Preview'] = img
|
|
||||||
this[slot] = file
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
},
|
|
||||||
resetAvatar () {
|
|
||||||
const confirmed = window.confirm(this.$t('settings.reset_avatar_confirm'))
|
|
||||||
if (confirmed) {
|
|
||||||
this.submitAvatar(undefined, '')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetBanner () {
|
|
||||||
const confirmed = window.confirm(this.$t('settings.reset_banner_confirm'))
|
|
||||||
if (confirmed) {
|
|
||||||
this.submitBanner('')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetBackground () {
|
|
||||||
const confirmed = window.confirm(this.$t('settings.reset_background_confirm'))
|
|
||||||
if (confirmed) {
|
|
||||||
this.submitBackground('')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submitAvatar (canvas, file) {
|
|
||||||
const that = this
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
function updateAvatar (avatar, avatarName) {
|
|
||||||
that.$store.state.api.backendInteractor.updateProfileImages({ avatar, avatarName })
|
|
||||||
.then((user) => {
|
|
||||||
that.$store.commit('addNewUsers', [user])
|
|
||||||
that.$store.commit('setCurrentUser', user)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
that.displayUploadError(error)
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canvas) {
|
|
||||||
canvas.toBlob((data) => updateAvatar(data, file.name), file.type)
|
|
||||||
} else {
|
|
||||||
updateAvatar(file, file.name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
submitBanner (banner) {
|
|
||||||
if (!this.bannerPreview && banner !== '') { return }
|
|
||||||
|
|
||||||
this.bannerUploading = true
|
|
||||||
this.$store.state.api.backendInteractor.updateProfileImages({ banner })
|
|
||||||
.then((user) => {
|
|
||||||
this.$store.commit('addNewUsers', [user])
|
|
||||||
this.$store.commit('setCurrentUser', user)
|
|
||||||
this.bannerPreview = null
|
|
||||||
})
|
|
||||||
.catch(this.displayUploadError)
|
|
||||||
.finally(() => { this.bannerUploading = false })
|
|
||||||
},
|
|
||||||
submitBackground (background) {
|
|
||||||
if (!this.backgroundPreview && background !== '') { return }
|
|
||||||
|
|
||||||
this.backgroundUploading = true
|
|
||||||
this.$store.state.api.backendInteractor.updateProfileImages({ background })
|
|
||||||
.then((data) => {
|
|
||||||
this.$store.commit('addNewUsers', [data])
|
|
||||||
this.$store.commit('setCurrentUser', data)
|
|
||||||
this.backgroundPreview = null
|
|
||||||
})
|
|
||||||
.catch(this.displayUploadError)
|
|
||||||
.finally(() => { this.backgroundUploading = false })
|
|
||||||
},
|
|
||||||
displayUploadError (error) {
|
|
||||||
useInterfaceStore().pushGlobalNotice({
|
|
||||||
messageKey: 'upload.error.message',
|
|
||||||
messageArgs: [error.message],
|
|
||||||
level: 'error'
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
propsToNative (props) {
|
propsToNative (props) {
|
||||||
return propsToNative(props)
|
return propsToNative(props)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
locked () {
|
||||||
|
this.updateProfile()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,131 +3,12 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visibility-tray {
|
|
||||||
padding-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="file"] {
|
|
||||||
padding: 5px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-background-preview {
|
|
||||||
max-width: 100%;
|
|
||||||
width: 300px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploading {
|
.uploading {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
margin: 0.25em;
|
margin: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-changer {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-avatar-container {
|
|
||||||
position: relative;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-avatar {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: var(--roundness);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.2em;
|
|
||||||
right: 0.2em;
|
|
||||||
border-radius: var(--roundness);
|
|
||||||
background-color: rgb(0 0 0 / 60%);
|
|
||||||
opacity: 0.7;
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.5em;
|
|
||||||
font-size: 1.5em;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.oauth-tokens {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-usersearch-wrapper {
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-bulk-actions {
|
|
||||||
text-align: right;
|
|
||||||
padding: 0 1em;
|
|
||||||
min-height: 2em;
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-domain-mute-form {
|
|
||||||
padding: 1em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
button {
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-top: 1em;
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-subitem {
|
.setting-subitem {
|
||||||
margin-left: 1.75em;
|
margin-left: 1.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-fields {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
& > .emoji-input {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
margin: 0 0.2em 0.5em;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-field {
|
|
||||||
width: 20px;
|
|
||||||
align-self: center;
|
|
||||||
margin: 0 0.2em 0.5em;
|
|
||||||
padding: 0 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.birthday-input {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,283 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="profile-tab">
|
<div class="profile-tab">
|
||||||
<div class="setting-item">
|
<UserCard
|
||||||
<h2>{{ $t('settings.name_bio') }}</h2>
|
:user-id="user.id"
|
||||||
<p>{{ $t('settings.name') }}</p>
|
:editable="true"
|
||||||
<EmojiInput
|
:switcher="false"
|
||||||
v-model="newName"
|
rounded="top"
|
||||||
enable-emoji-picker
|
|
||||||
:suggest="emojiSuggestor"
|
|
||||||
>
|
>
|
||||||
<template #default="inputProps">
|
</UserCard>
|
||||||
<input
|
|
||||||
id="username"
|
|
||||||
v-model="newName"
|
|
||||||
class="input name-changer"
|
|
||||||
v-bind="propsToNative(inputProps)"
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</EmojiInput>
|
|
||||||
<p>{{ $t('settings.bio') }}</p>
|
|
||||||
<EmojiInput
|
|
||||||
v-model="newBio"
|
|
||||||
enable-emoji-picker
|
|
||||||
:suggest="emojiUserSuggestor"
|
|
||||||
>
|
|
||||||
<template #default="inputProps">
|
|
||||||
<textarea
|
|
||||||
v-model="newBio"
|
|
||||||
class="input bio resize-height"
|
|
||||||
v-bind="propsToNative(inputProps)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</EmojiInput>
|
|
||||||
<p v-if="role === 'admin' || role === 'moderator'">
|
|
||||||
<Checkbox v-model="showRole">
|
|
||||||
<template v-if="role === 'admin'">
|
|
||||||
{{ $t('settings.show_admin_badge') }}
|
|
||||||
</template>
|
|
||||||
<template v-if="role === 'moderator'">
|
|
||||||
{{ $t('settings.show_moderator_badge') }}
|
|
||||||
</template>
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.birthday.label') }}</p>
|
|
||||||
<input
|
|
||||||
id="birthday"
|
|
||||||
v-model="newBirthday"
|
|
||||||
type="date"
|
|
||||||
class="input birthday-input"
|
|
||||||
>
|
|
||||||
<Checkbox v-model="showBirthday">
|
|
||||||
{{ $t('settings.birthday.show_birthday') }}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div v-if="maxFields > 0">
|
|
||||||
<p>{{ $t('settings.profile_fields.label') }}</p>
|
|
||||||
<div
|
|
||||||
v-for="(_, i) in newFields"
|
|
||||||
:key="i"
|
|
||||||
class="profile-fields"
|
|
||||||
>
|
|
||||||
<EmojiInput
|
|
||||||
v-model="newFields[i].name"
|
|
||||||
enable-emoji-picker
|
|
||||||
hide-emoji-button
|
|
||||||
:suggest="userSuggestor"
|
|
||||||
>
|
|
||||||
<template #default="inputProps">
|
|
||||||
<input
|
|
||||||
v-model="newFields[i].name"
|
|
||||||
:placeholder="$t('settings.profile_fields.name')"
|
|
||||||
v-bind="propsToNative(inputProps)"
|
|
||||||
class="input"
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</EmojiInput>
|
|
||||||
<EmojiInput
|
|
||||||
v-model="newFields[i].value"
|
|
||||||
enable-emoji-picker
|
|
||||||
hide-emoji-button
|
|
||||||
:suggest="userSuggestor"
|
|
||||||
>
|
|
||||||
<template #default="inputProps">
|
|
||||||
<input
|
|
||||||
v-model="newFields[i].value"
|
|
||||||
:placeholder="$t('settings.profile_fields.value')"
|
|
||||||
v-bind="propsToNative(inputProps)"
|
|
||||||
class="input"
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</EmojiInput>
|
|
||||||
<button
|
|
||||||
class="delete-field button-unstyled -hover-highlight"
|
|
||||||
@click="deleteField(i)"
|
|
||||||
>
|
|
||||||
<!-- TODO something is wrong with v-show here -->
|
|
||||||
<FAIcon
|
|
||||||
v-if="newFields.length > 1"
|
|
||||||
icon="times"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="newFields.length < maxFields"
|
|
||||||
class="add-field faint button-unstyled -hover-highlight"
|
|
||||||
@click="addField"
|
|
||||||
>
|
|
||||||
<FAIcon icon="plus" />
|
|
||||||
{{ $t("settings.profile_fields.add_field") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<label>
|
|
||||||
{{ $t('settings.actor_type') }}
|
|
||||||
<Select v-model="actorType">
|
|
||||||
<option
|
|
||||||
v-for="option in availableActorTypes"
|
|
||||||
:key="option"
|
|
||||||
:value="option"
|
|
||||||
>
|
|
||||||
{{ $t('settings.actor_type_' + option) }}
|
|
||||||
</option>
|
|
||||||
</Select>
|
|
||||||
</label>
|
|
||||||
</p>
|
|
||||||
<div v-if="groupActorAvailable">
|
|
||||||
<small>
|
|
||||||
{{ $t('settings.actor_type_description') }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<interface-language-switcher
|
|
||||||
:prompt-text="$t('settings.email_language')"
|
|
||||||
:language="emailLanguage"
|
|
||||||
:set-language="val => emailLanguage = val"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
:disabled="newName && newName.length === 0"
|
|
||||||
class="btn button-default"
|
|
||||||
@click="updateProfile"
|
|
||||||
>
|
|
||||||
{{ $t('settings.save') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.avatar') }}</h2>
|
|
||||||
<p class="visibility-notice">
|
|
||||||
{{ $t('settings.avatar_size_instruction') }}
|
|
||||||
</p>
|
|
||||||
<div class="current-avatar-container">
|
|
||||||
<img
|
|
||||||
:src="user.profile_image_url_original"
|
|
||||||
class="current-avatar"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="!isDefaultAvatar && pickAvatarBtnVisible"
|
|
||||||
:title="$t('settings.reset_avatar')"
|
|
||||||
class="button-unstyled reset-button"
|
|
||||||
@click="resetAvatar"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
icon="times"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p>{{ $t('settings.set_new_avatar') }}</p>
|
|
||||||
<button
|
|
||||||
v-show="pickAvatarBtnVisible"
|
|
||||||
id="pick-avatar"
|
|
||||||
class="button-default btn"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{{ $t('settings.upload_a_photo') }}
|
|
||||||
</button>
|
|
||||||
<image-cropper
|
|
||||||
trigger="#pick-avatar"
|
|
||||||
:submit-handler="submitAvatar"
|
|
||||||
@open="pickAvatarBtnVisible=false"
|
|
||||||
@close="pickAvatarBtnVisible=true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.profile_banner') }}</h2>
|
|
||||||
<div class="banner-background-preview">
|
|
||||||
<img :src="user.cover_photo">
|
|
||||||
<button
|
|
||||||
v-if="!isDefaultBanner"
|
|
||||||
class="button-unstyled reset-button"
|
|
||||||
:title="$t('settings.reset_profile_banner')"
|
|
||||||
@click="resetBanner"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
icon="times"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p>{{ $t('settings.set_new_profile_banner') }}</p>
|
|
||||||
<img
|
|
||||||
v-if="bannerPreview"
|
|
||||||
class="banner-background-preview"
|
|
||||||
:src="bannerPreview"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
class="input"
|
|
||||||
@change="uploadFile('banner', $event)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<FAIcon
|
|
||||||
v-if="bannerUploading"
|
|
||||||
class="uploading"
|
|
||||||
spin
|
|
||||||
icon="circle-notch"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-else-if="bannerPreview"
|
|
||||||
class="btn button-default"
|
|
||||||
@click="submitBanner(banner)"
|
|
||||||
>
|
|
||||||
{{ $t('settings.save') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.profile_background') }}</h2>
|
|
||||||
<div class="banner-background-preview">
|
|
||||||
<img :src="user.background_image">
|
|
||||||
<button
|
|
||||||
v-if="!isDefaultBackground"
|
|
||||||
class="button-unstyled reset-button"
|
|
||||||
:title="$t('settings.reset_profile_background')"
|
|
||||||
@click="resetBackground"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
icon="times"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p>{{ $t('settings.set_new_profile_background') }}</p>
|
|
||||||
<img
|
|
||||||
v-if="backgroundPreview"
|
|
||||||
class="banner-background-preview"
|
|
||||||
:src="backgroundPreview"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
class="input"
|
|
||||||
@change="uploadFile('background', $event)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<FAIcon
|
|
||||||
v-if="backgroundUploading"
|
|
||||||
class="uploading"
|
|
||||||
spin
|
|
||||||
icon="circle-notch"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-else-if="backgroundPreview"
|
|
||||||
class="btn button-default"
|
|
||||||
@click="submitBackground(background)"
|
|
||||||
>
|
|
||||||
{{ $t('settings.save') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{ $t('settings.account_privacy') }}</h2>
|
<h2>{{ $t('settings.account_privacy') }}</h2>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<Checkbox v-model="locked">
|
||||||
source="profile"
|
|
||||||
path="locked"
|
|
||||||
>
|
|
||||||
{{ $t('settings.lock_account_description') }}
|
{{ $t('settings.lock_account_description') }}
|
||||||
</BooleanSetting>
|
</Checkbox>
|
||||||
|
<ProfileSettingIndicator :is-profile="true" />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ const UserAvatar = {
|
||||||
props: [
|
props: [
|
||||||
'user',
|
'user',
|
||||||
'compact',
|
'compact',
|
||||||
'showActorTypeIndicator'
|
'showActorTypeIndicator',
|
||||||
|
'url'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:alt="user.screen_name_ui"
|
:alt="user.screen_name_ui"
|
||||||
:title="user.screen_name_ui"
|
:title="user.screen_name_ui"
|
||||||
:src="imgSrc(user.profile_image_url_original)"
|
:src="url ? url : imgSrc(user.profile_image_url_original)"
|
||||||
:image-load-error="imageLoadError"
|
:image-load-error="imageLoadError"
|
||||||
:class="{ '-compact': compact, '-better-shadow': betterShadow }"
|
:class="{ '-compact': compact, '-better-shadow': betterShadow }"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import merge from 'lodash/merge'
|
||||||
|
import unescape from 'lodash/unescape'
|
||||||
|
|
||||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||||
|
|
@ -10,11 +13,19 @@ import Select from '../select/select.vue'
|
||||||
import UserLink from '../user_link/user_link.vue'
|
import UserLink from '../user_link/user_link.vue'
|
||||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||||
|
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||||
|
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
||||||
|
|
||||||
import localeService from 'src/services/locale/locale.service.js'
|
import localeService from 'src/services/locale/locale.service.js'
|
||||||
|
import suggestor from 'src/components/emoji_input/suggestor.js'
|
||||||
|
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { usePostStatusStore } from 'src/stores/post_status'
|
import { usePostStatusStore } from 'src/stores/post_status'
|
||||||
|
import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faBell,
|
faBell,
|
||||||
|
|
@ -24,13 +35,16 @@ import {
|
||||||
faEdit,
|
faEdit,
|
||||||
faTimes,
|
faTimes,
|
||||||
faExpandAlt,
|
faExpandAlt,
|
||||||
faBirthdayCake
|
faBirthdayCake,
|
||||||
|
faSave,
|
||||||
|
faClockRotateLeft
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import { useMediaViewerStore } from '../../stores/media_viewer'
|
import { useMediaViewerStore } from '../../stores/media_viewer'
|
||||||
import { useInterfaceStore } from '../../stores/interface'
|
import { useInterfaceStore } from '../../stores/interface'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
faSave,
|
||||||
faRss,
|
faRss,
|
||||||
faBell,
|
faBell,
|
||||||
faSearchPlus,
|
faSearchPlus,
|
||||||
|
|
@ -38,11 +52,13 @@ library.add(
|
||||||
faEdit,
|
faEdit,
|
||||||
faTimes,
|
faTimes,
|
||||||
faExpandAlt,
|
faExpandAlt,
|
||||||
faBirthdayCake
|
faBirthdayCake,
|
||||||
|
faClockRotateLeft
|
||||||
)
|
)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: [
|
props: [
|
||||||
|
'editable',
|
||||||
'userId',
|
'userId',
|
||||||
'switcher',
|
'switcher',
|
||||||
'selected',
|
'selected',
|
||||||
|
|
@ -54,7 +70,9 @@ export default {
|
||||||
'hasNoteEditor'
|
'hasNoteEditor'
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
|
DialogModal,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
|
Checkbox,
|
||||||
RemoteFollow,
|
RemoteFollow,
|
||||||
ModerationTools,
|
ModerationTools,
|
||||||
AccountActions,
|
AccountActions,
|
||||||
|
|
@ -65,22 +83,58 @@ export default {
|
||||||
UserLink,
|
UserLink,
|
||||||
UserNote,
|
UserNote,
|
||||||
UserTimedFilterModal,
|
UserTimedFilterModal,
|
||||||
ColorInput
|
ColorInput,
|
||||||
|
EmojiInput,
|
||||||
|
ImageCropper
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
const user = this.$store.getters.findUser(this.userId)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
followRequestInProgress: false,
|
followRequestInProgress: false,
|
||||||
muteExpiryAmount: 0,
|
muteExpiryAmount: 0,
|
||||||
muteExpiryUnit: 'minutes'
|
muteExpiryUnit: 'minutes',
|
||||||
|
|
||||||
|
// Editable stuff
|
||||||
|
editImage: false,
|
||||||
|
|
||||||
|
newName: user.name_unescaped,
|
||||||
|
editingName: false,
|
||||||
|
|
||||||
|
newBio: unescape(user.description),
|
||||||
|
editingBio: false,
|
||||||
|
|
||||||
|
newAvatar: null,
|
||||||
|
newAvatarFile: null,
|
||||||
|
|
||||||
|
newBanner: null,
|
||||||
|
newBannerFile: null,
|
||||||
|
|
||||||
|
newActorType: user.actor_type,
|
||||||
|
newBirthday: user.birthday,
|
||||||
|
newShowBirthday: user.show_birthday,
|
||||||
|
newShowRole: user.show_role,
|
||||||
|
|
||||||
|
newFields: user.fields?.map(field => ({ name: field.name, value: field.value })),
|
||||||
|
editingFields: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
groupActorAvailable () {
|
||||||
|
return this.$store.state.instance.groupActorAvailable
|
||||||
|
},
|
||||||
|
availableActorTypes () {
|
||||||
|
return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service']
|
||||||
|
},
|
||||||
user () {
|
user () {
|
||||||
return this.$store.getters.findUser(this.userId)
|
return this.$store.getters.findUser(this.userId)
|
||||||
},
|
},
|
||||||
|
role () {
|
||||||
|
return this.user.role
|
||||||
|
},
|
||||||
relationship () {
|
relationship () {
|
||||||
return this.$store.getters.relationship(this.userId)
|
return this.$store.getters.relationship(this.userId)
|
||||||
},
|
},
|
||||||
|
|
@ -96,7 +150,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
backgroundImage: [
|
backgroundImage: [
|
||||||
'linear-gradient(to bottom, var(--profileTint), var(--profileTint))',
|
'linear-gradient(to bottom, var(--profileTint), var(--profileTint))',
|
||||||
`url(${this.user.cover_photo})`
|
`url(${this.bannerImgSrc})`
|
||||||
].join(', ')
|
].join(', ')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -114,6 +168,13 @@ export default {
|
||||||
const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
|
const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
|
||||||
return Math.round(this.user.statuses_count / days)
|
return Math.round(this.user.statuses_count / days)
|
||||||
},
|
},
|
||||||
|
emoji () {
|
||||||
|
return this.$store.state.instance.customEmoji.map(e => ({
|
||||||
|
shortcode: e.displayText,
|
||||||
|
static_url: e.imageUrl,
|
||||||
|
url: e.imageUrl
|
||||||
|
}))
|
||||||
|
},
|
||||||
userHighlightType: {
|
userHighlightType: {
|
||||||
get () {
|
get () {
|
||||||
const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
|
const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
|
||||||
|
|
@ -139,6 +200,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
visibleRole () {
|
visibleRole () {
|
||||||
|
if (!this.newShowRole) { return }
|
||||||
const rights = this.user.rights
|
const rights = this.user.rights
|
||||||
if (!rights) { return }
|
if (!rights) { return }
|
||||||
const validRole = rights.admin || rights.moderator
|
const validRole = rights.admin || rights.moderator
|
||||||
|
|
@ -184,6 +246,60 @@ export default {
|
||||||
const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
|
const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||||
return this.user.birthday && new Date(Date.parse(this.user.birthday)).toLocaleDateString(browserLocale, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' })
|
return this.user.birthday && new Date(Date.parse(this.user.birthday)).toLocaleDateString(browserLocale, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Editable stuff
|
||||||
|
avatarImgSrc () {
|
||||||
|
const currentUrl = this.user.profile_image_url_original || this.defaultAvatar
|
||||||
|
const newUrl = this.newAvatar === '' ? this.defaultAvatar : this.newAvatar
|
||||||
|
return (this.newAvatar === null) ? currentUrl : newUrl
|
||||||
|
},
|
||||||
|
bannerImgSrc () {
|
||||||
|
const currentUrl = this.user.cover_photo || this.defaultBanner
|
||||||
|
const newUrl = this.newBanner === '' ? this.defaultBanner : this.newBanner
|
||||||
|
return (this.newBanner === null) ? currentUrl : newUrl
|
||||||
|
},
|
||||||
|
defaultAvatar () {
|
||||||
|
return this.$store.state.instance.server + this.$store.state.instance.defaultAvatar
|
||||||
|
},
|
||||||
|
defaultBanner () {
|
||||||
|
return this.$store.state.instance.server + this.$store.state.instance.defaultBanner
|
||||||
|
},
|
||||||
|
isDefaultAvatar () {
|
||||||
|
const baseAvatar = this.$store.state.instance.defaultAvatar
|
||||||
|
return !(this.$store.state.users.currentUser.profile_image_url) ||
|
||||||
|
this.$store.state.users.currentUser.profile_image_url.includes(baseAvatar)
|
||||||
|
},
|
||||||
|
isDefaultBanner () {
|
||||||
|
const baseBanner = this.$store.state.instance.defaultBanner
|
||||||
|
return !(this.$store.state.users.currentUser.cover_photo) ||
|
||||||
|
this.$store.state.users.currentUser.cover_photo.includes(baseBanner)
|
||||||
|
},
|
||||||
|
isDefaultBackground () {
|
||||||
|
return !(this.$store.state.users.currentUser.background_image)
|
||||||
|
},
|
||||||
|
fieldsLimits () {
|
||||||
|
return this.$store.state.instance.fieldsLimits
|
||||||
|
},
|
||||||
|
maxFields () {
|
||||||
|
return this.fieldsLimits ? this.fieldsLimits.maxFields : 0
|
||||||
|
},
|
||||||
|
emojiUserSuggestor () {
|
||||||
|
return suggestor({
|
||||||
|
emoji: [
|
||||||
|
...this.$store.getters.standardEmojiList,
|
||||||
|
...this.$store.state.instance.customEmoji
|
||||||
|
],
|
||||||
|
store: this.$store
|
||||||
|
})
|
||||||
|
},
|
||||||
|
emojiSuggestor () {
|
||||||
|
return suggestor({
|
||||||
|
emoji: [
|
||||||
|
...this.$store.getters.standardEmojiList,
|
||||||
|
...this.$store.state.instance.customEmoji
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -238,6 +354,130 @@ export default {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.onAvatarClick()
|
this.onAvatarClick()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Editable stuff
|
||||||
|
changeAvatar () {
|
||||||
|
this.editImage = 'avatar'
|
||||||
|
},
|
||||||
|
changeBanner () {
|
||||||
|
this.editImage = 'banner'
|
||||||
|
},
|
||||||
|
submitImage ({ canvas, file }) {
|
||||||
|
if (canvas) {
|
||||||
|
return canvas.toBlob((data) => this.submitImage({ canvas: null, file: data }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new window.FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const dataUrl = e.target.result
|
||||||
|
|
||||||
|
if (this.editImage === 'avatar') {
|
||||||
|
this.newAvatar = dataUrl
|
||||||
|
this.newAvatarFile = file
|
||||||
|
} else {
|
||||||
|
this.newBanner = dataUrl
|
||||||
|
this.newBannerFile = file
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editImage = false
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
|
||||||
|
},
|
||||||
|
resetImage () {
|
||||||
|
if (this.editImage === 'avatar') {
|
||||||
|
this.newAvatar = ''
|
||||||
|
this.newAvatarFile = ''
|
||||||
|
} else {
|
||||||
|
this.newBanner = ''
|
||||||
|
this.newBannerFile = ''
|
||||||
|
}
|
||||||
|
this.editImage = false
|
||||||
|
},
|
||||||
|
addField () {
|
||||||
|
if (this.newFields.length < this.maxFields) {
|
||||||
|
this.newFields.push({ name: '', value: '' })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
deleteField (index) {
|
||||||
|
this.newFields.splice(index, 1)
|
||||||
|
},
|
||||||
|
propsToNative (props) {
|
||||||
|
return propsToNative(props)
|
||||||
|
},
|
||||||
|
cancelImageText () {
|
||||||
|
return
|
||||||
|
},
|
||||||
|
resetState () {
|
||||||
|
const user = this.$store.state.users.currentUser
|
||||||
|
|
||||||
|
this.newName = user.name_unescaped
|
||||||
|
this.newBio = unescape(user.description)
|
||||||
|
|
||||||
|
this.newAvatar = null
|
||||||
|
this.newAvatarFile = null
|
||||||
|
|
||||||
|
this.newBanner = null
|
||||||
|
this.newBannerFile = null
|
||||||
|
|
||||||
|
this.newActorType = user.actor_type
|
||||||
|
this.newBirthday = user.birthday
|
||||||
|
this.newShowBirthday = user.show_birthday
|
||||||
|
this.newShowRole = user.show_role
|
||||||
|
|
||||||
|
this.newFields = user.fields.map(field => ({ name: field.name, value: field.value }))
|
||||||
|
},
|
||||||
|
updateProfile () {
|
||||||
|
const params = {
|
||||||
|
note: this.newBio,
|
||||||
|
|
||||||
|
// Backend notation.
|
||||||
|
display_name: this.newName,
|
||||||
|
fields_attributes: this.newFields.filter(el => el != null),
|
||||||
|
show_role: !!this.newShowRole,
|
||||||
|
birthday: this.newBirthday || '',
|
||||||
|
show_birthday: !!this.showBirthday,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.actorType) {
|
||||||
|
params.actor_type = this.actorType
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.newAvatarFile !== null) {
|
||||||
|
params.avatar = this.newAvatarFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.newBannerFile !== null) {
|
||||||
|
params.header = this.newBannerFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.emailLanguage) {
|
||||||
|
params.language = localeService.internalToBackendLocaleMulti(this.emailLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.state.api.backendInteractor
|
||||||
|
.updateProfile({ params })
|
||||||
|
.then((user) => {
|
||||||
|
this.newFields.splice(this.newFields.length)
|
||||||
|
merge(this.newFields, user.fields)
|
||||||
|
this.$store.commit('addNewUsers', [user])
|
||||||
|
this.$store.commit('setCurrentUser', user)
|
||||||
|
this.resetState()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.displayUploadError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
displayUploadError (error) {
|
||||||
|
useInterfaceStore().pushGlobalNotice({
|
||||||
|
messageKey: 'upload.error.message',
|
||||||
|
messageArgs: [error.message],
|
||||||
|
level: 'error'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,61 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
// editing headers
|
||||||
|
h4 {
|
||||||
|
line-height: 2;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 1.0em;
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input.bio {
|
||||||
|
height: auto; // override settings default textarea size
|
||||||
|
}
|
||||||
|
|
||||||
.user-card-inner {
|
.user-card-inner {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-setting,
|
||||||
|
&-bio {
|
||||||
|
color: var(--lightText);
|
||||||
|
display: block;
|
||||||
|
line-height: 1.3;
|
||||||
|
padding: 0 0.6em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card-setting {
|
||||||
|
margin-left: 0.6em;
|
||||||
|
margin-right: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
.user-card-bio {
|
.user-card-bio {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0.6em;
|
||||||
|
|
||||||
|
&.input {
|
||||||
|
margin: 0 1em;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
text-align: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&, * {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
&.-justify-left {
|
&.-justify-left {
|
||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
|
@ -80,22 +130,6 @@
|
||||||
z-index: -2;
|
z-index: -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-bio {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--lightText);
|
|
||||||
display: block;
|
|
||||||
line-height: 1.3;
|
|
||||||
padding: 0.6em;
|
|
||||||
margin: 0 0.6em;
|
|
||||||
|
|
||||||
img {
|
|
||||||
object-fit: contain;
|
|
||||||
vertical-align: middle;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 400px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.-rounded-t {
|
&.-rounded-t {
|
||||||
border-top-left-radius: var(--roundness);
|
border-top-left-radius: var(--roundness);
|
||||||
border-top-right-radius: var(--roundness);
|
border-top-right-radius: var(--roundness);
|
||||||
|
|
@ -129,7 +163,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0.6em;
|
margin: 0.6em;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
text-align: right;
|
text-align: left;
|
||||||
|
|
||||||
.user-identity {
|
.user-identity {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -179,6 +213,12 @@
|
||||||
padding: 0.6em;
|
padding: 0.6em;
|
||||||
margin: -0.6em;
|
margin: -0.6em;
|
||||||
|
|
||||||
|
&.save-profile-button,
|
||||||
|
&.reset-profile-button,
|
||||||
|
&.edit-banner-button {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover .icon {
|
&:hover .icon {
|
||||||
color: var(--textFaint);
|
color: var(--textFaint);
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +239,6 @@
|
||||||
inset: -0.6em;
|
inset: -0.6em;
|
||||||
left: -0.6em;
|
left: -0.6em;
|
||||||
right: -1.2em;
|
right: -1.2em;
|
||||||
background-color: rgb(0 0 0 / 30%);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -209,11 +248,21 @@
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
margin: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover &.-overlay {
|
&:hover &.-overlay {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
background-color: rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-avatar.-editable {
|
||||||
|
.-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
place-items: start end;
|
||||||
|
justify-content: end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,6 +287,39 @@
|
||||||
--link: var(--text) !important;
|
--link: var(--text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.edit-button {
|
||||||
|
width: 3em;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:hover .icon {
|
||||||
|
color: var(--textFaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:hover) .icon {
|
||||||
|
color: var(--lightText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.user-name {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 2;
|
||||||
|
margin-right: 1em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin: 0 -0.5em;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.top-line {
|
.top-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -246,8 +328,6 @@
|
||||||
// these two normalize position and height when custom emoji are used
|
// these two normalize position and height when custom emoji are used
|
||||||
line-height: 2;
|
line-height: 2;
|
||||||
margin-bottom: -0.2em;
|
margin-bottom: -0.2em;
|
||||||
font-weight: 600;
|
|
||||||
font-size: 110%;
|
|
||||||
font-size: calc(max(110%, 4cqw));
|
font-size: calc(max(110%, 4cqw));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,6 +338,7 @@
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
font-size: calc(max(90%, 2.5cqw));
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,13 +362,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-name {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-right: 1em;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlighter {
|
.highlighter {
|
||||||
margin: 5em;
|
margin: 5em;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
|
@ -336,10 +410,13 @@
|
||||||
|
|
||||||
.user-interactions {
|
.user-interactions {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(7.5em, 20%));
|
flex-wrap: wrap;
|
||||||
grid-gap: 0.6em;
|
gap: 0.6em;
|
||||||
max-width: 98vw;
|
|
||||||
|
> * {
|
||||||
|
flex: 0 0 10%;
|
||||||
|
}
|
||||||
|
|
||||||
.popover-trigger-button, .moderation-tools-button {
|
.popover-trigger-button, .moderation-tools-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -399,38 +476,76 @@
|
||||||
|
|
||||||
.user-profile-fields {
|
.user-profile-fields {
|
||||||
margin: 0 0.5em;
|
margin: 0 0.5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
--emoji-size: 1.8em;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
|
|
||||||
&.emoji {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-profile-field-add,
|
||||||
.user-profile-field {
|
.user-profile-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
margin: 0.25em;
|
margin: 0.25em;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--roundness);
|
border-radius: var(--roundness);
|
||||||
|
line-height: 2em;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-field-add {
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-field {
|
||||||
|
|
||||||
|
/* stylelint-disable no-descending-specificity */
|
||||||
|
// input is a generic class
|
||||||
|
.input {
|
||||||
|
text-align: inherit;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
/* stylelint-enable no-descending-specificity */
|
||||||
|
|
||||||
|
.delete-field {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
.user-profile-field-name,
|
.user-profile-field-name,
|
||||||
.user-profile-field-value {
|
.user-profile-field-value,
|
||||||
|
.user-profile-field-add {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
line-height: 2em;
|
display: inline-flex;
|
||||||
|
|
||||||
|
&.-edit {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-profile-field-name {
|
.user-profile-field-name {
|
||||||
flex: 0 1 50%;
|
flex: 0 1 50%;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
justify-content: end;
|
||||||
color: var(--lightText);
|
color: var(--lightText);
|
||||||
min-width: 9em;
|
min-width: 9em;
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
|
|
@ -446,3 +561,63 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-image {
|
||||||
|
.panel-body {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-avatar {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
display: flex;
|
||||||
|
margin: 0 1em 0.5em;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
.new-image {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1 0 10em;
|
||||||
|
max-height: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
min-width: 1.1em;
|
||||||
|
font-size: 500%;
|
||||||
|
align-self: center;
|
||||||
|
flex: 0 1 5em;
|
||||||
|
aspect-ratio: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-banner {
|
||||||
|
.images-container {
|
||||||
|
grid-template-rows: 20em 5em 20em;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1 0 10em;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
min-height: 1.1em;
|
||||||
|
font-size: 500%;
|
||||||
|
justify-self: center;
|
||||||
|
flex: 0 1 5em;
|
||||||
|
aspect-ratio: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
class="user-card"
|
class="user-card"
|
||||||
:class="classes"
|
:class="classes"
|
||||||
>
|
>
|
||||||
<div :class="onClose ? '' : 'panel-heading -flexible-height'" class="user-card-inner">
|
<div
|
||||||
|
:class="onClose ? '' : 'panel-heading -flexible-height'"
|
||||||
|
class="user-card-inner"
|
||||||
|
>
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="user-identity">
|
<div class="user-identity">
|
||||||
<div
|
<div
|
||||||
|
|
@ -24,6 +27,23 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
<button
|
||||||
|
v-else-if="editable"
|
||||||
|
class="user-info-avatar button-unstyled -link"
|
||||||
|
:class="{ '-editable': editable }"
|
||||||
|
@click="changeAvatar"
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
:user="user"
|
||||||
|
:url="avatarImgSrc"
|
||||||
|
/>
|
||||||
|
<div class="user-info-avatar -link -overlay">
|
||||||
|
<FAIcon
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="pencil"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
v-else-if="typeof avatarAction === 'function'"
|
v-else-if="typeof avatarAction === 'function'"
|
||||||
class="user-info-avatar"
|
class="user-info-avatar"
|
||||||
|
|
@ -39,9 +59,25 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="user-summary">
|
<div class="user-summary">
|
||||||
<div class="top-line">
|
<div class="top-line">
|
||||||
<div class="other-actions">
|
<div
|
||||||
|
class="other-actions"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-if="!isOtherUser && user.is_local"
|
v-if="editable"
|
||||||
|
:disabled="newName && newName.length === 0"
|
||||||
|
class="btn button-unstyled edit-banner-button"
|
||||||
|
@click="changeBanner"
|
||||||
|
>
|
||||||
|
{{ $t('settings.change_banner') }}
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="icon"
|
||||||
|
icon="pencil"
|
||||||
|
:title="$t('user_card.change_banner')"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="!editable && !isOtherUser && user.is_local"
|
||||||
class="button-unstyled edit-profile-button"
|
class="button-unstyled edit-profile-button"
|
||||||
@click.stop="openProfileTab"
|
@click.stop="openProfileTab"
|
||||||
>
|
>
|
||||||
|
|
@ -90,16 +126,45 @@
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<div class="name-wrapper">
|
||||||
:to="userProfileLink(user)"
|
<router-link
|
||||||
class="user-name"
|
v-if="!editable || !editingName"
|
||||||
>
|
:to="userProfileLink(user)"
|
||||||
<RichContent
|
class="user-name"
|
||||||
:title="user.name"
|
>
|
||||||
:html="user.name"
|
<RichContent
|
||||||
:emoji="user.emoji"
|
:title="editable ? newName : user.name_unescaped"
|
||||||
/>
|
:html="editable ? newName : user.name_unescaped"
|
||||||
</router-link>
|
:emoji="editable ? emoji : user.emoji"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<EmojiInput
|
||||||
|
v-else-if="editingName"
|
||||||
|
v-model="newName"
|
||||||
|
enable-emoji-picker
|
||||||
|
:suggest="emojiSuggestor"
|
||||||
|
>
|
||||||
|
<template #default="inputProps">
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="newName"
|
||||||
|
class="input name-changer"
|
||||||
|
v-bind="propsToNative(inputProps)"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</EmojiInput>
|
||||||
|
<button
|
||||||
|
v-if="editable"
|
||||||
|
class="button-unstyled edit-button"
|
||||||
|
@click="editingName = !editingName"
|
||||||
|
:title="$t('settings.toggle_edit')"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="icon"
|
||||||
|
icon="pencil"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-line">
|
<div class="bottom-line">
|
||||||
<user-link
|
<user-link
|
||||||
|
|
@ -163,72 +228,102 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="loggedIn && isOtherUser"
|
v-if="loggedIn"
|
||||||
class="user-interactions"
|
class="user-interactions"
|
||||||
>
|
>
|
||||||
<div class="btn-group">
|
<template v-if="isOtherUser">
|
||||||
<FollowButton
|
<div class="btn-group">
|
||||||
:relationship="relationship"
|
<FollowButton
|
||||||
|
:relationship="relationship"
|
||||||
|
:user="user"
|
||||||
|
/>
|
||||||
|
<template v-if="relationship.following">
|
||||||
|
<ProgressButton
|
||||||
|
v-if="!relationship.notifying"
|
||||||
|
class="btn button-default"
|
||||||
|
:click="subscribeUser"
|
||||||
|
:title="$t('user_card.subscribe')"
|
||||||
|
>
|
||||||
|
<FAIcon icon="bell" />
|
||||||
|
</ProgressButton>
|
||||||
|
<ProgressButton
|
||||||
|
v-else
|
||||||
|
class="btn button-default toggled"
|
||||||
|
:click="unsubscribeUser"
|
||||||
|
:title="$t('user_card.unsubscribe')"
|
||||||
|
>
|
||||||
|
<FALayers>
|
||||||
|
<FAIcon
|
||||||
|
icon="rss"
|
||||||
|
transform="left-5 shrink-6 up-3 rotate-20"
|
||||||
|
flip="horizontal"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
icon="rss"
|
||||||
|
transform="right-5 shrink-6 up-3 rotate-20"
|
||||||
|
/>
|
||||||
|
<FAIcon icon="bell" />
|
||||||
|
</FALayers>
|
||||||
|
</ProgressButton>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="relationship.muting"
|
||||||
|
class="btn button-default btn-mute toggled"
|
||||||
|
:disabled="user.deactivated"
|
||||||
|
@click="unmuteUser"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.muted') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="btn button-default btn-mute"
|
||||||
|
:disabled="user.deactivated"
|
||||||
|
@click="muteUser"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.mute') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn button-default btn-mention"
|
||||||
|
:disabled="user.deactivated"
|
||||||
|
@click="mentionUser"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.mention') }}
|
||||||
|
</button>
|
||||||
|
<ModerationTools
|
||||||
|
v-if="showModerationMenu"
|
||||||
|
class="moderation-menu"
|
||||||
:user="user"
|
:user="user"
|
||||||
/>
|
/>
|
||||||
<template v-if="relationship.following">
|
</template>
|
||||||
<ProgressButton
|
|
||||||
v-if="!relationship.notifying"
|
|
||||||
class="btn button-default"
|
|
||||||
:click="subscribeUser"
|
|
||||||
:title="$t('user_card.subscribe')"
|
|
||||||
>
|
|
||||||
<FAIcon icon="bell" />
|
|
||||||
</ProgressButton>
|
|
||||||
<ProgressButton
|
|
||||||
v-else
|
|
||||||
class="btn button-default toggled"
|
|
||||||
:click="unsubscribeUser"
|
|
||||||
:title="$t('user_card.unsubscribe')"
|
|
||||||
>
|
|
||||||
<FALayers>
|
|
||||||
<FAIcon
|
|
||||||
icon="rss"
|
|
||||||
transform="left-5 shrink-6 up-3 rotate-20"
|
|
||||||
flip="horizontal"
|
|
||||||
/>
|
|
||||||
<FAIcon
|
|
||||||
icon="rss"
|
|
||||||
transform="right-5 shrink-6 up-3 rotate-20"
|
|
||||||
/>
|
|
||||||
<FAIcon icon="bell" />
|
|
||||||
</FALayers>
|
|
||||||
</ProgressButton>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
v-if="relationship.muting"
|
v-if="editable"
|
||||||
class="btn button-default btn-mute toggled"
|
:disabled="somethingToSave"
|
||||||
:disabled="user.deactivated"
|
class="btn button-default reset-profile-button"
|
||||||
@click="unmuteUser"
|
@click="resetState"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.muted') }}
|
{{ $t('settings.reset') }}
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="icon"
|
||||||
|
icon="clock-rotate-left"
|
||||||
|
:title="$t('user_card.edit_profile')"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-if="editable"
|
||||||
class="btn button-default btn-mute"
|
:disabled="somethingToSave"
|
||||||
:disabled="user.deactivated"
|
class="btn button-default save-profile-button"
|
||||||
@click="muteUser"
|
@click="updateProfile"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.mute') }}
|
{{ $t('settings.save') }}
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="icon"
|
||||||
|
icon="save"
|
||||||
|
:title="$t('user_card.edit_profile')"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
class="btn button-default btn-mention"
|
|
||||||
:disabled="user.deactivated"
|
|
||||||
@click="mentionUser"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.mention') }}
|
|
||||||
</button>
|
|
||||||
<ModerationTools
|
|
||||||
v-if="showModerationMenu"
|
|
||||||
class="moderation-menu"
|
|
||||||
:user="user"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!loggedIn && user.is_local"
|
v-if="!loggedIn && user.is_local"
|
||||||
|
|
@ -238,7 +333,45 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="personal-marks" v-if="loggedIn && isOtherUser && (hasNote || !hideBio) && !mergedConfig.userCardHidePersonalMarks">
|
<template v-if="editable">
|
||||||
|
<h4>{{ $t('settings.user_preferences') }}</h4>
|
||||||
|
<p
|
||||||
|
v-if="role === 'admin' || role === 'moderator'"
|
||||||
|
class="user-card-setting"
|
||||||
|
>
|
||||||
|
<Checkbox v-model="newShowRole">
|
||||||
|
<template v-if="role === 'admin'">
|
||||||
|
{{ $t('settings.show_admin_badge') }}
|
||||||
|
</template>
|
||||||
|
<template v-if="role === 'moderator'">
|
||||||
|
{{ $t('settings.show_moderator_badge') }}
|
||||||
|
</template>
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
<p class="user-card-setting">
|
||||||
|
<label>
|
||||||
|
{{ $t('settings.actor_type') }}
|
||||||
|
<Select v-model="newActorType">
|
||||||
|
<option
|
||||||
|
v-for="option in availableActorTypes"
|
||||||
|
:key="option"
|
||||||
|
:value="option"
|
||||||
|
>
|
||||||
|
{{ $t('settings.actor_type_' + (option === 'Person' ? 'person_proper' : option)) }}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
<div v-if="groupActorAvailable">
|
||||||
|
<small>
|
||||||
|
{{ $t('settings.actor_type_description') }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="!editable && loggedIn && isOtherUser && (hasNote || !hideBio) && !mergedConfig.userCardHidePersonalMarks"
|
||||||
|
class="personal-marks"
|
||||||
|
>
|
||||||
<UserNote
|
<UserNote
|
||||||
v-if="hasNote || (hasNoteEditor && supportsNote)"
|
v-if="hasNote || (hasNoteEditor && supportsNote)"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
|
@ -272,60 +405,184 @@
|
||||||
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
|
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-if="userHighlightType !== 'disabled'"
|
v-if="userHighlightType !== 'disabled'"
|
||||||
class="highlighter-color"
|
|
||||||
v-model="userHighlightColor"
|
v-model="userHighlightColor"
|
||||||
|
class="highlighter-color"
|
||||||
:show-optional-checkbox="false"
|
:show-optional-checkbox="false"
|
||||||
name="'userHighlightColorTx'+user.id"
|
name="'userHighlightColorTx'+user.id"
|
||||||
:unstyled="true"
|
:unstyled="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RichContent
|
<h4 v-if="editable">
|
||||||
v-if="!hideBio"
|
<span>
|
||||||
class="user-card-bio"
|
{{ $t('settings.bio') }}
|
||||||
:class="{ '-justify-left': mergedConfig.userCardLeftJustify }"
|
</span>
|
||||||
:html="user.description_html"
|
<button
|
||||||
:emoji="user.emoji"
|
class="button-default"
|
||||||
:handle-links="true"
|
@click="editingBio = !editingBio"
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="!hideBio && user.fields_html && user.fields_html.length > 0"
|
|
||||||
class="user-profile-fields"
|
|
||||||
>
|
|
||||||
<dl
|
|
||||||
v-for="(field, index) in user.fields_html"
|
|
||||||
:key="index"
|
|
||||||
class="user-profile-field"
|
|
||||||
>
|
>
|
||||||
<dt
|
{{ $t('settings.toggle_edit') }}
|
||||||
:title="user.fields_text[index].name"
|
<FAIcon
|
||||||
class="user-profile-field-name"
|
class="fa-scale-110 fa-old-padding"
|
||||||
>
|
icon="pencil"
|
||||||
<RichContent
|
/>
|
||||||
:html="field.name"
|
</button>
|
||||||
:emoji="user.emoji"
|
</h4>
|
||||||
|
<template v-if="!editable || !editingBio">
|
||||||
|
<RichContent
|
||||||
|
v-if="!hideBio"
|
||||||
|
class="user-card-bio"
|
||||||
|
:class="{ '-justify-left': mergedConfig.userCardLeftJustify }"
|
||||||
|
:html="editable ? newBio.replace(/\n/g, '<br>') : user.description_html"
|
||||||
|
:emoji="editable ? emoji : user.emoji"
|
||||||
|
:handle-links="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="editingBio">
|
||||||
|
<EmojiInput
|
||||||
|
v-model="newBio"
|
||||||
|
enable-emoji-picker
|
||||||
|
class="user-card-bio"
|
||||||
|
:class="{ '-justify-left': mergedConfig.userCardLeftJustify }"
|
||||||
|
:suggest="emojiUserSuggestor"
|
||||||
|
>
|
||||||
|
<template #default="inputProps">
|
||||||
|
<textarea
|
||||||
|
v-model="newBio"
|
||||||
|
class="input bio resize-height"
|
||||||
|
v-bind="propsToNative(inputProps)"
|
||||||
|
:rows="newBio.split(/\n/g).length"
|
||||||
/>
|
/>
|
||||||
</dt>
|
</template>
|
||||||
<dd
|
</EmojiInput>
|
||||||
:title="user.fields_text[index].value"
|
</template>
|
||||||
class="user-profile-field-value"
|
<h4 v-if="editable">
|
||||||
|
<span>
|
||||||
|
{{ $t('settings.profile_fields.label') }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="button-default"
|
||||||
|
@click="editingFields = !editingFields"
|
||||||
|
>
|
||||||
|
{{ $t('settings.toggle_edit') }}
|
||||||
|
<FAIcon
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="pencil"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
<template v-if="!editable || !editingFields">
|
||||||
|
<div
|
||||||
|
v-if="!hideBio && user.fields_html && user.fields_html.length > 0"
|
||||||
|
class="user-profile-fields"
|
||||||
|
>
|
||||||
|
<dl
|
||||||
|
v-for="(field, index) in (editable ? newFields : user.fields_html)"
|
||||||
|
:key="index"
|
||||||
|
class="user-profile-field"
|
||||||
>
|
>
|
||||||
<RichContent
|
<dt
|
||||||
:html="field.value"
|
:title="field.name"
|
||||||
:emoji="user.emoji"
|
class="user-profile-field-name"
|
||||||
/>
|
>
|
||||||
</dd>
|
<RichContent
|
||||||
</dl>
|
:html="field.name"
|
||||||
</div>
|
:emoji="editable ? emoji : user.emoji"
|
||||||
<div class="user-extras" v-if="!hideBio">
|
/>
|
||||||
|
</dt>
|
||||||
|
<dd
|
||||||
|
:title="field.value"
|
||||||
|
class="user-profile-field-value"
|
||||||
|
>
|
||||||
|
<RichContent
|
||||||
|
:html="field.value"
|
||||||
|
:emoji="editable ? emoji : user.emoji"
|
||||||
|
/>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="editingFields">
|
||||||
|
<div
|
||||||
|
v-if="maxFields > 0"
|
||||||
|
class="user-profile-fields"
|
||||||
|
>
|
||||||
|
<dl
|
||||||
|
v-for="(_, i) in newFields"
|
||||||
|
:key="i"
|
||||||
|
class="user-profile-field"
|
||||||
|
>
|
||||||
|
<dt
|
||||||
|
class="user-profile-field-name -edit"
|
||||||
|
>
|
||||||
|
<EmojiInput
|
||||||
|
v-model="newFields[i].name"
|
||||||
|
enable-emoji-picker
|
||||||
|
:suggest="emojiSuggestor"
|
||||||
|
>
|
||||||
|
<template #default="inputProps">
|
||||||
|
<input
|
||||||
|
v-model="newFields[i].name"
|
||||||
|
:placeholder="$t('settings.profile_fields.name')"
|
||||||
|
v-bind="propsToNative(inputProps)"
|
||||||
|
class="input"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</EmojiInput>
|
||||||
|
</dt>
|
||||||
|
<dd
|
||||||
|
class="user-profile-field-value -edit"
|
||||||
|
>
|
||||||
|
<EmojiInput
|
||||||
|
v-model="newFields[i].value"
|
||||||
|
enable-emoji-picker
|
||||||
|
:suggest="emojiSuggestor"
|
||||||
|
>
|
||||||
|
<template #default="inputProps">
|
||||||
|
<input
|
||||||
|
v-model="newFields[i].value"
|
||||||
|
:placeholder="$t('settings.profile_fields.value')"
|
||||||
|
v-bind="propsToNative(inputProps)"
|
||||||
|
class="input input"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</EmojiInput>
|
||||||
|
<button
|
||||||
|
class="delete-field button-default -hover-highlight"
|
||||||
|
@click="deleteField(i)"
|
||||||
|
>
|
||||||
|
<!-- TODO something is wrong with v-show here -->
|
||||||
|
<FAIcon
|
||||||
|
v-if="newFields.length > 1"
|
||||||
|
icon="times"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<button
|
||||||
|
v-if="newFields.length < maxFields"
|
||||||
|
class="user-profile-field-add add-field button-default -hover-highlight"
|
||||||
|
@click="addField"
|
||||||
|
>
|
||||||
|
<FAIcon icon="plus" class="icon" />
|
||||||
|
<span class="label">
|
||||||
|
{{ $t("settings.profile_fields.add_field") }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
class="user-extras"
|
||||||
|
v-if="!hideBio"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
v-if="!mergedConfig.hideUserStats"
|
v-if="!editable && !mergedConfig.hideUserStats"
|
||||||
class="user-stats"
|
class="user-stats"
|
||||||
>
|
>
|
||||||
<dl
|
<dl
|
||||||
|
v-if="!mergedConfig.hideUserStats && !hideBio"
|
||||||
class="user-count"
|
class="user-count"
|
||||||
@click.prevent="setProfileView('statuses')"
|
@click.prevent="setProfileView('statuses')"
|
||||||
v-if="!mergedConfig.hideUserStats && !hideBio"
|
|
||||||
>
|
>
|
||||||
<dd>{{ user.statuses_count }}</dd>
|
<dd>{{ user.statuses_count }}</dd>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
|
|
@ -356,21 +613,106 @@
|
||||||
<dt>{{ $t('user_card.followers') }}</dt>
|
<dt>{{ $t('user_card.followers') }}</dt>
|
||||||
</dl>
|
</dl>
|
||||||
</span>
|
</span>
|
||||||
<div class="birthday" v-if="!hideBio && !!user.birthday">
|
<template v-if="!hideBio">
|
||||||
<FAIcon
|
<div
|
||||||
class="fa-old-padding"
|
v-if="user.birthday && !editable"
|
||||||
icon="birthday-cake"
|
class="birthday"
|
||||||
/>
|
>
|
||||||
{{ $t('user_card.birthday', { birthday: formattedBirthday }) }}
|
<FAIcon
|
||||||
</div>
|
class="fa-old-padding"
|
||||||
|
icon="birthday-cake"
|
||||||
|
/>
|
||||||
|
{{ $t('user_card.birthday', { birthday: formattedBirthday }) }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="editable"
|
||||||
|
class="birthday"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Checkbox v-model="showBirthday">
|
||||||
|
{{ $t('settings.birthday.show_birthday') }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<FAIcon
|
||||||
|
class="fa-old-padding"
|
||||||
|
icon="birthday-cake"
|
||||||
|
/>
|
||||||
|
{{ $t('settings.birthday.label') }}
|
||||||
|
<input
|
||||||
|
id="birthday"
|
||||||
|
v-model="newBirthday"
|
||||||
|
type="date"
|
||||||
|
class="input birthday-input"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<teleport to="#modal">
|
<teleport to="#modal">
|
||||||
<UserTimedFilterModal
|
<UserTimedFilterModal
|
||||||
|
ref="timedMuteDialog"
|
||||||
:user="user"
|
:user="user"
|
||||||
:is-mute="true"
|
:is-mute="true"
|
||||||
ref="timedMuteDialog"
|
|
||||||
/>
|
/>
|
||||||
</teleport>
|
</teleport>
|
||||||
|
<teleport to="#modal">
|
||||||
|
<DialogModal
|
||||||
|
v-if="editImage"
|
||||||
|
class="edit-image"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
{{ editImage === 'avatar' ? $t('settings.change_avatar') : $t('settings.change_banner') }}
|
||||||
|
</template>
|
||||||
|
<div class="image-container">
|
||||||
|
<image-cropper
|
||||||
|
ref="cropper"
|
||||||
|
class="cropper"
|
||||||
|
:aspect-ratio="editImage === 'avatar' ? 1 : 3"
|
||||||
|
@submit="submitImage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="pick-image"
|
||||||
|
class="button-default btn"
|
||||||
|
type="button"
|
||||||
|
@click="() => this.$refs.cropper.pickImage()"
|
||||||
|
>
|
||||||
|
{{ $t('settings.upload_picture') }}
|
||||||
|
</button>
|
||||||
|
<p class="visibility-notice">
|
||||||
|
{{ editImage === 'avatar' ? $t('settings.avatar_size_instruction') : $t('settings.banner_size_instruction' )}}
|
||||||
|
</p>
|
||||||
|
<template #footer>
|
||||||
|
<button
|
||||||
|
class="button-default btn"
|
||||||
|
type="button"
|
||||||
|
@click="editImage = false"
|
||||||
|
>
|
||||||
|
{{ this.$t('image_cropper.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:title="editImage === 'avatar' ? $t('settings.reset_avatar') : $t('settings.reset_banner')"
|
||||||
|
class="button-default btn reset-button"
|
||||||
|
@click="resetImage"
|
||||||
|
>
|
||||||
|
{{ editImage === 'avatar' ? $t('settings.reset_avatar') : $t('settings.reset_banner' )}}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default btn"
|
||||||
|
type="button"
|
||||||
|
@click="this.$refs.cropper.submit(false)"
|
||||||
|
>
|
||||||
|
{{ $t('image_cropper.save_without_cropping') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default btn"
|
||||||
|
type="button"
|
||||||
|
@click="this.$refs.cropper.submit(true)"
|
||||||
|
>
|
||||||
|
{{ $t('image_cropper.save') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,14 @@
|
||||||
v-model="localNote"
|
v-model="localNote"
|
||||||
class="input note-text"
|
class="input note-text"
|
||||||
:class="{ unstyled: !editing }"
|
:class="{ unstyled: !editing }"
|
||||||
@focus="startEditing"
|
|
||||||
@blur="finalizeEditing"
|
|
||||||
rows="1"
|
rows="1"
|
||||||
:placeholder="$t('user_card.note_blank_click')"
|
:placeholder="$t('user_card.note_blank_click')"
|
||||||
|
@focus="startEditing"
|
||||||
|
@blur="finalizeEditing"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="overlay"
|
|
||||||
v-if="frozen"
|
v-if="frozen"
|
||||||
|
class="overlay"
|
||||||
>
|
>
|
||||||
<PanelLoading />
|
<PanelLoading />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
|
|
||||||
.Avatar {
|
.Avatar {
|
||||||
width: 5em;
|
width: 5em;
|
||||||
width: calc(min(5em, 20cqw));
|
width: calc(min(5em, 20cqw));
|
||||||
|
|
|
||||||
|
|
@ -387,13 +387,18 @@
|
||||||
"actor_type": "This account is:",
|
"actor_type": "This account is:",
|
||||||
"actor_type_description": "Marking your account as a group will make it automatically repeat statuses that mention it.",
|
"actor_type_description": "Marking your account as a group will make it automatically repeat statuses that mention it.",
|
||||||
"actor_type_Person": "a normal user",
|
"actor_type_Person": "a normal user",
|
||||||
|
"actor_type_person_proper": "a person",
|
||||||
"actor_type_Service": "a bot",
|
"actor_type_Service": "a bot",
|
||||||
"actor_type_Group": "a group",
|
"actor_type_Group": "a group",
|
||||||
"mobile_center_dialog": "Vertically center dialogs on mobile",
|
"mobile_center_dialog": "Vertically center dialogs on mobile",
|
||||||
"app_name": "App name",
|
"app_name": "App name",
|
||||||
"expert_mode": "Show advanced",
|
"expert_mode": "Show advanced",
|
||||||
"save": "Save changes",
|
"save": "Save changes",
|
||||||
|
"reset": "Reset changes",
|
||||||
"security": "Security",
|
"security": "Security",
|
||||||
|
"toggle_edit": "Toggle edit",
|
||||||
|
"change_banner": "Change banner",
|
||||||
|
"change_avatar": "Change avatar",
|
||||||
"setting_changed": "Setting is different from default",
|
"setting_changed": "Setting is different from default",
|
||||||
"setting_server_side": "This setting is tied to your profile and affects all sessions and clients",
|
"setting_server_side": "This setting is tied to your profile and affects all sessions and clients",
|
||||||
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
|
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
|
||||||
|
|
@ -488,6 +493,7 @@
|
||||||
"avatarRadius": "Avatars",
|
"avatarRadius": "Avatars",
|
||||||
"background": "Background",
|
"background": "Background",
|
||||||
"bio": "Bio",
|
"bio": "Bio",
|
||||||
|
"user_preferences": "Profile settings",
|
||||||
"email_language": "Language for receiving emails from the server",
|
"email_language": "Language for receiving emails from the server",
|
||||||
"block_export": "Block export",
|
"block_export": "Block export",
|
||||||
"block_export_button": "Export your blocks to a csv file",
|
"block_export_button": "Export your blocks to a csv file",
|
||||||
|
|
@ -563,7 +569,8 @@
|
||||||
"move_account_error": "Error moving account: {error}",
|
"move_account_error": "Error moving account: {error}",
|
||||||
"discoverable": "Allow discovery of this account in search results and other services",
|
"discoverable": "Allow discovery of this account in search results and other services",
|
||||||
"domain_mutes": "Domains",
|
"domain_mutes": "Domains",
|
||||||
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
|
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels. Recommended aspect ratio is 1:1",
|
||||||
|
"banner_size_instruction": "The recommended minimum size for banner images is 450x150 pixels. Recommended aspect ratio is 3:1",
|
||||||
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
||||||
"autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available",
|
"autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available",
|
||||||
"unsaved_post_action": "When you try to close an unsaved posting form",
|
"unsaved_post_action": "When you try to close an unsaved posting form",
|
||||||
|
|
@ -722,8 +729,10 @@
|
||||||
"minimal_scopes_mode": "Minimize post scope selection options",
|
"minimal_scopes_mode": "Minimize post scope selection options",
|
||||||
"set_new_avatar": "Set new avatar",
|
"set_new_avatar": "Set new avatar",
|
||||||
"set_new_profile_background": "Set new profile background",
|
"set_new_profile_background": "Set new profile background",
|
||||||
|
"set_new_background": "Set new background",
|
||||||
"set_new_profile_banner": "Set new profile banner",
|
"set_new_profile_banner": "Set new profile banner",
|
||||||
"reset_avatar": "Reset avatar",
|
"reset_avatar": "Reset avatar",
|
||||||
|
"reset_banner": "Reset banner",
|
||||||
"reset_profile_background": "Reset profile background",
|
"reset_profile_background": "Reset profile background",
|
||||||
"reset_profile_banner": "Reset profile banner",
|
"reset_profile_banner": "Reset profile banner",
|
||||||
"reset_avatar_confirm": "Do you really want to reset the avatar?",
|
"reset_avatar_confirm": "Do you really want to reset the avatar?",
|
||||||
|
|
@ -776,6 +785,7 @@
|
||||||
"tooltipRadius": "Tooltips/alerts",
|
"tooltipRadius": "Tooltips/alerts",
|
||||||
"type_domains_to_mute": "Search domains to mute",
|
"type_domains_to_mute": "Search domains to mute",
|
||||||
"upload_a_photo": "Upload a photo",
|
"upload_a_photo": "Upload a photo",
|
||||||
|
"upload_picture": "Upload picture",
|
||||||
"user_settings": "User Settings",
|
"user_settings": "User Settings",
|
||||||
"values": {
|
"values": {
|
||||||
"false": "no",
|
"false": "no",
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ const defaultState = {
|
||||||
|
|
||||||
// Nasty stuff
|
// Nasty stuff
|
||||||
customEmoji: [],
|
customEmoji: [],
|
||||||
|
rawCustomEmoji: [],
|
||||||
customEmojiFetched: false,
|
customEmojiFetched: false,
|
||||||
emoji: {},
|
emoji: {},
|
||||||
emojiFetched: false,
|
emojiFetched: false,
|
||||||
|
|
|
||||||
|
|
@ -215,12 +215,26 @@ const updateProfileImages = ({ credentials, avatar = null, avatarName = null, ba
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateProfile = ({ credentials, params }) => {
|
const updateProfile = ({ credentials, params }) => {
|
||||||
return promisedRequest({
|
const formData = new FormData();
|
||||||
url: MASTODON_PROFILE_UPDATE_URL,
|
|
||||||
|
for(const name in params) {
|
||||||
|
if (name === 'fields_attributes') {
|
||||||
|
params[name].forEach((param, i) => {
|
||||||
|
formData.append(name + `[${i}][name]`, param.name)
|
||||||
|
formData.append(name + `[${i}][value]`, param.value)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
formData.append(name, params[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(MASTODON_PROFILE_UPDATE_URL, {
|
||||||
|
headers: authHeaders(credentials),
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
payload: params,
|
body: formData
|
||||||
credentials
|
})
|
||||||
}).then((data) => parseUser(data))
|
.then((data) => data.json())
|
||||||
|
.then((data) => parseUser(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Params needed:
|
// Params needed:
|
||||||
|
|
@ -323,7 +337,7 @@ const unmuteConversation = ({ id, credentials }) => {
|
||||||
const blockUser = ({ id, expiresIn, credentials }) => {
|
const blockUser = ({ id, expiresIn, credentials }) => {
|
||||||
const payload = {}
|
const payload = {}
|
||||||
if (expiresIn) {
|
if (expiresIn) {
|
||||||
payload.expires_in = expiresIn
|
payload.duration = expiresIn
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,7 @@ const mastoApiNotificationTypes = new Set([
|
||||||
'move',
|
'move',
|
||||||
'poll',
|
'poll',
|
||||||
'pleroma:emoji_reaction',
|
'pleroma:emoji_reaction',
|
||||||
'pleroma:report',
|
'pleroma:report'
|
||||||
'test'
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
|
const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue