Merge branch 'develop' into user_management
This commit is contained in:
commit
71cf3a52f2
115 changed files with 3080 additions and 2422 deletions
8
.gitlab/merge_request_templates/Release.md
Normal file
8
.gitlab/merge_request_templates/Release.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
### Release checklist
|
||||
* [ ] Bump version in `package.json`
|
||||
* [ ] Compile a changelog with the `tools/collect-changelog` script
|
||||
* [ ] Create an MR with an announcement to pleroma.social
|
||||
#### post-merge
|
||||
* [ ] Tag the release on the merge commit
|
||||
* [ ] Make the tag into a Gitlab Release™
|
||||
* [ ] Merge `master` into `develop` (in case the fixes are already in develop, use `git merge -s ours --no-commit` and manually merge the changelogs)
|
||||
36
CHANGELOG.md
36
CHANGELOG.md
|
|
@ -2,6 +2,42 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## 2.9.2
|
||||
### Changed
|
||||
- BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible
|
||||
- User card/profile got an overhaul
|
||||
- Profile editing overhaul
|
||||
- Visually combined subject and content fields in post form
|
||||
- Moved post form's emoji button into input field
|
||||
- Minor visual changes and fixes
|
||||
- Clicking on fav/rt/emoji notifications' contents expands/collapses it
|
||||
- Reduced time taken processing theme by half
|
||||
- Splash screen only appears if loading takes more than 2 seconds
|
||||
|
||||
### Added
|
||||
- Mutes received an update, adding support for regex, muting based on username and expiration time.
|
||||
- Mutes are now synchronized across sessions
|
||||
- Support for expiring mutes and blocks (if available)
|
||||
- Clicking on emoji shows bigger version of it alongside with its shortcode
|
||||
- Admins also are able to copy it into a local pack
|
||||
- Added support for Akkoma and IceShrimp.NET backends
|
||||
- Compatibility with stricter CSP (Akkoma backend)
|
||||
- Added a way to upload new packs from a URL or ZIP file via the Admin Dashboard
|
||||
- Unify show/hide content buttons
|
||||
- Add support for detachable scrollTop button
|
||||
- Option to left-align user bio
|
||||
- Cache assets and emojis with service worker
|
||||
- Indicate currently active V3 theme as a body element class
|
||||
- Add arithmetic blend ISS function
|
||||
|
||||
### Fixed
|
||||
- Display counter for status action buttons when they are in the menu
|
||||
- Fix bookmark button alignment in the extra actions menu
|
||||
- Instance favicons are no longer stretched
|
||||
- A lot more scalable UI fixes
|
||||
- Emoji picker now should work fine when emoji size is increased
|
||||
|
||||
## 2.8.0
|
||||
### Changed
|
||||
- BREAKING: static/img/nsfw.2958239.png is now static/img/nsfw.DepQPhG0.png, which may affect people who specify exactly this path as the cover image
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Display counter for status action buttons when they are on the menu
|
||||
|
|
@ -1 +0,0 @@
|
|||
Added support for Akkoma and IceShrimp.NET backend
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
Add arithmetic blend ISS function
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
Add support for detachable scrollTop button
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix bookmark button alignment in the extra actions menu
|
||||
|
|
@ -1 +0,0 @@
|
|||
Compatibility with stricter CSP (Akkoma backend)
|
||||
|
|
@ -1 +0,0 @@
|
|||
Synchronized mutes, advanced mute control (regexp, expiry, naming)
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix error styling for user profiles
|
||||
|
|
@ -1 +0,0 @@
|
|||
Cache assets and emojis with service worker
|
||||
|
|
@ -1 +0,0 @@
|
|||
Indicate currently active V3 theme as a body element class
|
||||
|
|
@ -1 +0,0 @@
|
|||
Support for expiring mutes and blocks (if available)
|
||||
|
|
@ -1 +0,0 @@
|
|||
Unify show/hide content buttons
|
||||
|
|
@ -1 +0,0 @@
|
|||
User card/profile got an overhaul
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<noscript>To use Pleroma, please enable JavaScript.</noscript>
|
||||
<div id="splash">
|
||||
<div id="splash" class="initial-hidden">
|
||||
<!-- we are hiding entire graphic so no point showing credit -->
|
||||
<div aria-hidden="true" id="splash-credit">
|
||||
Art by pipivovott
|
||||
|
|
|
|||
50
package.json
50
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "2.7.1",
|
||||
"version": "2.9.2",
|
||||
"description": "Pleroma frontend, the default frontend of Pleroma social network server",
|
||||
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
|
||||
"private": false,
|
||||
|
|
@ -17,29 +17,29 @@
|
|||
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.27.1",
|
||||
"@babel/runtime": "7.28.3",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "3.0.8",
|
||||
"@fortawesome/vue-fontawesome": "3.1.1",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.3.0",
|
||||
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.1.13",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22",
|
||||
"@vuelidate/core": "2.0.3",
|
||||
"@vuelidate/validators": "2.0.4",
|
||||
"@web3-storage/parse-link-header": "^3.1.0",
|
||||
"body-scroll-lock": "3.1.5",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "2.0.0",
|
||||
"cropperjs": "2.0.1",
|
||||
"escape-html": "1.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "2.0.0",
|
||||
"phoenix": "1.7.21",
|
||||
"phoenix": "1.8.0",
|
||||
"pinia": "^3.0.0",
|
||||
"punycode.js": "2.3.1",
|
||||
"qrcode": "1.5.4",
|
||||
|
|
@ -47,53 +47,53 @@
|
|||
"url": "0.11.4",
|
||||
"utf8": "3.0.0",
|
||||
"uuid": "11.1.0",
|
||||
"vue": "3.5.17",
|
||||
"vue": "3.5.19",
|
||||
"vue-i18n": "11",
|
||||
"vue-router": "4.5.1",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.7",
|
||||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.27.1",
|
||||
"@babel/eslint-parser": "7.27.1",
|
||||
"@babel/plugin-transform-runtime": "7.27.1",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/register": "7.27.1",
|
||||
"@babel/core": "7.28.3",
|
||||
"@babel/eslint-parser": "7.28.0",
|
||||
"@babel/plugin-transform-runtime": "7.28.3",
|
||||
"@babel/preset-env": "7.28.3",
|
||||
"@babel/register": "7.28.3",
|
||||
"@ungap/event-target": "0.2.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vitest/browser": "^3.0.7",
|
||||
"@vitest/ui": "^3.0.7",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||
"@vue/babel-plugin-jsx": "1.4.0",
|
||||
"@vue/compiler-sfc": "3.5.17",
|
||||
"@vue/babel-plugin-jsx": "1.5.0",
|
||||
"@vue/compiler-sfc": "3.5.19",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"autoprefixer": "10.4.21",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "5.2.0",
|
||||
"chalk": "5.4.1",
|
||||
"chai": "5.3.2",
|
||||
"chalk": "5.6.0",
|
||||
"chromedriver": "135.0.4",
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"eslint": "9.26.0",
|
||||
"vue-eslint-parser": "10.1.3",
|
||||
"eslint": "9.33.0",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-formatter-friendly": "7.0.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-n": "17.18.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-n": "17.21.3",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"eslint-plugin-vue": "10.1.0",
|
||||
"eslint-plugin-vue": "10.4.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "5.1.0",
|
||||
"function-bind": "1.1.2",
|
||||
"http-proxy-middleware": "3.0.5",
|
||||
"iso-639-1": "3.1.5",
|
||||
"lodash": "4.17.21",
|
||||
"msw": "2.10.2",
|
||||
"nightwatch": "3.12.1",
|
||||
"playwright": "1.52.0",
|
||||
"postcss": "8.5.3",
|
||||
"msw": "2.10.5",
|
||||
"nightwatch": "3.12.2",
|
||||
"playwright": "1.55.0",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"sass": "1.89.2",
|
||||
|
|
|
|||
|
|
@ -5,15 +5,14 @@ body {
|
|||
|
||||
#splash {
|
||||
--scale: 1;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
place-items: center;
|
||||
flex-direction: column;
|
||||
background: #0f161e;
|
||||
font-family: sans-serif;
|
||||
|
|
@ -21,13 +20,20 @@ body {
|
|||
position: absolute;
|
||||
z-index: 9999;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
opacity: 1;
|
||||
transition: opacity 500ms ease-out 2s;
|
||||
}
|
||||
|
||||
#splash.hidden,
|
||||
#splash.initial-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#splash-credit {
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
font-size: 1em;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
|
|
|
|||
|
|
@ -214,6 +214,9 @@ export default {
|
|||
splashscreenRoot.addEventListener('transitionend', () => {
|
||||
splashscreenRoot.remove()
|
||||
})
|
||||
setTimeout(() => {
|
||||
splashscreenRoot.remove() // forcibly remove it, should fix my plasma browser widget t. HJ
|
||||
}, 600)
|
||||
splashscreenRoot.classList.add('hidden')
|
||||
document.querySelector('#app').classList.remove('hidden')
|
||||
}
|
||||
|
|
|
|||
37
src/App.scss
37
src/App.scss
|
|
@ -21,7 +21,7 @@
|
|||
}
|
||||
|
||||
html {
|
||||
font-size: var(--textSize, 14px);
|
||||
font-size: var(--textSize, 1rem);
|
||||
|
||||
--navbar-height: var(--navbarSize, 3.5rem);
|
||||
--emoji-size: var(--emojiSize, 32px);
|
||||
|
|
@ -382,6 +382,10 @@ nav {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--font);
|
||||
|
||||
&.-transparent {
|
||||
backdrop-filter: blur(0.125em) contrast(60%);
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -741,17 +745,15 @@ option {
|
|||
}
|
||||
|
||||
&.-dot {
|
||||
min-height: 8px;
|
||||
max-height: 8px;
|
||||
min-width: 8px;
|
||||
max-width: 8px;
|
||||
padding: 0;
|
||||
min-height: 0.6em;
|
||||
max-height: 0.6em;
|
||||
min-width: 0.6em;
|
||||
max-width: 0.6em;
|
||||
left: calc(50% + 0.5em);
|
||||
top: calc(50% - 1em);
|
||||
line-height: 0;
|
||||
font-size: 0;
|
||||
left: calc(50% - 4px);
|
||||
top: calc(50% - 4px);
|
||||
margin-left: 6px;
|
||||
margin-top: -6px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.-counter {
|
||||
|
|
@ -782,12 +784,6 @@ option {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.visibility-notice {
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--textFaint);
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
.notice-dismissible {
|
||||
padding-right: 4rem;
|
||||
position: relative;
|
||||
|
|
@ -935,12 +931,7 @@ option {
|
|||
|
||||
#splash {
|
||||
pointer-events: none;
|
||||
transition: opacity 0.5s;
|
||||
opacity: 1;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
// transition: opacity 0.5s;
|
||||
|
||||
#status {
|
||||
&.css-ok {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ const AccountActions = {
|
|||
hideConfirmRemoveUserFromFollowers () {
|
||||
this.showingConfirmRemoveFollower = false
|
||||
},
|
||||
hideConfirmBlock () {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showRepeats () {
|
||||
this.$store.dispatch('showReblogs', this.user.id)
|
||||
},
|
||||
|
|
@ -56,7 +59,7 @@ const AccountActions = {
|
|||
}
|
||||
},
|
||||
doBlockUser () {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
this.hideConfirmBlock()
|
||||
},
|
||||
unblockUser () {
|
||||
|
|
|
|||
|
|
@ -107,9 +107,9 @@
|
|||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
font-size: 4.5em;
|
||||
top: calc(50% - 2.25rem);
|
||||
left: calc(50% - 2.25rem);
|
||||
color: rgb(255 255 255 / 75%);
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 40%);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
export default {
|
||||
name: 'Attachment',
|
||||
selector: '.Attachment',
|
||||
notEditable: true,
|
||||
validInnerComponents: [
|
||||
'Border',
|
||||
'Button',
|
||||
'Input'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
roundness: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Button',
|
||||
parent: {
|
||||
component: 'Attachment'
|
||||
},
|
||||
directives: {
|
||||
background: '#FFFFFF',
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
<FAIcon icon="trash-alt" />
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
>
|
||||
<button
|
||||
v-if="type === 'flash' && flashLoaded"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.attachment_stop_flash')"
|
||||
@click.prevent="stopFlash"
|
||||
>
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
>
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="!useModal && type !== 'unknown'"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.show_attachment_in_modal')"
|
||||
@click.prevent="openModalForce"
|
||||
>
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="nsfw && hideNsfwLocal"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.hide_attachment')"
|
||||
@click.prevent="toggleHidden"
|
||||
>
|
||||
|
|
@ -113,7 +113,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftUp"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.move_up')"
|
||||
@click.prevent="onShiftUp"
|
||||
>
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftDn"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.move_down')"
|
||||
@click.prevent="onShiftDn"
|
||||
>
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.remove_attachment')"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const BlockCard = {
|
|||
if (this.blockExpirationSupported) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
} else {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default {
|
|||
// normal: '' // normal state is implicitly added, it is always included
|
||||
toggled: '.toggled',
|
||||
focused: ':focus-within',
|
||||
pressed: ':focus:active',
|
||||
pressed: ':active',
|
||||
hover: ':is(:hover, :focus-visible):not(:disabled)',
|
||||
disabled: ':disabled'
|
||||
},
|
||||
|
|
@ -18,7 +18,8 @@ export default {
|
|||
variants: {
|
||||
// Variants save on computation time since adding new variant just adds one more "set".
|
||||
// normal: '', // you can override normal variant, it will be appenended to the main class
|
||||
danger: '.danger'
|
||||
danger: '.-danger',
|
||||
transparent: '.-transparent'
|
||||
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
|
||||
// This (currently) is further multipled by number of places where component can exist.
|
||||
},
|
||||
|
|
@ -51,6 +52,38 @@ export default {
|
|||
roundness: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'danger',
|
||||
directives: {
|
||||
background: '--cRed'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'transparent',
|
||||
directives: {
|
||||
opacity: 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
variant: 'transparent'
|
||||
},
|
||||
directives: {
|
||||
textColor: '--text'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
variant: 'transparent'
|
||||
},
|
||||
directives: {
|
||||
textColor: '--text'
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
|
|
@ -96,6 +129,13 @@ export default {
|
|||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'hover', 'focused'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'disabled'],
|
||||
directives: {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ export default {
|
|||
'Text',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'RichContent',
|
||||
'Attachment',
|
||||
'PollGraph'
|
||||
],
|
||||
defaultRules: [
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
:title="'@'+(user && user.screen_name_ui)"
|
||||
:html="htmlTitle"
|
||||
:emoji="user.emoji || []"
|
||||
:is-local="user.is_local"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@
|
|||
inset: 0;
|
||||
justify-content: center;
|
||||
place-items: center center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dialog-modal.panel {
|
||||
max-height: 80vh;
|
||||
max-width: 90vw;
|
||||
z-index: 2001;
|
||||
cursor: default;
|
||||
|
|
|
|||
|
|
@ -205,12 +205,6 @@ const EmojiInput = {
|
|||
return emoji.displayText
|
||||
}
|
||||
},
|
||||
onInputScroll () {
|
||||
this.$refs.hiddenOverlay.scrollTo({
|
||||
top: this.input.scrollTop,
|
||||
left: this.input.scrollLeft
|
||||
})
|
||||
},
|
||||
suggestionListId () {
|
||||
return `suggestions-${this.randomSeed}`
|
||||
},
|
||||
|
|
@ -239,7 +233,6 @@ const EmojiInput = {
|
|||
this.overlayStyle.fontSize = style.fontSize
|
||||
this.overlayStyle.wordWrap = style.wordWrap
|
||||
this.overlayStyle.whiteSpace = style.whiteSpace
|
||||
this.resize()
|
||||
input.addEventListener('blur', this.onBlur)
|
||||
input.addEventListener('focus', this.onFocus)
|
||||
input.addEventListener('paste', this.onPaste)
|
||||
|
|
@ -302,6 +295,13 @@ const EmojiInput = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
onInputScroll (e) {
|
||||
this.$refs.hiddenOverlay.scrollTo({
|
||||
top: this.input.scrollTop,
|
||||
left: this.input.scrollLeft
|
||||
})
|
||||
this.setCaret(e)
|
||||
},
|
||||
triggerShowPicker () {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.picker.showPicker()
|
||||
|
|
@ -561,8 +561,6 @@ const EmojiInput = {
|
|||
this.$refs.suggestorPopover.updateStyles()
|
||||
})
|
||||
},
|
||||
resize () {
|
||||
},
|
||||
autoCompleteItemLabel (suggestion) {
|
||||
if (suggestion.user) {
|
||||
return suggestion.displayText + ' ' + suggestion.detailText
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
margin: 0.2em 0.25em;
|
||||
font-size: 1.3em;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
line-height: 1.2em;
|
||||
|
||||
&:hover i {
|
||||
color: var(--text);
|
||||
|
|
@ -133,7 +133,7 @@
|
|||
.emoji-picker-panel {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
margin-top: 2px;
|
||||
margin-top: 0.2em;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
|
|
@ -152,7 +152,7 @@
|
|||
}
|
||||
|
||||
&.with-picker input {
|
||||
padding-right: 30px;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
.hidden-overlay {
|
||||
|
|
@ -215,8 +215,8 @@
|
|||
}
|
||||
|
||||
.detailText {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-size: 0.6em;
|
||||
line-height: 0.6em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,13 +140,13 @@ const EmojiPicker = {
|
|||
},
|
||||
updateEmojiSize () {
|
||||
const css = window.getComputedStyle(this.$refs.popover.$el)
|
||||
const fontSize = css.getPropertyValue('font-size') || '14px'
|
||||
const fontSize = css.getPropertyValue('font-size') || '1rem'
|
||||
const emojiSize = css.getPropertyValue('--emojiSize') || '2.2rem'
|
||||
|
||||
const fontSizeUnit = fontSize.replace(/[0-9,.]+/, '')
|
||||
const fontSizeUnit = fontSize.replace(/[0-9,.]+/, '').trim()
|
||||
const fontSizeValue = Number(fontSize.replace(/[^0-9,.]+/, ''))
|
||||
|
||||
const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '')
|
||||
const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '').trim()
|
||||
const emojiSizeValue = Number(emojiSize.replace(/[^0-9,.]+/, ''))
|
||||
|
||||
let fontSizeMultiplier
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@
|
|||
|
||||
.gallery-item {
|
||||
margin: 0;
|
||||
height: 200px;
|
||||
height: 15em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,46 +10,26 @@ library.add(
|
|||
|
||||
const ImageCropper = {
|
||||
props: {
|
||||
trigger: {
|
||||
type: [String, window.Element],
|
||||
required: true
|
||||
},
|
||||
submitHandler: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
// Mime-types to accept, i.e. which filetypes to accept (.gif, .png, etc.)
|
||||
mimes: {
|
||||
type: String,
|
||||
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
|
||||
},
|
||||
saveButtonLabel: {
|
||||
type: String
|
||||
},
|
||||
saveWithoutCroppingButtonlabel: {
|
||||
type: String
|
||||
},
|
||||
cancelButtonLabel: {
|
||||
type: String
|
||||
// Fixed aspect-ratio for selection box
|
||||
aspectRatio: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dataUrl: undefined,
|
||||
filename: undefined,
|
||||
submitting: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
saveText () {
|
||||
return this.saveButtonLabel || this.$t('image_cropper.save')
|
||||
},
|
||||
saveWithoutCroppingText () {
|
||||
return this.saveWithoutCroppingButtonlabel || this.$t('image_cropper.save_without_cropping')
|
||||
},
|
||||
cancelText () {
|
||||
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
|
||||
filename: undefined
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'submit', // cropping complete or uncropped image returned
|
||||
'close', // cropper is closed
|
||||
],
|
||||
methods: {
|
||||
destroy () {
|
||||
this.$refs.input.value = ''
|
||||
|
|
@ -57,28 +37,20 @@ const ImageCropper = {
|
|||
this.$emit('close')
|
||||
},
|
||||
submit (cropping = true) {
|
||||
this.submitting = true
|
||||
|
||||
let cropperPromise
|
||||
if (cropping) {
|
||||
cropperPromise = this.$refs.cropperSelection.$toCanvas()
|
||||
} else {
|
||||
cropperPromise = Promise.resolve()
|
||||
}
|
||||
|
||||
cropperPromise.then(canvas => {
|
||||
this.submitHandler(canvas, this.file)
|
||||
.then(() => this.destroy())
|
||||
.finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
this.$emit('submit', { canvas, file: this.file })
|
||||
})
|
||||
},
|
||||
pickImage () {
|
||||
this.$refs.input.click()
|
||||
},
|
||||
getTriggerDOM () {
|
||||
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
|
||||
},
|
||||
readFile () {
|
||||
const fileInput = this.$refs.input
|
||||
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||
|
|
@ -117,23 +89,11 @@ const ImageCropper = {
|
|||
}
|
||||
},
|
||||
mounted () {
|
||||
// listen for click event on trigger
|
||||
const trigger = this.getTriggerDOM()
|
||||
if (!trigger) {
|
||||
this.$emit('error', 'No image make trigger found.', 'user')
|
||||
} else {
|
||||
trigger.addEventListener('click', this.pickImage)
|
||||
}
|
||||
// listen for input file changes
|
||||
const fileInput = this.$refs.input
|
||||
fileInput.addEventListener('change', this.readFile)
|
||||
},
|
||||
beforeUnmount: function () {
|
||||
// remove the event listeners
|
||||
const trigger = this.getTriggerDOM()
|
||||
if (trigger) {
|
||||
trigger.removeEventListener('click', this.pickImage)
|
||||
}
|
||||
const fileInput = this.$refs.input
|
||||
fileInput.removeEventListener('change', this.readFile)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
<template>
|
||||
<div class="image-cropper">
|
||||
<div v-if="dataUrl">
|
||||
<div class="image">
|
||||
<cropper-canvas
|
||||
ref="cropperCanvas"
|
||||
background
|
||||
class="image-cropper-canvas"
|
||||
height="25em"
|
||||
height="100%"
|
||||
>
|
||||
<cropper-image
|
||||
v-if="dataUrl"
|
||||
ref="cropperImage"
|
||||
:src="dataUrl"
|
||||
alt="Picture"
|
||||
|
|
@ -22,8 +23,8 @@
|
|||
/>
|
||||
<cropper-selection
|
||||
ref="cropperSelection"
|
||||
initial-coverage="1"
|
||||
aspect-ratio="1"
|
||||
initial-coverage="0.9"
|
||||
:aspect-ratio="aspectRatio"
|
||||
movable
|
||||
resizable
|
||||
@change="onCropperSelectionChange"
|
||||
|
|
@ -47,34 +48,6 @@
|
|||
<cropper-handle action="sw-resize" />
|
||||
</cropper-selection>
|
||||
</cropper-canvas>
|
||||
<div class="image-cropper-buttons-wrapper">
|
||||
<button
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
:disabled="submitting"
|
||||
@click="submit()"
|
||||
v-text="saveText"
|
||||
/>
|
||||
<button
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
:disabled="submitting"
|
||||
@click="destroy"
|
||||
v-text="cancelText"
|
||||
/>
|
||||
<button
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
:disabled="submitting"
|
||||
@click="submit(false)"
|
||||
v-text="saveWithoutCroppingText"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="submitting"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref="input"
|
||||
|
|
@ -89,13 +62,15 @@
|
|||
|
||||
<style lang="scss">
|
||||
.image-cropper {
|
||||
&-img-input {
|
||||
display: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-canvas, .image {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-canvas {
|
||||
height: 25em;
|
||||
width: 25em;
|
||||
& &-img-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-buttons-wrapper {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import localeService from '../../services/locale/locale.service.js'
|
||||
|
||||
import Select from '../select/select.vue'
|
||||
import ProfileSettingIndicator from 'src/components/settings_modal/helpers/profile_setting_indicator.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Select,
|
||||
ProfileSettingIndicator
|
||||
},
|
||||
props: {
|
||||
// List of languages (or just one language)
|
||||
modelValue: {
|
||||
type: [Array, String],
|
||||
required: true
|
||||
},
|
||||
// Is this setting stored in user profile (true) or elsewhere (false)
|
||||
// Doesn't affect storage, just shows an icon if true
|
||||
profile: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
languages () {
|
||||
return localeService.languages
|
||||
},
|
||||
|
||||
controlledLanguage: {
|
||||
get: function () {
|
||||
return Array.isArray(this.modelValue) ? this.modelValue : [this.modelValue]
|
||||
},
|
||||
set: function (val) {
|
||||
this.$emit('update:modelValue', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLanguageName (code) {
|
||||
return localeService.getLanguageName(code)
|
||||
},
|
||||
addLanguage () {
|
||||
this.controlledLanguage = [...this.controlledLanguage, '']
|
||||
},
|
||||
setLanguageAt (index, val) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang[index] = val
|
||||
this.controlledLanguage = lang
|
||||
},
|
||||
removeLanguageAt (index) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang.splice(index, 1)
|
||||
this.controlledLanguage = lang
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<div class="interface-language-switcher">
|
||||
<label>
|
||||
{{ promptText }}
|
||||
<slot />
|
||||
<ProfileSettingIndicator :is-profile="profile" />
|
||||
</label>
|
||||
<ul class="setting-list">
|
||||
<li
|
||||
|
|
@ -44,64 +45,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
import Select from '../select/select.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Select
|
||||
},
|
||||
props: {
|
||||
promptText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
language: {
|
||||
type: [Array, String],
|
||||
required: true
|
||||
},
|
||||
setLanguage: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
languages () {
|
||||
return localeService.languages
|
||||
},
|
||||
|
||||
controlledLanguage: {
|
||||
get: function () {
|
||||
return Array.isArray(this.language) ? this.language : [this.language]
|
||||
},
|
||||
set: function (val) {
|
||||
this.setLanguage(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLanguageName (code) {
|
||||
return localeService.getLanguageName(code)
|
||||
},
|
||||
addLanguage () {
|
||||
this.controlledLanguage = [...this.controlledLanguage, '']
|
||||
},
|
||||
setLanguageAt (index, val) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang[index] = val
|
||||
this.controlledLanguage = lang
|
||||
},
|
||||
removeLanguageAt (index) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang.splice(index, 1)
|
||||
this.controlledLanguage = lang
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="./interface_language_switcher.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.interface-language-switcher {
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
export default {
|
||||
name: 'ListItem',
|
||||
selector: '.list-item',
|
||||
states: {
|
||||
active: '.-active',
|
||||
hover: ':is(:hover, :focus-visible, :has(:focus-visible)):not(.-non-interactive)'
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'RichContent',
|
||||
'Input',
|
||||
'Avatar'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--bg',
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['active'],
|
||||
directives: {
|
||||
background: '--inheritedBackground, 10',
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
background: '--inheritedBackground, 10',
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['hover', 'active'],
|
||||
directives: {
|
||||
background: '--inheritedBackground, 20',
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -168,7 +168,7 @@ $modal-view-button-icon-margin: 0.5em;
|
|||
flex: 0 0 auto;
|
||||
overflow-y: auto;
|
||||
min-height: 1em;
|
||||
max-width: 500px;
|
||||
max-width: 35.8em;
|
||||
max-height: 9.5em;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
& .new,
|
||||
& .original {
|
||||
display: inline;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.mention-avatar {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@ export default {
|
|||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Input',
|
||||
'Border',
|
||||
'ButtonUnstyled',
|
||||
'Badge',
|
||||
'Avatar'
|
||||
'Border'
|
||||
],
|
||||
states: {
|
||||
hover: ':is(:hover, :focus-visible, :has(:focus-visible)):not(.disabled)',
|
||||
|
|
|
|||
|
|
@ -2,18 +2,7 @@ export default {
|
|||
name: 'MobileDrawer',
|
||||
selector: '.mobile-drawer',
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'Input',
|
||||
'PanelHeader',
|
||||
'MenuItem',
|
||||
'Notification',
|
||||
'Alert',
|
||||
'UserCard'
|
||||
'MenuItem'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
|
|
@ -21,21 +10,6 @@ export default {
|
|||
background: '--bg',
|
||||
backgroundNoCssColor: 'yes'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'PanelHeader',
|
||||
parent: { component: 'MobileDrawer' },
|
||||
directives: {
|
||||
background: '--fg',
|
||||
shadow: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
padding-bottom: 20px;
|
||||
padding-left: 0.5em;
|
||||
padding-bottom: 1.1em;
|
||||
|
||||
th,
|
||||
td {
|
||||
width: 180px;
|
||||
max-width: 360px;
|
||||
width: 11em;
|
||||
max-width: 25em;
|
||||
overflow: hidden;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ library.add(
|
|||
const Notification = {
|
||||
data () {
|
||||
return {
|
||||
selecting: false,
|
||||
statusExpanded: false,
|
||||
unmuted: false,
|
||||
showingApproveConfirmDialog: false,
|
||||
|
|
@ -62,10 +63,35 @@ const Notification = {
|
|||
UserLink,
|
||||
ConfirmModal
|
||||
},
|
||||
mounted () {
|
||||
document.addEventListener('selectionchange', this.onContentSelect)
|
||||
},
|
||||
unmounted () {
|
||||
document.removeEventListener('selectionchange', this.onContentSelect)
|
||||
},
|
||||
methods: {
|
||||
toggleStatusExpanded () {
|
||||
if (!this.expandable) return
|
||||
this.statusExpanded = !this.statusExpanded
|
||||
},
|
||||
onContentSelect () {
|
||||
const { isCollapsed, anchorNode, offsetNode } = document.getSelection()
|
||||
if (isCollapsed) {
|
||||
this.selecting = false
|
||||
return
|
||||
}
|
||||
const within = this.$refs.root.contains(anchorNode) || this.$refs.root.contains(offsetNode)
|
||||
if (within) {
|
||||
this.selecting = true
|
||||
} else {
|
||||
this.selecting = false
|
||||
}
|
||||
},
|
||||
onContentClick (e) {
|
||||
if (!this.selecting && !e.target.closest('a') && !e.target.closest('button')) {
|
||||
this.toggleStatusExpanded()
|
||||
}
|
||||
},
|
||||
generateUserProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
|
|
@ -136,6 +162,9 @@ const Notification = {
|
|||
const user = this.notification.from_profile
|
||||
return highlightStyle(highlight[user.screen_name])
|
||||
},
|
||||
expandable () {
|
||||
return (new Set(['like', 'pleroma:emoji_reaction', 'repeat'])).has(this.notification.type)
|
||||
},
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.notification.from_profile.id)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
// TODO Copypaste from Status, should unify it somehow
|
||||
|
||||
.Notification {
|
||||
border-bottom: 1px solid;
|
||||
border-color: var(--border);
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
|
||||
.status-content {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.Status {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background-color: transparent !important;
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@ export default {
|
|||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'RichContent',
|
||||
'Input',
|
||||
'Avatar',
|
||||
'Attachment',
|
||||
'PollGraph'
|
||||
],
|
||||
defaultRules: []
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<article
|
||||
v-if="notification.type === 'mention' || notification.type === 'status'"
|
||||
ref="root"
|
||||
>
|
||||
<Status
|
||||
class="Notification"
|
||||
|
|
@ -9,9 +10,17 @@
|
|||
@interacted="interacted"
|
||||
/>
|
||||
</article>
|
||||
<article v-else>
|
||||
<article
|
||||
v-else
|
||||
ref="root"
|
||||
class="NotificationParent"
|
||||
:class="{ '-expandable': expandable }"
|
||||
>
|
||||
<div
|
||||
v-if="needMute && !unmuted"
|
||||
:id="'notif-' +notification.id"
|
||||
:aria-expanded="statusExpanded"
|
||||
:aria-controls="'notif-' +notification.id"
|
||||
class="Notification container -muted"
|
||||
>
|
||||
<small>
|
||||
|
|
@ -62,6 +71,7 @@
|
|||
:title="'@'+notification.from_profile.screen_name_ui"
|
||||
:html="notification.from_profile.name_html"
|
||||
:emoji="notification.from_profile.emoji"
|
||||
:is-local="notification.from_profile.is_local"
|
||||
/>
|
||||
</bdi>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
|
|
@ -245,8 +255,11 @@
|
|||
/>
|
||||
<template v-else>
|
||||
<StatusContent
|
||||
class="status-content"
|
||||
:compact="!statusExpanded"
|
||||
:status="notification.status"
|
||||
:collapse="!statusExpanded"
|
||||
@click="onContentClick"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,29 +6,17 @@ export default {
|
|||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'Input',
|
||||
'PanelHeader',
|
||||
'MenuItem',
|
||||
'Post',
|
||||
'Notification',
|
||||
'Alert',
|
||||
'UserCard',
|
||||
'Chat',
|
||||
'Attachment',
|
||||
'Tab',
|
||||
'ListItem'
|
||||
'MenuItem'
|
||||
],
|
||||
validInnerComponentsLite: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'Input',
|
||||
'PanelHeader',
|
||||
'Alert'
|
||||
'PanelHeader'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ export default {
|
|||
'Icon',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'Badge',
|
||||
'Alert',
|
||||
'Avatar'
|
||||
'Alert'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,16 +6,7 @@ export default {
|
|||
modal: '.modal'
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'Input',
|
||||
'MenuItem',
|
||||
'Post',
|
||||
'UserCard'
|
||||
'MenuItem'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -726,10 +726,6 @@ const PostStatusForm = {
|
|||
scrollerRef.scrollTop = targetScroll
|
||||
}
|
||||
},
|
||||
showEmojiPicker () {
|
||||
this.$refs.textarea.focus()
|
||||
this.$refs['emoji-input'].triggerShowPicker()
|
||||
},
|
||||
clearError () {
|
||||
this.error = null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,10 +41,12 @@
|
|||
|
||||
.form-bottom-left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding-right: 7px;
|
||||
margin-right: 7px;
|
||||
max-width: 10em;
|
||||
gap: 1.5em;
|
||||
|
||||
button {
|
||||
padding: 0.5em;
|
||||
margin: -0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-heading {
|
||||
|
|
@ -102,10 +104,16 @@
|
|||
.visibility-tray {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: 5px;
|
||||
padding-top: 0.5em;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.visibility-notice {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--roundness);
|
||||
padding: 0.5em 1em
|
||||
}
|
||||
|
||||
.visibility-notice.edit-warning {
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
|
|
@ -193,6 +201,10 @@
|
|||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.inputs-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input.form-post-body {
|
||||
// TODO: make a resizable textarea component?
|
||||
box-sizing: content-box; // needed for easier computation of dynamic size
|
||||
|
|
@ -200,6 +212,7 @@
|
|||
transition: min-height 200ms 100ms;
|
||||
// stock padding + 1 line of text (for counter)
|
||||
padding-bottom: calc(var(--_padding) + var(--post-line-height) * 1em);
|
||||
padding-right: 1.5em;
|
||||
// two lines of text
|
||||
height: calc(var(--post-line-height) * 1em);
|
||||
min-height: calc(var(--post-line-height) * 1em);
|
||||
|
|
@ -216,6 +229,10 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.subject-input {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.character-counter {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
|
|
|||
|
|
@ -156,67 +156,68 @@
|
|||
class="preview-status"
|
||||
/>
|
||||
</div>
|
||||
<EmojiInput
|
||||
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
|
||||
v-model="newStatus.spoilerText"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
class="input form-control"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<input
|
||||
v-model="newStatus.spoilerText"
|
||||
type="text"
|
||||
:placeholder="$t('post_status.content_warning')"
|
||||
:disabled="posting && !optimisticPosting"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
size="1"
|
||||
class="input form-post-subject"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
<EmojiInput
|
||||
ref="emoji-input"
|
||||
v-model="newStatus.status"
|
||||
:suggest="emojiUserSuggestor"
|
||||
:placement="emojiPickerPlacement"
|
||||
class="input form-control main-input"
|
||||
enable-sticker-picker
|
||||
enable-emoji-picker
|
||||
hide-emoji-button
|
||||
:newline-on-ctrl-enter="submitOnEnter"
|
||||
@input="onEmojiInputInput"
|
||||
@sticker-uploaded="addMediaFile"
|
||||
@sticker-upload-failed="uploadFailed"
|
||||
@shown="handleEmojiInputShow"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<textarea
|
||||
ref="textarea"
|
||||
v-model="newStatus.status"
|
||||
:placeholder="placeholder || $t('post_status.default')"
|
||||
rows="1"
|
||||
cols="1"
|
||||
:disabled="posting && !optimisticPosting"
|
||||
class="input form-post-body"
|
||||
:class="{ 'scrollable-form': !!maxHeight }"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
||||
@keydown.meta.enter="postStatus($event, newStatus)"
|
||||
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
|
||||
@input="resize"
|
||||
@compositionupdate="resize"
|
||||
@paste="paste"
|
||||
/>
|
||||
<p
|
||||
v-if="hasStatusLengthLimit"
|
||||
class="character-counter faint"
|
||||
:class="{ error: isOverLengthLimit }"
|
||||
>
|
||||
{{ charactersLeft }}
|
||||
</p>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
<div class="input inputs-wrapper">
|
||||
<EmojiInput
|
||||
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
|
||||
v-model="newStatus.spoilerText"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
class="input form-control subject-input unstyled"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<input
|
||||
v-model="newStatus.spoilerText"
|
||||
type="text"
|
||||
:placeholder="$t('post_status.content_warning')"
|
||||
:disabled="posting && !optimisticPosting"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
size="1"
|
||||
class="input form-post-subject unstyled"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
<EmojiInput
|
||||
ref="emoji-input"
|
||||
v-model="newStatus.status"
|
||||
:suggest="emojiUserSuggestor"
|
||||
:placement="emojiPickerPlacement"
|
||||
class="input form-control main-input unstyled"
|
||||
enable-sticker-picker
|
||||
enable-emoji-picker
|
||||
:newline-on-ctrl-enter="submitOnEnter"
|
||||
@input="onEmojiInputInput"
|
||||
@sticker-uploaded="addMediaFile"
|
||||
@sticker-upload-failed="uploadFailed"
|
||||
@shown="handleEmojiInputShow"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<textarea
|
||||
ref="textarea"
|
||||
v-model="newStatus.status"
|
||||
:placeholder="placeholder || $t('post_status.default')"
|
||||
rows="1"
|
||||
cols="1"
|
||||
:disabled="posting && !optimisticPosting"
|
||||
class="input form-post-body"
|
||||
:class="{ 'scrollable-form': !!maxHeight }"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
||||
@keydown.meta.enter="postStatus($event, newStatus)"
|
||||
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
|
||||
@input="resize"
|
||||
@compositionupdate="resize"
|
||||
@paste="paste"
|
||||
/>
|
||||
<p
|
||||
v-if="hasStatusLengthLimit"
|
||||
class="character-counter faint"
|
||||
:class="{ error: isOverLengthLimit }"
|
||||
>
|
||||
{{ charactersLeft }}
|
||||
</p>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
</div>
|
||||
<div
|
||||
v-if="!disableScopeSelector"
|
||||
class="visibility-tray"
|
||||
|
|
@ -236,8 +237,9 @@
|
|||
>
|
||||
<Select
|
||||
v-model="newStatus.contentType"
|
||||
class="input form-control"
|
||||
class="input form-control unstyled"
|
||||
:attrs="{ 'aria-label': $t('post_status.content_type_selection') }"
|
||||
unstyled="true"
|
||||
>
|
||||
<option
|
||||
v-for="postFormat in postFormats"
|
||||
|
|
@ -285,13 +287,6 @@
|
|||
@upload-failed="uploadFailed"
|
||||
@all-uploaded="finishedUploadingFiles"
|
||||
/>
|
||||
<button
|
||||
class="emoji-icon button-unstyled"
|
||||
:title="$t('emoji.add_emoji')"
|
||||
@click="showEmojiPicker"
|
||||
>
|
||||
<FAIcon icon="smile-beam" />
|
||||
</button>
|
||||
<button
|
||||
v-if="pollsAvailable"
|
||||
class="poll-icon button-unstyled"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { unescape, flattenDeep } from 'lodash'
|
|||
import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
|
||||
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
|
||||
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
|
||||
import StillImage from 'src/components/still-image/still-image.vue'
|
||||
import StillImageEmojiPopover from 'src/components/still-image/still-image-emoji-popover.vue'
|
||||
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
||||
import { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.js'
|
||||
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
|
||||
|
|
@ -86,6 +86,24 @@ export default {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Collapse newlines
|
||||
collapse: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/* Content comes from current instance
|
||||
*
|
||||
* This is used for emoji stealing popover.
|
||||
* By default we assume it is, so that steal
|
||||
* emoji button isn't shown where it probably
|
||||
* should not be.
|
||||
*/
|
||||
isLocal: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
// NEVER EVER TOUCH DATA INSIDE RENDER
|
||||
|
|
@ -162,11 +180,14 @@ export default {
|
|||
item,
|
||||
this.emoji,
|
||||
({ shortcode, url }) => {
|
||||
return <StillImage
|
||||
return <StillImageEmojiPopover
|
||||
class="emoji img"
|
||||
src={url}
|
||||
title={`:${shortcode}:`}
|
||||
alt={`:${shortcode}:`}
|
||||
|
||||
shortcode={shortcode}
|
||||
isLocal={this.isLocal}
|
||||
/>
|
||||
}
|
||||
)]
|
||||
|
|
@ -281,11 +302,20 @@ export default {
|
|||
|
||||
const pass1 = convertHtmlToTree(html).map(processItem)
|
||||
const pass2 = [...pass1].reverse().map(processItemReverse).reverse()
|
||||
|
||||
// DO NOT USE SLOTS they cause a re-render feedback loop here.
|
||||
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
|
||||
// at least until vue3?
|
||||
const result = <span class={['RichContent', this.faint ? '-faint' : '']}>
|
||||
{ pass2 }
|
||||
const result =
|
||||
<span class={['RichContent', this.faint ? '-faint' : '']}>
|
||||
{
|
||||
this.collapse
|
||||
? pass2.map(x => {
|
||||
if (!Array.isArray(x)) return x.replace(/\n/g, ' ')
|
||||
return x.map(y => y.type === 'br' ? ' ' : y)
|
||||
})
|
||||
: pass2
|
||||
}
|
||||
</span>
|
||||
|
||||
const event = {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
font-family: var(--font);
|
||||
|
||||
&.-faint {
|
||||
color: var(--text);
|
||||
/* stylelint-disable declaration-no-important */
|
||||
--text: var(--textFaint) !important;
|
||||
--link: var(--linkFaint) !important;
|
||||
|
|
@ -63,6 +64,11 @@
|
|||
|
||||
.img {
|
||||
display: inline-block;
|
||||
|
||||
// fix vertical alignment of stealable emoji
|
||||
button {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
export default {
|
||||
name: 'RichContent',
|
||||
selector: '.RichContent',
|
||||
notEditable: true,
|
||||
transparent: true,
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'FunText',
|
||||
'Link'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
'--font': 'generic | inherit',
|
||||
'--monoFont': 'generic | monospace',
|
||||
textNoCssColor: 'yes'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -32,7 +32,10 @@ const EmojiTab = {
|
|||
newPackName: '',
|
||||
deleteModalVisible: false,
|
||||
remotePackInstance: '',
|
||||
remotePackDownloadAs: ''
|
||||
remotePackDownloadAs: '',
|
||||
|
||||
remotePackURL: '',
|
||||
remotePackFile: null
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -142,9 +145,9 @@ const EmojiTab = {
|
|||
})
|
||||
},
|
||||
|
||||
updatePackFiles (newFiles) {
|
||||
this.pack.files = newFiles
|
||||
this.sortPackFiles(this.packName)
|
||||
updatePackFiles (newFiles, packName) {
|
||||
this.knownPacks[packName].files = newFiles
|
||||
this.sortPackFiles(packName)
|
||||
},
|
||||
|
||||
loadPacksPaginated (listFunction) {
|
||||
|
|
@ -220,7 +223,7 @@ const EmojiTab = {
|
|||
.then(data => data.json())
|
||||
.then(resp => {
|
||||
if (resp === 'ok') {
|
||||
this.$refs.dlPackPopover.hidePopover()
|
||||
this.$refs.downloadPackPopover.hidePopover()
|
||||
|
||||
return this.refreshPackList()
|
||||
} else {
|
||||
|
|
@ -232,6 +235,47 @@ const EmojiTab = {
|
|||
this.remotePackDownloadAs = ''
|
||||
})
|
||||
},
|
||||
downloadRemoteURLPack () {
|
||||
this.$store.state.api.backendInteractor.downloadRemoteEmojiPackZIP({
|
||||
url: this.remotePackURL, packName: this.newPackName
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(resp => {
|
||||
if (resp === 'ok') {
|
||||
this.$refs.additionalRemotePopover.hidePopover()
|
||||
|
||||
return this.refreshPackList()
|
||||
} else {
|
||||
this.displayError(resp.error)
|
||||
return Promise.reject(resp)
|
||||
}
|
||||
}).then(() => {
|
||||
this.packName = this.newPackName
|
||||
this.newPackName = ''
|
||||
this.remotePackURL = ''
|
||||
})
|
||||
},
|
||||
downloadRemoteFilePack () {
|
||||
this.$store.state.api.backendInteractor.downloadRemoteEmojiPackZIP({
|
||||
file: this.remotePackFile[0], packName: this.newPackName
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(resp => {
|
||||
if (resp === 'ok') {
|
||||
this.$refs.additionalRemotePopover.hidePopover()
|
||||
|
||||
return this.refreshPackList()
|
||||
} else {
|
||||
this.displayError(resp.error)
|
||||
return Promise.reject(resp)
|
||||
}
|
||||
}).then(() => {
|
||||
this.packName = this.newPackName
|
||||
this.newPackName = ''
|
||||
this.remotePackURL = ''
|
||||
})
|
||||
},
|
||||
|
||||
displayError (msg) {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'admin_dash.emoji.error',
|
||||
|
|
|
|||
|
|
@ -62,6 +62,64 @@
|
|||
</template>
|
||||
</Popover>
|
||||
</button>
|
||||
<button
|
||||
class="button button-default emoji-panel-additional-actions"
|
||||
@click="$refs.additionalRemotePopover.showPopover"
|
||||
>
|
||||
<FAIcon
|
||||
icon="chevron-down"
|
||||
/>
|
||||
|
||||
<Popover
|
||||
ref="additionalRemotePopover"
|
||||
popover-class="emoji-tab-edit-popover popover-default"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
bound-to-selector=".emoji-tab"
|
||||
:bound-to="{ x: 'container' }"
|
||||
:offset="{ y: 5 }"
|
||||
>
|
||||
<template #content>
|
||||
<div class="emoji-tab-popover-input">
|
||||
<h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3>
|
||||
<input
|
||||
v-model="newPackName"
|
||||
:placeholder="$t('admin_dash.emoji.new_pack_name')"
|
||||
class="input"
|
||||
>
|
||||
<h3>Import pack from URL</h3>
|
||||
<input
|
||||
v-model="remotePackURL"
|
||||
class="input"
|
||||
placeholder="Pack .zip URL"
|
||||
>
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
:disabled="newPackName.trim() === '' || remotePackURL.trim() === ''"
|
||||
@click="downloadRemoteURLPack"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<h3>Import pack from a file</h3>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/zip"
|
||||
class="emoji-tab-popover-file input"
|
||||
@change="remotePackFile = $event.target.files"
|
||||
>
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
:disabled="newPackName.trim() === '' || remotePackFile === null || remotePackFile.length === 0"
|
||||
@click="downloadRemoteFilePack"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<h3>{{ $t('admin_dash.emoji.emoji_packs') }}</h3>
|
||||
|
|
@ -240,12 +298,12 @@
|
|||
v-if="pack.remote !== undefined"
|
||||
class="button button-default btn"
|
||||
type="button"
|
||||
@click="$refs.dlPackPopover.showPopover"
|
||||
@click="$refs.downloadPackPopover.showPopover"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.download_pack') }}
|
||||
|
||||
<Popover
|
||||
ref="dlPackPopover"
|
||||
ref="downloadPackPopover"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
bound-to-selector=".emoji-tab"
|
||||
|
|
@ -329,11 +387,12 @@
|
|||
ref="emojiPopovers"
|
||||
:key="shortcode"
|
||||
placement="top"
|
||||
:title="$t('admin_dash.emoji.editing', [shortcode])"
|
||||
:disabled="pack.remote !== undefined"
|
||||
:title="$t(`admin_dash.emoji.${pack.remote === undefined ? 'editing' : 'copying'}`, [shortcode])"
|
||||
:shortcode="shortcode"
|
||||
:file="file"
|
||||
:pack-name="packName"
|
||||
:remote="pack.remote"
|
||||
:known-local-packs="knownLocalPacks"
|
||||
@update-pack-files="updatePackFiles"
|
||||
@display-error="displayError"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
popover-class="emoji-tab-edit-popover popover-default"
|
||||
:bound-to="{ x: 'container' }"
|
||||
:offset="{ y: 5 }"
|
||||
:disabled="disabled"
|
||||
:class="{'emoji-unsaved': isEdited}"
|
||||
>
|
||||
<template #trigger>
|
||||
|
|
@ -30,15 +29,27 @@
|
|||
|
||||
<div
|
||||
v-if="newUpload"
|
||||
class="emoji-tab-popover-input"
|
||||
class="emoji-tab-popover-new-upload"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="emoji-tab-popover-file input"
|
||||
@change="uploadFile = $event.target.files"
|
||||
>
|
||||
<h4>{{ $t('admin_dash.emoji.emoji_source') }}</h4>
|
||||
|
||||
<div class="emoji-tab-popover-input">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="emoji-tab-popover-file input"
|
||||
@change="uploadFile = $event.target.files"
|
||||
>
|
||||
</div>
|
||||
<div class="emoji-tab-popover-input ">
|
||||
<input
|
||||
v-model="uploadURL"
|
||||
:placeholder="$t('admin_dash.emoji.upload_url')"
|
||||
class="emoji-data-input input"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="emoji-tab-popover-input">
|
||||
<label>
|
||||
|
|
@ -63,16 +74,50 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="remote !== undefined"
|
||||
class="emoji-tab-popover-input"
|
||||
>
|
||||
<label>
|
||||
{{ $t('admin_dash.emoji.copy_to') }}
|
||||
|
||||
<SelectComponent
|
||||
v-model="copyToPack"
|
||||
class="form-control"
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
hidden
|
||||
>
|
||||
{{ $t('admin_dash.emoji.emoji_pack') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(pack, listPackName) in knownLocalPacks"
|
||||
:key="listPackName"
|
||||
:label="listPackName"
|
||||
>
|
||||
{{ listPackName }}
|
||||
</option>
|
||||
</SelectComponent>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
For local emojis, disable the button if nothing was edited.
|
||||
For remote emojis, also disable it if a local pack is not selected.
|
||||
Remote emojis are processed by the same function that uploads new ones, as that is effectively what it does
|
||||
-->
|
||||
<button
|
||||
class="button button-default btn"
|
||||
type="button"
|
||||
:disabled="newUpload ? uploadFile.length == 0 : !isEdited"
|
||||
@click="newUpload ? uploadEmoji() : saveEditedEmoji()"
|
||||
:disabled="saveButtonDisabled"
|
||||
@click="(newUpload || remote !== undefined) ? uploadEmoji() : saveEditedEmoji()"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.save') }}
|
||||
</button>
|
||||
|
||||
<template v-if="!newUpload">
|
||||
<template v-if="!newUpload && remote === undefined">
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
|
|
@ -107,19 +152,16 @@
|
|||
import Popover from 'components/popover/popover.vue'
|
||||
import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
|
||||
import StillImage from 'components/still-image/still-image.vue'
|
||||
import SelectComponent from 'components/select/select.vue'
|
||||
|
||||
export default {
|
||||
components: { Popover, ConfirmModal, StillImage },
|
||||
components: { Popover, ConfirmModal, StillImage, SelectComponent },
|
||||
inject: ['emojiAddr'],
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
newUpload: Boolean,
|
||||
|
||||
|
|
@ -140,21 +182,35 @@ export default {
|
|||
type: String,
|
||||
// Only exists when this is not a new upload
|
||||
default: ''
|
||||
},
|
||||
|
||||
// Only exists for emojis from remote packs
|
||||
remote: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
knownLocalPacks: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
emits: ['updatePackFiles', 'displayError'],
|
||||
data () {
|
||||
return {
|
||||
uploadFile: [],
|
||||
uploadURL: '',
|
||||
editedShortcode: this.shortcode,
|
||||
editedFile: this.file,
|
||||
deleteModalVisible: false
|
||||
deleteModalVisible: false,
|
||||
copyToPack: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
emojiPreview () {
|
||||
if (this.newUpload && this.uploadFile.length > 0) {
|
||||
return URL.createObjectURL(this.uploadFile[0])
|
||||
} else if (this.newUpload && this.uploadURL !== '') {
|
||||
return this.uploadURL
|
||||
} else if (!this.newUpload) {
|
||||
return this.emojiAddr(this.file)
|
||||
}
|
||||
|
|
@ -163,6 +219,12 @@ export default {
|
|||
},
|
||||
isEdited () {
|
||||
return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file)
|
||||
},
|
||||
saveButtonDisabled() {
|
||||
if (this.remote === undefined)
|
||||
return this.newUpload ? (this.uploadURL === "" && this.uploadFile.length == 0) : !this.isEdited
|
||||
else
|
||||
return this.copyToPack === ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -181,9 +243,12 @@ export default {
|
|||
}).then(resp => this.$emit('updatePackFiles', resp))
|
||||
},
|
||||
uploadEmoji () {
|
||||
let packName = this.remote === undefined ? this.packName : this.copyToPack
|
||||
this.$store.state.api.backendInteractor.addNewEmojiFile({
|
||||
packName: this.packName,
|
||||
file: this.uploadFile[0],
|
||||
packName: packName,
|
||||
file: this.remote === undefined
|
||||
? (this.uploadURL !== "" ? this.uploadURL : this.uploadFile[0])
|
||||
: this.emojiAddr(this.file),
|
||||
shortcode: this.editedShortcode,
|
||||
filename: this.editedFile
|
||||
}).then(resp => resp.json()).then(resp => {
|
||||
|
|
@ -192,7 +257,7 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
this.$emit('updatePackFiles', resp)
|
||||
this.$emit('updatePackFiles', resp, packName)
|
||||
this.$refs.emojiPopover.hidePopover()
|
||||
|
||||
this.editedFile = ''
|
||||
|
|
@ -215,7 +280,7 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
this.$emit('updatePackFiles', resp)
|
||||
this.$emit('updatePackFiles', resp, this.packName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -228,9 +293,22 @@ export default {
|
|||
padding-right: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
|
||||
.emoji-tab-popover-new-upload {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 2.3em;
|
||||
height: 2.3em;
|
||||
}
|
||||
|
||||
.Select {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from 'src/services/theme_data/css_utils.js'
|
||||
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
||||
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
|
||||
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||
|
|
@ -72,7 +73,10 @@ const AppearanceTab = {
|
|||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
|
||||
}))
|
||||
})),
|
||||
backgroundUploading: false,
|
||||
background: null,
|
||||
backgroundPreview: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -187,6 +191,9 @@ const AppearanceTab = {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
isDefaultBackground () {
|
||||
return !(this.$store.state.users.currentUser.background_image)
|
||||
},
|
||||
switchInProgress () {
|
||||
return useInterfaceStore().themeChangeInProgress
|
||||
},
|
||||
|
|
@ -420,7 +427,55 @@ const AppearanceTab = {
|
|||
].join(''))
|
||||
sheet.ready = true
|
||||
adoptStyleSheets()
|
||||
}
|
||||
},
|
||||
uploadFile (slot, e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) { return }
|
||||
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'upload.error.message',
|
||||
messageArgs: [
|
||||
this.$t('upload.error.file_too_big', {
|
||||
filesize: filesize.num,
|
||||
filesizeunit: filesize.unit,
|
||||
allowedsize: allowedsize.num,
|
||||
allowedsizeunit: allowedsize.unit
|
||||
})
|
||||
],
|
||||
level: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
const img = target.result
|
||||
this[slot + 'Preview'] = img
|
||||
this[slot] = file
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
resetBackground () {
|
||||
const confirmed = window.confirm(this.$t('settings.reset_background_confirm'))
|
||||
if (confirmed) {
|
||||
this.submitBackground('')
|
||||
}
|
||||
},
|
||||
submitBackground (background) {
|
||||
if (!this.backgroundPreview && background !== '') { return }
|
||||
|
||||
this.backgroundUploading = true
|
||||
this.$store.state.api.backendInteractor.updateProfileImages({ background })
|
||||
.then((data) => {
|
||||
this.$store.commit('addNewUsers', [data])
|
||||
this.$store.commit('setCurrentUser', data)
|
||||
this.backgroundPreview = null
|
||||
})
|
||||
.catch(this.displayUploadError)
|
||||
.finally(() => { this.backgroundUploading = false })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,46 @@
|
|||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
padding: 0.25em;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.banner-background-preview {
|
||||
max-width: 100%;
|
||||
width: 300px;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
position: absolute;
|
||||
top: 0.2em;
|
||||
right: 0.2em;
|
||||
border-radius: var(--roundness);
|
||||
background-color: rgb(0 0 0 / 60%);
|
||||
opacity: 0.7;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
text-align: center;
|
||||
line-height: 1.5em;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.palettes-container {
|
||||
height: 15em;
|
||||
overflow: hidden auto;
|
||||
|
|
|
|||
|
|
@ -151,6 +151,49 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.background') }}</h2>
|
||||
<div class="banner-background-preview">
|
||||
<img :src="user.background_image">
|
||||
<button
|
||||
v-if="!isDefaultBackground"
|
||||
class="button-unstyled reset-button"
|
||||
:title="$t('settings.reset_profile_background')"
|
||||
@click="resetBackground"
|
||||
>
|
||||
<FAIcon
|
||||
icon="times"
|
||||
type="button"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p>{{ $t('settings.set_new_background') }}</p>
|
||||
<img
|
||||
v-if="backgroundPreview"
|
||||
class="banner-background-preview"
|
||||
:src="backgroundPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
class="input"
|
||||
@change="uploadFile('background', $event)"
|
||||
>
|
||||
</div>
|
||||
<FAIcon
|
||||
v-if="backgroundUploading"
|
||||
class="uploading"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
<button
|
||||
v-else-if="backgroundPreview"
|
||||
class="btn button-default"
|
||||
@click="submitBackground(background)"
|
||||
>
|
||||
{{ $t('settings.save') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.scale_and_layout') }}</h2>
|
||||
<div class="alert neutral theme-notice">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||
|
|
@ -6,10 +8,11 @@ import FloatSetting from '../helpers/float_setting.vue'
|
|||
import UnitSetting from '../helpers/unit_setting.vue'
|
||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||
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 SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
import localeService from 'src/services/locale/locale.service.js'
|
||||
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
|
@ -64,7 +67,8 @@ const GeneralTab = {
|
|||
// Chrome-likes
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
|
||||
// Future spec, still not supported in Nightly 63 as of 08/2018
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
|
||||
emailLanguage: this.$store.state.users.currentUser.language || ['']
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -74,8 +78,8 @@ const GeneralTab = {
|
|||
FloatSetting,
|
||||
UnitSetting,
|
||||
InterfaceLanguageSwitcher,
|
||||
ScopeSelector,
|
||||
ProfileSettingIndicator,
|
||||
ScopeSelector,
|
||||
Select
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -97,7 +101,10 @@ const GeneralTab = {
|
|||
},
|
||||
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||
...SharedComputedObject()
|
||||
...SharedComputedObject(),
|
||||
...mapState({
|
||||
blockExpirationSupported: state => state.instance.blockExpiration,
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
changeDefaultScope (value) {
|
||||
|
|
@ -117,7 +124,19 @@ const GeneralTab = {
|
|||
},
|
||||
clearEmojiCache () {
|
||||
this.clearCache(emojiCacheKey)
|
||||
}
|
||||
},
|
||||
updateProfile () {
|
||||
const params = {
|
||||
language: localeService.internalToBackendLocaleMulti(this.emailLanguage)
|
||||
}
|
||||
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({ params })
|
||||
.then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,20 @@
|
|||
<ul class="setting-list">
|
||||
<li>
|
||||
<interface-language-switcher
|
||||
:prompt-text="$t('settings.interfaceLanguage')"
|
||||
:language="language"
|
||||
:set-language="val => language = val"
|
||||
/>
|
||||
v-model="language"
|
||||
@update="val => language = val"
|
||||
>
|
||||
{{ $t('settings.interfaceLanguage') }}
|
||||
</interface-language-switcher>
|
||||
</li>
|
||||
<li>
|
||||
<interface-language-switcher
|
||||
v-model="emailLanguage"
|
||||
:profile="true"
|
||||
@update:model-value="updateProfile()"
|
||||
>
|
||||
{{ $t('settings.email_language') }}
|
||||
</interface-language-switcher>
|
||||
</li>
|
||||
<li v-if="instanceSpecificPanelPresent">
|
||||
<BooleanSetting path="hideISP">
|
||||
|
|
|
|||
|
|
@ -1,19 +1,8 @@
|
|||
import unescape from 'lodash/unescape'
|
||||
import merge from 'lodash/merge'
|
||||
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||
import suggestor from 'src/components/emoji_input/suggestor.js'
|
||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||
import UserCard from 'src/components/user_card/user_card.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import localeService from 'src/services/locale/locale.service.js'
|
||||
import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js'
|
||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
|
@ -21,7 +10,6 @@ import {
|
|||
faPlus,
|
||||
faCircleNotch
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
|
|
@ -32,248 +20,42 @@ library.add(
|
|||
const ProfileTab = {
|
||||
data () {
|
||||
return {
|
||||
newName: this.$store.state.users.currentUser.name_unescaped,
|
||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||
newLocked: this.$store.state.users.currentUser.locked,
|
||||
newBirthday: this.$store.state.users.currentUser.birthday,
|
||||
showBirthday: this.$store.state.users.currentUser.show_birthday,
|
||||
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
||||
showRole: this.$store.state.users.currentUser.show_role,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
bot: this.$store.state.users.currentUser.bot,
|
||||
actorType: this.$store.state.users.currentUser.actor_type,
|
||||
pickAvatarBtnVisible: true,
|
||||
bannerUploading: false,
|
||||
backgroundUploading: false,
|
||||
banner: null,
|
||||
bannerPreview: null,
|
||||
background: null,
|
||||
backgroundPreview: null,
|
||||
emailLanguage: this.$store.state.users.currentUser.language || ['']
|
||||
// Whether user is locked or not
|
||||
locked: this.$store.state.users.currentUser.locked,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ScopeSelector,
|
||||
ImageCropper,
|
||||
EmojiInput,
|
||||
Autosuggest,
|
||||
ProgressButton,
|
||||
UserCard,
|
||||
Checkbox,
|
||||
BooleanSetting,
|
||||
InterfaceLanguageSwitcher,
|
||||
Select
|
||||
ProfileSettingIndicator
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
...SharedComputedObject(),
|
||||
emojiUserSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
...this.$store.getters.standardEmojiList,
|
||||
...this.$store.state.instance.customEmoji
|
||||
],
|
||||
store: this.$store
|
||||
})
|
||||
},
|
||||
emojiSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
...this.$store.getters.standardEmojiList,
|
||||
...this.$store.state.instance.customEmoji
|
||||
]
|
||||
})
|
||||
},
|
||||
userSuggestor () {
|
||||
return suggestor({ store: this.$store })
|
||||
},
|
||||
fieldsLimits () {
|
||||
return this.$store.state.instance.fieldsLimits
|
||||
},
|
||||
maxFields () {
|
||||
return this.fieldsLimits ? this.fieldsLimits.maxFields : 0
|
||||
},
|
||||
defaultAvatar () {
|
||||
return this.$store.state.instance.server + this.$store.state.instance.defaultAvatar
|
||||
},
|
||||
defaultBanner () {
|
||||
return this.$store.state.instance.server + this.$store.state.instance.defaultBanner
|
||||
},
|
||||
isDefaultAvatar () {
|
||||
const baseAvatar = this.$store.state.instance.defaultAvatar
|
||||
return !(this.$store.state.users.currentUser.profile_image_url) ||
|
||||
this.$store.state.users.currentUser.profile_image_url.includes(baseAvatar)
|
||||
},
|
||||
isDefaultBanner () {
|
||||
const baseBanner = this.$store.state.instance.defaultBanner
|
||||
return !(this.$store.state.users.currentUser.cover_photo) ||
|
||||
this.$store.state.users.currentUser.cover_photo.includes(baseBanner)
|
||||
},
|
||||
isDefaultBackground () {
|
||||
return !(this.$store.state.users.currentUser.background_image)
|
||||
},
|
||||
avatarImgSrc () {
|
||||
const src = this.$store.state.users.currentUser.profile_image_url_original
|
||||
return (!src) ? this.defaultAvatar : src
|
||||
},
|
||||
bannerImgSrc () {
|
||||
const src = this.$store.state.users.currentUser.cover_photo
|
||||
return (!src) ? this.defaultBanner : src
|
||||
},
|
||||
groupActorAvailable () {
|
||||
return this.$store.state.instance.groupActorAvailable
|
||||
},
|
||||
availableActorTypes () {
|
||||
return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service']
|
||||
}
|
||||
...SharedComputedObject()
|
||||
},
|
||||
methods: {
|
||||
updateProfile () {
|
||||
const params = {
|
||||
note: this.newBio,
|
||||
locked: this.newLocked,
|
||||
|
||||
// Backend notation.
|
||||
display_name: this.newName,
|
||||
fields_attributes: this.newFields.filter(el => el != null),
|
||||
actor_type: this.actorType,
|
||||
show_role: this.showRole,
|
||||
birthday: this.newBirthday || '',
|
||||
show_birthday: this.showBirthday
|
||||
}
|
||||
|
||||
if (this.emailLanguage) {
|
||||
params.language = localeService.internalToBackendLocaleMulti(this.emailLanguage)
|
||||
locked: this.locked
|
||||
}
|
||||
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({ params })
|
||||
.then((user) => {
|
||||
this.newFields.splice(user.fields.length)
|
||||
merge(this.newFields, user.fields)
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
})
|
||||
},
|
||||
changeVis (visibility) {
|
||||
this.newDefaultScope = visibility
|
||||
},
|
||||
addField () {
|
||||
if (this.newFields.length < this.maxFields) {
|
||||
this.newFields.push({ name: '', value: '' })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
deleteField (index) {
|
||||
this.newFields.splice(index, 1)
|
||||
},
|
||||
uploadFile (slot, e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) { return }
|
||||
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'upload.error.message',
|
||||
messageArgs: [
|
||||
this.$t('upload.error.file_too_big', {
|
||||
filesize: filesize.num,
|
||||
filesizeunit: filesize.unit,
|
||||
allowedsize: allowedsize.num,
|
||||
allowedsizeunit: allowedsize.unit
|
||||
})
|
||||
],
|
||||
level: 'error'
|
||||
.catch((error) => {
|
||||
this.displayUploadError(error)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
const img = target.result
|
||||
this[slot + 'Preview'] = img
|
||||
this[slot] = file
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
resetAvatar () {
|
||||
const confirmed = window.confirm(this.$t('settings.reset_avatar_confirm'))
|
||||
if (confirmed) {
|
||||
this.submitAvatar(undefined, '')
|
||||
}
|
||||
},
|
||||
resetBanner () {
|
||||
const confirmed = window.confirm(this.$t('settings.reset_banner_confirm'))
|
||||
if (confirmed) {
|
||||
this.submitBanner('')
|
||||
}
|
||||
},
|
||||
resetBackground () {
|
||||
const confirmed = window.confirm(this.$t('settings.reset_background_confirm'))
|
||||
if (confirmed) {
|
||||
this.submitBackground('')
|
||||
}
|
||||
},
|
||||
submitAvatar (canvas, file) {
|
||||
const that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
function updateAvatar (avatar, avatarName) {
|
||||
that.$store.state.api.backendInteractor.updateProfileImages({ avatar, avatarName })
|
||||
.then((user) => {
|
||||
that.$store.commit('addNewUsers', [user])
|
||||
that.$store.commit('setCurrentUser', user)
|
||||
resolve()
|
||||
})
|
||||
.catch((error) => {
|
||||
that.displayUploadError(error)
|
||||
reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
if (canvas) {
|
||||
canvas.toBlob((data) => updateAvatar(data, file.name), file.type)
|
||||
} else {
|
||||
updateAvatar(file, file.name)
|
||||
}
|
||||
})
|
||||
},
|
||||
submitBanner (banner) {
|
||||
if (!this.bannerPreview && banner !== '') { return }
|
||||
|
||||
this.bannerUploading = true
|
||||
this.$store.state.api.backendInteractor.updateProfileImages({ banner })
|
||||
.then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
this.bannerPreview = null
|
||||
})
|
||||
.catch(this.displayUploadError)
|
||||
.finally(() => { this.bannerUploading = false })
|
||||
},
|
||||
submitBackground (background) {
|
||||
if (!this.backgroundPreview && background !== '') { return }
|
||||
|
||||
this.backgroundUploading = true
|
||||
this.$store.state.api.backendInteractor.updateProfileImages({ background })
|
||||
.then((data) => {
|
||||
this.$store.commit('addNewUsers', [data])
|
||||
this.$store.commit('setCurrentUser', data)
|
||||
this.backgroundPreview = null
|
||||
})
|
||||
.catch(this.displayUploadError)
|
||||
.finally(() => { this.backgroundUploading = false })
|
||||
},
|
||||
displayUploadError (error) {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'upload.error.message',
|
||||
messageArgs: [error.message],
|
||||
level: 'error'
|
||||
})
|
||||
},
|
||||
propsToNative (props) {
|
||||
return propsToNative(props)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
locked () {
|
||||
this.updateProfile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,133 +1,15 @@
|
|||
.profile-tab {
|
||||
.bio {
|
||||
// overriding global for better look
|
||||
.setting-item.profile-edit {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.visibility-tray {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.banner-background-preview {
|
||||
max-width: 100%;
|
||||
width: 300px;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.uploading {
|
||||
font-size: 1.5em;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.name-changer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.current-avatar-container {
|
||||
position: relative;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.current-avatar {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
position: absolute;
|
||||
top: 0.2em;
|
||||
right: 0.2em;
|
||||
border-radius: var(--roundness);
|
||||
background-color: rgb(0 0 0 / 60%);
|
||||
opacity: 0.7;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
text-align: center;
|
||||
line-height: 1.5em;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
h2 {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
.user-card {
|
||||
padding: 1.2em;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-tokens {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&-usersearch-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
&-bulk-actions {
|
||||
text-align: right;
|
||||
padding: 0 1em;
|
||||
min-height: 2em;
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
&-domain-mute-form {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1em;
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-subitem {
|
||||
margin-left: 1.75em;
|
||||
}
|
||||
|
||||
.profile-fields {
|
||||
display: flex;
|
||||
|
||||
& > .emoji-input {
|
||||
flex: 1 1 auto;
|
||||
margin: 0 0.2em 0.5em;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.delete-field {
|
||||
width: 20px;
|
||||
align-self: center;
|
||||
margin: 0 0.2em 0.5em;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.birthday-input {
|
||||
display: block;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,283 +1,21 @@
|
|||
<template>
|
||||
<div class="profile-tab">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.name_bio') }}</h2>
|
||||
<p>{{ $t('settings.name') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newName"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<input
|
||||
id="username"
|
||||
v-model="newName"
|
||||
class="input name-changer"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
<p>{{ $t('settings.bio') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newBio"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiUserSuggestor"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<textarea
|
||||
v-model="newBio"
|
||||
class="input bio resize-height"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
/>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
<p v-if="role === 'admin' || role === 'moderator'">
|
||||
<Checkbox v-model="showRole">
|
||||
<template v-if="role === 'admin'">
|
||||
{{ $t('settings.show_admin_badge') }}
|
||||
</template>
|
||||
<template v-if="role === 'moderator'">
|
||||
{{ $t('settings.show_moderator_badge') }}
|
||||
</template>
|
||||
</Checkbox>
|
||||
</p>
|
||||
<div>
|
||||
<p>{{ $t('settings.birthday.label') }}</p>
|
||||
<input
|
||||
id="birthday"
|
||||
v-model="newBirthday"
|
||||
type="date"
|
||||
class="input birthday-input"
|
||||
>
|
||||
<Checkbox v-model="showBirthday">
|
||||
{{ $t('settings.birthday.show_birthday') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div v-if="maxFields > 0">
|
||||
<p>{{ $t('settings.profile_fields.label') }}</p>
|
||||
<div
|
||||
v-for="(_, i) in newFields"
|
||||
:key="i"
|
||||
class="profile-fields"
|
||||
>
|
||||
<EmojiInput
|
||||
v-model="newFields[i].name"
|
||||
enable-emoji-picker
|
||||
hide-emoji-button
|
||||
:suggest="userSuggestor"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<input
|
||||
v-model="newFields[i].name"
|
||||
:placeholder="$t('settings.profile_fields.name')"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
class="input"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
<EmojiInput
|
||||
v-model="newFields[i].value"
|
||||
enable-emoji-picker
|
||||
hide-emoji-button
|
||||
:suggest="userSuggestor"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<input
|
||||
v-model="newFields[i].value"
|
||||
:placeholder="$t('settings.profile_fields.value')"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
class="input"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
<button
|
||||
class="delete-field button-unstyled -hover-highlight"
|
||||
@click="deleteField(i)"
|
||||
>
|
||||
<!-- TODO something is wrong with v-show here -->
|
||||
<FAIcon
|
||||
v-if="newFields.length > 1"
|
||||
icon="times"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-if="newFields.length < maxFields"
|
||||
class="add-field faint button-unstyled -hover-highlight"
|
||||
@click="addField"
|
||||
>
|
||||
<FAIcon icon="plus" />
|
||||
{{ $t("settings.profile_fields.add_field") }}
|
||||
</button>
|
||||
</div>
|
||||
<p>
|
||||
<label>
|
||||
{{ $t('settings.actor_type') }}
|
||||
<Select v-model="actorType">
|
||||
<option
|
||||
v-for="option in availableActorTypes"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ $t('settings.actor_type_' + option) }}
|
||||
</option>
|
||||
</Select>
|
||||
</label>
|
||||
</p>
|
||||
<div v-if="groupActorAvailable">
|
||||
<small>
|
||||
{{ $t('settings.actor_type_description') }}
|
||||
</small>
|
||||
</div>
|
||||
<p>
|
||||
<interface-language-switcher
|
||||
:prompt-text="$t('settings.email_language')"
|
||||
:language="emailLanguage"
|
||||
:set-language="val => emailLanguage = val"
|
||||
/>
|
||||
</p>
|
||||
<button
|
||||
:disabled="newName && newName.length === 0"
|
||||
class="btn button-default"
|
||||
@click="updateProfile"
|
||||
>
|
||||
{{ $t('settings.save') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.avatar') }}</h2>
|
||||
<p class="visibility-notice">
|
||||
{{ $t('settings.avatar_size_instruction') }}
|
||||
</p>
|
||||
<div class="current-avatar-container">
|
||||
<img
|
||||
:src="user.profile_image_url_original"
|
||||
class="current-avatar"
|
||||
>
|
||||
<button
|
||||
v-if="!isDefaultAvatar && pickAvatarBtnVisible"
|
||||
:title="$t('settings.reset_avatar')"
|
||||
class="button-unstyled reset-button"
|
||||
@click="resetAvatar"
|
||||
>
|
||||
<FAIcon
|
||||
icon="times"
|
||||
type="button"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p>{{ $t('settings.set_new_avatar') }}</p>
|
||||
<button
|
||||
v-show="pickAvatarBtnVisible"
|
||||
id="pick-avatar"
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
>
|
||||
{{ $t('settings.upload_a_photo') }}
|
||||
</button>
|
||||
<image-cropper
|
||||
trigger="#pick-avatar"
|
||||
:submit-handler="submitAvatar"
|
||||
@open="pickAvatarBtnVisible=false"
|
||||
@close="pickAvatarBtnVisible=true"
|
||||
<div class="setting-item profile-edit">
|
||||
<h2>{{ $t('settings.account_profile_edit') }}</h2>
|
||||
<UserCard
|
||||
:user-id="user.id"
|
||||
:editable="true"
|
||||
:switcher="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.profile_banner') }}</h2>
|
||||
<div class="banner-background-preview">
|
||||
<img :src="user.cover_photo">
|
||||
<button
|
||||
v-if="!isDefaultBanner"
|
||||
class="button-unstyled reset-button"
|
||||
:title="$t('settings.reset_profile_banner')"
|
||||
@click="resetBanner"
|
||||
>
|
||||
<FAIcon
|
||||
icon="times"
|
||||
type="button"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p>{{ $t('settings.set_new_profile_banner') }}</p>
|
||||
<img
|
||||
v-if="bannerPreview"
|
||||
class="banner-background-preview"
|
||||
:src="bannerPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
class="input"
|
||||
@change="uploadFile('banner', $event)"
|
||||
>
|
||||
</div>
|
||||
<FAIcon
|
||||
v-if="bannerUploading"
|
||||
class="uploading"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
<button
|
||||
v-else-if="bannerPreview"
|
||||
class="btn button-default"
|
||||
@click="submitBanner(banner)"
|
||||
>
|
||||
{{ $t('settings.save') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.profile_background') }}</h2>
|
||||
<div class="banner-background-preview">
|
||||
<img :src="user.background_image">
|
||||
<button
|
||||
v-if="!isDefaultBackground"
|
||||
class="button-unstyled reset-button"
|
||||
:title="$t('settings.reset_profile_background')"
|
||||
@click="resetBackground"
|
||||
>
|
||||
<FAIcon
|
||||
icon="times"
|
||||
type="button"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p>{{ $t('settings.set_new_profile_background') }}</p>
|
||||
<img
|
||||
v-if="backgroundPreview"
|
||||
class="banner-background-preview"
|
||||
:src="backgroundPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
class="input"
|
||||
@change="uploadFile('background', $event)"
|
||||
>
|
||||
</div>
|
||||
<FAIcon
|
||||
v-if="backgroundUploading"
|
||||
class="uploading"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
<button
|
||||
v-else-if="backgroundPreview"
|
||||
class="btn button-default"
|
||||
@click="submitBackground(background)"
|
||||
>
|
||||
{{ $t('settings.save') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.account_privacy') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
source="profile"
|
||||
path="locked"
|
||||
>
|
||||
<Checkbox v-model="locked">
|
||||
{{ $t('settings.lock_account_description') }}
|
||||
</BooleanSetting>
|
||||
</Checkbox>
|
||||
<ProfileSettingIndicator :is-profile="true" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
|
|
|
|||
|
|
@ -159,10 +159,16 @@
|
|||
|
||||
.qr-code {
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
padding-right: 0.7em;
|
||||
}
|
||||
|
||||
.verify {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0.3em 0 0;
|
||||
}
|
||||
.verify { flex: 1; }
|
||||
.error { margin: 4px 0 0; }
|
||||
|
||||
.confirm-otp-actions {
|
||||
button {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@
|
|||
</div>
|
||||
<div class="panel-body theme-preview-content">
|
||||
<div class="post">
|
||||
<div class="avatar still-image">
|
||||
<div
|
||||
class="avatar still-image"
|
||||
aria-hidden="true"
|
||||
>
|
||||
( ͡° ͜ʖ ͡°)
|
||||
</div>
|
||||
<div class="content">
|
||||
|
|
@ -71,7 +74,10 @@
|
|||
</div>
|
||||
|
||||
<div class="after-post">
|
||||
<div class="avatar-alt">
|
||||
<div
|
||||
class="avatar-alt"
|
||||
aria-hidden="true"
|
||||
>
|
||||
:^)
|
||||
</div>
|
||||
<div class="content">
|
||||
|
|
@ -149,7 +155,7 @@ export default {
|
|||
background-position: 50% 50%;
|
||||
|
||||
.theme-preview-content {
|
||||
padding: 20px;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.dummy {
|
||||
|
|
@ -203,19 +209,21 @@ export default {
|
|||
|
||||
.avatar-alt {
|
||||
flex: 0 auto;
|
||||
margin-left: 28px;
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
margin-left: 2em;
|
||||
font-size: 0.85em;
|
||||
min-width: 2.2rem;
|
||||
min-height: 2.2rem;
|
||||
line-height: 1.5em;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex: 0 auto;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 14px;
|
||||
line-height: 48px;
|
||||
width: 3.5em;
|
||||
height: 3.5em;
|
||||
font-size: 1rem;
|
||||
line-height: 3.5em;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
|
@ -241,7 +249,7 @@ export default {
|
|||
|
||||
.underlay-preview {
|
||||
position: absolute;
|
||||
inset: 0 10px;
|
||||
inset: 0 0.9em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
min-height: 2.1em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
p {
|
||||
|
|
@ -212,7 +212,7 @@
|
|||
.theme-color-cl,
|
||||
.theme-radius-in,
|
||||
.theme-color-in {
|
||||
margin-left: 4px;
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
|
||||
.theme-radius-in {
|
||||
|
|
|
|||
|
|
@ -122,8 +122,8 @@
|
|||
|
||||
.shout-avatar {
|
||||
img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
height: 0.6em;
|
||||
width: 0.6em;
|
||||
border-radius: var(--roundness);
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.25em;
|
||||
|
|
|
|||
|
|
@ -412,6 +412,10 @@
|
|||
display: flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.user-info {
|
||||
margin: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.side-drawer ul {
|
||||
|
|
|
|||
|
|
@ -9,23 +9,9 @@ export default {
|
|||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'RichContent',
|
||||
'Input',
|
||||
'Avatar',
|
||||
'Attachment',
|
||||
'PollGraph'
|
||||
],
|
||||
validInnerComponentsLite: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'ButtonUnstyled',
|
||||
'RichContent',
|
||||
'Avatar'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
|
|
|
|||
|
|
@ -76,9 +76,10 @@
|
|||
}
|
||||
|
||||
.status-favicon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
margin-right: 0.4em;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.status-heading {
|
||||
|
|
@ -100,7 +101,8 @@
|
|||
}
|
||||
|
||||
.account-name {
|
||||
min-width: 1.6em;
|
||||
display: inline-block;
|
||||
min-width: 1em;
|
||||
margin-right: 0.4em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
@ -116,10 +118,11 @@
|
|||
.heading-right {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-self: baseline;
|
||||
|
||||
.button-unstyled {
|
||||
padding: 5px;
|
||||
margin: -5px;
|
||||
padding: 0.2em;
|
||||
margin: -0.2em;
|
||||
}
|
||||
|
||||
.svg-inline--fa {
|
||||
|
|
@ -232,9 +235,9 @@
|
|||
|
||||
.repeater-avatar {
|
||||
border-radius: var(--roundness);
|
||||
margin-left: 28px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 2em; // 3.5 (poster avatar size) - 1.5 (repeater avatar size)
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
.repeater-name {
|
||||
|
|
@ -242,8 +245,8 @@
|
|||
margin-right: 0;
|
||||
|
||||
.emoji {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@
|
|||
<RichContent
|
||||
:html="retweeterHtml"
|
||||
:emoji="retweeterUser.emoji"
|
||||
:is-local="retweeterUser.is_local"
|
||||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
|
|
@ -139,6 +140,7 @@
|
|||
<RichContent
|
||||
:html="status.user.name"
|
||||
:emoji="status.user.emoji"
|
||||
:is-local="status.user.is_local"
|
||||
/>
|
||||
</h4>
|
||||
<h4
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@
|
|||
align-self: stretch;
|
||||
width: 1px;
|
||||
background-color: var(--icon);
|
||||
margin-left: 1em;
|
||||
margin-right: 0.5em;
|
||||
margin-left: 0.75em;
|
||||
margin-right: 0.125em;
|
||||
}
|
||||
|
||||
&.-pin {
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ library.add(
|
|||
faPollH
|
||||
)
|
||||
|
||||
const StatusContent = {
|
||||
name: 'StatusContent',
|
||||
const StatusBody = {
|
||||
name: 'StatusBody',
|
||||
props: [
|
||||
'compact',
|
||||
'collapse', // replaces newlines with spaces
|
||||
'status',
|
||||
'focused',
|
||||
'noHeading',
|
||||
|
|
@ -40,6 +41,7 @@ const StatusContent = {
|
|||
parseReadyDone: false
|
||||
}
|
||||
},
|
||||
emits: ['parseReady'],
|
||||
computed: {
|
||||
localCollapseSubjectDefault () {
|
||||
return this.mergedConfig.collapseMessageWithSubject
|
||||
|
|
@ -95,6 +97,9 @@ const StatusContent = {
|
|||
attachmentTypes () {
|
||||
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
|
||||
},
|
||||
collapsedStatus () {
|
||||
return this.status.raw_html.replace(/(\n|<br\s?\/?>)/g, ' ')
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
components: {
|
||||
|
|
@ -145,4 +150,4 @@ const StatusContent = {
|
|||
}
|
||||
}
|
||||
|
||||
export default StatusContent
|
||||
export default StatusBody
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
:faint="compact"
|
||||
:html="status.summary_raw_html"
|
||||
:emoji="status.emojis"
|
||||
:is-local="status.isLocal"
|
||||
/>
|
||||
<button
|
||||
v-show="longSubject && showingLongSubject"
|
||||
|
|
@ -39,11 +40,13 @@
|
|||
:class="{ '-single-line': singleLine }"
|
||||
class="text media-body"
|
||||
:html="status.raw_html"
|
||||
:collapse="collapse"
|
||||
:emoji="status.emojis"
|
||||
:handle-links="true"
|
||||
:faint="compact"
|
||||
:greentext="mergedConfig.greentext"
|
||||
:attentions="status.attentions"
|
||||
:is-local="status.is_local"
|
||||
@parse-ready="onParseReady"
|
||||
/>
|
||||
<div
|
||||
|
|
@ -57,32 +60,6 @@
|
|||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ toggleText }}
|
||||
<template v-if="!showingMore">
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('image')"
|
||||
icon="image"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('video')"
|
||||
icon="video"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('audio')"
|
||||
icon="music"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('unknown')"
|
||||
icon="file"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.poll && status.poll.options"
|
||||
icon="poll-h"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.card"
|
||||
icon="link"
|
||||
/>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ const StatusContent = {
|
|||
props: [
|
||||
'status',
|
||||
'compact',
|
||||
'collapse',
|
||||
'focused',
|
||||
'noHeading',
|
||||
'fullContent',
|
||||
|
|
@ -64,6 +65,7 @@ const StatusContent = {
|
|||
'controlledShowingLongSubject',
|
||||
'controlledToggleShowingLongSubject'
|
||||
],
|
||||
emits: ['parseReady', 'mediaplay', 'mediapause'],
|
||||
data () {
|
||||
return {
|
||||
uncontrolledShowingTall: this.fullContent || (this.inConversation && this.focused),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
:toggle-showing-tall="toggleShowingTall"
|
||||
:toggle-expanding-subject="toggleExpandingSubject"
|
||||
:toggle-showing-long-subject="toggleShowingLongSubject"
|
||||
:collapse="collapse"
|
||||
@parse-ready="$emit('parseReady', $event)"
|
||||
>
|
||||
<div v-if="status.poll && status.poll.options && !compact">
|
||||
|
|
@ -23,7 +24,10 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.poll && status.poll.options && compact">
|
||||
<div
|
||||
v-else-if="status.poll && status.poll.options && compact"
|
||||
class="poll-icon"
|
||||
>
|
||||
<FAIcon
|
||||
icon="poll-h"
|
||||
size="2x"
|
||||
|
|
@ -62,5 +66,9 @@
|
|||
.StatusContent {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.poll-icon {
|
||||
margin: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
189
src/components/still-image/still-image-emoji-popover.vue
Normal file
189
src/components/still-image/still-image-emoji-popover.vue
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<Popover
|
||||
ref="emojiPopover"
|
||||
trigger="click"
|
||||
placement="top"
|
||||
:bound-to="{ x: 'container' }"
|
||||
:offset="{ y: 10 }"
|
||||
@show="fetchEmojiPacksIfAdmin"
|
||||
>
|
||||
<template #trigger>
|
||||
<StillImage v-bind="$attrs" />
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="emoji-popover">
|
||||
<h3>{{ $attrs.title }}</h3>
|
||||
|
||||
<div class="emoji-popover-centered">
|
||||
<StillImage
|
||||
class="emoji"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isUserAdmin && !isLocal">
|
||||
<button
|
||||
class="button button-default btn emoji-popover-button"
|
||||
type="button"
|
||||
:disabled="packName == ''"
|
||||
@click="copyToLocalPack"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.copy_to_pack') }}
|
||||
</button>
|
||||
|
||||
<SelectComponent
|
||||
v-model="packName"
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
hidden
|
||||
>
|
||||
{{ $t('admin_dash.emoji.emoji_pack') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(pack, listPackName) in knownLocalPacks"
|
||||
:key="listPackName"
|
||||
:label="listPackName"
|
||||
>
|
||||
{{ listPackName }}
|
||||
</option>
|
||||
</SelectComponent>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { assign } from 'lodash'
|
||||
|
||||
import StillImage from './still-image.vue'
|
||||
import Popover from 'components/popover/popover.vue'
|
||||
import SelectComponent from 'components/select/select.vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
export default {
|
||||
components: { StillImage, Popover, SelectComponent },
|
||||
props: {
|
||||
shortcode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isLocal: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
knownLocalPacks: { },
|
||||
packName: ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isUserAdmin () {
|
||||
return this.$store.state.users.currentUser.rights.admin
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
displayError (msg) {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'admin_dash.emoji.error',
|
||||
messageArgs: [msg],
|
||||
level: 'error'
|
||||
})
|
||||
},
|
||||
copyToLocalPack() {
|
||||
this.$store.state.api.backendInteractor.addNewEmojiFile({
|
||||
packName: this.packName,
|
||||
file: this.$attrs.src,
|
||||
shortcode: this.shortcode,
|
||||
filename: ""
|
||||
}).then(resp => resp.json()).then(resp => {
|
||||
if (resp.error !== undefined) {
|
||||
this.displayError(resp.error)
|
||||
return
|
||||
}
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'admin_dash.emoji.copied_successfully',
|
||||
messageArgs: [this.shortcode, this.packName],
|
||||
level: 'success'
|
||||
})
|
||||
|
||||
this.$refs.emojiPopover.hidePopover()
|
||||
this.packName = ''
|
||||
})
|
||||
},
|
||||
|
||||
// Copied from emoji_tab.js
|
||||
loadPacksPaginated (listFunction) {
|
||||
const pageSize = 25
|
||||
const allPacks = {}
|
||||
|
||||
return listFunction({ instance: this.$store.state.instance.server, page: 1, pageSize: 0 })
|
||||
.then(data => data.json())
|
||||
.then(data => {
|
||||
if (data.error !== undefined) { return Promise.reject(data.error) }
|
||||
|
||||
let resultingPromise = Promise.resolve({})
|
||||
for (let i = 0; i < Math.ceil(data.count / pageSize); i++) {
|
||||
resultingPromise = resultingPromise.then(() => listFunction({ instance: this.$store.state.instance.server, page: i, pageSize })
|
||||
).then(data => data.json()).then(pageData => {
|
||||
if (pageData.error !== undefined) { return Promise.reject(pageData.error) }
|
||||
|
||||
assign(allPacks, pageData.packs)
|
||||
})
|
||||
}
|
||||
|
||||
return resultingPromise
|
||||
})
|
||||
.then(() => allPacks)
|
||||
.catch(data => {
|
||||
this.displayError(data)
|
||||
})
|
||||
},
|
||||
fetchEmojiPacksIfAdmin() {
|
||||
if (!this.isUserAdmin) return
|
||||
|
||||
this.loadPacksPaginated(this.$store.state.api.backendInteractor.listEmojiPacks)
|
||||
.then(allPacks => {
|
||||
// Sort by key
|
||||
const sorted = Object.keys(allPacks).sort().reduce((acc, key) => {
|
||||
if (key.length === 0) return acc
|
||||
acc[key] = allPacks[key]
|
||||
return acc
|
||||
}, {})
|
||||
this.knownLocalPacks = sorted
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.emoji-popover {
|
||||
margin: 0 0.5em 0.5em;
|
||||
text-align: center;
|
||||
|
||||
.emoji {
|
||||
width: 4.6em;
|
||||
height: 4.6em;
|
||||
}
|
||||
|
||||
.emoji-popover-centered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.emoji-popover-button {
|
||||
width: 100%;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.Select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -202,9 +202,9 @@
|
|||
}
|
||||
|
||||
img {
|
||||
max-height: 26px;
|
||||
max-height: 1.9em;
|
||||
vertical-align: top;
|
||||
margin-top: -5px;
|
||||
margin-top: -0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export default {
|
|||
'Link',
|
||||
'Text',
|
||||
'Icon',
|
||||
// Optimization: don't put heavy components unless needed
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'Input',
|
||||
|
|
|
|||
|
|
@ -14,11 +14,31 @@ library.add(
|
|||
)
|
||||
|
||||
const UserAvatar = {
|
||||
props: [
|
||||
'user',
|
||||
'compact',
|
||||
'showActorTypeIndicator'
|
||||
],
|
||||
props: {
|
||||
// User object to show avatar of
|
||||
user: {
|
||||
required: true,
|
||||
type: Object
|
||||
},
|
||||
// Use less space and use alternative roundness
|
||||
compact: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Show small icon indicating if account is a bot or group
|
||||
showActorTypeIndicator : {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Override avatar image URL, useful for profile editing
|
||||
url: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showPlaceholder: false,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
class="avatar"
|
||||
:alt="user.screen_name_ui"
|
||||
:title="user.screen_name_ui"
|
||||
:src="imgSrc(user.profile_image_url_original)"
|
||||
:src="url ? url : imgSrc(user.profile_image_url_original)"
|
||||
:image-load-error="imageLoadError"
|
||||
:class="{ '-compact': compact, '-better-shadow': betterShadow }"
|
||||
/>
|
||||
|
|
@ -40,12 +40,12 @@
|
|||
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 3.5em;
|
||||
height: 3.5em;
|
||||
|
||||
&.-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 2.2em;
|
||||
height: 2.2em;
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import merge from 'lodash/merge'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import unescape from 'lodash/unescape'
|
||||
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||
|
|
@ -10,11 +14,19 @@ import Select from '../select/select.vue'
|
|||
import UserLink from '../user_link/user_link.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
||||
|
||||
import localeService from 'src/services/locale/locale.service.js'
|
||||
import suggestor from 'src/components/emoji_input/suggestor.js'
|
||||
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { usePostStatusStore } from 'src/stores/post_status'
|
||||
import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faBell,
|
||||
|
|
@ -24,13 +36,16 @@ import {
|
|||
faEdit,
|
||||
faTimes,
|
||||
faExpandAlt,
|
||||
faBirthdayCake
|
||||
faBirthdayCake,
|
||||
faSave,
|
||||
faClockRotateLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { useMediaViewerStore } from '../../stores/media_viewer'
|
||||
import { useInterfaceStore } from '../../stores/interface'
|
||||
|
||||
library.add(
|
||||
faSave,
|
||||
faRss,
|
||||
faBell,
|
||||
faSearchPlus,
|
||||
|
|
@ -38,23 +53,58 @@ library.add(
|
|||
faEdit,
|
||||
faTimes,
|
||||
faExpandAlt,
|
||||
faBirthdayCake
|
||||
faBirthdayCake,
|
||||
faClockRotateLeft
|
||||
)
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'userId',
|
||||
'switcher',
|
||||
'selected',
|
||||
'hideBio',
|
||||
'rounded',
|
||||
'bordered',
|
||||
'avatarAction', // default - open profile, 'zoom' - zoom, function - call function
|
||||
'onClose',
|
||||
'hasNoteEditor'
|
||||
],
|
||||
props: {
|
||||
// Enables all the options for profile editing, used in settings -> profile tab
|
||||
editable: {
|
||||
required: false,
|
||||
default: false,
|
||||
type: Boolean
|
||||
},
|
||||
// ID of user to show data of
|
||||
userId: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
// Use a compact layout that hides bio, stats etc.
|
||||
hideBio: {
|
||||
required: false,
|
||||
default: false,
|
||||
type: Boolean
|
||||
},
|
||||
// default - open profile, 'zoom' - zoom, function - call function
|
||||
avatarAction: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
// Show note editor if supported
|
||||
hasNoteEditor: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Show close icon (for popovers)
|
||||
showClose: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Show close icon (for popovers)
|
||||
showExpand: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DialogModal,
|
||||
UserAvatar,
|
||||
Checkbox,
|
||||
RemoteFollow,
|
||||
ModerationTools,
|
||||
AccountActions,
|
||||
|
|
@ -65,41 +115,77 @@ export default {
|
|||
UserLink,
|
||||
UserNote,
|
||||
UserTimedFilterModal,
|
||||
ColorInput
|
||||
ColorInput,
|
||||
EmojiInput,
|
||||
ImageCropper
|
||||
},
|
||||
data () {
|
||||
const user = this.$store.getters.findUser(this.userId)
|
||||
|
||||
return {
|
||||
followRequestInProgress: false,
|
||||
muteExpiryAmount: 0,
|
||||
muteExpiryUnit: 'minutes'
|
||||
muteExpiryUnit: 'minutes',
|
||||
|
||||
// Editable stuff
|
||||
editImage: false,
|
||||
|
||||
newName: user.name_unescaped,
|
||||
editingName: false,
|
||||
|
||||
newBio: unescape(user.description),
|
||||
editingBio: false,
|
||||
|
||||
newAvatar: null,
|
||||
newAvatarFile: null,
|
||||
|
||||
newBanner: null,
|
||||
newBannerFile: null,
|
||||
|
||||
newActorType: user.actor_type,
|
||||
newBirthday: user.birthday,
|
||||
newShowBirthday: user.show_birthday,
|
||||
newShowRole: user.show_role,
|
||||
|
||||
newFields: user.fields?.map(field => ({ name: field.name, value: field.value })),
|
||||
|
||||
editingFields: false,
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||
},
|
||||
computed: {
|
||||
somethingToSave () {
|
||||
if (this.newName !== this.user.name_unescaped) return true
|
||||
if (this.newBio !== unescape(this.user.description)) return true
|
||||
if (this.newAvatar !== null) return true
|
||||
if (this.newBanner !== null) return true
|
||||
if (this.newActorType !== this.user.actor_type) return true
|
||||
if (this.newBirthday !== this.user.birthday) return true
|
||||
if (this.newShowBirthday !== this.user.show_birthday) return true
|
||||
if (this.newShowRole !== this.user.show_role) return true
|
||||
if (!isEqual(
|
||||
this.newFields,
|
||||
this.user.fields?.map(field => ({ name: field.name, value: field.value }))
|
||||
)) return true
|
||||
return false
|
||||
},
|
||||
groupActorAvailable () {
|
||||
return this.$store.state.instance.groupActorAvailable
|
||||
},
|
||||
availableActorTypes () {
|
||||
return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service']
|
||||
},
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
role () {
|
||||
return this.user.role
|
||||
},
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
classes () {
|
||||
return [{
|
||||
'-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
|
||||
'-rounded': this.rounded === true, // set border-radius for all sides
|
||||
'-bordered': this.bordered === true, // set border for all sides
|
||||
'-popover': !!this.onClose // set popover rounding
|
||||
}]
|
||||
},
|
||||
style () {
|
||||
return {
|
||||
backgroundImage: [
|
||||
'linear-gradient(to bottom, var(--profileTint), var(--profileTint))',
|
||||
`url(${this.user.cover_photo})`
|
||||
].join(', ')
|
||||
}
|
||||
},
|
||||
isOtherUser () {
|
||||
return this.user.id !== this.$store.state.users.currentUser.id
|
||||
},
|
||||
|
|
@ -114,6 +200,13 @@ export default {
|
|||
const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
|
||||
return Math.round(this.user.statuses_count / days)
|
||||
},
|
||||
emoji () {
|
||||
return this.$store.state.instance.customEmoji.map(e => ({
|
||||
shortcode: e.displayText,
|
||||
static_url: e.imageUrl,
|
||||
url: e.imageUrl
|
||||
}))
|
||||
},
|
||||
userHighlightType: {
|
||||
get () {
|
||||
const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
|
||||
|
|
@ -139,6 +232,7 @@ export default {
|
|||
}
|
||||
},
|
||||
visibleRole () {
|
||||
if (!this.newShowRole) { return }
|
||||
const rights = this.user.rights
|
||||
if (!rights) { return }
|
||||
const validRole = rights.admin || rights.moderator
|
||||
|
|
@ -184,6 +278,59 @@ export default {
|
|||
const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
return this.user.birthday && new Date(Date.parse(this.user.birthday)).toLocaleDateString(browserLocale, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' })
|
||||
},
|
||||
|
||||
// Editable stuff
|
||||
avatarImgSrc () {
|
||||
const currentUrl = this.user.profile_image_url_original || this.defaultAvatar
|
||||
if (!this.editable) return currentUrl
|
||||
const newUrl = this.newAvatar === null ? this.defaultAvatar : this.newAvatar
|
||||
return (this.newAvatar === null) ? currentUrl : newUrl
|
||||
},
|
||||
bannerImgSrc () {
|
||||
const currentUrl = this.user.cover_photo || this.defaultBanner
|
||||
if (!this.editable) return currentUrl
|
||||
const newUrl = this.newBanner === null ? this.defaultBanner : this.newBanner
|
||||
return (this.newBanner === null) ? currentUrl : newUrl
|
||||
},
|
||||
defaultAvatar () {
|
||||
return this.$store.state.instance.server + this.$store.state.instance.defaultAvatar
|
||||
},
|
||||
defaultBanner () {
|
||||
return this.$store.state.instance.server + this.$store.state.instance.defaultBanner
|
||||
},
|
||||
isDefaultAvatar () {
|
||||
const baseAvatar = this.$store.state.instance.defaultAvatar
|
||||
return !(this.$store.state.users.currentUser.profile_image_url) ||
|
||||
this.$store.state.users.currentUser.profile_image_url.includes(baseAvatar)
|
||||
},
|
||||
isDefaultBanner () {
|
||||
const baseBanner = this.$store.state.instance.defaultBanner
|
||||
return !(this.$store.state.users.currentUser.cover_photo) ||
|
||||
this.$store.state.users.currentUser.cover_photo.includes(baseBanner)
|
||||
},
|
||||
fieldsLimits () {
|
||||
return this.$store.state.instance.fieldsLimits
|
||||
},
|
||||
maxFields () {
|
||||
return this.fieldsLimits ? this.fieldsLimits.maxFields : 0
|
||||
},
|
||||
emojiUserSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
...this.$store.getters.standardEmojiList,
|
||||
...this.$store.state.instance.customEmoji
|
||||
],
|
||||
store: this.$store
|
||||
})
|
||||
},
|
||||
emojiSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
...this.$store.getters.standardEmojiList,
|
||||
...this.$store.state.instance.customEmoji
|
||||
]
|
||||
})
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -199,12 +346,6 @@ export default {
|
|||
unsubscribeUser () {
|
||||
return this.$store.dispatch('unsubscribeUser', this.user.id)
|
||||
},
|
||||
setProfileView (v) {
|
||||
if (this.switcher) {
|
||||
const store = this.$store
|
||||
store.commit('setProfileView', { v })
|
||||
}
|
||||
},
|
||||
linkClicked ({ target }) {
|
||||
if (target.tagName === 'SPAN') {
|
||||
target = target.parentNode
|
||||
|
|
@ -238,6 +379,124 @@ export default {
|
|||
e.preventDefault()
|
||||
this.onAvatarClick()
|
||||
}
|
||||
},
|
||||
|
||||
// Editable stuff
|
||||
changeAvatar () {
|
||||
this.editImage = 'avatar'
|
||||
},
|
||||
changeBanner () {
|
||||
this.editImage = 'banner'
|
||||
},
|
||||
submitImage ({ canvas, file }) {
|
||||
if (canvas) {
|
||||
return canvas.toBlob((data) => this.submitImage({ canvas: null, file: data }))
|
||||
}
|
||||
|
||||
const reader = new window.FileReader()
|
||||
reader.onload = (e) => {
|
||||
const dataUrl = e.target.result
|
||||
|
||||
if (this.editImage === 'avatar') {
|
||||
this.newAvatar = dataUrl
|
||||
this.newAvatarFile = file
|
||||
} else {
|
||||
this.newBanner = dataUrl
|
||||
this.newBannerFile = file
|
||||
}
|
||||
|
||||
this.editImage = false
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
},
|
||||
resetImage () {
|
||||
if (this.editImage === 'avatar') {
|
||||
this.newAvatar = null
|
||||
this.newAvatarFile = null
|
||||
} else {
|
||||
this.newBanner = null
|
||||
this.newBannerFile = null
|
||||
}
|
||||
this.editImage = false
|
||||
},
|
||||
addField () {
|
||||
if (this.newFields.length < this.maxFields) {
|
||||
this.newFields.push({ name: '', value: '' })
|
||||
}
|
||||
},
|
||||
deleteField (index) {
|
||||
this.newFields.splice(index, 1)
|
||||
},
|
||||
propsToNative (props) {
|
||||
return propsToNative(props)
|
||||
},
|
||||
cancelImageText () {
|
||||
return
|
||||
},
|
||||
resetState () {
|
||||
const user = this.$store.state.users.currentUser
|
||||
|
||||
this.newName = user.name_unescaped
|
||||
this.newBio = unescape(user.description)
|
||||
|
||||
this.newAvatar = null
|
||||
this.newAvatarFile = null
|
||||
|
||||
this.newBanner = null
|
||||
this.newBannerFile = null
|
||||
|
||||
this.newActorType = user.actor_type
|
||||
this.newBirthday = user.birthday
|
||||
this.newShowBirthday = user.show_birthday
|
||||
this.newShowRole = user.show_role
|
||||
|
||||
this.newFields = user.fields.map(field => ({ name: field.name, value: field.value }))
|
||||
},
|
||||
updateProfile () {
|
||||
const params = {
|
||||
note: this.newBio,
|
||||
|
||||
// Backend notation.
|
||||
display_name: this.newName,
|
||||
fields_attributes: this.newFields.filter(el => el != null),
|
||||
show_role: !!this.newShowRole,
|
||||
birthday: this.newBirthday || '',
|
||||
show_birthday: !!this.newShowBirthday,
|
||||
}
|
||||
|
||||
if (this.actorType) {
|
||||
params.actor_type = this.actorType
|
||||
}
|
||||
|
||||
if (this.newAvatarFile !== null) {
|
||||
params.avatar = this.newAvatarFile
|
||||
}
|
||||
|
||||
if (this.newBannerFile !== null) {
|
||||
params.header = this.newBannerFile
|
||||
}
|
||||
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({ params })
|
||||
.then((user) => {
|
||||
this.newFields.splice(this.newFields.length)
|
||||
merge(this.newFields, user.fields)
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
this.resetState()
|
||||
})
|
||||
.catch((error) => {
|
||||
this.displayUploadError(error)
|
||||
})
|
||||
},
|
||||
displayUploadError (error) {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'upload.error.message',
|
||||
messageArgs: [error.message],
|
||||
level: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,71 @@
|
|||
.user-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
border-top-left-radius: calc(var(--roundness) - 1px);
|
||||
border-top-right-radius: calc(var(--roundness) - 1px);
|
||||
|
||||
// editing headers
|
||||
h4 {
|
||||
line-height: 2;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h3 {
|
||||
padding-left: 0.6em;
|
||||
margin-bottom: 0;
|
||||
|
||||
.button-default {
|
||||
font-size: 1rem;
|
||||
line-height: 2;
|
||||
padding: 0 0.6em;
|
||||
}
|
||||
}
|
||||
|
||||
.input.bio {
|
||||
height: auto; // override settings default textarea size
|
||||
}
|
||||
|
||||
.user-card-inner {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&-setting,
|
||||
&-bio {
|
||||
color: var(--lightText);
|
||||
display: block;
|
||||
line-height: 1.3;
|
||||
padding: 0 0.6em;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-card-setting {
|
||||
margin-left: 0.6em;
|
||||
margin-right: 0.6em;
|
||||
}
|
||||
|
||||
.user-card-bio {
|
||||
text-align: center;
|
||||
margin: 0 0.6em;
|
||||
|
||||
&.input {
|
||||
margin: 0 1em;
|
||||
|
||||
textarea {
|
||||
text-align: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&, * {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&.-justify-left {
|
||||
text-align: start;
|
||||
}
|
||||
|
|
@ -18,17 +77,6 @@
|
|||
--_still-image-label-visibility: hidden;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
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;
|
||||
|
|
@ -63,73 +111,53 @@
|
|||
}
|
||||
}
|
||||
|
||||
.background-image {
|
||||
.header-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
right: -1.2em;
|
||||
left: -1.2em;
|
||||
top: -1.4em;
|
||||
padding: 0;
|
||||
mask: linear-gradient(to top, transparent 0, white 5em) bottom no-repeat;
|
||||
background-size: cover;
|
||||
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);
|
||||
border-bottom-right-radius: calc(var(--__roundnessBottom, --panelRadius) - 1px);
|
||||
background-color: var(--profileBg);
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
&-bio {
|
||||
text-align: center;
|
||||
color: var(--lightText);
|
||||
display: block;
|
||||
line-height: 1.3;
|
||||
padding: 0.6em;
|
||||
margin: 0 0.6em;
|
||||
.banner-overlay,
|
||||
.banner-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 0;
|
||||
border-top-left-radius: calc(var(--roundness) - 1px);
|
||||
border-top-right-radius: calc(var(--roundness) - 1px);
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
z-index: -2;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.-rounded-t {
|
||||
border-top-left-radius: var(--roundness);
|
||||
border-top-right-radius: var(--roundness);
|
||||
|
||||
--__roundnessTop: var(--roundness);
|
||||
--__roundnessBottom: 0;
|
||||
.banner-overlay {
|
||||
background-color: var(--profileTint);
|
||||
opacity: 0.5;
|
||||
pointer-events: none; // let user copy bg url
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&.-rounded {
|
||||
border-radius: var(--roundness);
|
||||
|
||||
--__roundnessTop: var(--roundness);
|
||||
--__roundnessBottom: var(--roundness);
|
||||
}
|
||||
|
||||
&.-popover {
|
||||
border-radius: var(--roundness);
|
||||
|
||||
--__roundnessTop: var(--roundness);
|
||||
--__roundnessBottom: var(--roundness);
|
||||
}
|
||||
|
||||
&.-bordered {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--border);
|
||||
.bottom-buttons {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
position: relative;
|
||||
margin: 0.6em;
|
||||
margin-bottom: 0;
|
||||
text-align: right;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
|
||||
.user-identity {
|
||||
position: relative;
|
||||
|
|
@ -137,7 +165,6 @@
|
|||
min-height: 6em;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 1em;
|
||||
container: user-card / inline-size;
|
||||
|
||||
> * {
|
||||
|
|
@ -179,6 +206,10 @@
|
|||
padding: 0.6em;
|
||||
margin: -0.6em;
|
||||
|
||||
&.edit-banner-button {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&:hover .icon {
|
||||
color: var(--textFaint);
|
||||
}
|
||||
|
|
@ -199,7 +230,6 @@
|
|||
inset: -0.6em;
|
||||
left: -0.6em;
|
||||
right: -1.2em;
|
||||
background-color: rgb(0 0 0 / 30%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
@ -209,11 +239,21 @@
|
|||
|
||||
svg {
|
||||
color: #fff;
|
||||
margin: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &.-overlay {
|
||||
opacity: 1;
|
||||
background-color: rgb(0 0 0 / 30%);
|
||||
}
|
||||
}
|
||||
|
||||
.user-info-avatar.-editable {
|
||||
.-overlay {
|
||||
opacity: 1;
|
||||
place-items: start end;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +270,13 @@
|
|||
// big one
|
||||
z-index: 1;
|
||||
line-height: 2em;
|
||||
filter: drop-shadow(0 0 0.5em var(--profileTint))
|
||||
drop-shadow(0 0 0.2em var(--profileTint));
|
||||
|
||||
.alert {
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
--emoji-size: 1.7em;
|
||||
|
||||
|
|
@ -238,6 +285,39 @@
|
|||
--link: var(--text) !important;
|
||||
}
|
||||
|
||||
.name-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.edit-button {
|
||||
width: 3em;
|
||||
text-align: center;
|
||||
|
||||
&:hover .icon {
|
||||
color: var(--textFaint);
|
||||
}
|
||||
|
||||
&:not(:hover) .icon {
|
||||
color: var(--lightText);
|
||||
}
|
||||
}
|
||||
|
||||
.input,
|
||||
.user-name {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
line-height: 2;
|
||||
margin-right: 1em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin: 0 -0.5em;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.top-line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -246,8 +326,6 @@
|
|||
// these two normalize position and height when custom emoji are used
|
||||
line-height: 2;
|
||||
margin-bottom: -0.2em;
|
||||
font-weight: 600;
|
||||
font-size: 110%;
|
||||
font-size: calc(max(110%, 4cqw));
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +336,9 @@
|
|||
white-space: normal;
|
||||
font-weight: 500;
|
||||
font-size: 0.9em;
|
||||
font-size: calc(max(90%, 2.5cqw));
|
||||
line-height: 1.5;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
|
|
@ -281,65 +361,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.user-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
margin-right: 1em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.highlighter {
|
||||
margin: 5em;
|
||||
align-items: baseline;
|
||||
line-height: 22px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.following {
|
||||
flex: 1 0 auto;
|
||||
margin: 0;
|
||||
margin-bottom: 0.25em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.highlighter {
|
||||
flex: 0 1 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -0.5em;
|
||||
align-self: start;
|
||||
|
||||
.userHighlightCl {
|
||||
padding: 2px 10px;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.userHighlightSel {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.userHighlightText {
|
||||
width: 70px;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.userHighlightCl,
|
||||
.userHighlightText,
|
||||
.userHighlightSel {
|
||||
vertical-align: top;
|
||||
margin-right: 0.5em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-interactions {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(7.5em, 20%));
|
||||
grid-gap: 0.6em;
|
||||
max-width: 98vw;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6em;
|
||||
|
||||
> * {
|
||||
flex: 0 0 8em;
|
||||
}
|
||||
|
||||
.popover-trigger-button, .moderation-tools-button {
|
||||
width: 100%;
|
||||
|
|
@ -399,38 +429,70 @@
|
|||
|
||||
.user-profile-fields {
|
||||
margin: 0 0.5em;
|
||||
padding: 0 0.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
||||
&.emoji {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
--emoji-size: 1.8em;
|
||||
|
||||
.user-profile-field-add,
|
||||
.user-profile-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.25em;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--roundness);
|
||||
line-height: 2em;
|
||||
|
||||
.label {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.user-profile-field-add {
|
||||
justify-content: center;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.user-profile-field {
|
||||
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
// input is a generic class
|
||||
.input {
|
||||
text-align: inherit;
|
||||
flex: 1;
|
||||
}
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
|
||||
.delete-field {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.user-profile-field-name,
|
||||
.user-profile-field-value {
|
||||
.user-profile-field-value,
|
||||
.user-profile-field-add {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
line-height: 2em;
|
||||
display: inline-flex;
|
||||
|
||||
&.-edit {
|
||||
padding: 0;
|
||||
|
||||
input {
|
||||
font-weight: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-profile-field-name {
|
||||
flex: 0 1 50%;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
justify-content: end;
|
||||
color: var(--lightText);
|
||||
min-width: 9em;
|
||||
border-right: 1px solid var(--border);
|
||||
|
|
@ -446,3 +508,88 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-image {
|
||||
.panel-body {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.current-avatar {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
margin: 0 0 1em;
|
||||
max-height: 30em;
|
||||
max-width: 100%;
|
||||
flex: 1 0 20em;
|
||||
aspect-ratio: 1;
|
||||
gap: 0.5em;
|
||||
|
||||
&.-banner {
|
||||
aspect-ratio: 3;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.new-image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cropper {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
> * {
|
||||
flex: 1 0 10em;
|
||||
max-height: 100%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.separator {
|
||||
min-width: 1.1em;
|
||||
font-size: 500%;
|
||||
align-self: center;
|
||||
flex: 0 1 5em;
|
||||
aspect-ratio: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&.-banner {
|
||||
.images-container {
|
||||
grid-template-rows: 20em 5em 20em;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
> * {
|
||||
flex: 1 0 10em;
|
||||
width: 100%;
|
||||
aspect-ratio: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
min-height: 1.1em;
|
||||
font-size: 500%;
|
||||
justify-self: center;
|
||||
flex: 0 1 5em;
|
||||
aspect-ratio: unset;
|
||||
}
|
||||
}
|
||||
|
||||
#modal.-mobile & {
|
||||
#pick-image {
|
||||
height: 3em;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
&.-banner {
|
||||
max-height: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,40 +2,10 @@ export default {
|
|||
name: 'UserCard',
|
||||
selector: '.user-card',
|
||||
notEditable: true,
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'Input',
|
||||
'RichContent',
|
||||
'Alert'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--bg',
|
||||
opacity: 0,
|
||||
roundness: 3,
|
||||
shadow: [{
|
||||
x: 1,
|
||||
y: 1,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
'--profileTint': 'color | $alpha(--background 0.5)'
|
||||
}
|
||||
},
|
||||
{
|
||||
parent: {
|
||||
component: 'UserCard'
|
||||
},
|
||||
component: 'RichContent',
|
||||
directives: {
|
||||
opacity: 0
|
||||
'--profileTint': 'color | $alpha(--background 1)'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
<template>
|
||||
<div
|
||||
class="user-card"
|
||||
:class="classes"
|
||||
>
|
||||
<div
|
||||
:class="onClose ? '' : 'panel-heading -flexible-height'"
|
||||
class="user-card-inner"
|
||||
>
|
||||
<div class="user-card">
|
||||
<div class="user-card-inner">
|
||||
<div class="user-info">
|
||||
<div class="user-identity">
|
||||
<div
|
||||
:class="{ 'hide-bio': hideBio }"
|
||||
:style="style"
|
||||
class="background-image"
|
||||
/>
|
||||
<div class="header-overlay">
|
||||
<div class="banner-image">
|
||||
<img
|
||||
:src="bannerImgSrc"
|
||||
:class="{ 'hide-bio': hideBio }"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="banner-overlay"
|
||||
:class="{ 'hide-bio': hideBio }"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
v-if="avatarAction === 'zoom'"
|
||||
class="user-info-avatar -link"
|
||||
|
|
@ -27,6 +28,23 @@
|
|||
/>
|
||||
</div>
|
||||
</a>
|
||||
<button
|
||||
v-else-if="editable"
|
||||
class="user-info-avatar button-unstyled -link"
|
||||
:class="{ '-editable': editable }"
|
||||
@click="changeAvatar"
|
||||
>
|
||||
<UserAvatar
|
||||
:user="user"
|
||||
:url="avatarImgSrc"
|
||||
/>
|
||||
<div class="user-info-avatar -link -overlay">
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="pencil"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<UserAvatar
|
||||
v-else-if="typeof avatarAction === 'function'"
|
||||
class="user-info-avatar"
|
||||
|
|
@ -42,9 +60,25 @@
|
|||
</router-link>
|
||||
<div class="user-summary">
|
||||
<div class="top-line">
|
||||
<div class="other-actions">
|
||||
<div
|
||||
class="other-actions"
|
||||
>
|
||||
<button
|
||||
v-if="!isOtherUser && user.is_local"
|
||||
v-if="editable"
|
||||
:disabled="newName && newName.length === 0"
|
||||
class="btn button-unstyled edit-banner-button"
|
||||
@click="changeBanner"
|
||||
>
|
||||
{{ $t('settings.change_banner') }}
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="icon"
|
||||
icon="pencil"
|
||||
:title="$t('settings.change_banner')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!editable && !isOtherUser && user.is_local"
|
||||
class="button-unstyled edit-profile-button"
|
||||
@click.stop="openProfileTab"
|
||||
>
|
||||
|
|
@ -72,10 +106,10 @@
|
|||
:relationship="relationship"
|
||||
/>
|
||||
<router-link
|
||||
v-if="onClose"
|
||||
v-if="showExpand"
|
||||
:to="userProfileLink(user)"
|
||||
class="button-unstyled external-link-button"
|
||||
@click="onClose"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<FAIcon
|
||||
class="icon"
|
||||
|
|
@ -83,9 +117,9 @@
|
|||
/>
|
||||
</router-link>
|
||||
<button
|
||||
v-if="onClose"
|
||||
v-if="showClose"
|
||||
class="button-unstyled external-link-button"
|
||||
@click="onClose"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<FAIcon
|
||||
class="icon"
|
||||
|
|
@ -93,19 +127,48 @@
|
|||
/>
|
||||
</button>
|
||||
</div>
|
||||
<router-link
|
||||
:to="userProfileLink(user)"
|
||||
class="user-name"
|
||||
>
|
||||
<RichContent
|
||||
:title="user.name"
|
||||
:html="user.name"
|
||||
:emoji="user.emoji"
|
||||
/>
|
||||
</router-link>
|
||||
<div class="name-wrapper">
|
||||
<router-link
|
||||
v-if="!editable || !editingName"
|
||||
:to="userProfileLink(user)"
|
||||
class="user-name"
|
||||
>
|
||||
<RichContent
|
||||
:title="editable ? newName : user.name_unescaped"
|
||||
:html="editable ? newName : user.name_unescaped"
|
||||
:emoji="editable ? emoji : user.emoji"
|
||||
/>
|
||||
</router-link>
|
||||
<EmojiInput
|
||||
v-else-if="editingName"
|
||||
v-model="newName"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<input
|
||||
id="username"
|
||||
v-model="newName"
|
||||
class="input name-changer"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
<button
|
||||
v-if="editable"
|
||||
class="button-unstyled edit-button"
|
||||
:title="$t('settings.toggle_edit')"
|
||||
@click="editingName = !editingName"
|
||||
>
|
||||
<FAIcon
|
||||
class="icon"
|
||||
icon="pencil"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-line">
|
||||
<user-link
|
||||
<UserLink
|
||||
class="user-screen-name"
|
||||
:user="user"
|
||||
/>
|
||||
|
|
@ -242,7 +305,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="loggedIn && isOtherUser && (hasNote || !hideBio) && !mergedConfig.userCardHidePersonalMarks"
|
||||
v-if="!editable && loggedIn && isOtherUser && (hasNote || !hideBio) && !mergedConfig.userCardHidePersonalMarks"
|
||||
class="personal-marks"
|
||||
>
|
||||
<UserNote
|
||||
|
|
@ -286,96 +349,300 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<RichContent
|
||||
v-if="!hideBio"
|
||||
class="user-card-bio"
|
||||
:class="{ '-justify-left': mergedConfig.userCardLeftJustify }"
|
||||
:html="user.description_html"
|
||||
:emoji="user.emoji"
|
||||
:handle-links="true"
|
||||
/>
|
||||
<div
|
||||
v-if="!hideBio && user.fields_html && user.fields_html.length > 0"
|
||||
class="user-profile-fields"
|
||||
>
|
||||
<dl
|
||||
v-for="(field, index) in user.fields_html"
|
||||
:key="index"
|
||||
class="user-profile-field"
|
||||
<h3 v-if="editable">
|
||||
<span>
|
||||
{{ $t('settings.bio') }}
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
class="button-default"
|
||||
@click="editingBio = !editingBio"
|
||||
>
|
||||
<dt
|
||||
:title="user.fields_text[index].name"
|
||||
class="user-profile-field-name"
|
||||
>
|
||||
<RichContent
|
||||
:html="field.name"
|
||||
:emoji="user.emoji"
|
||||
{{ $t('settings.toggle_edit') }}
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="pencil"
|
||||
/>
|
||||
</button>
|
||||
</h3>
|
||||
<template v-if="!editable || !editingBio">
|
||||
<RichContent
|
||||
v-if="!hideBio"
|
||||
class="user-card-bio"
|
||||
:class="{ '-justify-left': mergedConfig.userCardLeftJustify }"
|
||||
:html="editable ? newBio.replace(/\n/g, '<br>') : user.description_html"
|
||||
:emoji="editable ? emoji : user.emoji"
|
||||
:handle-links="true"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="editingBio">
|
||||
<EmojiInput
|
||||
v-model="newBio"
|
||||
enable-emoji-picker
|
||||
class="user-card-bio"
|
||||
:class="{ '-justify-left': mergedConfig.userCardLeftJustify }"
|
||||
:suggest="emojiUserSuggestor"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<textarea
|
||||
v-model="newBio"
|
||||
class="input bio resize-height"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
:rows="newBio.split(/\n/g).length"
|
||||
/>
|
||||
</dt>
|
||||
<dd
|
||||
:title="user.fields_text[index].value"
|
||||
class="user-profile-field-value"
|
||||
</template>
|
||||
</EmojiInput>
|
||||
</template>
|
||||
<h3 v-if="editable">
|
||||
<span>
|
||||
{{ $t('settings.profile_fields.label') }}
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
class="button-default"
|
||||
@click="editingFields = !editingFields"
|
||||
>
|
||||
{{ $t('settings.toggle_edit') }}
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="pencil"
|
||||
/>
|
||||
</button>
|
||||
</h3>
|
||||
<template v-if="!editable || !editingFields">
|
||||
<div
|
||||
v-if="!hideBio && user.fields_html && user.fields_html.length > 0"
|
||||
class="user-profile-fields"
|
||||
>
|
||||
<dl
|
||||
v-for="(field, index) in (editable ? newFields : user.fields_html)"
|
||||
:key="index"
|
||||
class="user-profile-field"
|
||||
>
|
||||
<RichContent
|
||||
:html="field.value"
|
||||
:emoji="user.emoji"
|
||||
<dt
|
||||
:title="field.name"
|
||||
class="user-profile-field-name"
|
||||
>
|
||||
<RichContent
|
||||
:html="field.name"
|
||||
:emoji="editable ? emoji : user.emoji"
|
||||
/>
|
||||
</dt>
|
||||
<dd
|
||||
:title="field.value"
|
||||
class="user-profile-field-value"
|
||||
>
|
||||
<RichContent
|
||||
:html="field.value"
|
||||
:emoji="editable ? emoji : user.emoji"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="editingFields">
|
||||
<div
|
||||
v-if="maxFields > 0"
|
||||
class="user-profile-fields"
|
||||
>
|
||||
<dl
|
||||
v-for="(_, i) in newFields"
|
||||
:key="i"
|
||||
class="user-profile-field"
|
||||
>
|
||||
<dt
|
||||
class="user-profile-field-name -edit"
|
||||
>
|
||||
<EmojiInput
|
||||
v-model="newFields[i].name"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<input
|
||||
v-model="newFields[i].name"
|
||||
:placeholder="$t('settings.profile_fields.name')"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
class="input"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
</dt>
|
||||
<dd
|
||||
class="user-profile-field-value -edit"
|
||||
>
|
||||
<EmojiInput
|
||||
v-model="newFields[i].value"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<input
|
||||
v-model="newFields[i].value"
|
||||
:placeholder="$t('settings.profile_fields.value')"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
class="input input"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
<button
|
||||
class="delete-field button-default -hover-highlight"
|
||||
@click="deleteField(i)"
|
||||
>
|
||||
<!-- TODO something is wrong with v-show here -->
|
||||
<FAIcon
|
||||
v-if="newFields.length > 1"
|
||||
icon="times"
|
||||
/>
|
||||
</button>
|
||||
</dd>
|
||||
</dl>
|
||||
<button
|
||||
v-if="newFields.length < maxFields"
|
||||
class="user-profile-field-add add-field button-default -hover-highlight"
|
||||
@click="addField"
|
||||
>
|
||||
<FAIcon
|
||||
icon="plus"
|
||||
class="icon"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<span class="label">
|
||||
{{ $t("settings.profile_fields.add_field") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="!hideBio"
|
||||
class="user-extras"
|
||||
>
|
||||
<span
|
||||
v-if="!mergedConfig.hideUserStats"
|
||||
v-if="!editable && !mergedConfig.hideUserStats"
|
||||
class="user-stats"
|
||||
>
|
||||
<dl
|
||||
v-if="!mergedConfig.hideUserStats && !hideBio"
|
||||
class="user-count"
|
||||
@click.prevent="setProfileView('statuses')"
|
||||
>
|
||||
<dd>{{ user.statuses_count }}</dd>
|
||||
{{ ' ' }}
|
||||
<dt>{{ $t('user_card.statuses') }}</dt>
|
||||
</dl>
|
||||
<dl
|
||||
class="user-count"
|
||||
@click.prevent="setProfileView('statuses')"
|
||||
>
|
||||
<dl class="user-count">
|
||||
<dd>{{ dailyAvg }}</dd>
|
||||
{{ ' ' }}
|
||||
<dt>{{ $t('user_card.statuses_per_day') }}</dt>
|
||||
</dl>
|
||||
<dl
|
||||
class="user-count"
|
||||
@click.prevent="setProfileView('friends')"
|
||||
>
|
||||
<dl class="user-count">
|
||||
<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')"
|
||||
>
|
||||
<dl class="user-count">
|
||||
<dd>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</dd>
|
||||
{{ ' ' }}
|
||||
<dt>{{ $t('user_card.followers') }}</dt>
|
||||
</dl>
|
||||
</span>
|
||||
<div
|
||||
v-if="!hideBio && !!user.birthday"
|
||||
class="birthday"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-old-padding"
|
||||
icon="birthday-cake"
|
||||
/>
|
||||
{{ $t('user_card.birthday', { birthday: formattedBirthday }) }}
|
||||
</div>
|
||||
<template v-if="!hideBio">
|
||||
<div
|
||||
v-if="user.birthday && !editable"
|
||||
class="birthday"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-old-padding"
|
||||
icon="birthday-cake"
|
||||
/>
|
||||
{{ $t('user_card.birthday', { birthday: formattedBirthday }) }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="editable"
|
||||
class="birthday"
|
||||
>
|
||||
<div>
|
||||
<Checkbox v-model="newShowBirthday">
|
||||
{{ $t('settings.birthday.show_birthday') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<FAIcon
|
||||
class="fa-old-padding"
|
||||
icon="birthday-cake"
|
||||
/>
|
||||
{{ $t('settings.birthday.label') }}
|
||||
<input
|
||||
id="birthday"
|
||||
v-model="newBirthday"
|
||||
type="date"
|
||||
class="input birthday-input"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="editable">
|
||||
<h3>{{ $t('settings.profile_other') }}</h3>
|
||||
<p
|
||||
v-if="role === 'admin' || role === 'moderator'"
|
||||
class="user-card-setting"
|
||||
>
|
||||
<Checkbox v-model="newShowRole">
|
||||
<template v-if="role === 'admin'">
|
||||
{{ $t('settings.show_admin_badge') }}
|
||||
</template>
|
||||
<template v-if="role === 'moderator'">
|
||||
{{ $t('settings.show_moderator_badge') }}
|
||||
</template>
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p class="user-card-setting">
|
||||
<label>
|
||||
{{ $t('settings.actor_type') }}
|
||||
<Select v-model="newActorType">
|
||||
<option
|
||||
v-for="option in availableActorTypes"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ $t('settings.actor_type_' + (option === 'Person' ? 'person_proper' : option)) }}
|
||||
</option>
|
||||
</Select>
|
||||
<div v-if="groupActorAvailable">
|
||||
<small>
|
||||
{{ $t('settings.actor_type_description') }}
|
||||
</small>
|
||||
</div>
|
||||
</label>
|
||||
</p>
|
||||
<div class="bottom-buttons">
|
||||
<button
|
||||
v-if="editable"
|
||||
:disabled="!somethingToSave"
|
||||
class="btn button-default reset-profile-button"
|
||||
@click="resetState"
|
||||
>
|
||||
{{ $t('settings.reset') }}
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="icon"
|
||||
icon="clock-rotate-left"
|
||||
:title="$t('user_card.edit_profile')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="editable"
|
||||
:disabled="!somethingToSave"
|
||||
class="btn button-default save-profile-button"
|
||||
@click="updateProfile"
|
||||
>
|
||||
{{ $t('settings.save') }}
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="icon"
|
||||
icon="save"
|
||||
:title="$t('user_card.edit_profile')"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<teleport to="#modal">
|
||||
<UserTimedFilterModal
|
||||
ref="timedMuteDialog"
|
||||
|
|
@ -383,6 +650,68 @@
|
|||
:is-mute="true"
|
||||
/>
|
||||
</teleport>
|
||||
<teleport to="#modal">
|
||||
<DialogModal
|
||||
v-if="editImage"
|
||||
class="edit-image"
|
||||
>
|
||||
<template #header>
|
||||
{{ editImage === 'avatar' ? $t('settings.change_avatar') : $t('settings.change_banner') }}
|
||||
</template>
|
||||
<p>
|
||||
{{ editImage === 'avatar' ? $t('settings.avatar_size_instruction') : $t('settings.banner_size_instruction' ) }}
|
||||
</p>
|
||||
<div
|
||||
class="image-container"
|
||||
:class="{ '-banner': editImage === 'banner' }"
|
||||
>
|
||||
<image-cropper
|
||||
ref="cropper"
|
||||
class="cropper"
|
||||
:aspect-ratio="editImage === 'avatar' ? 1 : 3"
|
||||
@submit="submitImage"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
id="pick-image"
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
@click="() => $refs.cropper.pickImage()"
|
||||
>
|
||||
{{ $t('settings.select_picture') }}
|
||||
</button>
|
||||
<template #footer>
|
||||
<button
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
@click="editImage = false"
|
||||
>
|
||||
{{ $t('image_cropper.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
:title="editImage === 'avatar' ? $t('settings.reset_avatar') : $t('settings.reset_banner')"
|
||||
class="button-default btn reset-button"
|
||||
@click="resetImage"
|
||||
>
|
||||
{{ editImage === 'avatar' ? $t('settings.reset_avatar') : $t('settings.reset_banner' ) }}
|
||||
</button>
|
||||
<button
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
@click="$refs.cropper.submit(false)"
|
||||
>
|
||||
{{ $t('image_cropper.save_without_cropping') }}
|
||||
</button>
|
||||
<button
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
@click="$refs.cropper.submit(true)"
|
||||
>
|
||||
{{ $t('image_cropper.save') }}
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
<UserCard
|
||||
:user-id="user.id"
|
||||
:hide-bio="true"
|
||||
rounded="top"
|
||||
/>
|
||||
<PostStatusForm />
|
||||
</div>
|
||||
|
|
@ -29,6 +28,8 @@
|
|||
}
|
||||
|
||||
.user-info {
|
||||
margin: 0.6em 0.6em 0;
|
||||
|
||||
.Avatar {
|
||||
width: 5em;
|
||||
width: calc(min(5em, 20cqw));
|
||||
|
|
@ -37,6 +38,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.post-status-form {
|
||||
form {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.signed-in {
|
||||
z-index: 10;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@
|
|||
<template #content="{close}">
|
||||
<UserCard
|
||||
class="user-popover"
|
||||
:show-close="true"
|
||||
:show-expand="true"
|
||||
:user-id="userId"
|
||||
:hide-bio="true"
|
||||
:avatar-action="userPopoverAvatarAction == 'close' ? close : userPopoverAvatarAction"
|
||||
:on-close="close"
|
||||
@close="close"
|
||||
/>
|
||||
</template>
|
||||
</Popover>
|
||||
|
|
@ -27,7 +29,12 @@
|
|||
/* popover styles load on-demand, so we need to override */
|
||||
/* stylelint-disable block-no-empty */
|
||||
.user-popover {
|
||||
margin-bottom: 0.6em;
|
||||
margin: 0;
|
||||
|
||||
.user-info {
|
||||
width: 100%;
|
||||
margin: 1.2em;
|
||||
}
|
||||
|
||||
.user-identity {
|
||||
aspect-ratio: unset;
|
||||
|
|
@ -40,7 +47,6 @@
|
|||
|
||||
&.popover {
|
||||
overflow: hidden;
|
||||
padding: 0.6em;
|
||||
}
|
||||
}
|
||||
/* stylelint-enable block-no-empty */
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue