Merge branch 'profile-overhaul' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2025-07-29 19:03:39 +03:00
commit a1d3d04087
33 changed files with 683 additions and 556 deletions

View file

@ -12,6 +12,8 @@
"custom-property-pattern": null,
"keyframes-name-pattern": null,
"scss/operator-no-newline-after": null,
"declaration-property-value-no-unknown": true,
"scss/declaration-property-value-no-unknown": true,
"declaration-block-no-redundant-longhand-properties": [
true,
{

0
changelog.d/linter.skip Normal file
View file

1
changelog.d/timed.add Normal file
View file

@ -0,0 +1 @@
Support for expiring mutes and blocks (if available)

View file

@ -525,6 +525,10 @@ textarea {
height: unset;
}
&::placeholder {
color: var(--textFaint)
}
--_padding: 0.5em;
border: none;
@ -1075,7 +1079,7 @@ option {
scale: 1.0063 0.9938;
translate: 0 -10%;
transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-in-ou;
animation-timing-function: ease-in-out;
}
90% {

View file

@ -254,7 +254,6 @@ const getNodeInfo = async ({ store }) => {
const data = await res.json()
const metadata = data.metadata
const features = metadata.features
console.log(features)
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })

View file

@ -3,7 +3,6 @@
<Popover
trigger="click"
placement="bottom"
:bound-to="{ x: 'container' }"
remove-padding
>
<template #content>

View file

@ -14,8 +14,10 @@ const BlockCard = {
blocked () {
return this.relationship.blocking
},
blockExpiryAvailable () {
return this.user.block_expires_at !== undefined
},
blockExpiry () {
console.log(this.user)
return this.user.block_expires_at == null
? this.$t('user_card.block_expires_forever')
: this.$t('user_card.block_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
@ -29,7 +31,6 @@ const BlockCard = {
},
methods: {
unblockUser () {
this.progress = true
this.$store.dispatch('unblockUser', this.user.id)
},
blockUser () {

View file

@ -1,7 +1,10 @@
<template>
<basic-user-card :user="user">
<div class="block-card-content-container">
<span v-if="blocked" class="alert neutral">
<span
v-if="blocked && blockExpiryAvailable"
class="alert neutral"
>
{{ blockExpiry }}
</span>
{{ ' ' }}
@ -22,9 +25,9 @@
</div>
<teleport to="#modal">
<UserTimedFilterModal
ref="timedBlockDialog"
:user="user"
:is-mute="false"
ref="timedBlockDialog"
/>
</teleport>
</basic-user-card>

View file

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

View file

@ -11,6 +11,7 @@
<slot />
<template #footer>
<slot name="footerLeft" />
<button
class="btn button-default"
@click.prevent="onAccept"

View file

@ -154,10 +154,12 @@
// Autoprefixed seem to ignore this one, and also syntax is different
/* stylelint-disable mask-composite */
/* stylelint-disable declaration-property-value-no-unknown */
/* stylelint-disable scss/declaration-property-value-no-unknown */
/* TODO check if this is still needed */
mask-composite: xor;
/* stylelint-enable declaration-property-value-no-unknown */
/* stylelint-enable scss/declaration-property-value-no-unknown */
/* stylelint-enable mask-composite */
mask-composite: exclude;

View file

@ -159,9 +159,11 @@
/* Autoprefixed seem to ignore this one, and also syntax is different */
/* stylelint-disable mask-composite */
/* stylelint-disable declaration-property-value-no-unknown */
/* stylelint-disable scss/declaration-property-value-no-unknown */
/* TODO check if this is still needed */
mask-composite: xor;
/* stylelint-enable scss/declaration-property-value-no-unknown */
/* stylelint-enable declaration-property-value-no-unknown */
/* stylelint-enable mask-composite */
mask-composite: exclude;

View file

@ -13,6 +13,9 @@ const MuteCard = {
muted () {
return this.relationship.muting
},
muteExpiryAvailable () {
return this.user.mute_expires_at !== undefined
},
muteExpiry () {
return this.user.mute_expires_at == null
? this.$t('user_card.mute_expires_forever')
@ -25,7 +28,6 @@ const MuteCard = {
},
methods: {
unmuteUser () {
this.progress = true
this.$store.dispatch('unmuteUser', this.userId)
},
muteUser () {

View file

@ -1,7 +1,10 @@
<template>
<basic-user-card :user="user">
<div class="mute-card-content-container">
<span v-if="muted" class="alert neutral">
<span
v-if="muted && muteExpiryAvailable"
class="alert neutral"
>
{{ muteExpiry }}
</span>
{{ ' ' }}
@ -22,9 +25,9 @@
</div>
<teleport to="#modal">
<UserTimedFilterModal
ref="timedMuteDialog"
:user="user"
:is-mute="true"
ref="timedMuteDialog"
/>
</teleport>
</basic-user-card>

View file

@ -1,5 +1,6 @@
import { cloneDeep } from 'lodash'
import { mapState, mapActions } from 'pinia'
import { mapState as mapVuexState } from 'vuex'
import { v4 as uuidv4 } from 'uuid';
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
@ -30,6 +31,11 @@ const FilteringTab = {
value: mode,
label: this.$t(`settings.reply_visibility_${mode}`)
})),
muteBlockLv1Options: ['ask', 'forever', 'temporarily'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`user_card.mute_block_${mode}`)
})),
muteFiltersDraftObject: cloneDeep(useServerSideStorageStore().prefsStorage.simple.muteFilters),
muteFiltersDraftDirty: Object.fromEntries(
Object.entries(
@ -93,6 +99,43 @@ const FilteringTab = {
muteFiltersObject: store => store.prefsStorage.simple.muteFilters
}
),
...mapVuexState({
blockExpirationSupported: state => state.instance.blockExpiration
}),
onMuteDefaultActionLv1: {
get () {
const value = this.$store.state.config.onMuteDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
set (value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
this.$store.dispatch('setOption', { name: 'onMuteDefaultAction', value: realValue })
}
},
onBlockDefaultActionLv1: {
get () {
const value = this.$store.state.config.onBlockDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
set (value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
this.$store.dispatch('setOption', { name: 'onBlockDefaultAction', value: realValue })
}
},
muteFiltersDraft () {
return Object.entries(this.muteFiltersDraftObject)
},

View file

@ -80,6 +80,66 @@
<div class="setting-item">
<h2>{{ $t('settings.filter.mute_filter') }}</h2>
<ul class="setting-list">
<li>
{{ $t('user_card.default_mute_expiration') }}
<Select
id="onMuteDefaultActionLv1"
v-model="onMuteDefaultActionLv1"
>
<option
v-for="option in muteBlockLv1Options"
:key="option.key"
:value="option.value"
>
{{ option.label }}
</option>
</Select>
<ul
v-if="onMuteDefaultActionLv1 === 'temporarily'"
class="setting-list suboptions"
>
<li>
<UnitSetting
path="onMuteDefaultAction"
unit-set="time"
:units="['s', 'm', 'h', 'd']"
:min="0"
>
{{ $t('user_card.default_expiration_time') }}
</UnitSetting>
</li>
</ul>
</li>
<li v-if="blockExpirationSupported">
{{ $t('user_card.default_block_expiration') }}
<Select
id="onBlockDefaultActionLv1"
v-model="onBlockDefaultActionLv1"
>
<option
v-for="option in muteBlockLv1Options"
:key="option.key"
:value="option.value"
>
{{ option.label }}
</option>
</Select>
<ul
v-if="onBlockDefaultActionLv1 === 'temporarily'"
class="setting-list suboptions"
>
<li>
<UnitSetting
path="onBlockDefaultAction"
unit-set="time"
:units="['s', 'm', 'h', 'd']"
:min="0"
>
{{ $t('user_card.default_expiration_time') }}
</UnitSetting>
</li>
</ul>
</li>
<li>
<BooleanSetting path="hideFilteredStatuses">
{{ $t('settings.hide_muted_statuses') }}

View file

@ -10,7 +10,6 @@ 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 { mapState } from 'vuex'
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -59,11 +58,6 @@ const GeneralTab = {
value: mode,
label: this.$t(`settings.unsaved_post_action_${mode}`)
})),
muteBlockLv1Options: ['ask', 'forever', 'temporarily'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`user_card.mute_block_${mode}`)
})),
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
@ -88,40 +82,6 @@ const GeneralTab = {
postFormats () {
return this.$store.state.instance.postFormats || []
},
onMuteDefaultActionLv1: {
get () {
const value = this.$store.state.config.onMuteDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
set (value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
this.$store.dispatch('setOption', { name: 'onMuteDefaultAction', value: realValue })
}
},
onBlockDefaultActionLv1: {
get () {
const value = this.$store.state.config.onBlockDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
set (value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
this.$store.dispatch('setOption', { name: 'onBlockDefaultAction', value: realValue })
}
},
postContentOptions () {
return this.postFormats.map(format => ({
key: format,
@ -137,10 +97,7 @@ const GeneralTab = {
},
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject(),
...mapState({
blockExpirationSupported: state => state.instance.blockExpiration
})
...SharedComputedObject()
},
methods: {
changeDefaultScope (value) {

View file

@ -69,66 +69,6 @@
{{ $t('settings.user_popover_avatar_overlay') }}
</BooleanSetting>
</li>
<li>
{{ $t('user_card.default_mute_expiration') }}
<Select
id="onMuteDefaultActionLv1"
v-model="onMuteDefaultActionLv1"
>
<option
v-for="option in muteBlockLv1Options"
:key="option.key"
:value="option.value"
>
{{ option.label }}
</option>
</Select>
<ul
class="setting-list suboptions"
v-if="onMuteDefaultActionLv1 === 'temporarily'"
>
<li>
<UnitSetting
path="onMuteDefaultAction"
unit-set="time"
:units="['s', 'm', 'h', 'd']"
:min="0"
>
{{ $t('user_card.default_expiration_time') }}
</UnitSetting>
</li>
</ul>
</li>
<li v-if="blockExpirationSupported">
{{ $t('user_card.default_block_expiration') }}
<Select
id="onBlockDefaultActionLv1"
v-model="onBlockDefaultActionLv1"
>
<option
v-for="option in muteBlockLv1Options"
:key="option.key"
:value="option.value"
>
{{ option.label }}
</option>
</Select>
<ul
class="setting-list suboptions"
v-if="onBlockDefaultActionLv1 === 'temporarily'"
>
<li>
<UnitSetting
path="onBlockDefaultAction"
unit-set="time"
:units="['s', 'm', 'h', 'd']"
:min="0"
>
{{ $t('user_card.default_expiration_time') }}
</UnitSetting>
</li>
</ul>
</li>
<li>
<BooleanSetting
path="alwaysShowNewPostButton"
@ -167,7 +107,10 @@
</BooleanSetting>
</li>
<li>
<BooleanSetting v-if="!blockExpirationSupported" path="modalOnBlock">
<BooleanSetting
v-if="!blockExpirationSupported"
path="modalOnBlock"
>
{{ $t('settings.confirm_dialogs_block') }}
</BooleanSetting>
</li>

View file

@ -95,9 +95,9 @@
:user="user"
/>
<UserTimedFilterModal
ref="confirmUser"
:is-mute="true"
:user="user"
ref="confirmUser"
/>
</teleport>
</div>

View file

@ -73,9 +73,11 @@
/* Autoprefixed seem to ignore this one, and also syntax is different */
/* stylelint-disable mask-composite */
/* stylelint-disable declaration-property-value-no-unknown */
/* stylelint-disable scss/declaration-property-value-no-unknown */
/* TODO check if this is still needed */
mask-composite: xor;
/* stylelint-enable scss/declaration-property-value-no-unknown */
/* stylelint-enable declaration-property-value-no-unknown */
/* stylelint-enable mask-composite */
mask-composite: exclude;
@ -156,9 +158,11 @@
/* Autoprefixed seem to ignore this one, and also syntax is different */
/* stylelint-disable mask-composite */
/* stylelint-disable declaration-property-value-no-unknown */
/* stylelint-disable scss/declaration-property-value-no-unknown */
/* TODO check if this is still needed */
mask-composite: xor;
/* stylelint-enable scss/declaration-property-value-no-unknown */
/* stylelint-enable declaration-property-value-no-unknown */
/* stylelint-enable mask-composite */
mask-composite: exclude;

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 RemoteFollow from '../remote_follow/remote_follow.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 RichContent from 'src/components/rich_content/rich_content.jsx'
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 { mapGetters } from 'vuex'
@ -21,7 +23,8 @@ import {
faExternalLinkAlt,
faEdit,
faTimes,
faExpandAlt
faExpandAlt,
faBirthdayCake
} from '@fortawesome/free-solid-svg-icons'
import { useMediaViewerStore } from '../../stores/media_viewer'
@ -34,7 +37,8 @@ library.add(
faExternalLinkAlt,
faEdit,
faTimes,
faExpandAlt
faExpandAlt,
faBirthdayCake
)
export default {
@ -60,7 +64,8 @@ export default {
RichContent,
UserLink,
UserNote,
UserTimedFilterModal
UserTimedFilterModal,
ColorInput
},
data () {
return {
@ -159,16 +164,26 @@ export default {
supportsNote () {
return 'note' in this.relationship
},
muteExpiryAvailable () {
return this.user.mute_expires_at !== undefined
},
muteExpiry () {
return this.user.mute_expires_at == null
? this.$t('user_card.mute_expires_forever')
: this.$t('user_card.mute_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
},
blockExpiryAvailable () {
return this.user.block_expires_at !== undefined
},
blockExpiry () {
return this.user.block_expires_at == null
? this.$t('user_card.block_expires_forever')
: 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'])
},
methods: {

View file

@ -2,45 +2,86 @@
position: relative;
z-index: 1;
.user-card-inner {
padding-bottom: 0;
}
&:hover {
--_still-image-img-visibility: visible;
--_still-image-canvas-visibility: hidden;
--_still-image-label-visibility: hidden;
}
.btn-mute, .btn-mention {
display: block;
width: 100%;
}
.panel-heading {
padding: 0.5em 0;
text-align: center;
box-shadow: none;
background: transparent;
backdrop-filter: none;
flex-direction: column;
align-items: stretch;
// create new stacking context
position: relative;
}
.personal-marks {
margin: 0.6em;
padding: 0.6em;
&:not(:last-child) {
border-bottom: 1px solid var(--border);
}
.highlighter {
h4 {
margin-top: 0.6em;
}
.userHighlightSel {
vertical-align: bottom;
margin-left: -0.1em;
&.-none select {
color: var(--textFaint);
option {
color: var(--text);
background: var(--background);
}
}
}
.highlighter-color {
vertical-align: bottom;
margin-left: 0.6em;
}
}
}
.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;
mask:
linear-gradient(to top, white, transparent) bottom no-repeat,
linear-gradient(to top, white, white);
linear-gradient(to top, white 0%, transparent 20%) bottom no-repeat,
linear-gradient(to top, white 0%, white 100%);
// Autoprefixer seem to ignore this one, and also syntax is different
/* stylelint-disable mask-composite */
/* stylelint-disable declaration-property-value-no-unknown */
/* stylelint-disable scss/declaration-property-value-no-unknown */
/* TODO check if this is still needed */
mask-composite: xor;
/* stylelint-enable scss/declaration-property-value-no-unknown */
/* stylelint-enable declaration-property-value-no-unknown */
/* stylelint-enable mask-composite */
mask-composite: exclude;
background-size: cover;
mask-size: 100% 60%;
mask-size: 100%;
border-top-left-radius: calc(var(--__roundnessTop, --panelRadius) - 1px);
border-top-right-radius: calc(var(--__roundnessTop, --panelRadius) - 1px);
border-bottom-left-radius: calc(var(--__roundnessBottom, --panelRadius) - 1px);
@ -55,10 +96,11 @@
&-bio {
text-align: center;
color: var(--lightText);
display: block;
line-height: 1.3;
padding: 1em;
margin: 0;
padding: 0.6em;
margin: 0 0.6em;
img {
object-fit: contain;
@ -98,14 +140,15 @@
}
.user-info {
padding: 0 26px;
position: relative;
margin: 0.6em;
margin-bottom: 0;
.container {
.user-identity {
min-width: 0;
padding: 16px 0 6px;
display: flex;
align-items: flex-start;
max-height: 56px;
align-items: flex-end;
margin-bottom: 1em;
> * {
min-width: 0;
@ -121,19 +164,42 @@
--_avatarShadowFilter: var(--avatarShadowFilter);
--_avatarShadowInset: var(--avatarShadowInset);
width: 56px;
height: 56px;
width: 7em;
height: 7em;
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 {
position: relative;
cursor: pointer;
margin-right: 0.6em;
&.-overlay {
position: absolute;
inset: 0;
inset: -0.6em;
left: -0.6em;
right: -1.2em;
background-color: rgb(0 0 0 / 30%);
display: flex;
justify-content: center;
@ -152,22 +218,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 {
font-weight: lighter;
font-size: 1.1em;
font-size: 0.9em;
align-items: baseline;
.lock-icon {
@ -182,16 +235,9 @@
overflow: hidden;
}
.dailyAvg {
min-width: 1px;
flex: 0 0 auto;
margin-left: 1em;
font-size: 0.7em;
color: var(--text);
}
.user-role {
flex: none;
align-self: baseline;
}
}
@ -214,9 +260,16 @@
--link: var(--text) !important;
}
.top-line,
.top-line, {
display: flex;
line-height: 2;
font-weight: 600;
}
.bottom-line {
display: flex;
font-weight: 500;
line-height: 1.5;
}
}
@ -228,9 +281,8 @@
font-size: 1.1em;
}
.user-meta {
margin-bottom: 0.15em;
display: flex;
.highlighter {
margin: 5em;
align-items: baseline;
line-height: 22px;
flex-wrap: wrap;
@ -277,23 +329,17 @@
.user-interactions {
position: relative;
display: flex;
flex-flow: row wrap;
margin-right: -0.75em;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(17%, 7em));
grid-gap: 0.6em;
> * {
margin: 0 0.75em 0.6em 0;
.popover-trigger-button, .moderation-tools-button {
width: 100%;
}
> .btn-group, > button {
white-space: nowrap;
min-width: 95px;
}
button {
margin: 0;
}
}
.user-note {
margin: 0 0.75em 0.6em 0;
}
}
@ -301,28 +347,93 @@
display: none;
}
.user-counts {
display: flex;
line-height: 16px;
padding: 0.5em 1.5em 0;
text-align: center;
justify-content: space-between;
flex-wrap: wrap;
.user-extras {
padding: 1.2em 0.6em;
line-height: 1.5;
.user-stats {
display: block;
text-align: center;
width: 100%;
dl {
display: inline;
margin-right: 1em;
white-space: nowrap;
}
dd {
font-weight: bolder;
text-overflow: ellipsis;
overflow: hidden;
margin: 0;
}
dd, dt {
display: inline
}
/* stylelint-disable-next-line no-descending-specificity */
a {
text-decoration: none;
}
}
.birthday {
width: 100%;
text-align: center;
white-space: nowrap;
--icon: var(--text);
}
}
.user-count {
flex: 1 0 auto;
padding: 0.5em 0;
.user-profile-fields {
margin: 0 0.5em;
h5 {
font-size: 1em;
font-weight: bolder;
margin: 0 0 0.25em;
img {
object-fit: contain;
vertical-align: middle;
max-width: 100%;
max-height: 400px;
&.emoji {
width: 18px;
height: 18px;
}
}
/* stylelint-disable-next-line no-descending-specificity */
a {
text-decoration: none;
.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;
}
}
}

View file

@ -3,14 +3,65 @@
class="user-card"
:class="classes"
>
<div
:class="{ 'hide-bio': hideBio }"
:style="style"
class="background-image"
/>
<div :class="onClose ? '' : 'panel-heading -flexible-height'">
<div :class="onClose ? '' : 'panel-heading -flexible-height'" class="user-card-inner">
<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
v-if="avatarAction === 'zoom'"
class="user-info-avatar -link"
@ -32,6 +83,7 @@
/>
<router-link
v-else
class="user-info-avatar"
:to="userProfileLink(user)"
>
<UserAvatar :user="user" />
@ -48,55 +100,6 @@
:emoji="user.emoji"
/>
</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 class="bottom-line">
<user-link
@ -137,71 +140,26 @@
/>
</span>
<span
v-if="relationship.muting"
v-if="relationship.muting && muteExpiryAvailable"
class="alert neutral user-role"
>
{{ muteExpiry }}
</span>
<span
v-if="relationship.blocking"
v-if="relationship.blocking && blockExpiryAvailable"
class="alert neutral user-role"
>
{{ blockExpiry }}
</span>
<span
v-if="!mergedConfig.hideUserStats && !hideBio"
class="dailyAvg"
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</span>
<div
v-if="relationship.followed_by && loggedIn && isOtherUser"
class="alert neutral user-role"
>
{{ $t('user_card.follows_you') }}
</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
v-if="loggedIn && isOtherUser"
class="user-interactions"
@ -241,35 +199,32 @@
</ProgressButton>
</template>
</div>
<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>
</div>
<div>
<button
class="btn button-default btn-mention"
:disabled="user.deactivated"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
</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"
/>
</div>
@ -279,51 +234,132 @@
>
<RemoteFollow :user="user" />
</div>
<UserNote
v-if="loggedIn && isOtherUser && (hasNote || (hasNoteEditor && supportsNote))"
:user="user"
:relationship="relationship"
:editable="hasNoteEditor"
</div>
</div>
<div class="personal-marks" v-if="loggedIn && isOtherUser && (hasNote || !hideBio)">
<UserNote
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
<RichContent
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
v-if="!mergedConfig.hideUserStats && switcher"
class="user-counts"
<dl
v-for="(field, index) in user.fields_html"
:key="index"
class="user-profile-field"
>
<div
<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 class="user-extras" v-if="!hideBio">
<span
v-if="!mergedConfig.hideUserStats"
class="user-stats"
>
<dl
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')"
>
<h5>{{ $t('user_card.statuses') }}</h5>
<span>{{ user.statuses_count }} <br></span>
</div>
<div
<dd>{{ dailyAvg }}</dd>
{{ ' ' }}
<dt>{{ $t('user_card.statuses_per_day') }}</dt>
</dl>
<dl
class="user-count"
@click.prevent="setProfileView('friends')"
>
<h5>{{ $t('user_card.followees') }}</h5>
<span>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</span>
</div>
<div
<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')"
>
<h5>{{ $t('user_card.followers') }}</h5>
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
</div>
<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>
<RichContent
v-if="!hideBio"
class="user-card-bio"
:html="user.description_html"
:emoji="user.emoji"
:handle-links="true"
/>
</div>
<teleport to="#modal">
<UserTimedFilterModal

View file

@ -1,19 +1,24 @@
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
const UserNote = {
props: {
user: Object,
relationship: Object,
editable: Boolean
},
components: {
PanelLoading
},
data () {
return {
localNote: '',
localNote: this.relationship.note,
editing: false,
frozen: false
}
},
computed: {
shouldShow () {
return this.relationship.note || this.editing
watch: {
relationship () {
this.localNote = this.relationship.note
}
},
methods: {
@ -21,9 +26,6 @@ const UserNote = {
this.localNote = this.relationship.note
this.editing = true
},
cancelEditing () {
this.editing = false
},
finalizeEditing () {
this.frozen = true
@ -37,6 +39,7 @@ const UserNote = {
})
.catch(() => {
this.frozen = false
this.editing = false
})
}
}

View file

@ -1,46 +1,23 @@
<template>
<div
class="user-note"
:class="{ '-frozen': frozen }"
>
<div class="heading">
<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>
<h4>{{ $t('user_card.personal_note') }}</h4>
<textarea
v-show="editing"
v-model="localNote"
class="input note-text"
:class="{ unstyled: !editing }"
@focus="startEditing"
@blur="finalizeEditing"
rows="1"
:placeholder="$t('user_card.note_blank_click')"
/>
<span
v-show="!editing"
class="note-text"
:class="{ '-blank': !relationship.note }"
class="overlay"
v-if="frozen"
>
{{ relationship.note || $t('user_card.note_blank') }}
<PanelLoading />
</span>
</div>
</template>
@ -49,38 +26,27 @@
<style lang="scss">
.user-note {
display: flex;
flex-direction: column;
position: relative;
.heading {
display: flex;
flex-direction: row;
h4 {
justify-content: space-between;
align-items: center;
margin-bottom: 0.75em;
.btn {
min-width: 95px;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: right;
.btn {
margin-left: 0.5em;
}
}
margin-bottom: 0.5em;
}
.note-text {
align-self: stretch;
min-width: 100%;
margin: -0.6em;
}
.note-text.-blank {
font-style: italic;
color: var(--textFaint);
.overlay {
position: absolute;
inset: 0;
background-color: rgb(0 0 0 / 30%);
.panel-loading {
font-size: 1em;
}
}
}
</style>

View file

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

View file

@ -26,7 +26,24 @@
<style lang="scss">
/* popover styles load on-demand, so we need to override */
/* 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 */

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 List from '../list/list.vue'
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 {
faCircleNotch,
faBirthdayCake
faCircleNotch
} from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch,
faBirthdayCake
faCircleNotch
)
const FollowerList = withLoadMore({
@ -82,10 +79,6 @@ const UserProfile = {
},
favoritesTabVisible () {
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: {

View file

@ -13,45 +13,6 @@
rounded="top"
: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>
<tab-switcher
:active-tab="tab"
@ -173,58 +134,6 @@
// No sticky header on user profile
--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 {
display: flex;
justify-content: center;

View file

@ -6,4 +6,8 @@
.input-expire-at {
margin-left: 0.25em;
}
.footer-left-checkbox {
width: max-content;
}
}

View file

@ -1,23 +1,23 @@
<template>
<confirm-modal
v-if="showing"
class="UserTimedFilterModal"
:title="$t(isMute ? $t('user_card.mute') : $t('user_card.block'))"
:confirm-text="$t(isMute ? 'user_card.mute_confirm_accept_button' : 'user_card.block_confirm_accept_button')"
:cancel-text="$t(isMute ? 'user_card.mute_confirm_cancel_button' : 'user_card.block_confirm_cancel_button')"
@accepted="accept"
@cancelled="cancel"
>
<p>
{{ $t(isMute ? 'user_card.expire_mute_message' : 'user_card.expire_block_message', [user.screen_name]) }}
</p>
<p>
{{ $t('user_card.expire_in') }}
{{ $t('user_card.expire_in') }}
<input
id="userFilterExpires"
v-model="expiration"
class="input input-expire-in"
:class="{ disabled: forever }"
v-model="expiration"
:disabled="forever"
min="1"
type="number"
@ -28,31 +28,56 @@
class="input unit-input unstyled"
:disabled="forever"
>
<option key="s" value="s"> {{ $t('time.unit.seconds_suffix') }} </option>
<option key="m" value="m"> {{ $t('time.unit.minutes_suffix') }} </option>
<option key="h" value="h"> {{ $t('time.unit.hours_suffix') }} </option>
<option key="d" value="d"> {{ $t('time.unit.days_suffix') }} </option>
<option
key="s"
value="s"
>
{{ $t('time.unit.seconds_suffix') }}
</option>
<option
key="m"
value="m"
>
{{ $t('time.unit.minutes_suffix') }}
</option>
<option
key="h"
value="h"
>
{{ $t('time.unit.hours_suffix') }}
</option>
<option
key="d"
value="d"
>
{{ $t('time.unit.days_suffix') }}
</option>
</Select>
<br />
{{ $t('user_card.mute_or') }}
<Checkbox
id="forever"
v-model="forever"
name="forever"
class="input-forever"
>
{{ $t('user_card.mute_block_forever') }}
{{ $t('user_card.mute_block_never') }}
</Checkbox>
</p>
<Checkbox
id="dontAskAgain"
v-model="dontAskAgain"
name="dontAskAgain"
class="input-dont-ask-again"
>
{{ $t(isMute ? 'user_card.dont_ask_again_mute' : 'user_card.dont_ask_again_block') }}
</Checkbox>
<template #footerLeft>
<div class="footer-left-checkbox">
<Checkbox
id="dontAskAgain"
v-model="dontAskAgain"
name="dontAskAgain"
class="input-dont-ask-again"
>
{{ $t(isMute ? 'user_card.dont_ask_again_mute' : 'user_card.dont_ask_again_block') }}
</Checkbox>
</div>
</template>
</confirm-modal>
</template>

View file

@ -1405,6 +1405,7 @@
"mute_confirm": "Do you really want to mute {user}?",
"mute_confirm_accept_button": "Mute",
"mute_confirm_cancel_button": "Do not mute",
"mute_or": "or",
"expire_in": "Expire in",
"expire_mute_message": "Are you sure you want to mute {0}?",
"expire_block_message": "Are you sure you want to block {0}?",
@ -1412,6 +1413,7 @@
"dont_ask_again_block": "Always block users this way",
"mute_block_temporarily": "Temporarily",
"mute_block_forever": "Forever",
"mute_block_never": "Never",
"mute_block_ask": "Ask",
"default_mute_expiration": "Always mute users",
"default_block_expiration": "Always block users",
@ -1421,7 +1423,7 @@
"block_expires_forever": "Blocked forever",
"block_expires_at": "Blocked until {0}",
"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",
"remove_follower": "Remove follower",
"remove_follower_confirm_title": "Remove follower confirmation",
@ -1462,17 +1464,15 @@
"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?"
},
"highlight": {
"disabled": "No highlight",
"solid": "Solid bg",
"striped": "Striped bg",
"highlight_new": {
"disabled": "Don't highlight",
"solid": "Solid background",
"striped": "Striped background",
"side": "Side stripe"
},
"note": "Note",
"note_blank": "(None)",
"edit_note": "Edit note",
"edit_note_apply": "Apply",
"edit_note_cancel": "Cancel"
"personal_note": "Personal note",
"note_blank_click": "Click to add note",
"highlight_header": "Highlight user's posts and mentions"
},
"user_profile": {
"timeline_title": "User timeline",

View file

@ -53,7 +53,6 @@ const blockUser = (store, args) => {
return store.rootState.api.backendInteractor.blockUser({ id, expiresIn })
.then((relationship) => {
console.log(relationship)
store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id)