diff --git a/.stylelintrc.json b/.stylelintrc.json index fbf3a245c..d6689cc01 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,19 +1,41 @@ { "extends": [ "stylelint-rscss/config", - "stylelint-config-recommended", - "stylelint-config-standard" + "stylelint-config-standard", + "stylelint-config-recommended-scss", + "stylelint-config-html", + "stylelint-config-recommended-vue/scss" ], "rules": { "declaration-no-important": true, "rscss/no-descendant-combinator": false, "rscss/class-format": [ - true, + false, { "component": "pascal-case", "variant": "^-[a-z]\\w+", "element": "^[a-z]\\w+" } + ], + "selector-class-pattern": null, + "import-notation": null, + "custom-property-pattern": null, + "keyframes-name-pattern": null, + "scss/operator-no-newline-after": null, + "declaration-block-no-redundant-longhand-properties": [ + true, + { + "ignoreShorthands": [ + "grid-template", + "margin", + "padding", + "border", + "border-width", + "border-style", + "border-color", + "border-radius" + ] + } ] } } diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e529787..8ed1c1860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,52 +3,76 @@ 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/). -## Unreleased +## 2.5.0 - 23.12.2022 ### Fixed -- AdminFE button no longer scrolls page to top when clicked +- UI no longer lags when switching between mobile and desktop mode +- Popovers no longer constrained by DOM hierarchy, shouldn't be cut off by anything +- Emoji autocomplete popover and picker popover stick to the text cursor. +- Attachments are ALWAYS in same order as user uploaded, no more "videos first" - Pinned statuses no longer appear at bottom of user timeline (still appear as part of the timeline when fetched deep enough) - Fixed many many bugs related to new mentions, including spacing and alignment issues - Links in profile bios now properly open in new tabs +- "Always show mobile button" is working now - Inline images now respect their intended width/height attributes - Links with `&` in them work properly now -- Interaction list popovers now properly emojify names -- Completely hidden posts still had 1px border -- Attachments are ALWAYS in same order as user uploaded, no more "videos first" - Attachment description is prefilled with backend-provided default when uploading - Proper visual feedback that next image is loading when browsing -- UI no longer lags when switching between mobile and desktop mode -- Popovers no longer constrained by DOM hierarchy, shouldn't be cut off by anything -- "Always show mobile button" is working now +- Additional HTML sanitization on frontend side in case backend sanitization fails +- Interaction list popovers now properly emojify names +- AdminFE button no longer scrolls page to top when clicked +- User handles with non-ascii domains now have less intrusive indicator for the domain name +- Completely hidden posts still no longer have 1px border +- A lot of accessibility improvements ### Changed - Using Vue 3 now -- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out) +- A lot of internal dependencies updated +- "(You)s" are optional (opt-in) now, bolding your nickname is also optional (opt-out) - User highlight background now also covers the `@` - Reverted back to textual `@`, svg version is opt-in. - Settings window has been thoroughly rearranged to make more sense and make navigation settings easier. - Uploaded attachments are uniform with displayed attachments - Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues) -- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post. +- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post. (You can expand them to full if need be) - Slight width/spacing adjustments - More sizing stuff is font-size dependent now - Scrollbars are styled/colorized now - Scrollbars are toggleable (for stuff that didn't have visible scrollbars before) (opt-in) +- Updated localization files +- Top bar is more useful in mobile mode now. +- "Show new" button is way more compact in mobile mode +- Slightly adjusted placement and spacing of the topbar buttons so it's less easy to accidentally log yourself out ### Added - 3 column mode: only enables when there's space for it (opt-out, customizable) +- Apologetic pleroma-tan +- New button on timeline header to change some of the new and often-used settings +- Support for lists +- Added ability to edit posts and view post edit history etc. +- Added ability to add personal note to users +- Added initial support for admin announcements +- Added ui for account migration +- Added ui for backups +- Added ability to force-unfollow a user from you +- Emoji are now grouped by pack +- Ability to pin navigation items and collapse the navigation menu +- Ability to rearrange order of attachments when uploading +- Ability to scroll column (or page) to top via panel header button - Options to show domains in mentions - Option to show user avatars in mention links (opt-in) - Option to disable the tooltip for mentions - Option to completely hide muted threads +- Option to customize what clicking user avatar does in user popover +- Notifications for poll results +- "Favorites" link in navigation +- Very early and somewhat experimental system for automatic settings sync (used only for pinned navigation and apologetic pleroma-tan) +- Implemented remote interaction with statuses for anon visitors - Ability to open videos in modal even if you disabled that feature, via an icon button - New button on attachment that indicates that attachment has a description and shows a bar filled with description - Attachments are truncated just like post contents - Media modal now also displays description and counter position in gallery (i.e. 1/5) -- Ability to rearrange order of attachments when uploading - Enabled users to zoom and pan images in media viewer with mouse and touch -- Timelines/panels and conversations have sticky headers now -- Added frontend ui for account migration -- Implemented remote interaction with statuses +- Timelines/panels and conversations have sticky headers now (a bit glitchy on some browsers like safari) (opt-out) ## [2.4.2] - 2022-01-09 diff --git a/README.md b/README.md index 54529a705..6a37195d5 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ # Pleroma-FE -> A single column frontend designed for Pleroma. +> Highly-customizable frontend designed for Pleroma. -![screenshot](/uploads/796c5ecf985ed1e2b0943ee0df131ed0/DJVqSJ0.png) +![screenshot](./image-1.png) # For Translators -To translate Pleroma-FE, add your language to [src/i18n/messages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/messages.js). Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js. +To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/languages.js). -# FOR ADMINS +Pleroma-FE will set your language by your browser locale, but you can change language in settings. -You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box. +# For instance admins +You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box. Information of customizing PleromaFE settings/defaults is in our [guide](https://docs-develop.pleroma.social/frontend/CONFIGURATION/) and in case you want to build your own custom version there's [another](https://docs-develop.pleroma.social/frontend/HACKING/) -## Build Setup +# Build Setup ``` bash # install dependencies @@ -20,13 +21,13 @@ npm install -g yarn yarn # serve with hot reload at localhost:8080 -npm run dev +yarn dev # build for production with minification -npm run build +yarn build # run unit tests -npm run unit +yarn unit ``` # For Contributors: @@ -40,10 +41,4 @@ FE Build process also leaves current commit hash in global variable `___pleromaf # Configuration -Edit config.json for configuration. - -## Options - -### Login methods - -```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations. +Set configuration settings in AdminFE, additionally you can edit config.json. For more details see [documentation](https://docs-develop.pleroma.social/frontend/CONFIGURATION/). diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index bf9469222..7e69352a6 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -6,7 +6,7 @@ var ServiceWorkerWebpackPlugin = require('serviceworker-webpack5-plugin') var CopyPlugin = require('copy-webpack-plugin'); var { VueLoaderPlugin } = require('vue-loader') var ESLintPlugin = require('eslint-webpack-plugin'); - +var StylelintPlugin = require('stylelint-webpack-plugin'); var env = process.env.NODE_ENV // check env & config/index.js to decide weither to enable CSS Sourcemaps for the @@ -111,6 +111,7 @@ module.exports = { extensions: ['js', 'vue'], formatter: require('eslint-formatter-friendly') }), + new StylelintPlugin({}), new VueLoaderPlugin(), // This copies Ruffle's WASM to a directory so that JS side can access it new CopyPlugin({ diff --git a/docs/HACKING.md b/docs/HACKING.md index 7f2964b4e..a5c491136 100644 --- a/docs/HACKING.md +++ b/docs/HACKING.md @@ -25,7 +25,17 @@ This could be a bit trickier, you basically need steps 1-4 from *develop build* ### Replacing your instance's frontend with custom FE build -This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](../backend/configuration/static_dir.md), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder. +#### New way (via AdminFE, a bit janky but works) + +In backend's [static directory](../backend/configuration/static_dir.md) there should be a folder called `frontends` if you installed any frontends from AdminFE before, otherwise you can create it yourself (ensuring correct permissions). Backend will serve given frontend from path `frontends/{frontend}/{reference}`, where `{frontend}` is name of frontend (`pleroma-fe`) and `{reference}` is version. You could make a production build, move `dist` folder into `frontends/pleroma-fe` and rename it into something like `myCustomVersion`. To actually make backend serve this frontend by default, in AdminFE you'll need to set name/reference in Settings -> Frontend -> Frontends -> Primary. + +You could also install from a zip file (i.e. CI build) but AdminFE UI is a bit buggy and lacking, so this approach is not recommended. + +Take note that frontend management is in early development and currently there's no way for user to change frontend or version for themselves, primary frontend becomes default frontend for all users and visitors. + +#### Old way (replaces everything, hard to maintain, not recommended) + +Copy or symlink contents of `dist` folder into backend's [static directory](../backend/configuration/static_dir.md), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder, and this could remove emojis, other frontends etc. and therefore this approach is not recommended. ### Running production build locally or on a separate server diff --git a/image-1.png b/image-1.png new file mode 100644 index 000000000..602cbb263 Binary files /dev/null and b/image-1.png differ diff --git a/image.png b/image.png new file mode 100644 index 000000000..c1004c7c8 Binary files /dev/null and b/image.png differ diff --git a/index.html b/index.html index 4af84a594..a02939f7e 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,7 @@
+
diff --git a/package.json b/package.json index 261a1c241..df15f5775 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "pleroma_fe", - "version": "1.0.0", - "description": "A Qvitter-style frontend for certain GS servers.", - "author": "Roger Braun ", - "private": true, + "version": "2.5.0", + "description": "Pleroma frontend, the default frontend of Pleroma social network server", + "author": "Pleroma contributors ", + "private": false, "scripts": { "dev": "node build/dev-server.js", "build": "node build/build.js", @@ -11,17 +11,17 @@ "unit:watch": "karma start test/unit/karma.conf.js --single-run=false", "e2e": "node test/e2e/runner.js", "test": "npm run unit && npm run e2e", - "stylelint": "npx stylelint src/components/status/status.scss", + "stylelint": "npx stylelint '**/*.scss' '**/*.vue'", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs", "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { - "@babel/runtime": "7.20.0", + "@babel/runtime": "7.20.7", "@chenfengyuan/vue-qrcode": "2.0.0", - "@fortawesome/fontawesome-svg-core": "6.2.0", - "@fortawesome/free-regular-svg-icons": "6.2.0", - "@fortawesome/free-solid-svg-icons": "6.2.0", - "@fortawesome/vue-fontawesome": "3.0.1", + "@fortawesome/fontawesome-svg-core": "6.2.1", + "@fortawesome/free-regular-svg-icons": "6.2.1", + "@fortawesome/free-solid-svg-icons": "6.2.1", + "@fortawesome/vue-fontawesome": "3.0.2", "@kazvmoe-infra/pinch-zoom-element": "1.2.0", "@kazvmoe-infra/unicode-emoji-json": "0.4.0", "@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12", @@ -34,51 +34,51 @@ "escape-html": "1.0.3", "js-cookie": "3.0.1", "localforage": "1.10.0", - "lozad": "1.16.0", "parse-link-header": "2.0.0", "phoenix": "1.6.2", - "punycode.js": "2.1.0", + "punycode.js": "2.3.0", "qrcode": "1.5.0", "querystring-es3": "0.2.1", "url": "0.11.0", "utf8": "3.0.0", - "vue": "3.2.41", + "vue": "3.2.45", "vue-i18n": "9.2.2", "vue-router": "4.1.6", - "vue-template-compiler": "2.7.13", + "vue-template-compiler": "2.7.14", + "vue-virtual-scroller": "^2.0.0-beta.7", "vuex": "4.1.0" }, "devDependencies": { - "@babel/core": "7.19.6", + "@babel/core": "7.20.7", "@babel/eslint-parser": "7.19.1", "@babel/plugin-transform-runtime": "7.19.6", - "@babel/preset-env": "7.19.4", + "@babel/preset-env": "7.20.2", "@babel/register": "7.18.9", "@intlify/vue-i18n-loader": "5.0.0", "@ungap/event-target": "0.2.3", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-plugin-jsx": "1.1.1", - "@vue/compiler-sfc": "3.2.41", - "@vue/test-utils": "2.2.6", - "autoprefixer": "10.4.12", - "babel-loader": "8.2.5", + "@vue/compiler-sfc": "3.2.45", + "@vue/test-utils": "2.2.7", + "autoprefixer": "10.4.13", + "babel-loader": "9.1.2", "babel-plugin-lodash": "3.3.4", "chai": "4.3.7", "chalk": "1.1.3", - "chromedriver": "104.0.0", + "chromedriver": "108.0.0", "connect-history-api-fallback": "2.0.0", "copy-webpack-plugin": "11.0.0", "cross-spawn": "7.0.3", - "css-loader": "6.7.1", + "css-loader": "6.7.3", "css-minimizer-webpack-plugin": "4.2.2", "custom-event-polyfill": "1.0.7", - "eslint": "8.29.0", + "eslint": "8.32.0", "eslint-config-standard": "17.0.0", "eslint-formatter-friendly": "7.0.0", - "eslint-plugin-import": "2.26.0", - "eslint-plugin-n": "15.6.0", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-n": "15.6.1", "eslint-plugin-promise": "6.1.1", - "eslint-plugin-vue": "9.7.0", + "eslint-plugin-vue": "9.9.0", "eslint-webpack-plugin": "3.2.0", "eventsource-polyfill": "0.9.6", "express": "4.18.2", @@ -94,36 +94,42 @@ "karma-mocha-reporter": "2.2.5", "karma-sinon-chai": "2.0.2", "karma-sourcemap-loader": "0.3.8", - "karma-spec-reporter": "0.0.34", + "karma-spec-reporter": "0.0.36", "karma-webpack": "5.0.0", "lodash": "4.17.21", - "mini-css-extract-plugin": "2.6.1", - "mocha": "10.0.0", - "nightwatch": "2.3.3", + "mini-css-extract-plugin": "2.7.2", + "mocha": "10.2.0", + "nightwatch": "2.6.10", "opn": "5.5.0", "ora": "0.4.1", - "postcss": "8.4.16", - "postcss-loader": "7.0.1", - "sass": "1.55.0", - "sass-loader": "13.0.2", + "postcss": "8.4.20", + "postcss-html": "^1.5.0", + "postcss-loader": "7.0.2", + "postcss-scss": "^4.0.6", + "sass": "1.57.1", + "sass-loader": "13.2.0", "selenium-server": "2.53.1", "semver": "7.3.8", "serviceworker-webpack5-plugin": "2.0.0", "shelljs": "0.8.5", - "sinon": "14.0.2", + "sinon": "15.0.1", "sinon-chai": "3.7.0", - "stylelint": "13.13.1", - "stylelint-config-standard": "20.0.0", + "stylelint": "14.16.1", + "stylelint-config-html": "^1.1.0", + "stylelint-config-recommended-scss": "^8.0.0", + "stylelint-config-recommended-vue": "^1.4.0", + "stylelint-config-standard": "29.0.0", "stylelint-rscss": "0.4.0", + "stylelint-webpack-plugin": "^3.3.0", "vue-loader": "17.0.1", "vue-style-loader": "4.1.3", - "webpack": "5.74.0", + "webpack": "5.75.0", "webpack-dev-middleware": "3.7.3", - "webpack-hot-middleware": "2.25.2", + "webpack-hot-middleware": "2.25.3", "webpack-merge": "0.20.0" }, "engines": { - "node": ">= 4.0.0", + "node": ">= 16.0.0", "npm": ">= 3.0.0" } } diff --git a/src/App.scss b/src/App.scss index 75b2667c1..1c4c8941b 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,5 +1,7 @@ // stylelint-disable rscss/class-format -@import './_variables.scss'; +/* stylelint-disable no-descending-specificity */ +@import "./variables"; +@import "./panel"; :root { --navbar-height: 3.5rem; @@ -123,7 +125,7 @@ h4 { font-weight: 1000; } -i[class*=icon-], +i[class*="icon-"], .svg-inline--fa, .iconLetter { color: $fallback--icon; @@ -132,7 +134,7 @@ i[class*=icon-], .button-unstyled:hover, a:hover { - > i[class*=icon-], + > i[class*="icon-"], > .svg-inline--fa, > .iconLetter { color: var(--text); @@ -141,12 +143,11 @@ a:hover { nav { z-index: var(--ZI_navbar); - color: var(--topBarText); background-color: $fallback--fg; background-color: var(--topBar, $fallback--fg); color: $fallback--faint; color: var(--faint, $fallback--faint); - box-shadow: 0 0 4px rgba(0, 0, 0, 0.6); + box-shadow: 0 0 4px rgb(0 0 0 / 60%); box-shadow: var(--topBarShadow); box-sizing: border-box; height: var(--navbar-height); @@ -191,13 +192,11 @@ nav { } .underlay { - grid-column-start: 1; - grid-column-end: span 3; - grid-row-start: 1; - grid-row-end: 1; + grid-column: 1 / span 3; + grid-row: 1 / 1; pointer-events: none; - background-color: rgba(0, 0, 0, 0.15); - background-color: var(--underlay, rgba(0, 0, 0, 0.15)); + background-color: rgb(0 0 0 / 15%); + background-color: var(--underlay, rgb(0 0 0 / 15%)); z-index: -1000; } @@ -231,8 +230,7 @@ nav { display: grid; grid-template-columns: 100%; box-sizing: border-box; - grid-row-start: 1; - grid-row-end: 1; + grid-row: 1 / 1; margin: 0 calc(var(--___columnMargin) / 2); padding: calc(var(--___columnMargin)) 0; row-gap: var(--___columnMargin); @@ -307,7 +305,7 @@ nav { align-content: start; } - &.-reverse:not(.-wide):not(.-mobile) { + &.-reverse:not(.-wide, .-mobile) { grid-template-columns: var(--effectiveContentColumnWidth) var(--effectiveSidebarColumnWidth); @@ -336,11 +334,8 @@ nav { padding: 0; .column { - margin-left: 0; - margin-right: 0; padding-top: 0; - margin-top: var(--navbar-height); - margin-bottom: 0; + margin: var(--navbar-height) 0 0 0; } .panel-heading, @@ -389,7 +384,7 @@ nav { background: transparent; } - i[class*=icon-], + i[class*="icon-"], .svg-inline--fa { color: $fallback--text; color: var(--btnText, $fallback--text); @@ -400,12 +395,15 @@ nav { } &:hover { - box-shadow: 0 0 4px rgba(255, 255, 255, 0.3); + box-shadow: 0 0 4px rgb(255 255 255 / 30%); box-shadow: var(--buttonHoverShadow); } &:active { - box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset; + box-shadow: + 0 0 4px 0 rgb(255 255 255 / 30%), + 0 1px 0 0 rgb(0 0 0 / 20%) inset, + 0 -1px 0 0 rgb(255 255 255 / 20%) inset; box-shadow: var(--buttonPressedShadow); color: $fallback--text; color: var(--btnPressedText, $fallback--text); @@ -438,7 +436,10 @@ nav { color: var(--btnToggledText, $fallback--text); background-color: $fallback--fg; background-color: var(--btnToggled, $fallback--fg); - box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset; + box-shadow: + 0 0 4px 0 rgb(255 255 255 / 30%), + 0 1px 0 0 rgb(0 0 0 / 20%) inset, + 0 -1px 0 0 rgb(255 255 255 / 20%) inset; box-shadow: var(--buttonPressedShadow); svg, @@ -503,7 +504,10 @@ textarea, border: none; border-radius: $fallback--inputRadius; border-radius: var(--inputRadius, $fallback--inputRadius); - box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset; + box-shadow: + 0 1px 0 0 rgb(0 0 0 / 20%) inset, + 0 -1px 0 0 rgb(255 255 255 / 20%) inset, + 0 0 2px 0 rgb(0 0 0 / 100%) inset; box-shadow: var(--inputShadow); background-color: $fallback--fg; background-color: var(--input, $fallback--fg); @@ -521,13 +525,13 @@ textarea, padding: 0 var(--_padding); &:disabled, - &[disabled=disabled], + &[disabled="disabled"], &.disabled { cursor: not-allowed; opacity: 0.5; } - &[type=range] { + &[type="range"] { background: none; border: none; margin: 0; @@ -535,7 +539,7 @@ textarea, flex: 1; } - &[type=radio] { + &[type="radio"] { display: none; &:checked + label::before { @@ -555,7 +559,7 @@ textarea, + label::before { flex-shrink: 0; display: inline-block; - content: ''; + content: ""; transition: box-shadow 200ms; width: 1.1em; height: 1.1em; @@ -575,7 +579,7 @@ textarea, } } - &[type=checkbox] { + &[type="checkbox"] { display: none; &:checked + label::before { @@ -594,7 +598,7 @@ textarea, + label::before { flex-shrink: 0; display: inline-block; - content: '✓'; + content: "✓"; transition: color 200ms; width: 1.1em; height: 1.1em; @@ -634,10 +638,10 @@ option { } .hide-number-spinner { - -moz-appearance: textfield; + appearance: textfield; - &[type=number]::-webkit-inner-spin-button, - &[type=number]::-webkit-outer-spin-button { + &[type="number"]::-webkit-inner-spin-button, + &[type="number"]::-webkit-outer-spin-button { opacity: 0; display: none; } @@ -669,8 +673,6 @@ option { } } -@import './panel.scss'; - .fa { color: grey; } @@ -686,7 +688,7 @@ option { max-width: 10em; min-width: 1.7em; height: 1.3em; - padding: 0.15em 0.15em; + padding: 0.15em; vertical-align: middle; font-weight: normal; font-style: normal; @@ -789,7 +791,8 @@ option { .fa-old-padding { &.iconLetter, - &.svg-inline--fa, &-layer { + &.svg-inline--fa, + &-layer { padding: 0 0.3em; } } @@ -883,3 +886,4 @@ option { .fade-leave-active { opacity: 0; } +/* stylelint-enable no-descending-specificity */ diff --git a/src/App.vue b/src/App.vue index 23a388a62..fe214ce71 100644 --- a/src/App.vue +++ b/src/App.vue @@ -71,7 +71,6 @@ - diff --git a/src/_mixins.scss b/src/_mixins.scss index 1fed16c3d..e99fe26f8 100644 --- a/src/_mixins.scss +++ b/src/_mixins.scss @@ -1,13 +1,14 @@ @mixin unfocused-style { @content; - &:focus:not(:focus-visible):not(:hover) { + &:focus:not(:focus-visible, :hover) { @content; } } @mixin focused-style { - &:hover, &:focus { + &:hover, + &:focus { @content; } diff --git a/src/_variables.scss b/src/_variables.scss index 099d36064..751fc9a46 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -4,20 +4,20 @@ $darkened-background: whitesmoke; $fallback--bg: #121a24; $fallback--fg: #182230; -$fallback--faint: rgba(185, 185, 186, .5); +$fallback--faint: rgb(185 185 186 / 50%); $fallback--text: #b9b9ba; $fallback--link: #d8a070; $fallback--icon: #666; -$fallback--lightBg: rgb(21, 30, 42); +$fallback--lightBg: rgb(21 30 42); $fallback--lightText: #b9b9ba; $fallback--border: #222; -$fallback--cRed: #ff0000; +$fallback--cRed: #f00; $fallback--cBlue: #0095ff; $fallback--cGreen: #0fa00f; $fallback--cOrange: orange; -$fallback--alertError: rgba(211,16,20,.5); -$fallback--alertWarning: rgba(111,111,20,.5); +$fallback--alertError: rgb(211 16 20 / 50%); +$fallback--alertWarning: rgb(111 111 20 / 50%); $fallback--panelRadius: 10px; $fallback--checkboxRadius: 2px; @@ -29,6 +29,8 @@ $fallback--avatarAltRadius: 10px; $fallback--attachmentRadius: 10px; $fallback--chatMessageRadius: 10px; -$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; +$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%), + 0 1px 0 0 rgb(255 255 255 / 20%) inset, + 0 -1px 0 0 rgb(0 0 0 / 20%) inset; $status-margin: 0.75em; diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 7a4672b65..d2e7f488b 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -1,6 +1,8 @@ import { createApp } from 'vue' import { createRouter, createWebHistory } from 'vue-router' import vClickOutside from 'click-outside-vue3' +import VueVirtualScroller from 'vue-virtual-scroller' +import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' @@ -58,6 +60,8 @@ const getInstanceConfig = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) + store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma.metadata.birthday_required }) + store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma.metadata.birthday_min_age || 0 }) if (vapidPublicKey) { store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) @@ -397,6 +401,7 @@ const afterStoreSetup = async ({ store, i18n }) => { app.use(vClickOutside) app.use(VBodyScrollLock) + app.use(VueVirtualScroller) app.component('FAIcon', FontAwesomeIcon) app.component('FALayers', FontAwesomeLayers) diff --git a/src/components/about/about.vue b/src/components/about/about.vue index 33586c970..8a551485f 100644 --- a/src/components/about/about.vue +++ b/src/components/about/about.vue @@ -9,6 +9,3 @@ - - diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index c23407f90..acd93e065 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -2,6 +2,7 @@ import { mapState } from 'vuex' import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faEllipsisV @@ -16,14 +17,30 @@ const AccountActions = { 'user', 'relationship' ], data () { - return { } + return { + showingConfirmBlock: false, + showingConfirmRemoveFollower: false + } }, components: { ProgressButton, Popover, - UserListMenu + UserListMenu, + ConfirmModal }, methods: { + showConfirmBlock () { + this.showingConfirmBlock = true + }, + hideConfirmBlock () { + this.showingConfirmBlock = false + }, + showConfirmRemoveUserFromFollowers () { + this.showingConfirmRemoveFollower = true + }, + hideConfirmRemoveUserFromFollowers () { + this.showingConfirmRemoveFollower = false + }, showRepeats () { this.$store.dispatch('showReblogs', this.user.id) }, @@ -31,13 +48,29 @@ const AccountActions = { this.$store.dispatch('hideReblogs', this.user.id) }, blockUser () { + if (!this.shouldConfirmBlock) { + this.doBlockUser() + } else { + this.showConfirmBlock() + } + }, + doBlockUser () { this.$store.dispatch('blockUser', this.user.id) + this.hideConfirmBlock() }, unblockUser () { this.$store.dispatch('unblockUser', this.user.id) }, removeUserFromFollowers () { + if (!this.shouldConfirmRemoveUserFromFollowers) { + this.doRemoveUserFromFollowers() + } else { + this.showConfirmRemoveUserFromFollowers() + } + }, + doRemoveUserFromFollowers () { this.$store.dispatch('removeUserFromFollowers', this.user.id) + this.hideConfirmRemoveUserFromFollowers() }, reportUser () { this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) @@ -50,6 +83,12 @@ const AccountActions = { } }, computed: { + shouldConfirmBlock () { + return this.$store.getters.mergedConfig.modalOnBlock + }, + shouldConfirmRemoveUserFromFollowers () { + return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers + }, ...mapState({ pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable }) diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 218aa6b35..ce19291a1 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -74,13 +74,56 @@ + + + + + + + + + + + + + +
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 5dc504755..6e14b24d7 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -36,6 +36,7 @@ library.add( const Attachment = { props: [ 'attachment', + 'compact', 'description', 'hideDescription', 'nsfw', @@ -71,7 +72,8 @@ const Attachment = { { '-loading': this.loading, '-nsfw-placeholder': this.hidden, - '-editable': this.edit !== undefined + '-editable': this.edit !== undefined, + '-compact': this.compact }, '-type-' + this.type, this.size && '-size-' + this.size, diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss index b2dea98d5..681bab291 100644 --- a/src/components/attachment/attachment.scss +++ b/src/components/attachment/attachment.scss @@ -1,4 +1,4 @@ -@import '../../_variables.scss'; +@import "../../variables"; .Attachment { display: inline-flex; @@ -102,14 +102,13 @@ padding-top: 0.5em; } - .play-icon { position: absolute; font-size: 64px; top: calc(50% - 32px); left: calc(50% - 32px); - color: rgba(255, 255, 255, 0.75); - text-shadow: 0 0 2px rgba(0, 0, 0, 0.4); + color: rgb(255 255 255 / 75%); + text-shadow: 0 0 2px rgb(0 0 0 / 40%); &::before { margin: 0; @@ -135,18 +134,32 @@ margin-left: 0.5em; font-size: 1.25em; // TODO: theming? hard to theme with unknown background image color - background: rgba(230, 230, 230, 0.7); + background: rgb(230 230 230 / 70%); .svg-inline--fa { - color: rgba(0, 0, 0, 0.6); + color: rgb(0 0 0 / 60%); } &:hover .svg-inline--fa { - color: rgba(0, 0, 0, 0.9); + color: rgb(0 0 0 / 90%); } } } + &.-contain-fit { + img, + canvas { + object-fit: contain; + } + } + + &.-cover-fit { + img, + canvas { + object-fit: cover; + } + } + .oembed-container { line-height: 1.2em; flex: 1 0 100%; @@ -160,8 +173,9 @@ .image { flex: 1; + img { - border: 0px; + border: 0; border-radius: 5px; height: 100%; object-fit: cover; @@ -172,9 +186,10 @@ flex: 2; margin: 8px; word-break: break-all; + h1 { font-size: 1rem; - margin: 0px; + margin: 0; } } } @@ -252,17 +267,9 @@ cursor: progress; } - &.-contain-fit { - img, - canvas { - object-fit: contain; - } - } - - &.-cover-fit { - img, - canvas { - object-fit: cover; + &.-compact { + .placeholder-container { + padding-bottom: 0.5em; } } } diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 2a89886d7..79f628061 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -162,10 +162,11 @@ target="_blank" > -

+

{{ localDescription }}

diff --git a/src/components/autosuggest/autosuggest.vue b/src/components/autosuggest/autosuggest.vue index f283ab82a..7b7102fd2 100644 --- a/src/components/autosuggest/autosuggest.vue +++ b/src/components/autosuggest/autosuggest.vue @@ -24,7 +24,7 @@ diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue index 1248c4c8c..27a475edf 100644 --- a/src/components/chat_list/chat_list.vue +++ b/src/components/chat_list/chat_list.vue @@ -45,7 +45,7 @@ diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss index 1913479f2..fd5b7aa45 100644 --- a/src/components/chat_message/chat_message.scss +++ b/src/components/chat_message/chat_message.scss @@ -1,12 +1,12 @@ -@import '../../_variables.scss'; +@import "../../variables"; .chat-message-wrapper { - &.hovered-message-chain { .animated.Avatar { canvas { display: none; } + img { visibility: visible; } @@ -28,7 +28,8 @@ .menu-icon { cursor: pointer; - &:hover, .extra-button-popover.open & { + &:hover, + .extra-button-popover.open & { color: $fallback--text; color: var(--text, $fallback--text); } @@ -54,27 +55,11 @@ width: 32px; } - .link-preview, .attachments { + .link-preview, + .attachments { margin-bottom: 1em; } - .chat-message-inner { - display: flex; - flex-direction: column; - align-items: flex-start; - max-width: 80%; - min-width: 10em; - width: 100%; - - &.with-media { - width: 100%; - - .status { - width: 100%; - } - } - } - .status { border-radius: $fallback--chatMessageRadius; border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius); @@ -86,7 +71,7 @@ position: relative; float: right; font-size: 0.8em; - margin: -1em 0 -0.5em 0; + margin: -1em 0 -0.5em; font-style: italic; opacity: 0.8; } @@ -103,18 +88,54 @@ } .pending { - .status-content.media-body, .created-at { + .status-content.media-body, + .created-at { color: var(--faint); } } .error { - .status-content.media-body, .created-at { + .status-content.media-body, + .created-at { color: $fallback--cRed; color: var(--badgeNotification, $fallback--cRed); } } + .chat-message-inner { + display: flex; + flex-direction: column; + align-items: flex-start; + max-width: 80%; + min-width: 10em; + width: 100%; + } + + .outgoing { + display: flex; + flex-flow: row wrap; + align-content: end; + justify-content: flex-end; + + a { + color: var(--chatMessageOutgoingLink, $fallback--link); + } + + .status { + color: var(--chatMessageOutgoingText, $fallback--text); + background-color: var(--chatMessageOutgoingBg, $fallback--lightBg); + border: 1px solid var(--chatMessageOutgoingBorder, --lightBg); + } + + .chat-message-inner { + align-items: flex-end; + } + + .chat-message-menu { + right: 0.4rem; + } + } + .incoming { a { color: var(--chatMessageIncomingLink, $fallback--link); @@ -137,36 +158,17 @@ } } - .outgoing { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-content: end; - justify-content: flex-end; - - a { - color: var(--chatMessageOutgoingLink, $fallback--link); - } + .chat-message-inner.with-media { + width: 100%; .status { - color: var(--chatMessageOutgoingText, $fallback--text); - background-color: var(--chatMessageOutgoingBg, $fallback--lightBg); - border: 1px solid var(--chatMessageOutgoingBorder, --lightBg); - } - - .chat-message-inner { - align-items: flex-end; - } - - .chat-message-menu { - right: 0.4rem; + width: 100%; } } .visible { opacity: 1; } - } .chat-message-date-separator { diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue index d635c47e4..381574c32 100644 --- a/src/components/chat_message/chat_message.vue +++ b/src/components/chat_message/chat_message.vue @@ -33,7 +33,7 @@
@@ -98,6 +98,6 @@ diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss index 240e1a380..b145ecf9a 100644 --- a/src/components/chat_new/chat_new.scss +++ b/src/components/chat_new/chat_new.scss @@ -1,7 +1,7 @@ .chat-new { .input-wrap { display: flex; - margin: 0.7em 0.5em 0.7em 0.5em; + margin: 0.7em 0.5em; input { width: 100%; diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue index bf09a3796..52306c1da 100644 --- a/src/components/chat_new/chat_new.vue +++ b/src/components/chat_new/chat_new.vue @@ -46,6 +46,6 @@ diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue index ab7491fae..93db4fa7f 100644 --- a/src/components/chat_title/chat_title.vue +++ b/src/components/chat_title/chat_title.vue @@ -26,7 +26,7 @@ diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss index 3de31fded..ca46199a5 100644 --- a/src/components/color_input/color_input.scss +++ b/src/components/color_input/color_input.scss @@ -1,4 +1,4 @@ -@import '../../_variables.scss'; +@import "../../variables"; .color-input { display: inline-flex; @@ -8,7 +8,7 @@ flex: 0 0 0; max-width: 9em; align-items: stretch; - padding: .2em 8px; + padding: 0.2em 8px; input { background: none; @@ -31,6 +31,7 @@ min-height: 100%; } } + .computedIndicator, .transparentIndicator { flex: 0 0 2em; @@ -38,22 +39,27 @@ align-self: stretch; min-height: 100%; } + .transparentIndicator { // forgot to install counter-strike source, ooops - background-color: #FF00FF; + background-color: #f0f; position: relative; - &::before, &::after { + + &::before, + &::after { display: block; - content: ''; - background-color: #000000; + content: ""; + background-color: #000; position: absolute; height: 50%; width: 50%; } + &::after { top: 0; left: 0; } + &::before { bottom: 0; right: 0; @@ -64,5 +70,4 @@ .label { flex: 1 1 auto; } - } diff --git a/src/components/confirm_modal/confirm_modal.js b/src/components/confirm_modal/confirm_modal.js new file mode 100644 index 000000000..96ddc118f --- /dev/null +++ b/src/components/confirm_modal/confirm_modal.js @@ -0,0 +1,37 @@ +import DialogModal from '../dialog_modal/dialog_modal.vue' + +/** + * This component emits the following events: + * cancelled, emitted when the action should not be performed; + * accepted, emitted when the action should be performed; + * + * The caller should close this dialog after receiving any of the two events. + */ +const ConfirmModal = { + components: { + DialogModal + }, + props: { + title: { + type: String + }, + cancelText: { + type: String + }, + confirmText: { + type: String + } + }, + computed: { + }, + methods: { + onCancel () { + this.$emit('cancelled') + }, + onAccept () { + this.$emit('accepted') + } + } +} + +export default ConfirmModal diff --git a/src/components/confirm_modal/confirm_modal.vue b/src/components/confirm_modal/confirm_modal.vue new file mode 100644 index 000000000..3b98174aa --- /dev/null +++ b/src/components/confirm_modal/confirm_modal.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue index 374cb9ba2..bbd6fd4a5 100644 --- a/src/components/contrast_ratio/contrast_ratio.vue +++ b/src/components/contrast_ratio/contrast_ratio.vue @@ -87,7 +87,6 @@ export default { .contrast-ratio { display: flex; justify-content: flex-end; - margin-top: -4px; margin-bottom: 5px; diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index afa04db06..7577129e2 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -210,17 +210,16 @@ diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index 3dc968c98..48b960b2b 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -1,4 +1,5 @@ import Popover from '../popover/popover.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faEllipsisH, @@ -32,10 +33,14 @@ library.add( const ExtraButtons = { props: ['status'], - components: { Popover }, + components: { + Popover, + ConfirmModal + }, data () { return { - expanded: false + expanded: false, + showingDeleteDialog: false } }, methods: { @@ -46,11 +51,22 @@ const ExtraButtons = { this.expanded = false }, deleteStatus () { - const confirmed = window.confirm(this.$t('status.delete_confirm')) - if (confirmed) { - this.$store.dispatch('deleteStatus', { id: this.status.id }) + if (this.shouldConfirmDelete) { + this.showDeleteStatusConfirmDialog() + } else { + this.doDeleteStatus() } }, + doDeleteStatus () { + this.$store.dispatch('deleteStatus', { id: this.status.id }) + this.hideDeleteStatusConfirmDialog() + }, + showDeleteStatusConfirmDialog () { + this.showingDeleteDialog = true + }, + hideDeleteStatusConfirmDialog () { + this.showingDeleteDialog = false + }, pinStatus () { this.$store.dispatch('pinStatus', this.status.id) .then(() => this.$emit('onSuccess')) @@ -133,7 +149,10 @@ const ExtraButtons = { isEdited () { return this.status.edited_at !== null }, - editingAvailable () { return this.$store.state.instance.editingAvailable } + editingAvailable () { return this.$store.state.instance.editingAvailable }, + shouldConfirmDelete () { + return this.$store.getters.mergedConfig.modalOnDelete + } } } diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index b2fad1c95..c1c15c0fb 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -165,6 +165,18 @@ /> + + + {{ $t('status.delete_confirm') }} + + @@ -172,15 +184,10 @@ diff --git a/src/components/follow_button/follow_button.js b/src/components/follow_button/follow_button.js index 3edbcb86f..443aa9bcc 100644 --- a/src/components/follow_button/follow_button.js +++ b/src/components/follow_button/follow_button.js @@ -1,12 +1,20 @@ +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' export default { props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], + components: { + ConfirmModal + }, data () { return { - inProgress: false + inProgress: false, + showingConfirmUnfollow: false } }, computed: { + shouldConfirmUnfollow () { + return this.$store.getters.mergedConfig.modalOnUnfollow + }, isPressed () { return this.inProgress || this.relationship.following }, @@ -35,6 +43,12 @@ export default { } }, methods: { + showConfirmUnfollow () { + this.showingConfirmUnfollow = true + }, + hideConfirmUnfollow () { + this.showingConfirmUnfollow = false + }, onClick () { this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() }, @@ -45,12 +59,21 @@ export default { }) }, unfollow () { + if (this.shouldConfirmUnfollow) { + this.showConfirmUnfollow() + } else { + this.doUnfollow() + } + }, + doUnfollow () { const store = this.$store this.inProgress = true requestUnfollow(this.relationship.id, store).then(() => { this.inProgress = false store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id }) }) + + this.hideConfirmUnfollow() } } } diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue index 965d5256a..e421c15b3 100644 --- a/src/components/follow_button/follow_button.vue +++ b/src/components/follow_button/follow_button.vue @@ -7,6 +7,27 @@ @click="onClick" > {{ label }} + + + + + + + diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue index c919b11aa..bdb6b8092 100644 --- a/src/components/follow_card/follow_card.vue +++ b/src/components/follow_card/follow_card.vue @@ -24,6 +24,7 @@ />
+ + + {{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }} + + + {{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }} + + @@ -22,8 +44,8 @@ diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue index d828b8191..0e58476f1 100644 --- a/src/components/global_notice_list/global_notice_list.vue +++ b/src/components/global_notice_list/global_notice_list.vue @@ -25,7 +25,7 @@ diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue index 220527f29..09f341ac4 100644 --- a/src/components/link-preview/link-preview.vue +++ b/src/components/link-preview/link-preview.vue @@ -33,7 +33,7 @@ diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js index c22d1323d..c33659dfc 100644 --- a/src/components/lists_edit/lists_edit.js +++ b/src/components/lists_edit/lists_edit.js @@ -95,10 +95,10 @@ const ListsNew = { return this.addedUserIds.has(user.id) }, addUser (user) { - this.$store.dispatch('addListAccount', { accountId: this.user.id, listId: this.id }) + this.$store.dispatch('addListAccount', { accountId: user.id, listId: this.id }) }, removeUser (userId) { - this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id }) + this.$store.dispatch('removeListAccount', { accountId: userId, listId: this.id }) }, onSearchLoading (results) { this.searchLoading = true diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue index 6521aba62..eec0f9786 100644 --- a/src/components/lists_edit/lists_edit.vue +++ b/src/components/lists_edit/lists_edit.vue @@ -164,7 +164,7 @@ diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue index a538a5ede..2799495bb 100644 --- a/src/components/media_upload/media_upload.vue +++ b/src/components/media_upload/media_upload.vue @@ -29,7 +29,7 @@ diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue index 28a2c4404..ef0f51fe3 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.vue +++ b/src/components/mobile_post_status_button/mobile_post_status_button.vue @@ -13,7 +13,7 @@ diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index 8535ef273..b708cdc8f 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -166,18 +166,21 @@ diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue index ca33c6c53..97e91cbca 100644 --- a/src/components/mute_card/mute_card.vue +++ b/src/components/mute_card/mute_card.vue @@ -37,6 +37,7 @@ .mute-card-content-container { margin-top: 0.5em; text-align: right; + button { width: 10em; } diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index d628c3805..1a826cc43 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -102,7 +102,7 @@ diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue index f4d53836d..411ca5366 100644 --- a/src/components/navigation/navigation_entry.vue +++ b/src/components/navigation/navigation_entry.vue @@ -63,7 +63,7 @@ diff --git a/src/components/remove_follower_button/remove_follower_button.js b/src/components/remove_follower_button/remove_follower_button.js index e1a7531bf..052a519ff 100644 --- a/src/components/remove_follower_button/remove_follower_button.js +++ b/src/components/remove_follower_button/remove_follower_button.js @@ -1,10 +1,16 @@ +import ConfirmModal from '../confirm_modal/confirm_modal.vue' + export default { - props: ['relationship'], + props: ['user', 'relationship'], data () { return { - inProgress: false + inProgress: false, + showingConfirmRemoveFollower: false } }, + components: { + ConfirmModal + }, computed: { label () { if (this.inProgress) { @@ -12,14 +18,31 @@ export default { } else { return this.$t('user_card.remove_follower') } + }, + shouldConfirmRemoveUserFromFollowers () { + return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers } }, methods: { + showConfirmRemoveUserFromFollowers () { + this.showingConfirmRemoveFollower = true + }, + hideConfirmRemoveUserFromFollowers () { + this.showingConfirmRemoveFollower = false + }, onClick () { + if (!this.shouldConfirmRemoveUserFromFollowers) { + this.doRemoveUserFromFollowers() + } else { + this.showConfirmRemoveUserFromFollowers() + } + }, + doRemoveUserFromFollowers () { this.inProgress = true this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => { this.inProgress = false }) + this.hideConfirmRemoveUserFromFollowers() } } } diff --git a/src/components/remove_follower_button/remove_follower_button.vue b/src/components/remove_follower_button/remove_follower_button.vue index a3a4c2426..0012aebd1 100644 --- a/src/components/remove_follower_button/remove_follower_button.vue +++ b/src/components/remove_follower_button/remove_follower_button.vue @@ -7,6 +7,27 @@ @click="onClick" > {{ label }} + + + + + + + diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue index dada511be..6e3964b72 100644 --- a/src/components/reply_button/reply_button.vue +++ b/src/components/reply_button/reply_button.vue @@ -51,8 +51,8 @@ diff --git a/src/components/report/report.scss b/src/components/report/report.scss index 578b4eb1b..9762400b1 100644 --- a/src/components/report/report.scss +++ b/src/components/report/report.scss @@ -1,4 +1,4 @@ -@import '../../_variables.scss'; +@import "../../variables"; .Report { .report-content { diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js index 4d92b5fa7..198b6c14b 100644 --- a/src/components/retweet_button/retweet_button.js +++ b/src/components/retweet_button/retweet_button.js @@ -1,3 +1,4 @@ +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faRetweet, @@ -15,13 +16,24 @@ library.add( const RetweetButton = { props: ['status', 'loggedIn', 'visibility'], + components: { + ConfirmModal + }, data () { return { - animated: false + animated: false, + showingConfirmDialog: false } }, methods: { retweet () { + if (!this.status.repeated && this.shouldConfirmRepeat) { + this.showConfirmDialog() + } else { + this.doRetweet() + } + }, + doRetweet () { if (!this.status.repeated) { this.$store.dispatch('retweet', { id: this.status.id }) } else { @@ -31,6 +43,13 @@ const RetweetButton = { setTimeout(() => { this.animated = false }, 500) + this.hideConfirmDialog() + }, + showConfirmDialog () { + this.showingConfirmDialog = true + }, + hideConfirmDialog () { + this.showingConfirmDialog = false } }, computed: { @@ -39,6 +58,9 @@ const RetweetButton = { }, remoteInteractionLink () { return this.$store.getters.remoteInteractionLink({ statusId: this.status.id }) + }, + shouldConfirmRepeat () { + return this.mergedConfig.modalOnRepeat } } } diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index 240828e3d..134fcb36a 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -59,14 +59,26 @@ > {{ status.repeat_num }} + + + {{ $t('status.repeat_confirm') }} + + diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue index 199a75004..3969d8de3 100644 --- a/src/components/search_bar/search_bar.vue +++ b/src/components/search_bar/search_bar.vue @@ -56,7 +56,7 @@ diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue index 1f7683abb..14910fc5d 100644 --- a/src/components/selectable_list/selectable_list.vue +++ b/src/components/selectable_list/selectable_list.vue @@ -51,7 +51,7 @@ diff --git a/src/components/settings_modal/helpers/size_setting.vue b/src/components/settings_modal/helpers/size_setting.vue index 90c9f5389..5a78f1004 100644 --- a/src/components/settings_modal/helpers/size_setting.vue +++ b/src/components/settings_modal/helpers/size_setting.vue @@ -45,10 +45,11 @@ diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss index 13cb0e650..f58612299 100644 --- a/src/components/settings_modal/settings_modal.scss +++ b/src/components/settings_modal/settings_modal.scss @@ -1,4 +1,5 @@ -@import 'src/_variables.scss'; +@import "src/variables"; + .settings-modal { overflow: hidden; @@ -6,32 +7,13 @@ .option-list { list-style-type: none; padding-left: 2em; + li { margin-bottom: 0.5em; } + .suboptions { - margin-top: 0.3em - } - } - - &.peek { - .settings-modal-panel { - /* Explanation: - * Modal is positioned vertically centered. - * 100vh - 100% = Distance between modal's top+bottom boundaries and screen - * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen - * + 100% - we move modal completely off-screen, it's top boundary touches - * bottom of the screen - * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible - */ - transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px)); - - @media all and (max-width: 800px) { - /* For mobile, the modal takes 100% of the available screen. - This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible. - */ - transform: translateY(calc(100% - 50px)); - } + margin-top: 0.3em; } } @@ -63,6 +45,7 @@ .settings-footer { display: flex; + >* { margin-right: 0.5em; } @@ -72,4 +55,26 @@ flex-grow: 1; } } + + &.peek { + .settings-modal-panel { + /* Explanation: + * Modal is positioned vertically centered. + * 100vh - 100% = Distance between modal's top+bottom boundaries and screen + * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen + * + 100% - we move modal completely off-screen, it's top boundary touches + * bottom of the screen + * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible + */ + transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px)); + + @media all and (max-width: 800px) { + /* For mobile, the modal takes 100% of the available screen. + This ensures the minimized modal is always 50px above the browser bottom + bar regardless of whether or not it is visible. + */ + transform: translateY(calc(100% - 50px)); + } + } + } } diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_content.scss index 81ab434b6..87df79821 100644 --- a/src/components/settings_modal/settings_modal_content.scss +++ b/src/components/settings_modal/settings_modal_content.scss @@ -1,4 +1,5 @@ -@import 'src/_variables.scss'; +@import "src/variables"; + .settings_tab-switcher { height: 100%; @@ -10,7 +11,8 @@ > div, > label { display: block; - margin-bottom: .5em; + margin-bottom: 0.5em; + &:last-child { margin-bottom: 0; } @@ -21,7 +23,7 @@ .option-list { margin: 0; - padding-left: .5em; + padding-left: 0.5em; } } diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue index e3b7f407c..48356c9b5 100644 --- a/src/components/settings_modal/tabs/data_import_export_tab.vue +++ b/src/components/settings_modal/tabs/data_import_export_tab.vue @@ -77,6 +77,16 @@ > {{ $t('settings.download_backup') }} + + {{ $tc('settings.backup_running', backup.processed_number, { number: backup.processed_number }) }} + + + {{ $t('settings.backup_failed') }} + diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index 73413b488..7c37f0bcb 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -1,4 +1,4 @@ -import { filter, trim } from 'lodash' +import { filter, trim, debounce } from 'lodash' import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' import IntegerSetting from '../helpers/integer_setting.vue' @@ -29,24 +29,20 @@ const FilteringTab = { }, set (value) { this.muteWordsStringLocal = value + this.debouncedSetMuteWords(value) + } + }, + debouncedSetMuteWords () { + return debounce((value) => { this.$store.dispatch('setOption', { name: 'muteWords', value: filter(value.split('\n'), (word) => trim(word).length > 0) }) - } + }, 1000) } }, // Updating nested properties watch: { - notificationVisibility: { - handler (value) { - this.$store.dispatch('setOption', { - name: 'notificationVisibility', - value: this.$store.getters.mergedConfig.notificationVisibility - }) - }, - deep: true - }, replyVisibility () { this.$store.dispatch('queueFlushAll') } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 8561647b9..703e94a0f 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -148,6 +148,56 @@ +
  • + {{ $t('settings.confirm_dialogs') }} +
      +
    • + + {{ $t('settings.confirm_dialogs_repeat') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_unfollow') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_block') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_mute') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_delete') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_logout') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_approve_follow') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_deny_follow') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_remove_follower') }} + +
    • +
    +
  • @@ -464,6 +514,7 @@ justify-content: space-evenly; flex-wrap: wrap; } + .column-settings .size-label { display: block; margin-bottom: 0.5em; diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss index 2adff8475..5fa3a27b1 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss @@ -1,29 +1,29 @@ .mutes-and-blocks-tab { - height: 100%; + height: 100%; - .usersearch-wrapper { - padding: 1em; - } + .usersearch-wrapper { + padding: 1em; + } - .bulk-actions { - text-align: right; - padding: 0 1em; - min-height: 2em; - } + .bulk-actions { + text-align: right; + padding: 0 1em; + min-height: 2em; + } - .bulk-action-button { - width: 10em - } + .bulk-action-button { + width: 10em; + } - .domain-mute-form { - padding: 1em; - display: flex; - flex-direction: column - } + .domain-mute-form { + padding: 1em; + display: flex; + flex-direction: column; + } - .domain-mute-button { - align-self: flex-end; - margin-top: 1em; - width: 10em - } + .domain-mute-button { + align-self: flex-end; + margin-top: 1em; + width: 10em; + } } diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index b86faef0e..72d27083b 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -32,6 +32,8 @@ const ProfileTab = { 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, @@ -43,7 +45,7 @@ const ProfileTab = { bannerPreview: null, background: null, backgroundPreview: null, - emailLanguage: this.$store.state.users.currentUser.language || '' + emailLanguage: this.$store.state.users.currentUser.language || [''] } }, components: { @@ -125,12 +127,14 @@ const ProfileTab = { display_name: this.newName, fields_attributes: this.newFields.filter(el => el != null), bot: this.bot, - show_role: this.showRole + show_role: this.showRole, + birthday: this.newBirthday || '', + show_birthday: this.showBirthday /* eslint-enable camelcase */ } if (this.emailLanguage) { - params.language = localeService.internalToBackendLocale(this.emailLanguage) + params.language = localeService.internalToBackendLocaleMulti(this.emailLanguage) } this.$store.state.api.backendInteractor @@ -153,7 +157,7 @@ const ProfileTab = { return false }, deleteField (index, event) { - this.$delete(this.newFields, index) + this.newFields.splice(index, 1) }, uploadFile (slot, e) { const file = e.target.files[0] diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss index 201f1a764..ee253ffe3 100644 --- a/src/components/settings_modal/tabs/profile_tab.scss +++ b/src/components/settings_modal/tabs/profile_tab.scss @@ -1,4 +1,5 @@ -@import '../../../_variables.scss'; +@import "../../../variables"; + .profile-tab { .bio { margin: 0; @@ -8,7 +9,7 @@ padding-top: 5px; } - input[type=file] { + input[type="file"] { padding: 5px; height: auto; } @@ -52,7 +53,7 @@ right: 0.2em; border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - background-color: rgba(0, 0, 0, 0.6); + background-color: rgb(0 0 0 / 60%); opacity: 0.7; width: 1.5em; height: 1.5em; @@ -128,4 +129,9 @@ 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 642d54cab..faa05bcc9 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -35,6 +35,18 @@

    +
    +

    {{ $t('settings.birthday.label') }}

    + + + {{ $t('settings.birthday.show_birthday') }} + +

    {{ $t('settings.profile_fields.label') }}

    diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue index c6fffc71a..047272784 100644 --- a/src/components/thread_tree/thread_tree.vue +++ b/src/components/thread_tree/thread_tree.vue @@ -119,7 +119,8 @@ diff --git a/src/components/update_notification/update_notification.scss b/src/components/update_notification/update_notification.scss index ce8129d0f..4337acc4f 100644 --- a/src/components/update_notification/update_notification.scss +++ b/src/components/update_notification/update_notification.scss @@ -1,4 +1,5 @@ -@import 'src/_variables.scss'; +@import "src/variables"; + .UpdateNotification { overflow: hidden; } @@ -21,7 +22,8 @@ @media all and (max-width: 800px) { /* For mobile, the modal takes 100% of the available screen. - This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible. + This ensures the minimized modal is always 50px above the browser + bottom bar regardless of whether or not it is visible. */ width: 100vw; } @@ -44,7 +46,7 @@ } .panel-body { - border-width: 0 0 1px 0; + border-width: 0 0 1px; border-style: solid; border-color: var(--border, $fallback--border); } @@ -67,7 +69,7 @@ z-index: 20; position: relative; shape-margin: 0.5em; - filter: drop-shadow(5px 5px 10px rgba(0,0,0,0.5)); + filter: drop-shadow(5px 5px 10px rgb(0 0 0 / 50%)); pointer-events: none; } @@ -94,7 +96,7 @@ } &.-peek { - /* Explanation: + /* Explanation: * 100vh - 100% = Distance between modal's top+bottom boundaries and screen * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen */ @@ -103,7 +105,7 @@ .pleroma-tan { float: right; z-index: 10; - shape-image-threshold: 0.7; + shape-image-threshold: 70%; } .extra-info-group { diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue index f4d294df3..91c176118 100644 --- a/src/components/user_avatar/user_avatar.vue +++ b/src/components/user_avatar/user_avatar.vue @@ -27,7 +27,7 @@ diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 8b64a07e5..e17bf8eb2 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -1,12 +1,15 @@ +import { unitToSeconds } from 'src/services/date_utils/date_utils.js' import UserAvatar from '../user_avatar/user_avatar.vue' import RemoteFollow from '../remote_follow/remote_follow.vue' import ProgressButton from '../progress_button/progress_button.vue' import FollowButton from '../follow_button/follow_button.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue' import AccountActions from '../account_actions/account_actions.vue' +import UserNote from '../user_note/user_note.vue' 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 ConfirmModal from '../confirm_modal/confirm_modal.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' @@ -39,12 +42,16 @@ export default { 'rounded', 'bordered', 'avatarAction', // default - open profile, 'zoom' - zoom, function - call function - 'onClose' + 'onClose', + 'hasNoteEditor' ], data () { return { followRequestInProgress: false, - betterShadow: this.$store.state.interface.browserSupport.cssFilter + betterShadow: this.$store.state.interface.browserSupport.cssFilter, + showingConfirmMute: false, + muteExpiryAmount: 0, + muteExpiryUnit: 'minutes' } }, created () { @@ -129,6 +136,18 @@ export default { const privileges = this.loggedIn.privileges return this.loggedIn.role === 'admin' || privileges.includes('users_manage_activation_state') || privileges.includes('users_delete') || privileges.includes('users_manage_tags') }, + hasNote () { + return this.relationship.note + }, + supportsNote () { + return 'note' in this.relationship + }, + shouldConfirmMute () { + return this.mergedConfig.modalOnMute + }, + muteExpiryUnits () { + return ['minutes', 'hours', 'days'] + }, ...mapGetters(['mergedConfig']) }, components: { @@ -140,11 +159,30 @@ export default { FollowButton, Select, RichContent, - UserLink + UserLink, + UserNote, + ConfirmModal }, methods: { + showConfirmMute () { + this.showingConfirmMute = true + }, + hideConfirmMute () { + this.showingConfirmMute = false + }, muteUser () { - this.$store.dispatch('muteUser', this.user.id) + if (!this.shouldConfirmMute) { + this.doMuteUser() + } else { + this.showConfirmMute() + } + }, + doMuteUser () { + this.$store.dispatch('muteUser', { + id: this.user.id, + expiresIn: this.shouldConfirmMute ? unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount) : 0 + }) + this.hideConfirmMute() }, unmuteUser () { this.$store.dispatch('unmuteUser', this.user.id) diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss index a0bbc6a68..4ab93a8a3 100644 --- a/src/components/user_card/user_card.scss +++ b/src/components/user_card/user_card.scss @@ -1,4 +1,4 @@ -@import '../../_variables.scss'; +@import "../../variables"; .user-card { position: relative; @@ -11,7 +11,7 @@ } .panel-heading { - padding: .5em 0; + padding: 0.5em 0; text-align: center; box-shadow: none; background: transparent; @@ -35,10 +35,11 @@ left: 0; right: 0; bottom: 0; - mask: linear-gradient(to top, white, transparent) bottom no-repeat, - linear-gradient(to top, white, white); + mask: + linear-gradient(to top, white, transparent) bottom no-repeat, + linear-gradient(to top, white, white); // Autoprefixer seem to ignore this one, and also syntax is different - -webkit-mask-composite: xor; + mask-composite: xor; mask-composite: exclude; background-size: cover; mask-size: 100% 60%; @@ -159,17 +160,17 @@ top: 0; right: 0; bottom: 0; - background-color: rgba(0, 0, 0, 0.3); + background-color: rgb(0 0 0 / 30%); display: flex; justify-content: center; align-items: center; border-radius: $fallback--avatarRadius; border-radius: var(--avatarRadius, $fallback--avatarRadius); opacity: 0; - transition: opacity .2s ease; + transition: opacity 0.2s ease; svg { - color: #FFF; + color: #fff; } } @@ -178,7 +179,8 @@ } } - .external-link-button, .edit-profile-button { + .external-link-button, + .edit-profile-button { cursor: pointer; width: 2.5em; text-align: center; @@ -191,34 +193,6 @@ } } - .user-summary { - display: block; - margin-left: 0.6em; - text-align: left; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1 1 0; - // This is so that text doesn't get overlapped by avatar's shadow if it has - // big one - z-index: 1; - line-height: 2em; - - --emoji-size: 1.7em; - - .top-line, - .bottom-line { - display: flex; - } - } - - .user-name { - text-overflow: ellipsis; - overflow: hidden; - flex: 1 1 auto; - margin-right: 1em; - font-size: 1.1em; - } - .bottom-line { font-weight: light; font-size: 1.1em; @@ -253,8 +227,36 @@ } } + .user-summary { + display: block; + margin-left: 0.6em; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1 1 0; + // This is so that text doesn't get overlapped by avatar's shadow if it has + // big one + z-index: 1; + line-height: 2em; + + --emoji-size: 1.7em; + + .top-line, + .bottom-line { + display: flex; + } + } + + .user-name { + text-overflow: ellipsis; + overflow: hidden; + flex: 1 1 auto; + margin-right: 1em; + font-size: 1.1em; + } + .user-meta { - margin-bottom: .15em; + margin-bottom: 0.15em; display: flex; align-items: baseline; line-height: 22px; @@ -263,7 +265,7 @@ .following { flex: 1 0 auto; margin: 0; - margin-bottom: .25em; + margin-bottom: 0.25em; text-align: left; } @@ -271,7 +273,7 @@ flex: 0 1 auto; display: flex; flex-wrap: wrap; - margin-right: -.5em; + margin-right: -0.5em; align-self: start; .userHighlightCl { @@ -294,19 +296,20 @@ .userHighlightText, .userHighlightSel { vertical-align: top; - margin-right: .5em; - margin-bottom: .25em; + margin-right: 0.5em; + margin-bottom: 0.25em; } } } + .user-interactions { position: relative; display: flex; flex-flow: row wrap; - margin-right: -.75em; + margin-right: -0.75em; > * { - margin: 0 .75em .6em 0; + margin: 0 0.75em 0.6em 0; white-space: nowrap; min-width: 95px; } @@ -315,6 +318,10 @@ margin: 0; } } + + .user-note { + margin: 0 0.75em 0.6em 0; + } } .sidebar .edit-profile-button { @@ -323,8 +330,8 @@ .user-counts { display: flex; - line-height:16px; - padding: .5em 1.5em 0em 1.5em; + line-height: 16px; + padding: 0.5em 1.5em 0; text-align: center; justify-content: space-between; color: $fallback--lightText; @@ -334,15 +341,22 @@ .user-count { flex: 1 0 auto; - padding: .5em 0 .5em 0; - margin: 0 .5em; + padding: 0.5em 0; + margin: 0 0.5em; h5 { - font-size:1em; + font-size: 1em; font-weight: bolder; margin: 0 0 0.25em; } + + /* stylelint-disable-next-line no-descending-specificity */ a { text-decoration: none; } } + +.mute-expiry { + display: flex; + flex-direction: row; +} diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 897d89f99..2de14063d 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -268,6 +268,12 @@ >
    +
    + + + + + +
    + + + +
    +
    +
    diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue index 635dc7f6c..8307cc8af 100644 --- a/src/components/user_list_popover/user_list_popover.vue +++ b/src/components/user_list_popover/user_list_popover.vue @@ -48,7 +48,7 @@ diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue index 53d51fc4d..3b2bbc452 100644 --- a/src/components/user_popover/user_popover.vue +++ b/src/components/user_popover/user_popover.vue @@ -24,10 +24,12 @@ diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 08adaeab0..acb612ed4 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -7,13 +7,16 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx' import List from '../list/list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' +import localeService from 'src/services/locale/locale.service.js' import { library } from '@fortawesome/fontawesome-svg-core' import { - faCircleNotch + faCircleNotch, + faBirthdayCake } from '@fortawesome/free-solid-svg-icons' library.add( - faCircleNotch + faCircleNotch, + faBirthdayCake ) const FollowerList = withLoadMore({ @@ -76,6 +79,10 @@ const UserProfile = { }, followersTabVisible () { return this.isUs || !this.user.hide_followers + }, + formattedBirthday () { + const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale) + return this.user.birthday && new Date(Date.parse(this.user.birthday)).toLocaleDateString(browserLocale, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' }) } }, methods: { diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index d0da2b5b1..c63a303cd 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -10,7 +10,18 @@ :selected="timeline.viewing" avatar-action="zoom" rounded="top" + :has-note-editor="true" /> + + + {{ $t('user_card.birthday', { birthday: formattedBirthday }) }} +