Art by pipivovott
diff --git a/package.json b/package.json
index eea94a10e..edc122760 100644
--- a/package.json
+++ b/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 ",
"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",
diff --git a/public/static/splash.css b/public/static/splash.css
index abdc19fc2..f56f33d07 100644
--- a/public/static/splash.css
+++ b/public/static/splash.css
@@ -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 {
diff --git a/src/App.js b/src/App.js
index a251682dc..0027d908a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -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')
}
diff --git a/src/App.scss b/src/App.scss
index 64c8b8b82..ee1654bb7 100644
--- a/src/App.scss
+++ b/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 {
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index f8ad0e11b..2ac74ea76 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -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 () {
diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss
index 16346c97c..97515eb32 100644
--- a/src/components/attachment/attachment.scss
+++ b/src/components/attachment/attachment.scss
@@ -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%);
diff --git a/src/components/attachment/attachment.style.js b/src/components/attachment/attachment.style.js
deleted file mode 100644
index a9455e367..000000000
--- a/src/components/attachment/attachment.style.js
+++ /dev/null
@@ -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
- }
- }
- ]
-}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 0701a393e..696d3bb8a 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -23,7 +23,7 @@
>
@@ -81,7 +81,7 @@
>
@@ -89,7 +89,7 @@
@@ -97,7 +97,7 @@
@@ -105,7 +105,7 @@
@@ -113,7 +113,7 @@
@@ -121,7 +121,7 @@
@@ -129,7 +129,7 @@
diff --git a/src/components/block_card/block_card.js b/src/components/block_card/block_card.js
index b5be9e7b7..9a618db3f 100644
--- a/src/components/block_card/block_card.js
+++ b/src/components/block_card/block_card.js
@@ -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 })
}
}
}
diff --git a/src/components/button.style.js b/src/components/button.style.js
index 887ff91b5..5cffefd91 100644
--- a/src/components/button.style.js
+++ b/src/components/button.style.js
@@ -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: {
diff --git a/src/components/chat_message/chat_message.style.js b/src/components/chat_message/chat_message.style.js
index 9b57ad371..76b565823 100644
--- a/src/components/chat_message/chat_message.style.js
+++ b/src/components/chat_message/chat_message.style.js
@@ -8,9 +8,6 @@ export default {
'Text',
'Icon',
'Border',
- 'Button',
- 'RichContent',
- 'Attachment',
'PollGraph'
],
defaultRules: [
diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue
index 72660cca0..00521260f 100644
--- a/src/components/chat_title/chat_title.vue
+++ b/src/components/chat_title/chat_title.vue
@@ -19,6 +19,7 @@
:title="'@'+(user && user.screen_name_ui)"
:html="htmlTitle"
:emoji="user.emoji || []"
+ :is-local="user.is_local"
/>
diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue
index 939655f65..2b9c7a5d8 100644
--- a/src/components/dialog_modal/dialog_modal.vue
+++ b/src/components/dialog_modal/dialog_modal.vue
@@ -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;
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index f3b6dfe9b..f6ba6e245 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -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
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index f9788d874..a1cba33bc 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -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;
}
}
}
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 17a317a4d..8e572d1d2 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -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
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index 7209e9144..eb1054df6 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -129,7 +129,7 @@
.gallery-item {
margin: 0;
- height: 200px;
+ height: 15em;
}
}
}
diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js
index 3cb4f6fd1..afca328e8 100644
--- a/src/components/image_cropper/image_cropper.js
+++ b/src/components/image_cropper/image_cropper.js
@@ -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)
}
diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue
index 0122d1216..ee487b09c 100644
--- a/src/components/image_cropper/image_cropper.vue
+++ b/src/components/image_cropper/image_cropper.vue
@@ -1,13 +1,14 @@
-
+
.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 {
diff --git a/src/components/interface_language_switcher/interface_language_switcher.js b/src/components/interface_language_switcher/interface_language_switcher.js
new file mode 100644
index 000000000..bd7b641a5
--- /dev/null
+++ b/src/components/interface_language_switcher/interface_language_switcher.js
@@ -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
+ }
+ }
+}
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index ad11589b0..8b300d43b 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -1,7 +1,8 @@
- {{ promptText }}
+
+
-
+
diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index 838ac6d6c..7fbb0a5cd 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -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 })
+ },
}
}
diff --git a/src/components/settings_modal/tabs/appearance_tab.scss b/src/components/settings_modal/tabs/appearance_tab.scss
index ae8691f16..d786cfa38 100644
--- a/src/components/settings_modal/tabs/appearance_tab.scss
+++ b/src/components/settings_modal/tabs/appearance_tab.scss
@@ -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;
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index cbbb8ff9c..cd54e2c82 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -151,6 +151,49 @@
+
+
{{ $t('settings.background') }}
+
+
+
+
+
+
+
{{ $t('settings.set_new_background') }}
+
+
+
+
+
+
+ {{ $t('settings.save') }}
+
+
{{ $t('settings.scale_and_layout') }}
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 879dfd1a3..ff6d0e477 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -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)
+ })
+ },
}
}
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 411fd8772..49fc57c79 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -5,10 +5,20 @@
+ v-model="language"
+ @update="val => language = val"
+ >
+ {{ $t('settings.interfaceLanguage') }}
+
+
+
+
+ {{ $t('settings.email_language') }}
+
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index a549a8705..9e13eed69 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -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()
}
}
}
diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss
index 7eda943b7..ce85188bd 100644
--- a/src/components/settings_modal/tabs/profile_tab.scss
+++ b/src/components/settings_modal/tabs/profile_tab.scss
@@ -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;
- }
}
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 034034a12..947be4a18 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -1,283 +1,21 @@
-
-
{{ $t('settings.name_bio') }}
-
{{ $t('settings.name') }}
-
-
-
-
-
-
{{ $t('settings.bio') }}
-
-
-
-
-
-
-
-
- {{ $t('settings.show_admin_badge') }}
-
-
- {{ $t('settings.show_moderator_badge') }}
-
-
-
-
-
{{ $t('settings.birthday.label') }}
-
-
- {{ $t('settings.birthday.show_birthday') }}
-
-
-
-
{{ $t('settings.profile_fields.label') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t("settings.profile_fields.add_field") }}
-
-
-
-
- {{ $t('settings.actor_type') }}
-
-
- {{ $t('settings.actor_type_' + option) }}
-
-
-
-
-
-
- {{ $t('settings.actor_type_description') }}
-
-
-
-
-
-
- {{ $t('settings.save') }}
-
-
-
-
{{ $t('settings.avatar') }}
-
- {{ $t('settings.avatar_size_instruction') }}
-
-
-
-
-
-
-
-
{{ $t('settings.set_new_avatar') }}
-
- {{ $t('settings.upload_a_photo') }}
-
-
+ {{ $t('settings.account_profile_edit') }}
+
-
-
{{ $t('settings.profile_banner') }}
-
-
-
-
-
-
-
{{ $t('settings.set_new_profile_banner') }}
-
-
-
-
-
-
- {{ $t('settings.save') }}
-
-
-
-
{{ $t('settings.profile_background') }}
-
-
-
-
-
-
-
{{ $t('settings.set_new_profile_background') }}
-
-
-
-
-
-
- {{ $t('settings.save') }}
-
-
{{ $t('settings.account_privacy') }}
-
+
{{ $t('settings.lock_account_description') }}
-
+
+
-
+
( ͔° ĶŹ ͔°)
@@ -71,7 +74,10 @@
-
+
:^)
@@ -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;
}
}
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
index d83beffa0..ae2364dcc 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -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 {
diff --git a/src/components/shout_panel/shout_panel.vue b/src/components/shout_panel/shout_panel.vue
index 1b2b591c2..184fe81e0 100644
--- a/src/components/shout_panel/shout_panel.vue
+++ b/src/components/shout_panel/shout_panel.vue
@@ -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;
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 7dd6ff28f..eda60a96a 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -412,6 +412,10 @@
display: flex;
padding: 0;
margin: 0;
+
+ .user-info {
+ margin: 1em;
+ }
}
.side-drawer ul {
diff --git a/src/components/status/post.style.js b/src/components/status/post.style.js
index d0038424e..43e21a16e 100644
--- a/src/components/status/post.style.js
+++ b/src/components/status/post.style.js
@@ -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: {
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 93253b1a7..511a74074 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -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;
}
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index d0af91aef..fbc455ee6 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -79,6 +79,7 @@
fileType.fileType(file.mimetype))
},
+ collapsedStatus () {
+ return this.status.raw_html.replace(/(\n| )/g, ' ')
+ },
...mapGetters(['mergedConfig'])
},
components: {
@@ -145,4 +150,4 @@ const StatusContent = {
}
}
-export default StatusContent
+export default StatusBody
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 0fc024b04..677781d97 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -14,6 +14,7 @@
:faint="compact"
:html="status.summary_raw_html"
:emoji="status.emojis"
+ :is-local="status.isLocal"
/>
{{ toggleText }}
-
-
-
-
-
-
-
-
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 3bf25c75b..364123f49 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -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),
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 64a1d6a53..460a0714b 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -14,6 +14,7 @@
:toggle-showing-tall="toggleShowingTall"
:toggle-expanding-subject="toggleExpandingSubject"
:toggle-showing-long-subject="toggleShowingLongSubject"
+ :collapse="collapse"
@parse-ready="$emit('parseReady', $event)"
>
@@ -23,7 +24,10 @@
/>
-
+
diff --git a/src/components/still-image/still-image-emoji-popover.vue b/src/components/still-image/still-image-emoji-popover.vue
new file mode 100644
index 000000000..8317c6a0c
--- /dev/null
+++ b/src/components/still-image/still-image-emoji-popover.vue
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
{{ $attrs.title }}
+
+
+
+
+
+
+
+ {{ $t('admin_dash.emoji.copy_to_pack') }}
+
+
+
+
+ {{ $t('admin_dash.emoji.emoji_pack') }}
+
+
+ {{ listPackName }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 60413c5f4..b7274d86d 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -202,9 +202,9 @@
}
img {
- max-height: 26px;
+ max-height: 1.9em;
vertical-align: top;
- margin-top: -5px;
+ margin-top: -0.3em;
}
}
diff --git a/src/components/top_bar.style.js b/src/components/top_bar.style.js
index 945ae7781..89c5243db 100644
--- a/src/components/top_bar.style.js
+++ b/src/components/top_bar.style.js
@@ -5,6 +5,7 @@ export default {
'Link',
'Text',
'Icon',
+ // Optimization: don't put heavy components unless needed
'Button',
'ButtonUnstyled',
'Input',
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
index 09eadda61..8092ba62b 100644
--- a/src/components/user_avatar/user_avatar.js
+++ b/src/components/user_avatar/user_avatar.js
@@ -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,
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
index 83608c505..d9e96d52d 100644
--- a/src/components/user_avatar/user_avatar.vue
+++ b/src/components/user_avatar/user_avatar.vue
@@ -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);
}
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index bb36c1255..7daee4b76 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -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'
+ })
}
}
}
diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss
index 53f885446..bd7359ac9 100644
--- a/src/components/user_card/user_card.scss
+++ b/src/components/user_card/user_card.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/src/components/user_card/user_card.style.js b/src/components/user_card/user_card.style.js
index 3d5bb2989..b6e256392 100644
--- a/src/components/user_card/user_card.style.js
+++ b/src/components/user_card/user_card.style.js
@@ -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)'
}
}
]
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 9c62359c9..d6edc021f 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -1,19 +1,20 @@
-
-
+
+
+
+
+
+
+
+
-
+
+ {{ $t('settings.change_banner') }}
+
+
+
@@ -72,10 +106,10 @@
:relationship="relationship"
/>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
@@ -242,7 +305,7 @@
-
-
-
+
+ {{ $t('settings.bio') }}
+
+ {{ ' ' }}
+
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+ {{ $t('settings.profile_fields.label') }}
+
+ {{ ' ' }}
+
+ {{ $t('settings.toggle_edit') }}
+
+
+
+
+
+
+
+
+
+ {{ $t("settings.profile_fields.add_field") }}
+
+
+
+
+
+ {{ $t('settings.profile_other') }}
+
+
+
+ {{ $t('settings.show_admin_badge') }}
+
+
+ {{ $t('settings.show_moderator_badge') }}
+
+
+
+
+
+ {{ $t('settings.actor_type') }}
+
+
+ {{ $t('settings.actor_type_' + (option === 'Person' ? 'person_proper' : option)) }}
+
+
+
+
+ {{ $t('settings.actor_type_description') }}
+
+
+
+
+
+
+ {{ $t('settings.reset') }}
+
+
+
+ {{ $t('settings.save') }}
+
+
+
+
+
+
+
+ {{ editImage === 'avatar' ? $t('settings.change_avatar') : $t('settings.change_banner') }}
+
+
+ {{ editImage === 'avatar' ? $t('settings.avatar_size_instruction') : $t('settings.banner_size_instruction' ) }}
+
+
+
+
+ $refs.cropper.pickImage()"
+ >
+ {{ $t('settings.select_picture') }}
+
+
+
+ {{ $t('image_cropper.cancel') }}
+
+
+ {{ editImage === 'avatar' ? $t('settings.reset_avatar') : $t('settings.reset_banner' ) }}
+
+
+ {{ $t('image_cropper.save_without_cropping') }}
+
+
+ {{ $t('image_cropper.save') }}
+
+
+
+
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index 7ac4d429c..3f6922c31 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -8,7 +8,6 @@
@@ -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;
}
diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue
index 9bff355e9..ea289e53c 100644
--- a/src/components/user_popover/user_popover.vue
+++ b/src/components/user_popover/user_popover.vue
@@ -12,10 +12,12 @@
@@ -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 */
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 749b828b8..e05e62c46 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -4,13 +4,12 @@
v-if="user"
class="user-profile panel panel-default"
>
-