initial profile overhaul

This commit is contained in:
Henry Jameson 2025-07-29 17:38:08 +03:00
commit 79868aeeb9
11 changed files with 470 additions and 409 deletions

View file

@ -19,7 +19,7 @@
/> />
<div <div
class="input color-input-field" class="input color-input-field"
:class="{ disabled: !present || disabled }" :class="{ disabled: !present || disabled, unstyled }"
> >
<input <input
:id="name + '-t'" :id="name + '-t'"
@ -92,6 +92,11 @@ export default {
required: true, required: true,
type: String type: String
}, },
// use unstyled, uh, style
unstyled: {
required: false,
type: Boolean
},
// Color value, should be required but vue cannot tell the difference // Color value, should be required but vue cannot tell the difference
// between "property missing" and "property set to undefined" // between "property missing" and "property set to undefined"
modelValue: { modelValue: {

View file

@ -1,3 +1,4 @@
import ColorInput from 'src/components/color_input/color_input.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
@ -9,6 +10,7 @@ import Select from '../select/select.vue'
import UserLink from '../user_link/user_link.vue' import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue' import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
import localeService from 'src/services/locale/locale.service.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
@ -21,7 +23,8 @@ import {
faExternalLinkAlt, faExternalLinkAlt,
faEdit, faEdit,
faTimes, faTimes,
faExpandAlt faExpandAlt,
faBirthdayCake
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { useMediaViewerStore } from '../../stores/media_viewer' import { useMediaViewerStore } from '../../stores/media_viewer'
@ -34,7 +37,8 @@ library.add(
faExternalLinkAlt, faExternalLinkAlt,
faEdit, faEdit,
faTimes, faTimes,
faExpandAlt faExpandAlt,
faBirthdayCake
) )
export default { export default {
@ -60,7 +64,8 @@ export default {
RichContent, RichContent,
UserLink, UserLink,
UserNote, UserNote,
UserTimedFilterModal UserTimedFilterModal,
ColorInput
}, },
data () { data () {
return { return {
@ -175,6 +180,10 @@ export default {
? this.$t('user_card.block_expires_forever') ? this.$t('user_card.block_expires_forever')
: this.$t('user_card.block_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()]) : this.$t('user_card.block_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
}, },
formattedBirthday () {
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' })
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
methods: { methods: {

View file

@ -2,34 +2,67 @@
position: relative; position: relative;
z-index: 1; z-index: 1;
.user-card-inner {
padding-bottom: 0;
}
&:hover { &:hover {
--_still-image-img-visibility: visible; --_still-image-img-visibility: visible;
--_still-image-canvas-visibility: hidden; --_still-image-canvas-visibility: hidden;
--_still-image-label-visibility: hidden; --_still-image-label-visibility: hidden;
} }
.btn-mute, .btn-mention {
display: block;
width: 100%;
}
.panel-heading { .panel-heading {
padding: 0.5em 0;
text-align: center; text-align: center;
box-shadow: none; box-shadow: none;
background: transparent; background: transparent;
backdrop-filter: none;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
// create new stacking context // create new stacking context
position: relative; position: relative;
overflow: hidden;
}
.personal-marks {
margin: 0.6em;
padding: 0.6em;
border-bottom: 1px solid var(--border);
.highlighter {
h4 {
margin-top: 0.6em;
}
.userHighlightSel {
vertical-align: bottom;
margin-left: -0.1em;
&.-none {
font-style: italic;
color: var(--textFaint);
}
}
.highlighter-color {
vertical-align: bottom;
margin-left: 0.6em;
}
}
} }
.background-image { .background-image {
position: absolute; width: calc(100% + 2.4em);
margin-left: -1.2em;
margin-right: -1.2em;
margin-top: -1.2em;
margin-bottom: -4.5em;
aspect-ratio: 3;
padding: 0;
inset: 0; inset: 0;
mask: mask:
linear-gradient(to top, white, transparent) bottom no-repeat, linear-gradient(to top, white 0%, transparent 20%) bottom no-repeat,
linear-gradient(to top, white, white); linear-gradient(to top, white 0%, white 100%);
// Autoprefixer seem to ignore this one, and also syntax is different // Autoprefixer seem to ignore this one, and also syntax is different
/* stylelint-disable mask-composite */ /* stylelint-disable mask-composite */
/* stylelint-disable declaration-property-value-no-unknown */ /* stylelint-disable declaration-property-value-no-unknown */
@ -42,7 +75,7 @@
/* stylelint-enable mask-composite */ /* stylelint-enable mask-composite */
mask-composite: exclude; mask-composite: exclude;
background-size: cover; background-size: cover;
mask-size: 100% 60%; mask-size: 100%;
border-top-left-radius: calc(var(--__roundnessTop, --panelRadius) - 1px); border-top-left-radius: calc(var(--__roundnessTop, --panelRadius) - 1px);
border-top-right-radius: calc(var(--__roundnessTop, --panelRadius) - 1px); border-top-right-radius: calc(var(--__roundnessTop, --panelRadius) - 1px);
border-bottom-left-radius: calc(var(--__roundnessBottom, --panelRadius) - 1px); border-bottom-left-radius: calc(var(--__roundnessBottom, --panelRadius) - 1px);
@ -57,10 +90,11 @@
&-bio { &-bio {
text-align: center; text-align: center;
color: var(--lightText);
display: block; display: block;
line-height: 1.3; line-height: 1.3;
padding: 1em; padding: 0.6em;
margin: 0; margin: 0 0.6em;
img { img {
object-fit: contain; object-fit: contain;
@ -100,14 +134,15 @@
} }
.user-info { .user-info {
padding: 0 26px; position: relative;
margin: 0.6em;
margin-bottom: 0;
.container { .user-identity {
min-width: 0; min-width: 0;
padding: 16px 0 6px;
display: flex; display: flex;
align-items: flex-start; align-items: flex-end;
max-height: 56px; margin-bottom: 1em;
> * { > * {
min-width: 0; min-width: 0;
@ -123,19 +158,42 @@
--_avatarShadowFilter: var(--avatarShadowFilter); --_avatarShadowFilter: var(--avatarShadowFilter);
--_avatarShadowInset: var(--avatarShadowInset); --_avatarShadowInset: var(--avatarShadowInset);
width: 56px; min-width: 6em;
height: 56px; height: 6em;
object-fit: cover; object-fit: cover;
} }
} }
.other-actions {
position: absolute;
top: 0;
right: 0;
display: grid;
grid-auto-flow: column;
grid-gap: 0.6em;
a, button {
text-align: center;
width: 2em;
height: 2em;
line-height: 2em;
&:not(:hover) .icon {
color: var(--lightText);
}
}
}
&-avatar { &-avatar {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
margin-right: 0.6em;
&.-overlay { &.-overlay {
position: absolute; position: absolute;
inset: 0; inset: -0.6em;
left: -0.6em;
right: -1.2em;
background-color: rgb(0 0 0 / 30%); background-color: rgb(0 0 0 / 30%);
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -154,22 +212,9 @@
} }
} }
.external-link-button,
.edit-profile-button {
cursor: pointer;
width: 2.5em;
text-align: center;
margin: -0.5em 0;
padding: 0.5em 0;
&:not(:hover) .icon {
color: var(--lightText);
}
}
.bottom-line { .bottom-line {
font-weight: lighter; font-weight: lighter;
font-size: 1.1em; font-size: 0.9em;
align-items: baseline; align-items: baseline;
.lock-icon { .lock-icon {
@ -184,16 +229,9 @@
overflow: hidden; overflow: hidden;
} }
.dailyAvg {
min-width: 1px;
flex: 0 0 auto;
margin-left: 1em;
font-size: 0.7em;
color: var(--text);
}
.user-role { .user-role {
flex: none; flex: none;
align-self: baseline;
} }
} }
@ -216,9 +254,16 @@
--link: var(--text) !important; --link: var(--text) !important;
} }
.top-line, .top-line, {
display: flex;
line-height: 2;
font-weight: 600;
}
.bottom-line { .bottom-line {
display: flex; display: flex;
font-weight: 500;
line-height: 1.5;
} }
} }
@ -230,9 +275,8 @@
font-size: 1.1em; font-size: 1.1em;
} }
.user-meta { .highlighter {
margin-bottom: 0.15em; margin: 5em;
display: flex;
align-items: baseline; align-items: baseline;
line-height: 22px; line-height: 22px;
flex-wrap: wrap; flex-wrap: wrap;
@ -279,23 +323,17 @@
.user-interactions { .user-interactions {
position: relative; position: relative;
display: flex; display: grid;
flex-flow: row wrap; grid-template-columns: repeat(auto-fill, minmax(17%, 7em));
margin-right: -0.75em; grid-gap: 0.6em;
> * { .popover-trigger-button, .moderation-tools-button {
margin: 0 0.75em 0.6em 0; width: 100%;
}
> .btn-group, > button {
white-space: nowrap; white-space: nowrap;
min-width: 95px;
} }
button {
margin: 0;
}
}
.user-note {
margin: 0 0.75em 0.6em 0;
} }
} }
@ -303,24 +341,92 @@
display: none; display: none;
} }
.user-counts { .user-extras {
display: flex; display: grid;
line-height: 16px; place-items: center baseline;
padding: 0.5em 1.5em 0;
text-align: center;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; grid-template-columns: minmax(max-content, 1fr);
grid-auto-columns: 1fr;
grid-auto-flow: column;
padding: 1.2em 0.6em;
} }
.user-count { .birthday {
flex: 1 0 auto; width: 100%;
padding: 0.5em 0; text-align: right;
}
.user-birthday {
margin: 0 0.75em 0.5em;
}
.user-profile-fields {
margin: 0 0.5em; margin: 0 0.5em;
h5 { img {
font-size: 1em; object-fit: contain;
vertical-align: middle;
max-width: 100%;
max-height: 400px;
&.emoji {
width: 18px;
height: 18px;
}
}
.user-profile-field {
display: flex;
margin: 0.25em;
border: 1px solid var(--border);
border-radius: var(--roundness);
.user-profile-field-name,
.user-profile-field-value {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
line-height: 2em;
}
.user-profile-field-name {
flex: 0 1 50%;
font-weight: 600;
text-align: right;
color: var(--lightText);
min-width: 9em;
border-right: 1px solid var(--border);
padding-left: 1.2em;
padding-right: 0.6em;
}
.user-profile-field-value {
flex: 1 1 55%;
color: var(--text);
padding-left: 0.6em;
margin: 0;
}
}
}
.user-stats {
display: block;
text-align: center;
width: 100%;
dl {
display: inline;
margin-right: 1em;
}
dd {
font-weight: bolder; font-weight: bolder;
margin: 0 0 0.25em; margin: 0;
}
dd, dt {
display: inline
} }
/* stylelint-disable-next-line no-descending-specificity */ /* stylelint-disable-next-line no-descending-specificity */

View file

@ -3,14 +3,65 @@
class="user-card" class="user-card"
:class="classes" :class="classes"
> >
<div <div :class="onClose ? '' : 'panel-heading -flexible-height'" class="user-card-inner">
:class="{ 'hide-bio': hideBio }"
:style="style"
class="background-image"
/>
<div :class="onClose ? '' : 'panel-heading -flexible-height'">
<div class="user-info"> <div class="user-info">
<div class="container"> <div
:class="{ 'hide-bio': hideBio }"
:style="style"
class="background-image"
/>
<div class="other-actions">
<button
v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
>
<FAIcon
fixed-width
class="icon"
icon="edit"
:title="$t('user_card.edit_profile')"
/>
</button>
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="external-link-alt"
/>
</a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
:relationship="relationship"
/>
<router-link
v-if="onClose"
:to="userProfileLink(user)"
class="button-unstyled external-link-button"
@click="onClose"
>
<FAIcon
class="icon"
icon="expand-alt"
/>
</router-link>
<button
v-if="onClose"
class="button-unstyled external-link-button"
@click="onClose"
>
<FAIcon
class="icon"
icon="times"
/>
</button>
</div>
<div class="user-identity">
<a <a
v-if="avatarAction === 'zoom'" v-if="avatarAction === 'zoom'"
class="user-info-avatar -link" class="user-info-avatar -link"
@ -48,55 +99,6 @@
:emoji="user.emoji" :emoji="user.emoji"
/> />
</router-link> </router-link>
<button
v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
>
<FAIcon
fixed-width
class="icon"
icon="edit"
:title="$t('user_card.edit_profile')"
/>
</button>
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="external-link-alt"
/>
</a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
:relationship="relationship"
/>
<router-link
v-if="onClose"
:to="userProfileLink(user)"
class="button-unstyled external-link-button"
@click="onClose"
>
<FAIcon
class="icon"
icon="expand-alt"
/>
</router-link>
<button
v-if="onClose"
class="button-unstyled external-link-button"
@click="onClose"
>
<FAIcon
class="icon"
icon="times"
/>
</button>
</div> </div>
<div class="bottom-line"> <div class="bottom-line">
<user-link <user-link
@ -148,60 +150,15 @@
> >
{{ blockExpiry }} {{ blockExpiry }}
</span> </span>
<span <div
v-if="!mergedConfig.hideUserStats && !hideBio" v-if="relationship.followed_by && loggedIn && isOtherUser"
class="dailyAvg" class="alert neutral user-role"
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</span> >
{{ $t('user_card.follows_you') }}
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="user-meta">
<div
v-if="relationship.followed_by && loggedIn && isOtherUser"
class="following"
>
{{ $t('user_card.follows_you') }}
</div>
<div
v-if="isOtherUser && (loggedIn || !switcher)"
class="highlighter"
>
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
<input
v-if="userHighlightType !== 'disabled'"
:id="'userHighlightColorTx'+user.id"
v-model="userHighlightColor"
class="input userHighlightText"
type="text"
>
<input
v-if="userHighlightType !== 'disabled'"
:id="'userHighlightColor'+user.id"
v-model="userHighlightColor"
class="input userHighlightCl"
type="color"
>
{{ ' ' }}
<Select
:id="'userHighlightSel'+user.id"
v-model="userHighlightType"
class="userHighlightSel"
>
<option value="disabled">
{{ $t('user_card.highlight.disabled') }}
</option>
<option value="solid">
{{ $t('user_card.highlight.solid') }}
</option>
<option value="striped">
{{ $t('user_card.highlight.striped') }}
</option>
<option value="side">
{{ $t('user_card.highlight.side') }}
</option>
</Select>
</div>
</div>
<div <div
v-if="loggedIn && isOtherUser" v-if="loggedIn && isOtherUser"
class="user-interactions" class="user-interactions"
@ -241,35 +198,32 @@
</ProgressButton> </ProgressButton>
</template> </template>
</div> </div>
<div> <button
<button v-if="relationship.muting"
v-if="relationship.muting" class="btn button-default btn-mute toggled"
class="btn button-default btn-mute toggled" :disabled="user.deactivated"
:disabled="user.deactivated" @click="unmuteUser"
@click="unmuteUser" >
> {{ $t('user_card.muted') }}
{{ $t('user_card.muted') }} </button>
</button> <button
<button v-else
v-else class="btn button-default btn-mute"
class="btn button-default btn-mute" :disabled="user.deactivated"
:disabled="user.deactivated" @click="muteUser"
@click="muteUser" >
> {{ $t('user_card.mute') }}
{{ $t('user_card.mute') }} </button>
</button> <button
</div> class="btn button-default btn-mention"
<div> :disabled="user.deactivated"
<button @click="mentionUser"
class="btn button-default btn-mention" >
:disabled="user.deactivated" {{ $t('user_card.mention') }}
@click="mentionUser" </button>
>
{{ $t('user_card.mention') }}
</button>
</div>
<ModerationTools <ModerationTools
v-if="showModerationMenu" v-if="showModerationMenu"
class="moderation-menu"
:user="user" :user="user"
/> />
</div> </div>
@ -279,51 +233,132 @@
> >
<RemoteFollow :user="user" /> <RemoteFollow :user="user" />
</div> </div>
<UserNote </div>
v-if="loggedIn && isOtherUser && (hasNote || (hasNoteEditor && supportsNote))" </div>
:user="user" <div class="personal-marks" v-if="loggedIn && isOtherUser && (hasNote || !hideBio)">
:relationship="relationship" <UserNote
:editable="hasNoteEditor" v-if="hasNote || (hasNoteEditor && supportsNote)"
:user="user"
:relationship="relationship"
:editable="hasNoteEditor"
/>
<div
v-if="!hideBio"
class="highlighter"
>
<h4>{{ $t('user_card.highlight_header') }}</h4>
<Select
:id="'userHighlightSel'+user.id"
v-model="userHighlightType"
class="userHighlightSel unstyled"
:class="{ '-none': userHighlightType === 'disabled' }"
>
<option value="disabled">
{{ $t('user_card.highlight_new.disabled') }}
</option>
<option value="solid">
{{ $t('user_card.highlight_new.solid') }}
</option>
<option value="striped">
{{ $t('user_card.highlight_new.striped') }}
</option>
<option value="side">
{{ $t('user_card.highlight_new.side') }}
</option>
</Select>
<!-- 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"
:show-optional-checkbox="false"
name="'userHighlightColorTx'+user.id"
:unstyled="true"
/> />
</div> </div>
</div> </div>
<div <RichContent
v-if="!hideBio" v-if="!hideBio"
class="user-bio" class="user-card-bio"
: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"
> >
<div <dl
v-if="!mergedConfig.hideUserStats && switcher" v-for="(field, index) in user.fields_html"
class="user-counts" :key="index"
class="user-profile-field"
> >
<div <dt
class="user-count" :title="user.fields_text[index].name"
@click.prevent="setProfileView('statuses')" class="user-profile-field-name"
> >
<h5>{{ $t('user_card.statuses') }}</h5> <RichContent
<span>{{ user.statuses_count }} <br></span> :html="field.name"
</div> :emoji="user.emoji"
<div />
class="user-count" </dt>
@click.prevent="setProfileView('friends')" <dd
:title="user.fields_text[index].value"
class="user-profile-field-value"
> >
<h5>{{ $t('user_card.followees') }}</h5> <RichContent
<span>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</span> :html="field.value"
</div> :emoji="user.emoji"
<div />
class="user-count" </dd>
@click.prevent="setProfileView('followers')" </dl>
<div class="user-extras">
<span
v-if="!hideBio && !mergedConfig.hideUserStats"
class="user-stats"
> >
<h5>{{ $t('user_card.followers') }}</h5> <dl
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span> class="user-count"
@click.prevent="setProfileView('statuses')"
v-if="!mergedConfig.hideUserStats && !hideBio"
>
<dd>{{ user.statuses_count }}</dd>
{{ ' ' }}
<dt>{{ $t('user_card.statuses') }}</dt>
</dl>
<dl
class="user-count"
@click.prevent="setProfileView('statuses')"
>
<dd>{{ dailyAvg }}</dd>
{{ ' ' }}
<dt>{{ $t('user_card.statuses_per_day') }}</dt>
</dl>
<dl
class="user-count"
@click.prevent="setProfileView('friends')"
>
<dd>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</dd>
{{ ' ' }}
<dt>{{ $t('user_card.followees') }}</dt>
</dl>
<dl
class="user-count"
@click.prevent="setProfileView('followers')"
>
<dd>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</dd>
{{ ' ' }}
<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> </div>
</div> </div>
<RichContent
v-if="!hideBio"
class="user-card-bio"
:html="user.description_html"
:emoji="user.emoji"
:handle-links="true"
/>
</div> </div>
<teleport to="#modal"> <teleport to="#modal">
<UserTimedFilterModal <UserTimedFilterModal

View file

@ -1,9 +1,14 @@
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
const UserNote = { const UserNote = {
props: { props: {
user: Object, user: Object,
relationship: Object, relationship: Object,
editable: Boolean editable: Boolean
}, },
components: {
PanelLoading
},
data () { data () {
return { return {
localNote: '', localNote: '',
@ -21,9 +26,6 @@ const UserNote = {
this.localNote = this.relationship.note this.localNote = this.relationship.note
this.editing = true this.editing = true
}, },
cancelEditing () {
this.editing = false
},
finalizeEditing () { finalizeEditing () {
this.frozen = true this.frozen = true

View file

@ -1,46 +1,28 @@
<template> <template>
<div <div
class="user-note" class="user-note"
:class="{ '-frozen': frozen }"
> >
<div class="heading"> <h4>{{ $t('user_card.personal_note') }}</h4>
<span>{{ $t('user_card.note') }}</span>
<div class="buttons">
<button
v-show="!editing && editable"
class="button-default btn"
@click="startEditing"
>
{{ $t('user_card.edit_note') }}
</button>
<button
v-show="editing"
class="button-default btn"
:disabled="frozen"
@click="finalizeEditing"
>
{{ $t('user_card.edit_note_apply') }}
</button>
<button
v-show="editing"
class="button-default btn"
:disabled="frozen"
@click="cancelEditing"
>
{{ $t('user_card.edit_note_cancel') }}
</button>
</div>
</div>
<textarea <textarea
v-show="editing" v-show="editing"
v-model="localNote" v-model="localNote"
class="input note-text" class="input note-text"
@blur="finalizeEditing"
/> />
<span <span
v-show="!editing" v-show="!editing"
class="note-text" class="note-text"
:class="{ '-blank': !relationship.note }" :class="{ '-blank': !relationship.note }"
@click="startEditing"
> >
{{ relationship.note || $t('user_card.note_blank') }} {{ relationship.note || $t('user_card.note_blank_click') }}
</span>
<span
class="overlay"
v-if="frozen"
>
<PanelLoading />
</span> </span>
</div> </div>
</template> </template>
@ -49,29 +31,11 @@
<style lang="scss"> <style lang="scss">
.user-note { .user-note {
display: flex; position: relative;
flex-direction: column;
.heading { h4 {
display: flex;
flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; margin-bottom: 0.5em;
margin-bottom: 0.75em;
.btn {
min-width: 95px;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: right;
.btn {
margin-left: 0.5em;
}
}
} }
.note-text { .note-text {
@ -82,5 +46,15 @@
font-style: italic; font-style: italic;
color: var(--textFaint); color: var(--textFaint);
} }
.overlay {
position: absolute;
inset: 0;
background-color: rgb(0 0 0 / 30%);
.panel-loading {
font-size: 1em;
}
}
} }
</style> </style>

View file

@ -28,8 +28,21 @@
backdrop-filter: var(--backdrop-filter); backdrop-filter: var(--backdrop-filter);
} }
.user-info {
margin-bottom: 0;
.background-image {
max-height: 5.6em;
margin-bottom: -4.6em;
}
.Avatar {
width: 5em;
height: 5em;
}
}
.signed-in { .signed-in {
overflow: visible;
z-index: 10; z-index: 10;
} }
} }

View file

@ -26,7 +26,24 @@
<style lang="scss"> <style lang="scss">
/* popover styles load on-demand, so we need to override */ /* popover styles load on-demand, so we need to override */
/* stylelint-disable block-no-empty */ /* stylelint-disable block-no-empty */
.user-popover.popover { .user-popover{
.user-card-inner {
display: flex;
}
&.popover {
overflow: hidden;
padding: 0.6em;
.user-info {
width: 30em;
}
.background-image {
max-height: 6em;
margin-bottom: -4.5em;
}
}
} }
/* stylelint-enable block-no-empty */ /* stylelint-enable block-no-empty */

View file

@ -7,16 +7,13 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import List from '../list/list.vue' import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more' import withLoadMore from '../../hocs/with_load_more/with_load_more'
import localeService from 'src/services/locale/locale.service.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faCircleNotch, faCircleNotch
faBirthdayCake
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faCircleNotch, faCircleNotch
faBirthdayCake
) )
const FollowerList = withLoadMore({ const FollowerList = withLoadMore({
@ -82,10 +79,6 @@ const UserProfile = {
}, },
favoritesTabVisible () { favoritesTabVisible () {
return this.isUs || (this.$store.state.instance.pleromaPublicFavouritesAvailable && !this.user.hide_favorites) return this.isUs || (this.$store.state.instance.pleromaPublicFavouritesAvailable && !this.user.hide_favorites)
},
formattedBirthday () {
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' })
} }
}, },
methods: { methods: {

View file

@ -13,45 +13,6 @@
rounded="top" rounded="top"
:has-note-editor="true" :has-note-editor="true"
/> />
<span
v-if="!!user.birthday"
class="user-birthday"
>
<FAIcon
class="fa-old-padding"
icon="birthday-cake"
/>
{{ $t('user_card.birthday', { birthday: formattedBirthday }) }}
</span>
<div
v-if="user.fields_html && user.fields_html.length > 0"
class="user-profile-fields"
>
<dl
v-for="(field, index) in user.fields_html"
:key="index"
class="user-profile-field"
>
<dt
:title="user.fields_text[index].name"
class="user-profile-field-name"
>
<RichContent
:html="field.name"
:emoji="user.emoji"
/>
</dt>
<dd
:title="user.fields_text[index].value"
class="user-profile-field-value"
>
<RichContent
:html="field.value"
:emoji="user.emoji"
/>
</dd>
</dl>
</div>
</div> </div>
<tab-switcher <tab-switcher
:active-tab="tab" :active-tab="tab"
@ -173,58 +134,6 @@
// No sticky header on user profile // No sticky header on user profile
--currentPanelStack: 0; --currentPanelStack: 0;
.user-birthday {
margin: 0 0.75em 0.5em;
}
.user-profile-fields {
margin: 0 0.5em;
img {
object-fit: contain;
vertical-align: middle;
max-width: 100%;
max-height: 400px;
&.emoji {
width: 18px;
height: 18px;
}
}
.user-profile-field {
display: flex;
margin: 0.25em;
border: 1px solid var(--border);
border-radius: var(--roundness);
.user-profile-field-name {
flex: 0 1 30%;
font-weight: 500;
text-align: right;
color: var(--lightText);
min-width: 120px;
border-right: 1px solid var(--border);
}
.user-profile-field-value {
flex: 1 1 70%;
color: var(--text);
margin: 0 0 0 0.25em;
}
.user-profile-field-name,
.user-profile-field-value {
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding: 0.5em 1.5em;
box-sizing: border-box;
}
}
}
.userlist-placeholder { .userlist-placeholder {
display: flex; display: flex;
justify-content: center; justify-content: center;

View file

@ -1423,7 +1423,7 @@
"block_expires_forever": "Blocked forever", "block_expires_forever": "Blocked forever",
"block_expires_at": "Blocked until {0}", "block_expires_at": "Blocked until {0}",
"mute_duration_prompt": "Mute this user for (0 for indefinite time):", "mute_duration_prompt": "Mute this user for (0 for indefinite time):",
"per_day": "per day", "statuses_per_day": "Statuses per day",
"remote_follow": "Remote follow", "remote_follow": "Remote follow",
"remove_follower": "Remove follower", "remove_follower": "Remove follower",
"remove_follower_confirm_title": "Remove follower confirmation", "remove_follower_confirm_title": "Remove follower confirmation",
@ -1464,17 +1464,15 @@
"delete_user": "Delete user", "delete_user": "Delete user",
"delete_user_data_and_deactivate_confirmation": "This will permanently delete the data from this account and deactivate it. Are you absolutely sure?" "delete_user_data_and_deactivate_confirmation": "This will permanently delete the data from this account and deactivate it. Are you absolutely sure?"
}, },
"highlight": { "highlight_new": {
"disabled": "No highlight", "disabled": "Don't highlight",
"solid": "Solid bg", "solid": "Solid background",
"striped": "Striped bg", "striped": "Striped background",
"side": "Side stripe" "side": "Side stripe"
}, },
"note": "Note", "personal_note": "Personal note",
"note_blank": "(None)", "note_blank_click": "Click to add note",
"edit_note": "Edit note", "highlight_header": "Highlight user's posts and mentions"
"edit_note_apply": "Apply",
"edit_note_cancel": "Cancel"
}, },
"user_profile": { "user_profile": {
"timeline_title": "User timeline", "timeline_title": "User timeline",