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);
|
||||
}
|
||||
|
||||
.visibility-notice {
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--textFaint);
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
.notice-dismissible {
|
||||
padding-right: 4rem;
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -139,9 +139,9 @@
|
|||
</confirm-modal>
|
||||
<UserTimedFilterModal
|
||||
v-if="blockExpirationSupported"
|
||||
ref="timedBlockDialog"
|
||||
:is-mute="false"
|
||||
:user="user"
|
||||
ref="timedBlockDialog"
|
||||
/>
|
||||
</teleport>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -205,12 +205,6 @@ const EmojiInput = {
|
|||
return emoji.displayText
|
||||
}
|
||||
},
|
||||
onInputScroll () {
|
||||
this.$refs.hiddenOverlay.scrollTo({
|
||||
top: this.input.scrollTop,
|
||||
left: this.input.scrollLeft
|
||||
})
|
||||
},
|
||||
suggestionListId () {
|
||||
return `suggestions-${this.randomSeed}`
|
||||
},
|
||||
|
|
@ -239,7 +233,6 @@ const EmojiInput = {
|
|||
this.overlayStyle.fontSize = style.fontSize
|
||||
this.overlayStyle.wordWrap = style.wordWrap
|
||||
this.overlayStyle.whiteSpace = style.whiteSpace
|
||||
this.resize()
|
||||
input.addEventListener('blur', this.onBlur)
|
||||
input.addEventListener('focus', this.onFocus)
|
||||
input.addEventListener('paste', this.onPaste)
|
||||
|
|
@ -302,6 +295,13 @@ const EmojiInput = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
onInputScroll (e) {
|
||||
this.$refs.hiddenOverlay.scrollTo({
|
||||
top: this.input.scrollTop,
|
||||
left: this.input.scrollLeft
|
||||
})
|
||||
this.setCaret(e)
|
||||
},
|
||||
triggerShowPicker () {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.picker.showPicker()
|
||||
|
|
@ -561,8 +561,6 @@ const EmojiInput = {
|
|||
this.$refs.suggestorPopover.updateStyles()
|
||||
})
|
||||
},
|
||||
resize () {
|
||||
},
|
||||
autoCompleteItemLabel (suggestion) {
|
||||
if (suggestion.user) {
|
||||
return suggestion.displayText + ' ' + suggestion.detailText
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
.emoji-picker-icon {
|
||||
position: absolute;
|
||||
|
|
@ -123,7 +124,7 @@
|
|||
margin: 0.2em 0.25em;
|
||||
font-size: 1.3em;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
line-height: 1.2em;
|
||||
|
||||
&:hover i {
|
||||
color: var(--text);
|
||||
|
|
@ -133,7 +134,7 @@
|
|||
.emoji-picker-panel {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
margin-top: 2px;
|
||||
margin-top: 0.2em;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
|
|
@ -152,7 +153,7 @@
|
|||
}
|
||||
|
||||
&.with-picker input {
|
||||
padding-right: 30px;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
.hidden-overlay {
|
||||
|
|
@ -215,8 +216,8 @@
|
|||
}
|
||||
|
||||
.detailText {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-size: 0.6em;
|
||||
line-height: 0.6em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,6 @@ library.add(
|
|||
|
||||
const ImageCropper = {
|
||||
props: {
|
||||
trigger: {
|
||||
type: [String, window.Element],
|
||||
required: true
|
||||
},
|
||||
submitHandler: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
mimes: {
|
||||
type: String,
|
||||
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
|
||||
|
|
@ -30,6 +22,9 @@ const ImageCropper = {
|
|||
},
|
||||
cancelButtonLabel: {
|
||||
type: String
|
||||
},
|
||||
aspectRatio: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
data () {
|
||||
|
|
@ -39,17 +34,7 @@ const ImageCropper = {
|
|||
submitting: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
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')
|
||||
}
|
||||
},
|
||||
emits: ['submit'],
|
||||
methods: {
|
||||
destroy () {
|
||||
this.$refs.input.value = ''
|
||||
|
|
@ -65,20 +50,15 @@ const ImageCropper = {
|
|||
} else {
|
||||
cropperPromise = Promise.resolve()
|
||||
}
|
||||
|
||||
cropperPromise.then(canvas => {
|
||||
this.submitHandler(canvas, this.file)
|
||||
.then(() => this.destroy())
|
||||
.finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
this.$emit('submit', { canvas, file: this.file })
|
||||
this.submitting = false
|
||||
})
|
||||
},
|
||||
pickImage () {
|
||||
this.$refs.input.click()
|
||||
},
|
||||
getTriggerDOM () {
|
||||
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
|
||||
},
|
||||
readFile () {
|
||||
const fileInput = this.$refs.input
|
||||
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||
|
|
@ -117,23 +97,11 @@ const ImageCropper = {
|
|||
}
|
||||
},
|
||||
mounted () {
|
||||
// listen for click event on trigger
|
||||
const trigger = this.getTriggerDOM()
|
||||
if (!trigger) {
|
||||
this.$emit('error', 'No image make trigger found.', 'user')
|
||||
} else {
|
||||
trigger.addEventListener('click', this.pickImage)
|
||||
}
|
||||
// listen for input file changes
|
||||
const fileInput = this.$refs.input
|
||||
fileInput.addEventListener('change', this.readFile)
|
||||
},
|
||||
beforeUnmount: function () {
|
||||
// remove the event listeners
|
||||
const trigger = this.getTriggerDOM()
|
||||
if (trigger) {
|
||||
trigger.removeEventListener('click', this.pickImage)
|
||||
}
|
||||
const fileInput = this.$refs.input
|
||||
fileInput.removeEventListener('change', this.readFile)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
<template>
|
||||
<div class="image-cropper">
|
||||
<div v-if="dataUrl">
|
||||
<div class="image">
|
||||
<cropper-canvas
|
||||
ref="cropperCanvas"
|
||||
background
|
||||
class="image-cropper-canvas"
|
||||
height="25em"
|
||||
height="100%"
|
||||
>
|
||||
<cropper-image
|
||||
v-if="dataUrl"
|
||||
ref="cropperImage"
|
||||
:src="dataUrl"
|
||||
alt="Picture"
|
||||
|
|
@ -22,8 +23,8 @@
|
|||
/>
|
||||
<cropper-selection
|
||||
ref="cropperSelection"
|
||||
initial-coverage="1"
|
||||
aspect-ratio="1"
|
||||
initial-coverage="0.9"
|
||||
:aspect-ratio="aspectRatio"
|
||||
movable
|
||||
resizable
|
||||
@change="onCropperSelectionChange"
|
||||
|
|
@ -47,41 +48,13 @@
|
|||
<cropper-handle action="sw-resize" />
|
||||
</cropper-selection>
|
||||
</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>
|
||||
<input
|
||||
ref="input"
|
||||
type="file"
|
||||
class="input image-cropper-img-input"
|
||||
:accept="mimes"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -89,13 +62,15 @@
|
|||
|
||||
<style lang="scss">
|
||||
.image-cropper {
|
||||
&-img-input {
|
||||
display: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-canvas, .image {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-canvas {
|
||||
height: 25em;
|
||||
width: 25em;
|
||||
& &-img-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-buttons-wrapper {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<div class="interface-language-switcher">
|
||||
<label>
|
||||
{{ promptText }}
|
||||
<ProfileSettingIndicator :is-profile="profile" />
|
||||
</label>
|
||||
<ul class="setting-list">
|
||||
<li
|
||||
|
|
@ -46,12 +47,15 @@
|
|||
|
||||
<script>
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
|
||||
import Select from '../select/select.vue'
|
||||
import ProfileSettingIndicator from 'src/components/settings_modal/helpers/profile_setting_indicator.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Select
|
||||
Select,
|
||||
ProfileSettingIndicator
|
||||
},
|
||||
props: {
|
||||
promptText: {
|
||||
|
|
@ -62,11 +66,12 @@ export default {
|
|||
type: [Array, String],
|
||||
required: true
|
||||
},
|
||||
setLanguage: {
|
||||
type: Function,
|
||||
required: true
|
||||
profile: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['update'],
|
||||
computed: {
|
||||
languages () {
|
||||
return localeService.languages
|
||||
|
|
@ -77,7 +82,7 @@ export default {
|
|||
return Array.isArray(this.language) ? this.language : [this.language]
|
||||
},
|
||||
set: function (val) {
|
||||
this.setLanguage(val)
|
||||
this.$emit('update', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from 'src/services/theme_data/css_utils.js'
|
||||
import { deserialize } from 'src/services/theme_data/iss_deserializer.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 ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||
|
|
@ -72,7 +73,10 @@ const AppearanceTab = {
|
|||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
|
||||
}))
|
||||
})),
|
||||
backgroundUploading: false,
|
||||
background: null,
|
||||
backgroundPreview: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -420,7 +424,55 @@ const AppearanceTab = {
|
|||
].join(''))
|
||||
sheet.ready = true
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
height: 15em;
|
||||
overflow: hidden auto;
|
||||
|
|
|
|||
|
|
@ -151,6 +151,49 @@
|
|||
</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">
|
||||
<h2>{{ $t('settings.scale_and_layout') }}</h2>
|
||||
<div class="alert neutral theme-notice">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.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 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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
|
@ -75,7 +77,6 @@ const GeneralTab = {
|
|||
UnitSetting,
|
||||
InterfaceLanguageSwitcher,
|
||||
ScopeSelector,
|
||||
ProfileSettingIndicator,
|
||||
Select
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -97,7 +98,10 @@ const GeneralTab = {
|
|||
},
|
||||
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||
...SharedComputedObject()
|
||||
...SharedComputedObject(),
|
||||
...mapState({
|
||||
blockExpirationSupported: state => state.instance.blockExpiration,
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
changeDefaultScope (value) {
|
||||
|
|
@ -117,7 +121,19 @@ const GeneralTab = {
|
|||
},
|
||||
clearEmojiCache () {
|
||||
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
|
||||
:prompt-text="$t('settings.interfaceLanguage')"
|
||||
: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 v-if="instanceSpecificPanelPresent">
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import unescape from 'lodash/unescape'
|
||||
import merge from 'lodash/merge'
|
||||
import UserCard from 'src/components/user_card/user_card.vue'
|
||||
import ImageCropper from 'src/components/image_cropper/image_cropper.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 EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||
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 InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
|
@ -21,7 +19,6 @@ import {
|
|||
faPlus,
|
||||
faCircleNotch
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
|
|
@ -32,35 +29,19 @@ library.add(
|
|||
const ProfileTab = {
|
||||
data () {
|
||||
return {
|
||||
newName: this.$store.state.users.currentUser.name_unescaped,
|
||||
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 || ['']
|
||||
locked: this.$store.state.users.currentUser.locked,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserCard,
|
||||
ScopeSelector,
|
||||
ImageCropper,
|
||||
EmojiInput,
|
||||
Autosuggest,
|
||||
ProgressButton,
|
||||
Checkbox,
|
||||
BooleanSetting,
|
||||
InterfaceLanguageSwitcher,
|
||||
ProfileSettingIndicator,
|
||||
Select
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -88,193 +69,38 @@ const ProfileTab = {
|
|||
userSuggestor () {
|
||||
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 () {
|
||||
const src = this.$store.state.users.currentUser.cover_photo
|
||||
return (!src) ? this.defaultBanner : src
|
||||
},
|
||||
groupActorAvailable () {
|
||||
return this.$store.state.instance.groupActorAvailable
|
||||
},
|
||||
availableActorTypes () {
|
||||
return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeVis (visibility) {
|
||||
this.newDefaultScope = visibility
|
||||
},
|
||||
updateProfile () {
|
||||
const params = {
|
||||
note: this.newBio,
|
||||
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)
|
||||
locked: this.locked
|
||||
}
|
||||
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({ params })
|
||||
.then((user) => {
|
||||
this.newFields.splice(user.fields.length)
|
||||
merge(this.newFields, user.fields)
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
})
|
||||
},
|
||||
changeVis (visibility) {
|
||||
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'
|
||||
.catch((error) => {
|
||||
this.displayUploadError(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) {
|
||||
return propsToNative(props)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
locked () {
|
||||
this.updateProfile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,131 +3,12 @@
|
|||
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 {
|
||||
font-size: 1.5em;
|
||||
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 {
|
||||
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>
|
||||
<div class="profile-tab">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.name_bio') }}</h2>
|
||||
<p>{{ $t('settings.name') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newName"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
<UserCard
|
||||
:user-id="user.id"
|
||||
:editable="true"
|
||||
:switcher="false"
|
||||
rounded="top"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<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>
|
||||
</UserCard>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.account_privacy') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
source="profile"
|
||||
path="locked"
|
||||
>
|
||||
<Checkbox v-model="locked">
|
||||
{{ $t('settings.lock_account_description') }}
|
||||
</BooleanSetting>
|
||||
</Checkbox>
|
||||
<ProfileSettingIndicator :is-profile="true" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ const UserAvatar = {
|
|||
props: [
|
||||
'user',
|
||||
'compact',
|
||||
'showActorTypeIndicator'
|
||||
'showActorTypeIndicator',
|
||||
'url'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
class="avatar"
|
||||
:alt="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"
|
||||
: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 UserAvatar from '../user_avatar/user_avatar.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 RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
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 suggestor from 'src/components/emoji_input/suggestor.js'
|
||||
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { mapGetters } from 'vuex'
|
||||
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 {
|
||||
faBell,
|
||||
|
|
@ -24,13 +35,16 @@ import {
|
|||
faEdit,
|
||||
faTimes,
|
||||
faExpandAlt,
|
||||
faBirthdayCake
|
||||
faBirthdayCake,
|
||||
faSave,
|
||||
faClockRotateLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { useMediaViewerStore } from '../../stores/media_viewer'
|
||||
import { useInterfaceStore } from '../../stores/interface'
|
||||
|
||||
library.add(
|
||||
faSave,
|
||||
faRss,
|
||||
faBell,
|
||||
faSearchPlus,
|
||||
|
|
@ -38,11 +52,13 @@ library.add(
|
|||
faEdit,
|
||||
faTimes,
|
||||
faExpandAlt,
|
||||
faBirthdayCake
|
||||
faBirthdayCake,
|
||||
faClockRotateLeft
|
||||
)
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'editable',
|
||||
'userId',
|
||||
'switcher',
|
||||
'selected',
|
||||
|
|
@ -54,7 +70,9 @@ export default {
|
|||
'hasNoteEditor'
|
||||
],
|
||||
components: {
|
||||
DialogModal,
|
||||
UserAvatar,
|
||||
Checkbox,
|
||||
RemoteFollow,
|
||||
ModerationTools,
|
||||
AccountActions,
|
||||
|
|
@ -65,22 +83,58 @@ export default {
|
|||
UserLink,
|
||||
UserNote,
|
||||
UserTimedFilterModal,
|
||||
ColorInput
|
||||
ColorInput,
|
||||
EmojiInput,
|
||||
ImageCropper
|
||||
},
|
||||
data () {
|
||||
const user = this.$store.getters.findUser(this.userId)
|
||||
|
||||
return {
|
||||
followRequestInProgress: false,
|
||||
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 () {
|
||||
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||
},
|
||||
computed: {
|
||||
groupActorAvailable () {
|
||||
return this.$store.state.instance.groupActorAvailable
|
||||
},
|
||||
availableActorTypes () {
|
||||
return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service']
|
||||
},
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
role () {
|
||||
return this.user.role
|
||||
},
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
|
|
@ -96,7 +150,7 @@ export default {
|
|||
return {
|
||||
backgroundImage: [
|
||||
'linear-gradient(to bottom, var(--profileTint), var(--profileTint))',
|
||||
`url(${this.user.cover_photo})`
|
||||
`url(${this.bannerImgSrc})`
|
||||
].join(', ')
|
||||
}
|
||||
},
|
||||
|
|
@ -114,6 +168,13 @@ export default {
|
|||
const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
|
||||
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: {
|
||||
get () {
|
||||
const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
|
||||
|
|
@ -139,6 +200,7 @@ export default {
|
|||
}
|
||||
},
|
||||
visibleRole () {
|
||||
if (!this.newShowRole) { return }
|
||||
const rights = this.user.rights
|
||||
if (!rights) { return }
|
||||
const validRole = rights.admin || rights.moderator
|
||||
|
|
@ -184,6 +246,60 @@ export default {
|
|||
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' })
|
||||
},
|
||||
|
||||
// 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'])
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -238,6 +354,130 @@ export default {
|
|||
e.preventDefault()
|
||||
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;
|
||||
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 {
|
||||
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 {
|
||||
text-align: center;
|
||||
margin: 0 0.6em;
|
||||
|
||||
&.input {
|
||||
margin: 0 1em;
|
||||
|
||||
textarea {
|
||||
text-align: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&, * {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&.-justify-left {
|
||||
text-align: start;
|
||||
}
|
||||
|
|
@ -80,22 +130,6 @@
|
|||
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 {
|
||||
border-top-left-radius: var(--roundness);
|
||||
border-top-right-radius: var(--roundness);
|
||||
|
|
@ -129,7 +163,7 @@
|
|||
position: relative;
|
||||
margin: 0.6em;
|
||||
margin-bottom: 0;
|
||||
text-align: right;
|
||||
text-align: left;
|
||||
|
||||
.user-identity {
|
||||
position: relative;
|
||||
|
|
@ -179,6 +213,12 @@
|
|||
padding: 0.6em;
|
||||
margin: -0.6em;
|
||||
|
||||
&.save-profile-button,
|
||||
&.reset-profile-button,
|
||||
&.edit-banner-button {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&:hover .icon {
|
||||
color: var(--textFaint);
|
||||
}
|
||||
|
|
@ -199,7 +239,6 @@
|
|||
inset: -0.6em;
|
||||
left: -0.6em;
|
||||
right: -1.2em;
|
||||
background-color: rgb(0 0 0 / 30%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
@ -209,11 +248,21 @@
|
|||
|
||||
svg {
|
||||
color: #fff;
|
||||
margin: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &.-overlay {
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -246,8 +328,6 @@
|
|||
// these two normalize position and height when custom emoji are used
|
||||
line-height: 2;
|
||||
margin-bottom: -0.2em;
|
||||
font-weight: 600;
|
||||
font-size: 110%;
|
||||
font-size: calc(max(110%, 4cqw));
|
||||
}
|
||||
|
||||
|
|
@ -258,6 +338,7 @@
|
|||
white-space: normal;
|
||||
font-weight: 500;
|
||||
font-size: 0.9em;
|
||||
font-size: calc(max(90%, 2.5cqw));
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
|
@ -281,13 +362,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.user-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
margin-right: 1em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.highlighter {
|
||||
margin: 5em;
|
||||
align-items: baseline;
|
||||
|
|
@ -336,10 +410,13 @@
|
|||
|
||||
.user-interactions {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(7.5em, 20%));
|
||||
grid-gap: 0.6em;
|
||||
max-width: 98vw;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6em;
|
||||
|
||||
> * {
|
||||
flex: 0 0 10%;
|
||||
}
|
||||
|
||||
.popover-trigger-button, .moderation-tools-button {
|
||||
width: 100%;
|
||||
|
|
@ -399,38 +476,76 @@
|
|||
|
||||
.user-profile-fields {
|
||||
margin: 0 0.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
--emoji-size: 1.8em;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
||||
&.emoji {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-profile-field-add,
|
||||
.user-profile-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.25em;
|
||||
border: 1px solid var(--border);
|
||||
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-value {
|
||||
.user-profile-field-value,
|
||||
.user-profile-field-add {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
line-height: 2em;
|
||||
display: inline-flex;
|
||||
|
||||
&.-edit {
|
||||
padding: 0;
|
||||
|
||||
input {
|
||||
font-weight: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-profile-field-name {
|
||||
flex: 0 1 50%;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
justify-content: end;
|
||||
color: var(--lightText);
|
||||
min-width: 9em;
|
||||
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="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-identity">
|
||||
<div
|
||||
|
|
@ -24,6 +27,23 @@
|
|||
/>
|
||||
</div>
|
||||
</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
|
||||
v-else-if="typeof avatarAction === 'function'"
|
||||
class="user-info-avatar"
|
||||
|
|
@ -39,9 +59,25 @@
|
|||
</router-link>
|
||||
<div class="user-summary">
|
||||
<div class="top-line">
|
||||
<div class="other-actions">
|
||||
<div
|
||||
class="other-actions"
|
||||
>
|
||||
<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"
|
||||
@click.stop="openProfileTab"
|
||||
>
|
||||
|
|
@ -90,16 +126,45 @@
|
|||
/>
|
||||
</button>
|
||||
</div>
|
||||
<router-link
|
||||
:to="userProfileLink(user)"
|
||||
class="user-name"
|
||||
>
|
||||
<RichContent
|
||||
:title="user.name"
|
||||
:html="user.name"
|
||||
:emoji="user.emoji"
|
||||
/>
|
||||
</router-link>
|
||||
<div class="name-wrapper">
|
||||
<router-link
|
||||
v-if="!editable || !editingName"
|
||||
:to="userProfileLink(user)"
|
||||
class="user-name"
|
||||
>
|
||||
<RichContent
|
||||
:title="editable ? newName : user.name_unescaped"
|
||||
:html="editable ? newName : user.name_unescaped"
|
||||
: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 class="bottom-line">
|
||||
<user-link
|
||||
|
|
@ -163,72 +228,102 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="loggedIn && isOtherUser"
|
||||
v-if="loggedIn"
|
||||
class="user-interactions"
|
||||
>
|
||||
<div class="btn-group">
|
||||
<FollowButton
|
||||
:relationship="relationship"
|
||||
<template v-if="isOtherUser">
|
||||
<div class="btn-group">
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
</template>
|
||||
<button
|
||||
v-if="relationship.muting"
|
||||
class="btn button-default btn-mute toggled"
|
||||
:disabled="user.deactivated"
|
||||
@click="unmuteUser"
|
||||
v-if="editable"
|
||||
:disabled="somethingToSave"
|
||||
class="btn button-default reset-profile-button"
|
||||
@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
|
||||
v-else
|
||||
class="btn button-default btn-mute"
|
||||
:disabled="user.deactivated"
|
||||
@click="muteUser"
|
||||
v-if="editable"
|
||||
:disabled="somethingToSave"
|
||||
class="btn button-default save-profile-button"
|
||||
@click="updateProfile"
|
||||
>
|
||||
{{ $t('user_card.mute') }}
|
||||
{{ $t('settings.save') }}
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="icon"
|
||||
icon="save"
|
||||
:title="$t('user_card.edit_profile')"
|
||||
/>
|
||||
</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
|
||||
v-if="!loggedIn && user.is_local"
|
||||
|
|
@ -238,7 +333,45 @@
|
|||
</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
|
||||
v-if="hasNote || (hasNoteEditor && supportsNote)"
|
||||
:user="user"
|
||||
|
|
@ -272,60 +405,184 @@
|
|||
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
|
||||
<ColorInput
|
||||
v-if="userHighlightType !== 'disabled'"
|
||||
class="highlighter-color"
|
||||
v-model="userHighlightColor"
|
||||
class="highlighter-color"
|
||||
:show-optional-checkbox="false"
|
||||
name="'userHighlightColorTx'+user.id"
|
||||
:unstyled="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<RichContent
|
||||
v-if="!hideBio"
|
||||
class="user-card-bio"
|
||||
:class="{ '-justify-left': mergedConfig.userCardLeftJustify }"
|
||||
:html="user.description_html"
|
||||
:emoji="user.emoji"
|
||||
:handle-links="true"
|
||||
/>
|
||||
<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"
|
||||
<h4 v-if="editable">
|
||||
<span>
|
||||
{{ $t('settings.bio') }}
|
||||
</span>
|
||||
<button
|
||||
class="button-default"
|
||||
@click="editingBio = !editingBio"
|
||||
>
|
||||
<dt
|
||||
:title="user.fields_text[index].name"
|
||||
class="user-profile-field-name"
|
||||
>
|
||||
<RichContent
|
||||
:html="field.name"
|
||||
:emoji="user.emoji"
|
||||
{{ $t('settings.toggle_edit') }}
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="pencil"
|
||||
/>
|
||||
</button>
|
||||
</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>
|
||||
<dd
|
||||
:title="user.fields_text[index].value"
|
||||
class="user-profile-field-value"
|
||||
</template>
|
||||
</EmojiInput>
|
||||
</template>
|
||||
<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
|
||||
:html="field.value"
|
||||
:emoji="user.emoji"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="user-extras" v-if="!hideBio">
|
||||
<dt
|
||||
:title="field.name"
|
||||
class="user-profile-field-name"
|
||||
>
|
||||
<RichContent
|
||||
:html="field.name"
|
||||
:emoji="editable ? emoji : user.emoji"
|
||||
/>
|
||||
</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
|
||||
v-if="!mergedConfig.hideUserStats"
|
||||
v-if="!editable && !mergedConfig.hideUserStats"
|
||||
class="user-stats"
|
||||
>
|
||||
<dl
|
||||
v-if="!mergedConfig.hideUserStats && !hideBio"
|
||||
class="user-count"
|
||||
@click.prevent="setProfileView('statuses')"
|
||||
v-if="!mergedConfig.hideUserStats && !hideBio"
|
||||
>
|
||||
<dd>{{ user.statuses_count }}</dd>
|
||||
{{ ' ' }}
|
||||
|
|
@ -356,21 +613,106 @@
|
|||
<dt>{{ $t('user_card.followers') }}</dt>
|
||||
</dl>
|
||||
</span>
|
||||
<div class="birthday" v-if="!hideBio && !!user.birthday">
|
||||
<FAIcon
|
||||
class="fa-old-padding"
|
||||
icon="birthday-cake"
|
||||
/>
|
||||
{{ $t('user_card.birthday', { birthday: formattedBirthday }) }}
|
||||
</div>
|
||||
<template v-if="!hideBio">
|
||||
<div
|
||||
v-if="user.birthday && !editable"
|
||||
class="birthday"
|
||||
>
|
||||
<FAIcon
|
||||
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>
|
||||
<teleport to="#modal">
|
||||
<UserTimedFilterModal
|
||||
ref="timedMuteDialog"
|
||||
:user="user"
|
||||
:is-mute="true"
|
||||
ref="timedMuteDialog"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@
|
|||
v-model="localNote"
|
||||
class="input note-text"
|
||||
:class="{ unstyled: !editing }"
|
||||
@focus="startEditing"
|
||||
@blur="finalizeEditing"
|
||||
rows="1"
|
||||
:placeholder="$t('user_card.note_blank_click')"
|
||||
@focus="startEditing"
|
||||
@blur="finalizeEditing"
|
||||
/>
|
||||
<span
|
||||
class="overlay"
|
||||
v-if="frozen"
|
||||
class="overlay"
|
||||
>
|
||||
<PanelLoading />
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@
|
|||
}
|
||||
|
||||
.user-info {
|
||||
|
||||
.Avatar {
|
||||
width: 5em;
|
||||
width: calc(min(5em, 20cqw));
|
||||
|
|
|
|||
|
|
@ -387,13 +387,18 @@
|
|||
"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_Person": "a normal user",
|
||||
"actor_type_person_proper": "a person",
|
||||
"actor_type_Service": "a bot",
|
||||
"actor_type_Group": "a group",
|
||||
"mobile_center_dialog": "Vertically center dialogs on mobile",
|
||||
"app_name": "App name",
|
||||
"expert_mode": "Show advanced",
|
||||
"save": "Save changes",
|
||||
"reset": "Reset changes",
|
||||
"security": "Security",
|
||||
"toggle_edit": "Toggle edit",
|
||||
"change_banner": "Change banner",
|
||||
"change_avatar": "Change avatar",
|
||||
"setting_changed": "Setting is different from default",
|
||||
"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",
|
||||
|
|
@ -488,6 +493,7 @@
|
|||
"avatarRadius": "Avatars",
|
||||
"background": "Background",
|
||||
"bio": "Bio",
|
||||
"user_preferences": "Profile settings",
|
||||
"email_language": "Language for receiving emails from the server",
|
||||
"block_export": "Block export",
|
||||
"block_export_button": "Export your blocks to a csv file",
|
||||
|
|
@ -563,7 +569,8 @@
|
|||
"move_account_error": "Error moving account: {error}",
|
||||
"discoverable": "Allow discovery of this account in search results and other services",
|
||||
"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",
|
||||
"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",
|
||||
|
|
@ -722,8 +729,10 @@
|
|||
"minimal_scopes_mode": "Minimize post scope selection options",
|
||||
"set_new_avatar": "Set new avatar",
|
||||
"set_new_profile_background": "Set new profile background",
|
||||
"set_new_background": "Set new background",
|
||||
"set_new_profile_banner": "Set new profile banner",
|
||||
"reset_avatar": "Reset avatar",
|
||||
"reset_banner": "Reset banner",
|
||||
"reset_profile_background": "Reset profile background",
|
||||
"reset_profile_banner": "Reset profile banner",
|
||||
"reset_avatar_confirm": "Do you really want to reset the avatar?",
|
||||
|
|
@ -776,6 +785,7 @@
|
|||
"tooltipRadius": "Tooltips/alerts",
|
||||
"type_domains_to_mute": "Search domains to mute",
|
||||
"upload_a_photo": "Upload a photo",
|
||||
"upload_picture": "Upload picture",
|
||||
"user_settings": "User Settings",
|
||||
"values": {
|
||||
"false": "no",
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ const defaultState = {
|
|||
|
||||
// Nasty stuff
|
||||
customEmoji: [],
|
||||
rawCustomEmoji: [],
|
||||
customEmojiFetched: false,
|
||||
emoji: {},
|
||||
emojiFetched: false,
|
||||
|
|
|
|||
|
|
@ -215,12 +215,26 @@ const updateProfileImages = ({ credentials, avatar = null, avatarName = null, ba
|
|||
}
|
||||
|
||||
const updateProfile = ({ credentials, params }) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_PROFILE_UPDATE_URL,
|
||||
const formData = new FormData();
|
||||
|
||||
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',
|
||||
payload: params,
|
||||
credentials
|
||||
}).then((data) => parseUser(data))
|
||||
body: formData
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((data) => parseUser(data))
|
||||
}
|
||||
|
||||
// Params needed:
|
||||
|
|
@ -323,7 +337,7 @@ const unmuteConversation = ({ id, credentials }) => {
|
|||
const blockUser = ({ id, expiresIn, credentials }) => {
|
||||
const payload = {}
|
||||
if (expiresIn) {
|
||||
payload.expires_in = expiresIn
|
||||
payload.duration = expiresIn
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ const mastoApiNotificationTypes = new Set([
|
|||
'move',
|
||||
'poll',
|
||||
'pleroma:emoji_reaction',
|
||||
'pleroma:report',
|
||||
'test'
|
||||
'pleroma:report'
|
||||
])
|
||||
|
||||
const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue