editable meta and bdey

This commit is contained in:
Henry Jameson 2025-08-03 21:56:45 +03:00
commit 2df895ab02
9 changed files with 343 additions and 137 deletions

View file

@ -115,6 +115,7 @@
display: flex;
flex-direction: column;
position: relative;
display: flex;
.emoji-picker-icon {
position: absolute;

View file

@ -1,12 +1,12 @@
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'
@ -53,10 +53,10 @@ const ProfileTab = {
}
},
components: {
UserCard,
ScopeSelector,
ImageCropper,
EmojiInput,
Autosuggest,
ProgressButton,
Checkbox,
BooleanSetting,
@ -88,12 +88,6 @@ 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
},

View file

@ -125,9 +125,4 @@
padding: 0 0.5em;
}
}
.birthday-input {
display: block;
margin-bottom: 1em;
}
}

View file

@ -2,6 +2,12 @@
<div class="profile-tab">
<div class="setting-item">
<h2>{{ $t('settings.name_bio') }}</h2>
<UserCard
:user-id="user.id"
:editable="true"
:switcher="false"
rounded="top"
/>
<p>{{ $t('settings.name') }}</p>
<EmojiInput
v-model="newName"
@ -41,75 +47,6 @@
</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') }}

View file

@ -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,17 @@ 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 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 +33,15 @@ import {
faEdit,
faTimes,
faExpandAlt,
faBirthdayCake
faBirthdayCake,
faSave
} from '@fortawesome/free-solid-svg-icons'
import { useMediaViewerStore } from '../../stores/media_viewer'
import { useInterfaceStore } from '../../stores/interface'
library.add(
faSave,
faRss,
faBell,
faSearchPlus,
@ -43,6 +54,7 @@ library.add(
export default {
props: [
'editable',
'userId',
'switcher',
'selected',
@ -55,6 +67,7 @@ export default {
],
components: {
UserAvatar,
Checkbox,
RemoteFollow,
ModerationTools,
AccountActions,
@ -65,13 +78,27 @@ export default {
UserLink,
UserNote,
UserTimedFilterModal,
ColorInput
ColorInput,
EmojiInput
},
data () {
const user = this.$store.state.users.currentUser
return {
followRequestInProgress: false,
muteExpiryAmount: 0,
muteExpiryUnit: 'minutes'
muteExpiryUnit: 'minutes',
// Editable stuff
newName: user.name_unescaped,
newActorType: user.actor_type,
newBio: unescape(user.description),
newBirthday: user.birthday,
newShowBirthday: user.show_birthday,
newFields: user.fields.map(field => ({ name: field.name, value: field.value })),
editingFields: false,
newLocked: user.locked,
newShowRole: user.show_role,
}
},
created () {
@ -114,6 +141,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]
@ -184,6 +218,31 @@ 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
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 +297,48 @@ export default {
e.preventDefault()
this.onAvatarClick()
}
}
},
// Editable stuff
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)
},
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)
}
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)
})
},
}
}

View file

@ -2,6 +2,17 @@
position: relative;
z-index: 1;
// editing headers
h4 {
line-height: 2;
display: flex;
padding: 0 1.0em;
span {
flex: 1;
}
}
.user-card-inner {
padding-bottom: 0;
}
@ -179,6 +190,10 @@
padding: 0.6em;
margin: -0.6em;
&.save-profile-button {
width: auto;
}
&:hover .icon {
color: var(--textFaint);
}
@ -400,37 +415,64 @@
.user-profile-fields {
margin: 0 0.5em;
--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: baseline;
margin: 0.25em;
border: 1px solid var(--border);
border-radius: var(--roundness);
line-height: 2em;
}
.user-profile-field-add {
justify-content: center;
}
.user-profile-field {
.input {
text-align: inherit;
flex: 1;
}
.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);

View file

@ -27,6 +27,19 @@
/>
</div>
</a>
<button
v-else-if="editable"
class="user-info-avatar button-unstyled -link"
@click="editAvatar"
>
<UserAvatar :user="user" />
<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"
@ -42,9 +55,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 save-profile-button"
@click="updateProfile"
>
{{ $t('settings.save') }}
<FAIcon
fixed-width
class="icon"
icon="save"
:title="$t('user_card.edit_profile')"
/>
</button>
<button
v-else-if="!editable && !isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
>
@ -100,7 +129,7 @@
<RichContent
:title="user.name"
:html="user.name"
:emoji="user.emoji"
:emoji="editable ? emoji : user.emoji"
/>
</router-link>
</div>
@ -166,7 +195,7 @@
</div>
</div>
<div
v-if="loggedIn && isOtherUser"
v-if="!editable && loggedIn && isOtherUser"
class="user-interactions"
>
<div class="btn-group">
@ -242,7 +271,7 @@
</div>
</div>
<div
v-if="loggedIn && isOtherUser && (hasNote || !hideBio) && !mergedConfig.userCardHidePersonalMarks"
v-if="!editable && loggedIn && isOtherUser && (hasNote || !hideBio) && !mergedConfig.userCardHidePersonalMarks"
class="personal-marks"
>
<UserNote
@ -291,44 +320,127 @@
class="user-card-bio"
:class="{ '-justify-left': mergedConfig.userCardLeftJustify }"
:html="user.description_html"
:emoji="user.emoji"
:emoji="editable ? emoji : user.emoji"
:handle-links="true"
/>
<h4 v-if="editable">
<span>
{{ $t('settings.profile_fields.label') }}
</span>
<button
class="button-default"
@click="editingFields = !editingFields"
>
{{ $t('settings.toggle_edit') }}
</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 user.fields_html"
v-for="(field, index) in newFields"
:key="index"
class="user-profile-field"
>
<dt
:title="user.fields_text[index].name"
:title="field.name"
class="user-profile-field-name"
>
<RichContent
:html="field.name"
:emoji="user.emoji"
:emoji="editable ? emoji : user.emoji"
/>
</dt>
<dd
:title="user.fields_text[index].value"
:title="field.value"
class="user-profile-field-value"
>
<RichContent
:html="field.value"
:emoji="user.emoji"
: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-unstyled -hover-highlight"
@click="deleteField(i)"
>
<!-- TODO something is wrong with v-show here -->
<FAIcon
v-if="newFields.length > 1"
icon="times"
/>
</button>
</dd>
</dl>
<p class="user-profile-field-add">
<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>
</p>
</div>
</template>
<div
v-if="!hideBio"
class="user-extras"
v-if="!hideBio"
>
<span
v-if="!mergedConfig.hideUserStats"
v-if="!editable && !mergedConfig.hideUserStats"
class="user-stats"
>
<dl
@ -365,8 +477,9 @@
<dt>{{ $t('user_card.followers') }}</dt>
</dl>
</span>
<template v-if="!hideBio">
<div
v-if="!hideBio && !!user.birthday"
v-if="user.birthday && !editable"
class="birthday"
>
<FAIcon
@ -375,6 +488,27 @@
/>
{{ $t('user_card.birthday', { birthday: formattedBirthday }) }}
</div>
<div
v-else-if="editable"
class="birthday"
>
<FAIcon
class="fa-old-padding"
icon="birthday-cake"
/>
<input
id="birthday"
v-model="newBirthday"
type="date"
class="input birthday-input"
>
<div>
<Checkbox v-model="showBirthday">
{{ $t('settings.birthday.show_birthday') }}
</Checkbox>
</div>
</div>
</template>
</div>
<teleport to="#modal">
<UserTimedFilterModal

View file

@ -394,6 +394,7 @@
"expert_mode": "Show advanced",
"save": "Save changes",
"security": "Security",
"toggle_edit": "Toggle edit",
"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",

View file

@ -139,6 +139,7 @@ const defaultState = {
// Nasty stuff
customEmoji: [],
rawCustomEmoji: [],
customEmojiFetched: false,
emoji: {},
emojiFetched: false,