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, "custom-property-pattern": null,
"keyframes-name-pattern": null, "keyframes-name-pattern": null,
"scss/operator-no-newline-after": 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": [ "declaration-block-no-redundant-longhand-properties": [
true, 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; height: unset;
} }
&::placeholder {
color: var(--textFaint)
}
--_padding: 0.5em; --_padding: 0.5em;
border: none; border: none;
@ -1075,7 +1079,7 @@ option {
scale: 1.0063 0.9938; scale: 1.0063 0.9938;
translate: 0 -10%; translate: 0 -10%;
transform: rotateZ(var(--defaultZ)); transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-in-ou; animation-timing-function: ease-in-out;
} }
90% { 90% {

View file

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

View file

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

View file

@ -14,8 +14,10 @@ const BlockCard = {
blocked () { blocked () {
return this.relationship.blocking return this.relationship.blocking
}, },
blockExpiryAvailable () {
return this.user.block_expires_at !== undefined
},
blockExpiry () { blockExpiry () {
console.log(this.user)
return this.user.block_expires_at == null return this.user.block_expires_at == null
? 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()])
@ -29,7 +31,6 @@ const BlockCard = {
}, },
methods: { methods: {
unblockUser () { unblockUser () {
this.progress = true
this.$store.dispatch('unblockUser', this.user.id) this.$store.dispatch('unblockUser', this.user.id)
}, },
blockUser () { blockUser () {

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { mapState, mapActions } from 'pinia' import { mapState, mapActions } from 'pinia'
import { mapState as mapVuexState } from 'vuex'
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { useServerSideStorageStore } from 'src/stores/serverSideStorage' import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
@ -30,6 +31,11 @@ const FilteringTab = {
value: mode, value: mode,
label: this.$t(`settings.reply_visibility_${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), muteFiltersDraftObject: cloneDeep(useServerSideStorageStore().prefsStorage.simple.muteFilters),
muteFiltersDraftDirty: Object.fromEntries( muteFiltersDraftDirty: Object.fromEntries(
Object.entries( Object.entries(
@ -93,6 +99,43 @@ const FilteringTab = {
muteFiltersObject: store => store.prefsStorage.simple.muteFilters 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 () { muteFiltersDraft () {
return Object.entries(this.muteFiltersDraftObject) return Object.entries(this.muteFiltersDraftObject)
}, },

View file

@ -80,6 +80,66 @@
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.filter.mute_filter') }}</h2> <h2>{{ $t('settings.filter.mute_filter') }}</h2>
<ul class="setting-list"> <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> <li>
<BooleanSetting path="hideFilteredStatuses"> <BooleanSetting path="hideFilteredStatuses">
{{ $t('settings.hide_muted_statuses') }} {{ $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 SharedComputedObject from '../helpers/shared_computed_object.js'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue' import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { mapState } from 'vuex'
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js' import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -59,11 +58,6 @@ const GeneralTab = {
value: mode, value: mode,
label: this.$t(`settings.unsaved_post_action_${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: loopSilentAvailable:
// Firefox // Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
@ -88,40 +82,6 @@ const GeneralTab = {
postFormats () { postFormats () {
return this.$store.state.instance.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 () { postContentOptions () {
return this.postFormats.map(format => ({ return this.postFormats.map(format => ({
key: format, key: format,
@ -137,10 +97,7 @@ const GeneralTab = {
}, },
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject(), ...SharedComputedObject()
...mapState({
blockExpirationSupported: state => state.instance.blockExpiration
})
}, },
methods: { methods: {
changeDefaultScope (value) { changeDefaultScope (value) {

View file

@ -69,66 +69,6 @@
{{ $t('settings.user_popover_avatar_overlay') }} {{ $t('settings.user_popover_avatar_overlay') }}
</BooleanSetting> </BooleanSetting>
</li> </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> <li>
<BooleanSetting <BooleanSetting
path="alwaysShowNewPostButton" path="alwaysShowNewPostButton"
@ -167,7 +107,10 @@
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<BooleanSetting v-if="!blockExpirationSupported" path="modalOnBlock"> <BooleanSetting
v-if="!blockExpirationSupported"
path="modalOnBlock"
>
{{ $t('settings.confirm_dialogs_block') }} {{ $t('settings.confirm_dialogs_block') }}
</BooleanSetting> </BooleanSetting>
</li> </li>

View file

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

View file

@ -73,9 +73,11 @@
/* Autoprefixed seem to ignore this one, and also syntax is different */ /* Autoprefixed 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 */
/* stylelint-disable scss/declaration-property-value-no-unknown */
/* TODO check if this is still needed */ /* TODO check if this is still needed */
mask-composite: xor; mask-composite: xor;
/* stylelint-enable scss/declaration-property-value-no-unknown */
/* stylelint-enable declaration-property-value-no-unknown */ /* stylelint-enable declaration-property-value-no-unknown */
/* stylelint-enable mask-composite */ /* stylelint-enable mask-composite */
mask-composite: exclude; mask-composite: exclude;
@ -156,9 +158,11 @@
/* Autoprefixed seem to ignore this one, and also syntax is different */ /* Autoprefixed 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 */
/* stylelint-disable scss/declaration-property-value-no-unknown */
/* TODO check if this is still needed */ /* TODO check if this is still needed */
mask-composite: xor; mask-composite: xor;
/* stylelint-enable scss/declaration-property-value-no-unknown */
/* stylelint-enable declaration-property-value-no-unknown */ /* stylelint-enable declaration-property-value-no-unknown */
/* stylelint-enable mask-composite */ /* stylelint-enable mask-composite */
mask-composite: exclude; 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 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 {
@ -159,16 +164,26 @@ export default {
supportsNote () { supportsNote () {
return 'note' in this.relationship return 'note' in this.relationship
}, },
muteExpiryAvailable () {
return this.user.mute_expires_at !== undefined
},
muteExpiry () { muteExpiry () {
return this.user.mute_expires_at == null return this.user.mute_expires_at == null
? this.$t('user_card.mute_expires_forever') ? this.$t('user_card.mute_expires_forever')
: this.$t('user_card.mute_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()]) : this.$t('user_card.mute_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
}, },
blockExpiryAvailable () {
return this.user.block_expires_at !== undefined
},
blockExpiry () { blockExpiry () {
return this.user.block_expires_at == null return this.user.block_expires_at == null
? 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,45 +2,86 @@
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;
} }
.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 { .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 */
/* stylelint-disable scss/declaration-property-value-no-unknown */
/* TODO check if this is still needed */ /* TODO check if this is still needed */
mask-composite: xor; mask-composite: xor;
/* stylelint-enable scss/declaration-property-value-no-unknown */
/* stylelint-enable declaration-property-value-no-unknown */ /* stylelint-enable declaration-property-value-no-unknown */
/* 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);
@ -55,10 +96,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;
@ -98,14 +140,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;
@ -121,19 +164,42 @@
--_avatarShadowFilter: var(--avatarShadowFilter); --_avatarShadowFilter: var(--avatarShadowFilter);
--_avatarShadowInset: var(--avatarShadowInset); --_avatarShadowInset: var(--avatarShadowInset);
width: 56px; width: 7em;
height: 56px; height: 7em;
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;
@ -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 { .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 {
@ -182,16 +235,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;
} }
} }
@ -214,9 +260,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;
} }
} }
@ -228,9 +281,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;
@ -277,23 +329,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;
} }
} }
@ -301,28 +347,93 @@
display: none; display: none;
} }
.user-counts { .user-extras {
display: flex; padding: 1.2em 0.6em;
line-height: 16px; line-height: 1.5;
padding: 0.5em 1.5em 0;
text-align: center; .user-stats {
justify-content: space-between; display: block;
flex-wrap: wrap; 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 { .user-profile-fields {
flex: 1 0 auto;
padding: 0.5em 0;
margin: 0 0.5em; margin: 0 0.5em;
h5 { img {
font-size: 1em; object-fit: contain;
font-weight: bolder; vertical-align: middle;
margin: 0 0 0.25em; max-width: 100%;
max-height: 400px;
&.emoji {
width: 18px;
height: 18px;
}
} }
/* stylelint-disable-next-line no-descending-specificity */ .user-profile-field {
a { display: flex;
text-decoration: none; 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="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"
@ -32,6 +83,7 @@
/> />
<router-link <router-link
v-else v-else
class="user-info-avatar"
:to="userProfileLink(user)" :to="userProfileLink(user)"
> >
<UserAvatar :user="user" /> <UserAvatar :user="user" />
@ -48,55 +100,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
@ -137,71 +140,26 @@
/> />
</span> </span>
<span <span
v-if="relationship.muting" v-if="relationship.muting && muteExpiryAvailable"
class="alert neutral user-role" class="alert neutral user-role"
> >
{{ muteExpiry }} {{ muteExpiry }}
</span> </span>
<span <span
v-if="relationship.blocking" v-if="relationship.blocking && blockExpiryAvailable"
class="alert neutral user-role" class="alert neutral user-role"
> >
{{ 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 +199,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 +234,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
: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" class="user-count"
@click.prevent="setProfileView('statuses')" @click.prevent="setProfileView('statuses')"
> >
<h5>{{ $t('user_card.statuses') }}</h5> <dd>{{ dailyAvg }}</dd>
<span>{{ user.statuses_count }} <br></span> {{ ' ' }}
</div> <dt>{{ $t('user_card.statuses_per_day') }}</dt>
<div </dl>
<dl
class="user-count" class="user-count"
@click.prevent="setProfileView('friends')" @click.prevent="setProfileView('friends')"
> >
<h5>{{ $t('user_card.followees') }}</h5> <dd>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</dd>
<span>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</span> {{ ' ' }}
</div> <dt>{{ $t('user_card.followees') }}</dt>
<div </dl>
<dl
class="user-count" class="user-count"
@click.prevent="setProfileView('followers')" @click.prevent="setProfileView('followers')"
> >
<h5>{{ $t('user_card.followers') }}</h5> <dd>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</dd>
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span> {{ ' ' }}
</div> <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>
<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,19 +1,24 @@
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: this.relationship.note,
editing: false, editing: false,
frozen: false frozen: false
} }
}, },
computed: { watch: {
shouldShow () { relationship () {
return this.relationship.note || this.editing this.localNote = this.relationship.note
} }
}, },
methods: { methods: {
@ -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
@ -37,6 +39,7 @@ const UserNote = {
}) })
.catch(() => { .catch(() => {
this.frozen = false this.frozen = false
this.editing = false
}) })
} }
} }

View file

@ -1,46 +1,23 @@
<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-model="localNote" v-model="localNote"
class="input note-text" class="input note-text"
:class="{ unstyled: !editing }"
@focus="startEditing"
@blur="finalizeEditing"
rows="1"
:placeholder="$t('user_card.note_blank_click')"
/> />
<span <span
v-show="!editing" class="overlay"
class="note-text" v-if="frozen"
:class="{ '-blank': !relationship.note }"
> >
{{ relationship.note || $t('user_card.note_blank') }} <PanelLoading />
</span> </span>
</div> </div>
</template> </template>
@ -49,38 +26,27 @@
<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 {
align-self: stretch; align-self: stretch;
min-width: 100%;
margin: -0.6em;
} }
.note-text.-blank { .overlay {
font-style: italic; position: absolute;
color: var(--textFaint); inset: 0;
background-color: rgb(0 0 0 / 30%);
.panel-loading {
font-size: 1em;
}
} }
} }
</style> </style>

View file

@ -28,8 +28,26 @@
backdrop-filter: var(--backdrop-filter); 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 { .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

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

View file

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

View file

@ -1405,6 +1405,7 @@
"mute_confirm": "Do you really want to mute {user}?", "mute_confirm": "Do you really want to mute {user}?",
"mute_confirm_accept_button": "Mute", "mute_confirm_accept_button": "Mute",
"mute_confirm_cancel_button": "Do not mute", "mute_confirm_cancel_button": "Do not mute",
"mute_or": "or",
"expire_in": "Expire in", "expire_in": "Expire in",
"expire_mute_message": "Are you sure you want to mute {0}?", "expire_mute_message": "Are you sure you want to mute {0}?",
"expire_block_message": "Are you sure you want to block {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", "dont_ask_again_block": "Always block users this way",
"mute_block_temporarily": "Temporarily", "mute_block_temporarily": "Temporarily",
"mute_block_forever": "Forever", "mute_block_forever": "Forever",
"mute_block_never": "Never",
"mute_block_ask": "Ask", "mute_block_ask": "Ask",
"default_mute_expiration": "Always mute users", "default_mute_expiration": "Always mute users",
"default_block_expiration": "Always block users", "default_block_expiration": "Always block users",
@ -1421,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",
@ -1462,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",

View file

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