Merge branch 'develop' into 'master'

Update master branch

See merge request pleroma/pleroma-fe!1861
This commit is contained in:
HJ 2023-10-29 16:26:05 +00:00
commit b6accf9e7f
292 changed files with 10829 additions and 3523 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
/build/webpack.prod.conf.js export-subst

View File

@ -4,11 +4,36 @@
image: node:16 image: node:16
stages: stages:
- check-changelog
- lint - lint
- build - build
- test - test
- deploy - deploy
# https://git.pleroma.social/help/ci/yaml/workflow.md#switch-between-branch-pipelines-and-merge-request-pipelines
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
check-changelog:
stage: check-changelog
image: alpine
rules:
- if: $CI_MERGE_REQUEST_SOURCE_PROJECT_PATH == 'pleroma/pleroma-fe' && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^renovate/
when: never
- if: $CI_MERGE_REQUEST_SOURCE_PROJECT_PATH == 'pleroma/pleroma-fe' && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == 'weblate'
when: never
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
before_script: ''
after_script: ''
cache: {}
script:
- apk add git
- sh ./tools/check-changelog
lint: lint:
stage: lint stage: lint
script: script:

View File

@ -1,19 +1,41 @@
{ {
"extends": [ "extends": [
"stylelint-rscss/config", "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": { "rules": {
"declaration-no-important": true, "declaration-no-important": true,
"rscss/no-descendant-combinator": false, "rscss/no-descendant-combinator": false,
"rscss/class-format": [ "rscss/class-format": [
true, false,
{ {
"component": "pascal-case", "component": "pascal-case",
"variant": "^-[a-z]\\w+", "variant": "^-[a-z]\\w+",
"element": "^[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"
]
}
] ]
} }
} }

View File

@ -3,6 +3,34 @@ 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/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.5.1
### Fixed
- Checkboxes in settings can now work with screenreaders
- Autocomplete in edit boxes can now work with screenreaders
- Status interact buttons now have focus indicator for anonymous users
- Top bar buttons now correctly have text labels
- It is now possible to register if the site admin requires birthday to register
- User cards from search results will correctly popup
- Fix notification attachment icon overflow
- Editing mute words is less laggy
- Repeater's name will no longer mess up with the directionality of the text sitting on the same line
- Unauthenticated access will give better error messages
- It is now easier to close the media viewer with a mouse when there is only one image
- Deleting profile fields can work properly
- Clicking the react button will correctly focus the search box
- Clicking buttons on the top-bar will no longer bring you to the top of the page
- Emoji picker is much faster to load
- `blockquote`s have a better display style
- Announcements posting and editing are now available to everyone with such a privilege, not just admins
- Adding or removing list members will actually work
- Emojis without a pack are now correctly displayed in emoji picker
- Changing notification settings will actually work
### Added
- You can now set and see birthdays
- Optional confirmation dialogs when performing various actions
- You can now set fallback languages
## 2.5.0 - 23.12.2022 ## 2.5.0 - 23.12.2022
### Fixed ### Fixed
- UI no longer lags when switching between mobile and desktop mode - UI no longer lags when switching between mobile and desktop mode

View File

@ -6,7 +6,7 @@ var ServiceWorkerWebpackPlugin = require('serviceworker-webpack5-plugin')
var CopyPlugin = require('copy-webpack-plugin'); var CopyPlugin = require('copy-webpack-plugin');
var { VueLoaderPlugin } = require('vue-loader') var { VueLoaderPlugin } = require('vue-loader')
var ESLintPlugin = require('eslint-webpack-plugin'); var ESLintPlugin = require('eslint-webpack-plugin');
var StylelintPlugin = require('stylelint-webpack-plugin');
var env = process.env.NODE_ENV var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the // check env & config/index.js to decide weither to enable CSS Sourcemaps for the
@ -111,6 +111,7 @@ module.exports = {
extensions: ['js', 'vue'], extensions: ['js', 'vue'],
formatter: require('eslint-formatter-friendly') formatter: require('eslint-formatter-friendly')
}), }),
new StylelintPlugin({}),
new VueLoaderPlugin(), new VueLoaderPlugin(),
// This copies Ruffle's WASM to a directory so that JS side can access it // This copies Ruffle's WASM to a directory so that JS side can access it
new CopyPlugin({ new CopyPlugin({

View File

@ -11,9 +11,16 @@ var env = process.env.NODE_ENV === 'testing'
? require('../config/test.env') ? require('../config/test.env')
: config.build.env : config.build.env
let commitHash = require('child_process') let commitHash = (() => {
const subst = "$Format:%h$";
if(!subst.match(/Format:/)) {
return subst;
} else {
return require('child_process')
.execSync('git rev-parse --short HEAD') .execSync('git rev-parse --short HEAD')
.toString(); .toString();
}
})();
var webpackConfig = merge(baseWebpackConfig, { var webpackConfig = merge(baseWebpackConfig, {
mode: 'production', mode: 'production',

View File

@ -0,0 +1 @@
add the initial i18n translation file for Taiwanese (Hokkien), and modify some related files.

1
changelog.d/adminfe.add Normal file
View File

@ -0,0 +1 @@
Implemented a very basic instance administration screen

View File

View File

@ -0,0 +1 @@
Keep aspect ratio of custom emoji reaction in notification

View File

@ -0,0 +1 @@
Fix openSettingsModalTab so that it correctly opens Settings modal instead of Admin modal

View File

@ -0,0 +1 @@
Add alt text to emoji picker buttons

View File

@ -0,0 +1 @@
Use export-subst gitattribute to allow tarball builds

View File

@ -0,0 +1 @@
fix reports now showing reason/content:w

View File

@ -0,0 +1 @@
Fix HTML attribute parsing, discard attributes not strating with a letter

View File

@ -0,0 +1 @@
Fix a bug where mentioning a user twice will not fill the mention into the textarea

View File

@ -0,0 +1 @@
Make MentionsLine aware of line breaking by non-br elements

View File

@ -0,0 +1 @@
Fix parsing non-ascii tags

View File

@ -0,0 +1 @@
Fix OAuth2 token lingering after revocation

View File

@ -0,0 +1 @@
fix typo in code that prevented cards from showing at all

View File

@ -0,0 +1 @@
don't display quoted status twice

1
changelog.d/quote.add Normal file
View File

@ -0,0 +1 @@
Implement quoting

View File

@ -0,0 +1 @@
Fix react button misalignment on safari ios

View File

@ -0,0 +1 @@
Fix react button not working if reaction accounts are not loaded

View File

@ -0,0 +1 @@
Fix pinned statuses gone when reloading user timeline

View File

@ -0,0 +1 @@
Fix scrolling emoji selector in modal in safari ios

View File

@ -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 ### 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 ### Running production build locally or on a separate server

View File

@ -9,6 +9,7 @@
<body class="hidden"> <body class="hidden">
<noscript>To use Pleroma, please enable JavaScript.</noscript> <noscript>To use Pleroma, please enable JavaScript.</noscript>
<div id="app"></div> <div id="app"></div>
<div id="modal"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
<div id="popovers" /> <div id="popovers" />
</body> </body>

View File

@ -11,115 +11,121 @@
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false", "unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
"e2e": "node test/e2e/runner.js", "e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e", "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": "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" "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.20.0", "@babel/runtime": "7.21.5",
"@chenfengyuan/vue-qrcode": "2.0.0", "@chenfengyuan/vue-qrcode": "2.0.0",
"@fortawesome/fontawesome-svg-core": "6.2.0", "@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-regular-svg-icons": "6.2.0", "@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.2.0", "@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/vue-fontawesome": "3.0.1", "@fortawesome/vue-fontawesome": "3.0.3",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0", "@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0", "@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12", "@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
"@vuelidate/core": "2.0.0", "@vuelidate/core": "2.0.2",
"@vuelidate/validators": "2.0.0", "@vuelidate/validators": "2.0.0",
"body-scroll-lock": "3.1.5", "body-scroll-lock": "3.1.5",
"chromatism": "3.0.0", "chromatism": "3.0.0",
"click-outside-vue3": "4.0.1", "click-outside-vue3": "4.0.1",
"cropperjs": "1.5.12", "cropperjs": "1.5.13",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"localforage": "1.10.0", "localforage": "1.10.0",
"lozad": "1.16.0",
"parse-link-header": "2.0.0", "parse-link-header": "2.0.0",
"phoenix": "1.6.2", "phoenix": "1.7.7",
"punycode.js": "2.1.0", "punycode.js": "2.3.0",
"qrcode": "1.5.0", "qrcode": "1.5.3",
"querystring-es3": "0.2.1", "querystring-es3": "0.2.1",
"url": "0.11.0", "url": "0.11.0",
"utf8": "3.0.0", "utf8": "3.0.0",
"vue": "3.2.41", "vue": "3.2.45",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-router": "4.1.6", "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" "vuex": "4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.19.6", "@babel/core": "7.21.8",
"@babel/eslint-parser": "7.19.1", "@babel/eslint-parser": "7.21.8",
"@babel/plugin-transform-runtime": "7.19.6", "@babel/plugin-transform-runtime": "7.21.4",
"@babel/preset-env": "7.19.4", "@babel/preset-env": "7.21.5",
"@babel/register": "7.18.9", "@babel/register": "7.21.0",
"@intlify/vue-i18n-loader": "5.0.0", "@intlify/vue-i18n-loader": "5.0.1",
"@ungap/event-target": "0.2.3", "@ungap/event-target": "0.2.3",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.1.1", "@vue/babel-plugin-jsx": "1.1.1",
"@vue/compiler-sfc": "3.2.41", "@vue/compiler-sfc": "3.2.45",
"@vue/test-utils": "2.2.6", "@vue/test-utils": "2.2.8",
"autoprefixer": "10.4.12", "autoprefixer": "10.4.14",
"babel-loader": "8.2.5", "babel-loader": "9.1.2",
"babel-plugin-lodash": "3.3.4", "babel-plugin-lodash": "3.3.4",
"chai": "4.3.7", "chai": "4.3.7",
"chalk": "1.1.3", "chalk": "1.1.3",
"chromedriver": "104.0.0", "chromedriver": "108.0.0",
"connect-history-api-fallback": "2.0.0", "connect-history-api-fallback": "2.0.0",
"copy-webpack-plugin": "11.0.0", "copy-webpack-plugin": "11.0.0",
"cross-spawn": "7.0.3", "cross-spawn": "7.0.3",
"css-loader": "6.7.1", "css-loader": "6.7.3",
"css-minimizer-webpack-plugin": "4.2.2", "css-minimizer-webpack-plugin": "4.2.2",
"custom-event-polyfill": "1.0.7", "custom-event-polyfill": "1.0.7",
"eslint": "8.29.0", "eslint": "8.33.0",
"eslint-config-standard": "17.0.0", "eslint-config-standard": "17.0.0",
"eslint-formatter-friendly": "7.0.0", "eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.27.5",
"eslint-plugin-n": "15.6.0", "eslint-plugin-n": "15.6.1",
"eslint-plugin-promise": "6.1.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", "eslint-webpack-plugin": "3.2.0",
"eventsource-polyfill": "0.9.6", "eventsource-polyfill": "0.9.6",
"express": "4.18.2", "express": "4.18.2",
"function-bind": "1.1.1", "function-bind": "1.1.1",
"html-webpack-plugin": "5.5.0", "html-webpack-plugin": "5.5.1",
"http-proxy-middleware": "2.0.6", "http-proxy-middleware": "2.0.6",
"iso-639-1": "2.1.15", "iso-639-1": "2.1.15",
"json-loader": "0.5.7", "json-loader": "0.5.7",
"karma": "6.4.1", "karma": "6.4.2",
"karma-coverage": "2.2.0", "karma-coverage": "2.2.0",
"karma-firefox-launcher": "2.1.2", "karma-firefox-launcher": "2.1.2",
"karma-mocha": "2.0.1", "karma-mocha": "2.0.1",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"karma-sinon-chai": "2.0.2", "karma-sinon-chai": "2.0.2",
"karma-sourcemap-loader": "0.3.8", "karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.34", "karma-spec-reporter": "0.0.36",
"karma-webpack": "5.0.0", "karma-webpack": "5.0.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.1", "mini-css-extract-plugin": "2.7.6",
"mocha": "10.0.0", "mocha": "10.2.0",
"nightwatch": "2.3.3", "nightwatch": "2.6.20",
"opn": "5.5.0", "opn": "5.5.0",
"ora": "0.4.1", "ora": "0.4.1",
"postcss": "8.4.16", "postcss": "8.4.23",
"postcss-loader": "7.0.1", "postcss-html": "^1.5.0",
"sass": "1.55.0", "postcss-loader": "7.0.2",
"sass-loader": "13.0.2", "postcss-scss": "^4.0.6",
"sass": "1.60.0",
"sass-loader": "13.2.2",
"selenium-server": "2.53.1", "selenium-server": "2.53.1",
"semver": "7.3.8", "semver": "7.3.8",
"serviceworker-webpack5-plugin": "2.0.0", "serviceworker-webpack5-plugin": "2.0.0",
"shelljs": "0.8.5", "shelljs": "0.8.5",
"sinon": "14.0.2", "sinon": "15.0.4",
"sinon-chai": "3.7.0", "sinon-chai": "3.7.0",
"stylelint": "13.13.1", "stylelint": "14.16.1",
"stylelint-config-standard": "20.0.0", "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-rscss": "0.4.0",
"stylelint-webpack-plugin": "^3.3.0",
"vue-loader": "17.0.1", "vue-loader": "17.0.1",
"vue-style-loader": "4.1.3", "vue-style-loader": "4.1.3",
"webpack": "5.74.0", "webpack": "5.75.0",
"webpack-dev-middleware": "3.7.3", "webpack-dev-middleware": "3.7.3",
"webpack-hot-middleware": "2.25.2", "webpack-hot-middleware": "2.25.3",
"webpack-merge": "0.20.0" "webpack-merge": "0.20.0"
}, },
"engines": { "engines": {

View File

@ -1,5 +1,7 @@
// stylelint-disable rscss/class-format // stylelint-disable rscss/class-format
@import './_variables.scss'; /* stylelint-disable no-descending-specificity */
@import "./variables";
@import "./panel";
:root { :root {
--navbar-height: 3.5rem; --navbar-height: 3.5rem;
@ -123,7 +125,7 @@ h4 {
font-weight: 1000; font-weight: 1000;
} }
i[class*=icon-], i[class*="icon-"],
.svg-inline--fa, .svg-inline--fa,
.iconLetter { .iconLetter {
color: $fallback--icon; color: $fallback--icon;
@ -132,7 +134,7 @@ i[class*=icon-],
.button-unstyled:hover, .button-unstyled:hover,
a:hover { a:hover {
> i[class*=icon-], > i[class*="icon-"],
> .svg-inline--fa, > .svg-inline--fa,
> .iconLetter { > .iconLetter {
color: var(--text); color: var(--text);
@ -141,12 +143,11 @@ a:hover {
nav { nav {
z-index: var(--ZI_navbar); z-index: var(--ZI_navbar);
color: var(--topBarText);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--topBar, $fallback--fg); background-color: var(--topBar, $fallback--fg);
color: $fallback--faint; color: $fallback--faint;
color: var(--faint, $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-shadow: var(--topBarShadow);
box-sizing: border-box; box-sizing: border-box;
height: var(--navbar-height); height: var(--navbar-height);
@ -191,13 +192,11 @@ nav {
} }
.underlay { .underlay {
grid-column-start: 1; grid-column: 1 / span 3;
grid-column-end: span 3; grid-row: 1 / 1;
grid-row-start: 1;
grid-row-end: 1;
pointer-events: none; pointer-events: none;
background-color: rgba(0, 0, 0, 0.15); background-color: rgb(0 0 0 / 15%);
background-color: var(--underlay, rgba(0, 0, 0, 0.15)); background-color: var(--underlay, rgb(0 0 0 / 15%));
z-index: -1000; z-index: -1000;
} }
@ -231,8 +230,7 @@ nav {
display: grid; display: grid;
grid-template-columns: 100%; grid-template-columns: 100%;
box-sizing: border-box; box-sizing: border-box;
grid-row-start: 1; grid-row: 1 / 1;
grid-row-end: 1;
margin: 0 calc(var(--___columnMargin) / 2); margin: 0 calc(var(--___columnMargin) / 2);
padding: calc(var(--___columnMargin)) 0; padding: calc(var(--___columnMargin)) 0;
row-gap: var(--___columnMargin); row-gap: var(--___columnMargin);
@ -307,7 +305,7 @@ nav {
align-content: start; align-content: start;
} }
&.-reverse:not(.-wide):not(.-mobile) { &.-reverse:not(.-wide, .-mobile) {
grid-template-columns: grid-template-columns:
var(--effectiveContentColumnWidth) var(--effectiveContentColumnWidth)
var(--effectiveSidebarColumnWidth); var(--effectiveSidebarColumnWidth);
@ -336,11 +334,8 @@ nav {
padding: 0; padding: 0;
.column { .column {
margin-left: 0;
margin-right: 0;
padding-top: 0; padding-top: 0;
margin-top: var(--navbar-height); margin: var(--navbar-height) 0 0 0;
margin-bottom: 0;
} }
.panel-heading, .panel-heading,
@ -389,7 +384,7 @@ nav {
background: transparent; background: transparent;
} }
i[class*=icon-], i[class*="icon-"],
.svg-inline--fa { .svg-inline--fa {
color: $fallback--text; color: $fallback--text;
color: var(--btnText, $fallback--text); color: var(--btnText, $fallback--text);
@ -400,12 +395,15 @@ nav {
} }
&:hover { &: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); box-shadow: var(--buttonHoverShadow);
} }
&:active { &: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); box-shadow: var(--buttonPressedShadow);
color: $fallback--text; color: $fallback--text;
color: var(--btnPressedText, $fallback--text); color: var(--btnPressedText, $fallback--text);
@ -438,7 +436,10 @@ nav {
color: var(--btnToggledText, $fallback--text); color: var(--btnToggledText, $fallback--text);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--btnToggled, $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); box-shadow: var(--buttonPressedShadow);
svg, svg,
@ -503,7 +504,10 @@ textarea,
border: none; border: none;
border-radius: $fallback--inputRadius; border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $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); box-shadow: var(--inputShadow);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--input, $fallback--fg); background-color: var(--input, $fallback--fg);
@ -521,13 +525,13 @@ textarea,
padding: 0 var(--_padding); padding: 0 var(--_padding);
&:disabled, &:disabled,
&[disabled=disabled], &[disabled="disabled"],
&.disabled { &.disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5; opacity: 0.5;
} }
&[type=range] { &[type="range"] {
background: none; background: none;
border: none; border: none;
margin: 0; margin: 0;
@ -535,7 +539,7 @@ textarea,
flex: 1; flex: 1;
} }
&[type=radio] { &[type="radio"] {
display: none; display: none;
&:checked + label::before { &:checked + label::before {
@ -555,7 +559,7 @@ textarea,
+ label::before { + label::before {
flex-shrink: 0; flex-shrink: 0;
display: inline-block; display: inline-block;
content: ''; content: "";
transition: box-shadow 200ms; transition: box-shadow 200ms;
width: 1.1em; width: 1.1em;
height: 1.1em; height: 1.1em;
@ -575,9 +579,7 @@ textarea,
} }
} }
&[type=checkbox] { &[type="checkbox"] {
display: none;
&:checked + label::before { &:checked + label::before {
color: $fallback--text; color: $fallback--text;
color: var(--inputText, $fallback--text); color: var(--inputText, $fallback--text);
@ -594,7 +596,7 @@ textarea,
+ label::before { + label::before {
flex-shrink: 0; flex-shrink: 0;
display: inline-block; display: inline-block;
content: ''; content: "";
transition: color 200ms; transition: color 200ms;
width: 1.1em; width: 1.1em;
height: 1.1em; height: 1.1em;
@ -634,15 +636,29 @@ option {
} }
.hide-number-spinner { .hide-number-spinner {
-moz-appearance: textfield; appearance: textfield;
&[type=number]::-webkit-inner-spin-button, &[type="number"]::-webkit-inner-spin-button,
&[type=number]::-webkit-outer-spin-button { &[type="number"]::-webkit-outer-spin-button {
opacity: 0; opacity: 0;
display: none; display: none;
} }
} }
.cards-list {
list-style: none;
display: grid;
grid-auto-flow: row dense;
grid-template-columns: 1fr 1fr;
li {
border: 1px solid var(--border);
border-radius: var(--inputRadius);
padding: 0.5em;
margin: 0.25em;
}
}
.btn-block { .btn-block {
display: block; display: block;
width: 100%; width: 100%;
@ -653,24 +669,25 @@ option {
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
button { button,
.button-dropdown {
position: relative; position: relative;
flex: 1 1 auto; flex: 1 1 auto;
&:not(:last-child) { &:not(:last-child),
&:not(:last-child) .button-default {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
&:not(:first-child) { &:not(:first-child),
&:not(:first-child) .button-default {
border-top-left-radius: 0; border-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
} }
} }
@import './panel.scss';
.fa { .fa {
color: grey; color: grey;
} }
@ -686,7 +703,7 @@ option {
max-width: 10em; max-width: 10em;
min-width: 1.7em; min-width: 1.7em;
height: 1.3em; height: 1.3em;
padding: 0.15em 0.15em; padding: 0.15em;
vertical-align: middle; vertical-align: middle;
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
@ -789,7 +806,8 @@ option {
.fa-old-padding { .fa-old-padding {
&.iconLetter, &.iconLetter,
&.svg-inline--fa, &-layer { &.svg-inline--fa,
&-layer {
padding: 0 0.3em; padding: 0 0.3em;
} }
} }
@ -883,3 +901,16 @@ option {
.fade-leave-active { .fade-leave-active {
opacity: 0; opacity: 0;
} }
/* stylelint-enable no-descending-specificity */
.visible-for-screenreader-only {
display: block;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
visibility: visible;
clip: rect(0 0 0 0);
padding: 0;
position: absolute;
}

View File

@ -71,7 +71,6 @@
<StatusHistoryModal v-if="editingAvailable" /> <StatusHistoryModal v-if="editingAvailable" />
<SettingsModal /> <SettingsModal />
<UpdateNotification /> <UpdateNotification />
<div id="modal" />
<GlobalNoticeList /> <GlobalNoticeList />
</div> </div>
</template> </template>

View File

@ -1,13 +1,14 @@
@mixin unfocused-style { @mixin unfocused-style {
@content; @content;
&:focus:not(:focus-visible):not(:hover) { &:focus:not(:focus-visible, :hover) {
@content; @content;
} }
} }
@mixin focused-style { @mixin focused-style {
&:hover, &:focus { &:hover,
&:focus {
@content; @content;
} }

View File

@ -4,20 +4,20 @@ $darkened-background: whitesmoke;
$fallback--bg: #121a24; $fallback--bg: #121a24;
$fallback--fg: #182230; $fallback--fg: #182230;
$fallback--faint: rgba(185, 185, 186, .5); $fallback--faint: rgb(185 185 186 / 50%);
$fallback--text: #b9b9ba; $fallback--text: #b9b9ba;
$fallback--link: #d8a070; $fallback--link: #d8a070;
$fallback--icon: #666; $fallback--icon: #666;
$fallback--lightBg: rgb(21, 30, 42); $fallback--lightBg: rgb(21 30 42);
$fallback--lightText: #b9b9ba; $fallback--lightText: #b9b9ba;
$fallback--border: #222; $fallback--border: #222;
$fallback--cRed: #ff0000; $fallback--cRed: #f00;
$fallback--cBlue: #0095ff; $fallback--cBlue: #0095ff;
$fallback--cGreen: #0fa00f; $fallback--cGreen: #0fa00f;
$fallback--cOrange: orange; $fallback--cOrange: orange;
$fallback--alertError: rgba(211,16,20,.5); $fallback--alertError: rgb(211 16 20 / 50%);
$fallback--alertWarning: rgba(111,111,20,.5); $fallback--alertWarning: rgb(111 111 20 / 50%);
$fallback--panelRadius: 10px; $fallback--panelRadius: 10px;
$fallback--checkboxRadius: 2px; $fallback--checkboxRadius: 2px;
@ -29,6 +29,8 @@ $fallback--avatarAltRadius: 10px;
$fallback--attachmentRadius: 10px; $fallback--attachmentRadius: 10px;
$fallback--chatMessageRadius: 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; $status-margin: 0.75em;

View File

@ -1,6 +1,8 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3' 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' 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: 'textlimit', value: textlimit })
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) 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) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
@ -249,11 +253,13 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') }) store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') }) store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') }) store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
@ -397,6 +403,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
app.use(vClickOutside) app.use(vClickOutside)
app.use(VBodyScrollLock) app.use(VBodyScrollLock)
app.use(VueVirtualScroller)
app.component('FAIcon', FontAwesomeIcon) app.component('FAIcon', FontAwesomeIcon)
app.component('FALayers', FontAwesomeLayers) app.component('FALayers', FontAwesomeLayers)

View File

@ -9,6 +9,3 @@
</template> </template>
<script src="./about.js"></script> <script src="./about.js"></script>
<style lang="scss">
</style>

View File

@ -2,6 +2,7 @@ import { mapState } from 'vuex'
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import UserListMenu from 'src/components/user_list_menu/user_list_menu.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 { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faEllipsisV faEllipsisV
@ -16,14 +17,30 @@ const AccountActions = {
'user', 'relationship' 'user', 'relationship'
], ],
data () { data () {
return { } return {
showingConfirmBlock: false,
showingConfirmRemoveFollower: false
}
}, },
components: { components: {
ProgressButton, ProgressButton,
Popover, Popover,
UserListMenu UserListMenu,
ConfirmModal
}, },
methods: { methods: {
showConfirmBlock () {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
this.showingConfirmBlock = false
},
showConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = true
},
hideConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = false
},
showRepeats () { showRepeats () {
this.$store.dispatch('showReblogs', this.user.id) this.$store.dispatch('showReblogs', this.user.id)
}, },
@ -31,13 +48,29 @@ const AccountActions = {
this.$store.dispatch('hideReblogs', this.user.id) this.$store.dispatch('hideReblogs', this.user.id)
}, },
blockUser () { blockUser () {
if (!this.shouldConfirmBlock) {
this.doBlockUser()
} else {
this.showConfirmBlock()
}
},
doBlockUser () {
this.$store.dispatch('blockUser', this.user.id) this.$store.dispatch('blockUser', this.user.id)
this.hideConfirmBlock()
}, },
unblockUser () { unblockUser () {
this.$store.dispatch('unblockUser', this.user.id) this.$store.dispatch('unblockUser', this.user.id)
}, },
removeUserFromFollowers () { removeUserFromFollowers () {
if (!this.shouldConfirmRemoveUserFromFollowers) {
this.doRemoveUserFromFollowers()
} else {
this.showConfirmRemoveUserFromFollowers()
}
},
doRemoveUserFromFollowers () {
this.$store.dispatch('removeUserFromFollowers', this.user.id) this.$store.dispatch('removeUserFromFollowers', this.user.id)
this.hideConfirmRemoveUserFromFollowers()
}, },
reportUser () { reportUser () {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
@ -50,6 +83,12 @@ const AccountActions = {
} }
}, },
computed: { computed: {
shouldConfirmBlock () {
return this.$store.getters.mergedConfig.modalOnBlock
},
shouldConfirmRemoveUserFromFollowers () {
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
},
...mapState({ ...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
}) })

View File

@ -74,13 +74,56 @@
</button> </button>
</template> </template>
</Popover> </Popover>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmBlock"
:title="$t('user_card.block_confirm_title')"
:confirm-text="$t('user_card.block_confirm_accept_button')"
:cancel-text="$t('user_card.block_confirm_cancel_button')"
@accepted="doBlockUser"
@cancelled="hideConfirmBlock"
>
<i18n-t
keypath="user_card.block_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmRemoveFollower"
:title="$t('user_card.remove_follower_confirm_title')"
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
:cancel-text="$t('user_card.remove_follower_confirm_cancel_button')"
@accepted="doRemoveUserFromFollowers"
@cancelled="hideConfirmRemoveUserFromFollowers"
>
<i18n-t
keypath="user_card.remove_follower_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</div> </div>
</template> </template>
<script src="./account_actions.js"></script> <script src="./account_actions.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.AccountActions { .AccountActions {
.ellipsis-button { .ellipsis-button {
width: 2.5em; width: 2.5em;

View File

@ -27,6 +27,9 @@ const Announcement = {
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: state => state.users.currentUser
}), }),
canEditAnnouncement () {
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
},
content () { content () {
return this.announcement.content return this.announcement.content
}, },

View File

@ -45,14 +45,14 @@
{{ $t('announcements.mark_as_read_action') }} {{ $t('announcements.mark_as_read_action') }}
</button> </button>
<button <button
v-if="currentUser && currentUser.role === 'admin'" v-if="canEditAnnouncement"
class="btn button-default" class="btn button-default"
@click="enterEditMode" @click="enterEditMode"
> >
{{ $t('announcements.edit_action') }} {{ $t('announcements.edit_action') }}
</button> </button>
<button <button
v-if="currentUser && currentUser.role === 'admin'" v-if="canEditAnnouncement"
class="btn button-default" class="btn button-default"
@click="deleteAnnouncement" @click="deleteAnnouncement"
> >
@ -102,19 +102,19 @@
@import "../../variables"; @import "../../variables";
.announcement { .announcement {
border-bottom-width: 1px; border-bottom: 1px solid var(--border, $fallback--border);
border-bottom-style: solid;
border-bottom-color: var(--border, $fallback--border);
border-radius: 0; border-radius: 0;
padding: var(--status-margin, $status-margin); padding: var(--status-margin, $status-margin);
.heading, .body { .heading,
.body {
margin-bottom: var(--status-margin, $status-margin); margin-bottom: var(--status-margin, $status-margin);
} }
.footer { .footer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.times { .times {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -28,6 +28,9 @@ const AnnouncementsPage = {
}), }),
announcements () { announcements () {
return this.$store.state.announcements.announcements return this.$store.state.announcements.announcements
},
canPostAnnouncement () {
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
} }
}, },
methods: { methods: {

View File

@ -7,7 +7,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<section <section
v-if="currentUser && currentUser.role === 'admin'" v-if="canPostAnnouncement"
> >
<div class="post-form"> <div class="post-form">
<div class="heading"> <div class="heading">
@ -67,7 +67,8 @@
.post-form { .post-form {
padding: var(--status-margin, $status-margin); padding: var(--status-margin, $status-margin);
.heading, .body { .heading,
.body {
margin-bottom: var(--status-margin, $status-margin); margin-bottom: var(--status-margin, $status-margin);
} }

View File

@ -34,9 +34,10 @@ export default {
height: 100%; height: 100%;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.btn { .btn {
margin: .5em; margin: 0.5em;
padding: .5em 2em; padding: 0.5em 2em;
} }
} }
</style> </style>

View File

@ -36,6 +36,7 @@ library.add(
const Attachment = { const Attachment = {
props: [ props: [
'attachment', 'attachment',
'compact',
'description', 'description',
'hideDescription', 'hideDescription',
'nsfw', 'nsfw',
@ -71,7 +72,8 @@ const Attachment = {
{ {
'-loading': this.loading, '-loading': this.loading,
'-nsfw-placeholder': this.hidden, '-nsfw-placeholder': this.hidden,
'-editable': this.edit !== undefined '-editable': this.edit !== undefined,
'-compact': this.compact
}, },
'-type-' + this.type, '-type-' + this.type,
this.size && '-size-' + this.size, this.size && '-size-' + this.size,

View File

@ -1,4 +1,4 @@
@import '../../_variables.scss'; @import "../../variables";
.Attachment { .Attachment {
display: inline-flex; display: inline-flex;
@ -102,14 +102,13 @@
padding-top: 0.5em; padding-top: 0.5em;
} }
.play-icon { .play-icon {
position: absolute; position: absolute;
font-size: 64px; font-size: 64px;
top: calc(50% - 32px); top: calc(50% - 32px);
left: calc(50% - 32px); left: calc(50% - 32px);
color: rgba(255, 255, 255, 0.75); color: rgb(255 255 255 / 75%);
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4); text-shadow: 0 0 2px rgb(0 0 0 / 40%);
&::before { &::before {
margin: 0; margin: 0;
@ -135,18 +134,32 @@
margin-left: 0.5em; margin-left: 0.5em;
font-size: 1.25em; font-size: 1.25em;
// TODO: theming? hard to theme with unknown background image color // 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 { .svg-inline--fa {
color: rgba(0, 0, 0, 0.6); color: rgb(0 0 0 / 60%);
} }
&:hover .svg-inline--fa { &: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 { .oembed-container {
line-height: 1.2em; line-height: 1.2em;
flex: 1 0 100%; flex: 1 0 100%;
@ -160,8 +173,9 @@
.image { .image {
flex: 1; flex: 1;
img { img {
border: 0px; border: 0;
border-radius: 5px; border-radius: 5px;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
@ -172,9 +186,10 @@
flex: 2; flex: 2;
margin: 8px; margin: 8px;
word-break: break-all; word-break: break-all;
h1 { h1 {
font-size: 1rem; font-size: 1rem;
margin: 0px; margin: 0;
} }
} }
} }
@ -252,17 +267,9 @@
cursor: progress; cursor: progress;
} }
&.-contain-fit { &.-compact {
img, .placeholder-container {
canvas { padding-bottom: 0.5em;
object-fit: contain;
}
}
&.-cover-fit {
img,
canvas {
object-fit: cover;
} }
} }
} }

View File

@ -162,10 +162,11 @@
target="_blank" target="_blank"
> >
<FAIcon <FAIcon
size="5x" :size="compact ? '2x' : '5x'"
:icon="placeholderIconClass" :icon="placeholderIconClass"
:title="localDescription"
/> />
<p> <p v-if="!compact">
{{ localDescription }} {{ localDescription }}
</p> </p>
</a> </a>

View File

@ -24,7 +24,7 @@
<script src="./autosuggest.js"></script> <script src="./autosuggest.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.autosuggest { .autosuggest {
position: relative; position: relative;
@ -50,7 +50,7 @@
border-radius: var(--inputRadius, $fallback--inputRadius); border-radius: var(--inputRadius, $fallback--inputRadius);
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6); box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);
box-shadow: var(--panelShadow); box-shadow: var(--panelShadow);
overflow-y: auto; overflow-y: auto;
z-index: 1; z-index: 1;

View File

@ -17,7 +17,7 @@
<script src="./avatar_list.js"></script> <script src="./avatar_list.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.avatars { .avatars {
display: flex; display: flex;

View File

@ -37,6 +37,7 @@
.block-card-content-container { .block-card-content-container {
margin-top: 0.5em; margin-top: 0.5em;
text-align: right; text-align: right;
button { button {
width: 10em; width: 10em;
} }

View File

@ -17,7 +17,7 @@
width: 100%; width: 100%;
overflow: visible; overflow: visible;
min-height: calc(100vh - var(--navbar-height)); min-height: calc(100vh - var(--navbar-height));
margin: 0 0 0 0; margin: 0;
border-radius: 10px 10px 0 0; border-radius: 10px 10px 0 0;
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0; border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
@ -66,7 +66,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3); box-shadow: 0 1px 1px rgb(0 0 0 / 30%), 0 2px 4px rgb(0 0 0 / 30%);
z-index: 10; z-index: 10;
transition: 0.35s all; transition: 0.35s all;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1); transition-timing-function: cubic-bezier(0, 1, 0.5, 1);

View File

@ -95,6 +95,6 @@
<script src="./chat.js"></script> <script src="./chat.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
@import './chat.scss'; @import "./chat";
</style> </style>

View File

@ -45,7 +45,7 @@
<script src="./chat_list.js"></script> <script src="./chat_list.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.chat-list { .chat-list {
min-height: 25em; min-height: 25em;

View File

@ -13,7 +13,7 @@
&:hover { &:hover {
background-color: var(--selectedPost, $fallback--lightBg); background-color: var(--selectedPost, $fallback--lightBg);
box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.1); box-shadow: 0 0 3px 1px rgb(0 0 0 / 10%);
} }
.chat-list-item-left { .chat-list-item-left {
@ -67,6 +67,7 @@
canvas { canvas {
display: none; display: none;
} }
img { img {
visibility: visible; visibility: visible;
} }
@ -79,13 +80,11 @@
.chat-preview-body { .chat-preview-body {
--emoji-size: 1.4em; --emoji-size: 1.4em;
padding-right: 1em;
} }
.time-wrapper { .time-wrapper {
line-height: var(--post-line-height); line-height: var(--post-line-height);
} }
.chat-preview-body {
padding-right: 1em;
}
} }

View File

@ -48,6 +48,6 @@
<script src="./chat_list_item.js"></script> <script src="./chat_list_item.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
@import './chat_list_item.scss'; @import "./chat_list_item";
</style> </style>

View File

@ -1,12 +1,12 @@
@import '../../_variables.scss'; @import "../../variables";
.chat-message-wrapper { .chat-message-wrapper {
&.hovered-message-chain { &.hovered-message-chain {
.animated.Avatar { .animated.Avatar {
canvas { canvas {
display: none; display: none;
} }
img { img {
visibility: visible; visibility: visible;
} }
@ -28,7 +28,8 @@
.menu-icon { .menu-icon {
cursor: pointer; cursor: pointer;
&:hover, .extra-button-popover.open & { &:hover,
.extra-button-popover.open & {
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
} }
@ -54,27 +55,11 @@
width: 32px; width: 32px;
} }
.link-preview, .attachments { .link-preview,
.attachments {
margin-bottom: 1em; 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 { .status {
border-radius: $fallback--chatMessageRadius; border-radius: $fallback--chatMessageRadius;
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius); border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
@ -86,7 +71,7 @@
position: relative; position: relative;
float: right; float: right;
font-size: 0.8em; font-size: 0.8em;
margin: -1em 0 -0.5em 0; margin: -1em 0 -0.5em;
font-style: italic; font-style: italic;
opacity: 0.8; opacity: 0.8;
} }
@ -103,18 +88,54 @@
} }
.pending { .pending {
.status-content.media-body, .created-at { .status-content.media-body,
.created-at {
color: var(--faint); color: var(--faint);
} }
} }
.error { .error {
.status-content.media-body, .created-at { .status-content.media-body,
.created-at {
color: $fallback--cRed; color: $fallback--cRed;
color: var(--badgeNotification, $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 { .incoming {
a { a {
color: var(--chatMessageIncomingLink, $fallback--link); color: var(--chatMessageIncomingLink, $fallback--link);
@ -137,36 +158,17 @@
} }
} }
.outgoing { .chat-message-inner.with-media {
display: flex; width: 100%;
flex-direction: row;
flex-wrap: wrap;
align-content: end;
justify-content: flex-end;
a {
color: var(--chatMessageOutgoingLink, $fallback--link);
}
.status { .status {
color: var(--chatMessageOutgoingText, $fallback--text); width: 100%;
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;
} }
} }
.visible { .visible {
opacity: 1; opacity: 1;
} }
} }
.chat-message-date-separator { .chat-message-date-separator {

View File

@ -33,7 +33,7 @@
<div <div
class="media status" class="media status"
:class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }" :class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
style="position: relative" style="position: relative;"
@mouseenter="hovered = true" @mouseenter="hovered = true"
@mouseleave="hovered = false" @mouseleave="hovered = false"
> >
@ -98,6 +98,6 @@
<script src="./chat_message.js"></script> <script src="./chat_message.js"></script>
<style lang="scss"> <style lang="scss">
@import './chat_message.scss'; @import "./chat_message";
</style> </style>

View File

@ -1,7 +1,7 @@
.chat-new { .chat-new {
.input-wrap { .input-wrap {
display: flex; display: flex;
margin: 0.7em 0.5em 0.7em 0.5em; margin: 0.7em 0.5em;
input { input {
width: 100%; width: 100%;

View File

@ -46,6 +46,6 @@
<script src="./chat_new.js"></script> <script src="./chat_new.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
@import './chat_new.scss'; @import "./chat_new";
</style> </style>

View File

@ -26,7 +26,7 @@
<script src="./chat_title.js"></script> <script src="./chat_title.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.chat-title { .chat-title {
display: flex; display: flex;

View File

@ -1,16 +1,21 @@
<template> <template>
<label <label
class="checkbox" class="checkbox"
:class="{ disabled, indeterminate }" :class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }"
> >
<input <input
type="checkbox" type="checkbox"
class="visible-for-screenreader-only"
:disabled="disabled" :disabled="disabled"
:checked="modelValue" :checked="modelValue"
:indeterminate="indeterminate" :indeterminate="indeterminate"
@change="$emit('update:modelValue', $event.target.checked)" @change="$emit('update:modelValue', $event.target.checked)"
> >
<i class="checkbox-indicator" /> <i
class="checkbox-indicator"
:aria-hidden="true"
@transitionend.capture="onTransitionEnd"
/>
<span <span
v-if="!!$slots.default" v-if="!!$slots.default"
class="label" class="label"
@ -27,12 +32,30 @@ export default {
'indeterminate', 'indeterminate',
'disabled' 'disabled'
], ],
emits: ['update:modelValue'] emits: ['update:modelValue'],
data: (vm) => ({
indeterminateTransitionFix: vm.indeterminate
}),
watch: {
indeterminate (e) {
if (e) {
this.indeterminateTransitionFix = true
}
}
},
methods: {
onTransitionEnd (e) {
if (!this.indeterminate) {
this.indeterminateTransitionFix = false
}
}
}
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
@import "../../mixins";
.checkbox { .checkbox {
position: relative; position: relative;
@ -49,13 +72,13 @@ export default {
right: 0; right: 0;
top: 0; top: 0;
display: block; display: block;
content: '✓'; content: "✓";
transition: color 200ms; transition: color 200ms;
width: 1.1em; width: 1.1em;
height: 1.1em; height: 1.1em;
border-radius: $fallback--checkboxRadius; border-radius: $fallback--checkboxRadius;
border-radius: var(--checkboxRadius, $fallback--checkboxRadius); border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
box-shadow: 0px 0px 2px black inset; box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow); box-shadow: var(--inputShadow);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--input, $fallback--fg); background-color: var(--input, $fallback--fg);
@ -71,32 +94,36 @@ export default {
&.disabled { &.disabled {
.checkbox-indicator::before, .checkbox-indicator::before,
.label { .label {
opacity: .5; opacity: 0.5;
} }
.label { .label {
color: $fallback--faint; color: $fallback--faint;
color: var(--faint, $fallback--faint); color: var(--faint, $fallback--faint);
} }
} }
input[type=checkbox] { input[type="checkbox"] {
display: none;
&:checked + .checkbox-indicator::before { &:checked + .checkbox-indicator::before {
color: $fallback--text; color: $fallback--text;
color: var(--inputText, $fallback--text); color: var(--inputText, $fallback--text);
} }
&:indeterminate + .checkbox-indicator::before { &:indeterminate + .checkbox-indicator::before {
content: ''; content: "";
color: $fallback--text; color: $fallback--text;
color: var(--inputText, $fallback--text); color: var(--inputText, $fallback--text);
} }
}
&.indeterminate-fix {
input[type="checkbox"] + .checkbox-indicator::before {
content: "";
}
} }
& > span { & > span {
margin-left: .5em; margin-left: 0.5em;
} }
} }
</style> </style>

View File

@ -1,4 +1,4 @@
@import '../../_variables.scss'; @import "../../variables";
.color-input { .color-input {
display: inline-flex; display: inline-flex;
@ -8,7 +8,7 @@
flex: 0 0 0; flex: 0 0 0;
max-width: 9em; max-width: 9em;
align-items: stretch; align-items: stretch;
padding: .2em 8px; padding: 0.2em 8px;
input { input {
background: none; background: none;
@ -31,6 +31,7 @@
min-height: 100%; min-height: 100%;
} }
} }
.computedIndicator, .computedIndicator,
.transparentIndicator { .transparentIndicator {
flex: 0 0 2em; flex: 0 0 2em;
@ -38,22 +39,27 @@
align-self: stretch; align-self: stretch;
min-height: 100%; min-height: 100%;
} }
.transparentIndicator { .transparentIndicator {
// forgot to install counter-strike source, ooops // forgot to install counter-strike source, ooops
background-color: #FF00FF; background-color: #f0f;
position: relative; position: relative;
&::before, &::after {
&::before,
&::after {
display: block; display: block;
content: ''; content: "";
background-color: #000000; background-color: #000;
position: absolute; position: absolute;
height: 50%; height: 50%;
width: 50%; width: 50%;
} }
&::after { &::after {
top: 0; top: 0;
left: 0; left: 0;
} }
&::before { &::before {
bottom: 0; bottom: 0;
right: 0; right: 0;
@ -64,5 +70,4 @@
.label { .label {
flex: 1 1 auto; flex: 1 1 auto;
} }
} }

View File

@ -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

View File

@ -0,0 +1,29 @@
<template>
<dialog-modal
v-body-scroll-lock="true"
class="confirm-modal"
:on-cancel="onCancel"
>
<template #header>
<span v-text="title" />
</template>
<slot />
<template #footer>
<button
class="btn button-default"
@click.prevent="onAccept"
v-text="confirmText"
/>
<button
class="btn button-default"
@click.prevent="onCancel"
v-text="cancelText"
/>
</template>
</dialog-modal>
</template>
<script src="./confirm_modal.js"></script>

View File

@ -87,7 +87,6 @@ export default {
.contrast-ratio { .contrast-ratio {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: -4px; margin-top: -4px;
margin-bottom: 5px; margin-bottom: 5px;

View File

@ -210,17 +210,16 @@
<script src="./conversation.js"></script> <script src="./conversation.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.Conversation { .Conversation {
z-index: 1; z-index: 1;
.conversation-dive-to-top-level-box { .conversation-dive-to-top-level-box {
padding: var(--status-margin, $status-margin); padding: var(--status-margin, $status-margin);
border-bottom-width: 1px; border-bottom: 1px solid var(--border, $fallback--border);
border-bottom-style: solid;
border-bottom-color: var(--border, $fallback--border);
border-radius: 0; border-radius: 0;
/* Make the button stretch along the whole row */ /* Make the button stretch along the whole row */
display: flex; display: flex;
align-items: stretch; align-items: stretch;
@ -235,52 +234,48 @@
.thread-ancestor.-faded .StatusContent { .thread-ancestor.-faded .StatusContent {
--link: var(--faintLink); --link: var(--faintLink);
--text: var(--faint); --text: var(--faint);
color: var(--text); color: var(--text);
} }
.thread-ancestor-dive-box { .thread-ancestor-dive-box {
padding-left: var(--status-margin, $status-margin); padding-left: var(--status-margin, $status-margin);
border-bottom-width: 1px; border-bottom: 1px solid var(--border, $fallback--border);
border-bottom-style: solid;
border-bottom-color: var(--border, $fallback--border);
border-radius: 0; border-radius: 0;
/* Make the button stretch along the whole row */ /* Make the button stretch along the whole row */
&, &-inner { &,
&-inner {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
} }
} }
.thread-ancestor-dive-box-inner { .thread-ancestor-dive-box-inner {
padding: var(--status-margin, $status-margin); padding: var(--status-margin, $status-margin);
} }
.conversation-status { .conversation-status {
border-bottom-width: 1px; border-bottom: 1px solid var(--border, $fallback--border);
border-bottom-style: solid;
border-bottom-color: var(--border, $fallback--border);
border-radius: 0; border-radius: 0;
} }
.thread-ancestor-has-other-replies .conversation-status, .thread-ancestor-has-other-replies .conversation-status,
&:last-child .conversation-status,
.thread-ancestor:last-child .conversation-status, .thread-ancestor:last-child .conversation-status,
.thread-ancestor:last-child .thread-ancestor-dive-box, .thread-ancestor:last-child .thread-ancestor-dive-box,
&:last-child .conversation-status,
&.-expanded .thread-tree .conversation-status { &.-expanded .thread-tree .conversation-status {
border-bottom: none; border-bottom: none;
} }
.thread-ancestors + .thread-tree > .conversation-status { .thread-ancestors + .thread-tree > .conversation-status {
border-top-width: 1px; border-top: 1px solid var(--border, $fallback--border);
border-top-style: solid;
border-top-color: var(--border, $fallback--border);
} }
/* expanded conversation in timeline */ /* expanded conversation in timeline */
&.status-fadein.-expanded .thread-body { &.status-fadein.-expanded .thread-body {
border-left-width: 4px; border-left: 4px solid $fallback--cRed;
border-left-style: solid;
border-left-color: $fallback--cRed;
border-left-color: var(--cRed, $fallback--cRed); border-left-color: var(--cRed, $fallback--cRed);
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);

View File

@ -1,4 +1,5 @@
import SearchBar from 'components/search_bar/search_bar.vue' import SearchBar from 'components/search_bar/search_bar.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faSignInAlt, faSignInAlt,
@ -30,7 +31,8 @@ library.add(
export default { export default {
components: { components: {
SearchBar SearchBar,
ConfirmModal
}, },
data: () => ({ data: () => ({
searchBarHidden: true, searchBarHidden: true,
@ -40,7 +42,8 @@ export default {
window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain') window.CSS.supports('-o-mask-size', 'contain')
) ),
showingConfirmLogout: false
}), }),
computed: { computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
@ -73,21 +76,41 @@ export default {
hideSitename () { return this.$store.state.instance.hideSitename }, hideSitename () { return this.$store.state.instance.hideSitename },
logoLeft () { return this.$store.state.instance.logoLeft }, logoLeft () { return this.$store.state.instance.logoLeft },
currentUser () { return this.$store.state.users.currentUser }, currentUser () { return this.$store.state.users.currentUser },
privateMode () { return this.$store.state.instance.private } privateMode () { return this.$store.state.instance.private },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
}
}, },
methods: { methods: {
scrollToTop () { scrollToTop () {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
showConfirmLogout () {
this.showingConfirmLogout = true
},
hideConfirmLogout () {
this.showingConfirmLogout = false
},
logout () { logout () {
if (!this.shouldConfirmLogout) {
this.doLogout()
} else {
this.showConfirmLogout()
}
},
doLogout () {
this.$router.replace('/main/public') this.$router.replace('/main/public')
this.$store.dispatch('logout') this.$store.dispatch('logout')
this.hideConfirmLogout()
}, },
onSearchBarToggled (hidden) { onSearchBarToggled (hidden) {
this.searchBarHidden = hidden this.searchBarHidden = hidden
}, },
openSettingsModal () { openSettingsModal () {
this.$store.dispatch('openSettingsModal') this.$store.dispatch('openSettingsModal', 'user')
},
openAdminModal () {
this.$store.dispatch('openSettingsModal', 'admin')
} }
} }
} }

View File

@ -1,4 +1,4 @@
@import '../../_variables.scss'; @import "../../variables";
.DesktopNav { .DesktopNav {
width: 100%; width: 100%;
@ -27,18 +27,11 @@
--miniColumn: 25rem; --miniColumn: 25rem;
--maxiColumn: 45rem; --maxiColumn: 45rem;
--columnGap: 1em; --columnGap: 1em;
max-width: calc(
var(--sidebarColumnWidth, var(--miniColumn)) +
var(--contentColumnWidth, var(--maxiColumn)) +
var(--columnGap)
);
}
&.-column-stretch.-wide .inner-nav { max-width:
max-width: calc( calc(
var(--sidebarColumnWidth, var(--miniColumn)) + var(--sidebarColumnWidth, var(--miniColumn)) +
var(--contentColumnWidth, var(--maxiColumn)) + var(--contentColumnWidth, var(--maxiColumn)) +
var(--notifsColumnWidth, var(--miniColumn)) +
var(--columnGap) var(--columnGap)
); );
} }
@ -48,8 +41,19 @@
grid-template-areas: "logo sitename actions"; grid-template-areas: "logo sitename actions";
} }
&.-column-stretch.-wide .inner-nav {
max-width:
calc(
var(--sidebarColumnWidth, var(--miniColumn)) +
var(--contentColumnWidth, var(--maxiColumn)) +
var(--notifsColumnWidth, var(--miniColumn)) +
var(--columnGap)
);
}
.button-default { .button-default {
&, svg { &,
svg {
color: $fallback--text; color: $fallback--text;
color: var(--btnTopBarText, $fallback--text); color: var(--btnTopBarText, $fallback--text);
} }
@ -70,7 +74,7 @@
color: $fallback--text; color: $fallback--text;
color: var(--btnToggledTopBarText, $fallback--text); color: var(--btnToggledTopBarText, $fallback--text);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--btnToggledTopBar, $fallback--fg) background-color: var(--btnToggledTopBar, $fallback--fg);
} }
} }
@ -82,6 +86,7 @@
transition-duration: 100ms; transition-duration: 100ms;
@media all and (min-width: 800px) { @media all and (min-width: 800px) {
/* stylelint-disable-next-line declaration-no-important */
opacity: 1 !important; opacity: 1 !important;
} }

View File

@ -20,6 +20,7 @@
class="logo" class="logo"
:to="{ name: 'root' }" :to="{ name: 'root' }"
:style="logoBgStyle" :style="logoBgStyle"
:title="sitename"
> >
<div <div
class="mask" class="mask"
@ -38,44 +39,55 @@
/> />
<button <button
class="button-unstyled nav-icon" class="button-unstyled nav-icon"
@click="openSettingsModal" :title="$t('nav.preferences')"
@click.stop="openSettingsModal"
> >
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
icon="cog" icon="cog"
:title="$t('nav.preferences')"
/> />
</button> </button>
<a <button
v-if="currentUser && currentUser.role === 'admin'" v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma" class="button-unstyled nav-icon"
class="nav-icon"
target="_blank" target="_blank"
@click.stop :title="$t('nav.administration')"
@click.stop="openAdminModal"
> >
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
icon="tachometer-alt" icon="tachometer-alt"
:title="$t('nav.administration')"
/> />
</a> </button>
<span class="spacer" /> <span class="spacer" />
<button <button
v-if="currentUser" v-if="currentUser"
class="button-unstyled nav-icon" class="button-unstyled nav-icon"
@click.prevent="logout" :title="$t('login.logout')"
@click.stop.prevent="logout"
> >
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
icon="sign-out-alt" icon="sign-out-alt"
:title="$t('login.logout')"
/> />
</button> </button>
</div> </div>
</div> </div>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')"
:confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout"
@cancelled="hideConfirmLogout"
>
{{ $t('login.logout_confirm') }}
</confirm-modal>
</teleport>
</nav> </nav>
</template> </template>
<script src="./desktop_nav.js"></script> <script src="./desktop_nav.js"></script>

View File

@ -25,7 +25,7 @@
<script src="./dialog_modal.js"></script> <script src="./dialog_modal.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
// TODO: unify with other modals. // TODO: unify with other modals.
.dark-overlay { .dark-overlay {
@ -38,8 +38,8 @@
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
background: rgba(27,31,35,.5); background: rgb(27 31 35 / 50%);
z-index: 99; z-index: 2000;
} }
} }
@ -51,7 +51,7 @@
margin: 15vh auto; margin: 15vh auto;
position: fixed; position: fixed;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 999; z-index: 2001;
cursor: default; cursor: default;
display: block; display: block;
background-color: $fallback--bg; background-color: $fallback--bg;
@ -65,7 +65,7 @@
.dialog-modal-content { .dialog-modal-content {
margin: 0; margin: 0;
padding: 1rem 1rem; padding: 1rem;
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg); background-color: var(--bg, $fallback--bg);
white-space: normal; white-space: normal;
@ -73,7 +73,7 @@
.dialog-modal-footer { .dialog-modal-footer {
margin: 0; margin: 0;
padding: .5em .5em; padding: 0.5em;
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg); background-color: var(--bg, $fallback--bg);
border-top: 1px solid $fallback--border; border-top: 1px solid $fallback--border;
@ -83,7 +83,7 @@
button { button {
width: auto; width: auto;
margin-left: .5rem; margin-left: 0.5rem;
} }
} }
} }

View File

@ -26,6 +26,7 @@
.modal-view.edit-form-modal-view { .modal-view.edit-form-modal-view {
align-items: flex-start; align-items: flex-start;
} }
.edit-form-modal-panel { .edit-form-modal-panel {
flex-shrink: 0; flex-shrink: 0;
margin-top: 25%; margin-top: 25%;

View File

@ -1,6 +1,7 @@
import Completion from '../../services/completion/completion.js' import Completion from '../../services/completion/completion.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue' import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { take } from 'lodash' import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@ -109,9 +110,10 @@ const EmojiInput = {
}, },
data () { data () {
return { return {
randomSeed: `${Math.random()}`.replace('.', '-'),
input: undefined, input: undefined,
caretEl: undefined, caretEl: undefined,
highlighted: 0, highlighted: -1,
caret: 0, caret: 0,
focused: false, focused: false,
blurTimeout: null, blurTimeout: null,
@ -125,12 +127,16 @@ const EmojiInput = {
components: { components: {
Popover, Popover,
EmojiPicker, EmojiPicker,
UnicodeDomainIndicator UnicodeDomainIndicator,
ScreenReaderNotice
}, },
computed: { computed: {
padEmoji () { padEmoji () {
return this.$store.getters.mergedConfig.padEmoji return this.$store.getters.mergedConfig.padEmoji
}, },
defaultCandidateIndex () {
return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
},
preText () { preText () {
return this.modelValue.slice(0, this.caret) return this.modelValue.slice(0, this.caret)
}, },
@ -203,6 +209,12 @@ const EmojiInput = {
top: this.input.scrollTop, top: this.input.scrollTop,
left: this.input.scrollLeft left: this.input.scrollLeft
}) })
},
suggestionListId () {
return `suggestions-${this.randomSeed}`
},
suggestionItemId () {
return (index) => `suggestion-item-${index}-${this.randomSeed}`
} }
}, },
mounted () { mounted () {
@ -278,6 +290,11 @@ const EmojiInput = {
...rest, ...rest,
img: imageUrl || '' img: imageUrl || ''
})) }))
this.highlighted = this.defaultCandidateIndex
this.$refs.screenReaderNotice.announce(
this.$tc('tool_tip.autocomplete_available',
this.suggestions.length,
{ number: this.suggestions.length }))
} }
}, },
methods: { methods: {
@ -374,26 +391,27 @@ const EmojiInput = {
}, },
cycleBackward (e) { cycleBackward (e) {
const len = this.suggestions.length || 0 const len = this.suggestions.length || 0
if (len > 1) {
this.highlighted -= 1 this.highlighted -= 1
if (this.highlighted < 0) { if (this.highlighted === -1) {
this.highlighted = this.suggestions.length - 1 this.input.focus()
} else if (this.highlighted < -1) {
this.highlighted = len - 1
} }
if (len > 0) {
e.preventDefault() e.preventDefault()
} else {
this.highlighted = 0
} }
}, },
cycleForward (e) { cycleForward (e) {
const len = this.suggestions.length || 0 const len = this.suggestions.length || 0
if (len > 1) {
this.highlighted += 1 this.highlighted += 1
if (this.highlighted >= len) { if (this.highlighted >= len) {
this.highlighted = 0 this.highlighted = -1
this.input.focus()
} }
if (len > 0) {
e.preventDefault() e.preventDefault()
} else {
this.highlighted = 0
} }
}, },
scrollIntoView () { scrollIntoView () {
@ -540,6 +558,13 @@ const EmojiInput = {
}) })
}, },
resize () { resize () {
},
autoCompleteItemLabel (suggestion) {
if (suggestion.user) {
return suggestion.displayText + ' ' + suggestion.detailText
} else {
return this.maybeLocalizedEmojiName(suggestion)
}
} }
} }
} }

View File

@ -4,12 +4,19 @@
class="emoji-input" class="emoji-input"
:class="{ 'with-picker': !hideEmojiButton }" :class="{ 'with-picker': !hideEmojiButton }"
> >
<slot /> <slot
:id="'textbox-' + randomSeed"
:aria-owns="suggestionListId"
aria-autocomplete="both"
:aria-expanded="showSuggestions"
:aria-activedescendant="(!showSuggestions || highlighted === -1) ? '' : suggestionItemId(highlighted)"
/>
<!-- TODO: make the 'x' disappear if at the end maybe? --> <!-- TODO: make the 'x' disappear if at the end maybe? -->
<div <div
ref="hiddenOverlay" ref="hiddenOverlay"
class="hidden-overlay" class="hidden-overlay"
:style="overlayStyle" :style="overlayStyle"
:aria-hidden="true"
> >
<span>{{ preText }}</span> <span>{{ preText }}</span>
<span <span
@ -18,11 +25,16 @@
>x</span> >x</span>
<span>{{ postText }}</span> <span>{{ postText }}</span>
</div> </div>
<screen-reader-notice
ref="screenReaderNotice"
aria-live="assertive"
/>
<template v-if="enableEmojiPicker"> <template v-if="enableEmojiPicker">
<button <button
v-if="!hideEmojiButton" v-if="!hideEmojiButton"
class="button-unstyled emoji-picker-icon" class="button-unstyled emoji-picker-icon"
type="button" type="button"
:title="$t('emoji.add_emoji')"
@click.prevent="togglePicker" @click.prevent="togglePicker"
> >
<FAIcon :icon="['far', 'smile-beam']" /> <FAIcon :icon="['far', 'smile-beam']" />
@ -43,17 +55,24 @@
ref="suggestorPopover" ref="suggestorPopover"
class="autocomplete-panel" class="autocomplete-panel"
placement="bottom" placement="bottom"
:trigger-attrs="{ 'aria-hidden': true }"
> >
<template #content> <template #content>
<div <div
:id="suggestionListId"
ref="panel-body" ref="panel-body"
class="autocomplete-panel-body" class="autocomplete-panel-body"
role="listbox"
> >
<div <div
v-for="(suggestion, index) in suggestions" v-for="(suggestion, index) in suggestions"
:id="suggestionItemId(index)"
:key="index" :key="index"
class="autocomplete-item" class="autocomplete-item"
role="option"
:class="{ highlighted: index === highlighted }" :class="{ highlighted: index === highlighted }"
:aria-label="autoCompleteItemLabel(suggestion)"
:aria-selected="index === highlighted"
@click.stop.prevent="onClick($event, suggestion)" @click.stop.prevent="onClick($event, suggestion)"
> >
<span class="image"> <span class="image">
@ -91,22 +110,18 @@
<script src="./emoji_input.js"></script> <script src="./emoji_input.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.emoji-input { .emoji-input {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
&.with-picker input {
padding-right: 30px;
}
.emoji-picker-icon { .emoji-picker-icon {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
margin: .2em .25em; margin: 0.2em 0.25em;
font-size: 1.3em; font-size: 1.3em;
cursor: pointer; cursor: pointer;
line-height: 24px; line-height: 24px;
@ -123,14 +138,19 @@
margin-top: 2px; margin-top: 2px;
&.hide { &.hide {
display: none display: none;
} }
} }
input, textarea { input,
textarea {
flex: 1 0 auto; flex: 1 0 auto;
} }
&.with-picker input {
padding-right: 30px;
}
.hidden-overlay { .hidden-overlay {
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
@ -140,8 +160,10 @@
right: 0; right: 0;
left: 0; left: 0;
overflow: hidden; overflow: hidden;
/* DEBUG STUFF */ /* DEBUG STUFF */
color: red; color: red;
/* set opacity to non-zero to see the overlay */ /* set opacity to non-zero to see the overlay */
.caret { .caret {
@ -151,6 +173,7 @@
} }
} }
} }
.autocomplete { .autocomplete {
&-panel { &-panel {
position: absolute; position: absolute;
@ -160,7 +183,7 @@
display: flex; display: flex;
cursor: pointer; cursor: pointer;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
border-bottom: 1px solid rgba(0, 0, 0, 0.4); border-bottom: 1px solid rgb(0 0 0 / 40%);
height: 32px; height: 32px;
.image { .image {
@ -169,7 +192,6 @@
line-height: 32px; line-height: 32px;
text-align: center; text-align: center;
font-size: 32px; font-size: 32px;
margin-right: 4px; margin-right: 4px;
img { img {
@ -199,6 +221,7 @@
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--selectedMenuPopover, $fallback--fg); background-color: var(--selectedMenuPopover, $fallback--fg);
color: var(--selectedMenuPopoverText, $fallback--text); color: var(--selectedMenuPopoverText, $fallback--text);
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint); --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);

View File

@ -94,8 +94,9 @@ export const suggestUsers = ({ dispatch, state }) => {
const newSuggestions = state.users.users.filter( const newSuggestions = state.users.users.filter(
user => user =>
user.screen_name && user.name && (
user.screen_name.toLowerCase().startsWith(noPrefix) || user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix) user.name.toLowerCase().startsWith(noPrefix))
).slice(0, 20).sort((a, b) => { ).slice(0, 20).sort((a, b) => {
let aScore = 0 let aScore = 0
let bScore = 0 let bScore = 0

View File

@ -3,7 +3,6 @@ import Checkbox from '../checkbox/checkbox.vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import { ensureFinalFallback } from '../../i18n/languages.js' import { ensureFinalFallback } from '../../i18n/languages.js'
import lozad from 'lozad'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faBoxOpen, faBoxOpen,
@ -19,7 +18,7 @@ import {
faCode, faCode,
faFlag faFlag
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { debounce, trim } from 'lodash' import { debounce, trim, chunk } from 'lodash'
library.add( library.add(
faBoxOpen, faBoxOpen,
@ -82,14 +81,31 @@ const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
return orderedEmojiList.flat() return orderedEmojiList.flat()
} }
const getOffset = (elem) => {
const style = elem.style.transform
const res = /translateY\((\d+)px\)/.exec(style)
if (!res) { return 0 }
return res[1]
}
const toHeaderId = id => {
return id.replace(/^row-\d+-/, '')
}
const EmojiPicker = { const EmojiPicker = {
props: { props: {
enableStickerPicker: { enableStickerPicker: {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false
},
hideCustomEmoji: {
required: false,
type: Boolean,
default: false
} }
}, },
inject: ['popoversZLayer'],
data () { data () {
return { return {
keyword: '', keyword: '',
@ -102,7 +118,8 @@ const EmojiPicker = {
contentLoaded: false, contentLoaded: false,
groupRefs: {}, groupRefs: {},
emojiRefs: {}, emojiRefs: {},
filteredEmojiGroups: [] filteredEmojiGroups: [],
width: 0
} }
}, },
components: { components: {
@ -125,9 +142,6 @@ const EmojiPicker = {
setGroupRef (name) { setGroupRef (name) {
return el => { this.groupRefs[name] = el } return el => { this.groupRefs[name] = el }
}, },
setEmojiRef (name) {
return el => { this.emojiRefs[name] = el }
},
onPopoverShown () { onPopoverShown () {
this.$emit('show') this.$emit('show')
}, },
@ -147,18 +161,21 @@ const EmojiPicker = {
} }
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen }) this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
}, },
onScroll (e) { onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
const target = (e && e.target) || this.$refs['emoji-groups'] const target = this.$refs['emoji-groups'].$el
this.updateScrolledClass(target) this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
this.scrolledGroup(target)
}, },
scrolledGroup (target) { scrolledGroup (target, start, end) {
const top = target.scrollTop + 5 const top = target.scrollTop + 5
this.$nextTick(() => { this.$nextTick(() => {
this.allEmojiGroups.forEach(group => { this.emojiItems.slice(start, end + 1).forEach(group => {
const headerId = toHeaderId(group.id)
const ref = this.groupRefs['group-' + group.id] const ref = this.groupRefs['group-' + group.id]
if (ref && ref.offsetTop <= top) { if (!ref) { return }
this.activeGroup = group.id const elem = ref.$el.parentElement
if (!elem) { return }
if (elem && getOffset(elem) <= top) {
this.activeGroup = headerId
} }
}) })
this.scrollHeader() this.scrollHeader()
@ -181,14 +198,10 @@ const EmojiPicker = {
setScroll(right + margin - headerCont.clientWidth) setScroll(right + margin - headerCont.clientWidth)
} }
}, },
highlight (key) { highlight (groupId) {
const ref = this.groupRefs['group-' + key]
const top = ref.offsetTop
this.setShowStickers(false) this.setShowStickers(false)
this.activeGroup = key const indexInList = this.emojiItems.findIndex(k => k.id === groupId)
this.$nextTick(() => { this.$refs['emoji-groups'].scrollToItem(indexInList)
this.$refs['emoji-groups'].scrollTop = top + 1
})
}, },
updateScrolledClass (target) { updateScrolledClass (target) {
if (target.scrollTop <= 5) { if (target.scrollTop <= 5) {
@ -208,43 +221,13 @@ const EmojiPicker = {
filterByKeyword (list, keyword) { filterByKeyword (list, keyword) {
return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName) return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
}, },
initializeLazyLoad () {
this.destroyLazyLoad()
this.$nextTick(() => {
this.$lozad = lozad('.still-image.emoji-picker-emoji', {
load: el => {
const name = el.getAttribute('data-emoji-name')
const vn = this.emojiRefs[name]
if (!vn) {
return
}
vn.loadLazy()
}
})
this.$lozad.observe()
})
},
waitForDomAndInitializeLazyLoad () {
this.$nextTick(() => this.initializeLazyLoad())
},
destroyLazyLoad () {
if (this.$lozad) {
if (this.$lozad.observer) {
this.$lozad.observer.disconnect()
}
if (this.$lozad.mutationObserver) {
this.$lozad.mutationObserver.disconnect()
}
}
},
onShowing () { onShowing () {
const oldContentLoaded = this.contentLoaded const oldContentLoaded = this.contentLoaded
this.recalculateItemPerRow()
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.search.focus() this.$refs.search.focus()
}) })
this.contentLoaded = true this.contentLoaded = true
this.waitForDomAndInitializeLazyLoad()
this.filteredEmojiGroups = this.getFilteredEmojiGroups() this.filteredEmojiGroups = this.getFilteredEmojiGroups()
if (!oldContentLoaded) { if (!oldContentLoaded) {
this.$nextTick(() => { this.$nextTick(() => {
@ -261,6 +244,14 @@ const EmojiPicker = {
emojis: this.filterByKeyword(group.emojis, trim(this.keyword)) emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
})) }))
.filter(group => group.emojis.length > 0) .filter(group => group.emojis.length > 0)
},
recalculateItemPerRow () {
this.$nextTick(() => {
if (!this.$refs['emoji-groups']) {
return
}
this.width = this.$refs['emoji-groups'].$el.clientWidth
})
} }
}, },
watch: { watch: {
@ -269,14 +260,22 @@ const EmojiPicker = {
this.debouncedHandleKeywordChange() this.debouncedHandleKeywordChange()
}, },
allCustomGroups () { allCustomGroups () {
this.waitForDomAndInitializeLazyLoad()
this.filteredEmojiGroups = this.getFilteredEmojiGroups() this.filteredEmojiGroups = this.getFilteredEmojiGroups()
} }
}, },
destroyed () {
this.destroyLazyLoad()
},
computed: { computed: {
minItemSize () {
return this.emojiHeight
},
emojiHeight () {
return 32 + 4
},
emojiWidth () {
return 32 + 4
},
itemPerRow () {
return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6
},
activeGroupView () { activeGroupView () {
return this.showingStickers ? '' : this.activeGroup return this.showingStickers ? '' : this.activeGroup
}, },
@ -287,7 +286,14 @@ const EmojiPicker = {
return 0 return 0
}, },
allCustomGroups () { allCustomGroups () {
return this.$store.getters.groupedCustomEmojis if (this.hideCustomEmoji) {
return {}
}
const emojis = this.$store.getters.groupedCustomEmojis
if (emojis.unpacked) {
emojis.unpacked.text = this.$t('emoji.unpacked')
}
return emojis
}, },
defaultGroup () { defaultGroup () {
return Object.keys(this.allCustomGroups)[0] return Object.keys(this.allCustomGroups)[0]
@ -310,10 +316,20 @@ const EmojiPicker = {
}, },
debouncedHandleKeywordChange () { debouncedHandleKeywordChange () {
return debounce(() => { return debounce(() => {
this.waitForDomAndInitializeLazyLoad()
this.filteredEmojiGroups = this.getFilteredEmojiGroups() this.filteredEmojiGroups = this.getFilteredEmojiGroups()
}, 500) }, 500)
}, },
emojiItems () {
return this.filteredEmojiGroups.map(group =>
chunk(group.emojis, this.itemPerRow)
.map((items, index) => ({
...group,
id: index === 0 ? group.id : `row-${index}-${group.id}`,
emojis: items,
isFirstRow: index === 0
})))
.reduce((a, c) => a.concat(c), [])
},
languages () { languages () {
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage) return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
}, },
@ -335,6 +351,9 @@ const EmojiPicker = {
return emoji.displayText return emoji.displayText
} }
},
isInModal () {
return this.popoversZLayer === 'modals'
} }
} }
} }

View File

@ -1,4 +1,4 @@
@import '../../_variables.scss'; @import "../../variables";
$emoji-picker-header-height: 36px; $emoji-picker-header-height: 36px;
$emoji-picker-header-picture-width: 32px; $emoji-picker-header-picture-width: 32px;
@ -7,14 +7,14 @@ $emoji-picker-emoji-size: 32px;
.emoji-picker { .emoji-picker {
width: 25em; width: 25em;
max-width: 100vw; max-width: calc(100vw - 20px); // popover gives 10px margin from window edge
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--popover, $fallback--bg); background-color: var(--popover, $fallback--bg);
color: $fallback--link; color: $fallback--link;
color: var(--popoverText, $fallback--link); color: var(--popoverText, $fallback--link);
--lightText: var(--popoverLightText, $fallback--faint);
--faint: var(--popoverFaintText, $fallback--faint); --faint: var(--popoverFaintText, $fallback--faint);
--faintLink: var(--popoverFaintLink, $fallback--faint); --faintLink: var(--popoverFaintLink, $fallback--faint);
--lightText: var(--popoverLightText, $fallback--lightText); --lightText: var(--popoverLightText, $fallback--lightText);
@ -28,6 +28,7 @@ $emoji-picker-emoji-size: 32px;
max-width: $emoji-picker-header-picture-width; max-width: $emoji-picker-header-picture-width;
height: $emoji-picker-header-picture-height; height: $emoji-picker-header-picture-height;
max-height: $emoji-picker-header-picture-height; max-height: $emoji-picker-header-picture-height;
.still-image { .still-image {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
@ -62,24 +63,18 @@ $emoji-picker-emoji-size: 32px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0px; min-height: 0;
} }
.emoji-tabs { .emoji-tabs {
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: row; flex-flow: row nowrap;
flex-wrap: nowrap;
overflow-x: auto; overflow-x: auto;
} }
.emoji-groups {
min-height: 200px;
}
.additional-tabs { .additional-tabs {
display: flex; display: flex;
flex: 1;
border-left: 1px solid; border-left: 1px solid;
border-left-color: $fallback--icon; border-left-color: $fallback--icon;
border-left-color: var(--icon, $fallback--icon); border-left-color: var(--icon, $fallback--icon);
@ -121,7 +116,7 @@ $emoji-picker-emoji-size: 32px;
} }
.sticker-picker { .sticker-picker {
flex: 1 1 auto flex: 1 1 auto;
} }
.stickers, .stickers,
@ -151,22 +146,27 @@ $emoji-picker-emoji-size: 32px;
} }
&-groups { &-groups {
height: 100%;
min-height: 200px;
flex: 1 1 1px; flex: 1 1 1px;
position: relative; position: relative;
overflow: auto; overflow: auto;
user-select: none; user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, mask:
linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat, linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white); linear-gradient(to top, white, white);
transition: mask-size 150ms; transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto; mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different // Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor; mask-composite: xor;
mask-composite: exclude; mask-composite: exclude;
&.scrolled { &.scrolled {
&-top { &-top {
mask-size: 100% 20px, 100% 0, auto; mask-size: 100% 20px, 100% 0, auto;
} }
&-bottom { &-bottom {
mask-size: 100% 0, 100% 20px, auto; mask-size: 100% 0, 100% 20px, auto;
} }
@ -200,7 +200,6 @@ $emoji-picker-emoji-size: 32px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 4px; margin: 4px;
cursor: pointer; cursor: pointer;
.emoji-picker-emoji.-custom { .emoji-picker-emoji.-custom {
@ -208,12 +207,11 @@ $emoji-picker-emoji-size: 32px;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
} }
.emoji-picker-emoji.-unicode { .emoji-picker-emoji.-unicode {
font-size: 24px; font-size: 24px;
overflow: hidden; overflow: hidden;
} }
} }
} }
} }

View File

@ -3,13 +3,20 @@
ref="popover" ref="popover"
trigger="click" trigger="click"
popover-class="emoji-picker popover-default" popover-class="emoji-picker popover-default"
:trigger-attrs="{ 'aria-hidden': true }"
@show="onPopoverShown" @show="onPopoverShown"
@close="onPopoverClosed" @close="onPopoverClosed"
> >
<template #content> <template #content>
<div class="heading"> <div class="heading">
<!--
Body scroll lock needs to be on every scrollable element on safari iOS.
Here we tell it to enable scrolling for this element.
See https://github.com/willmcpo/body-scroll-lock#vanilla-js
-->
<span <span
ref="header" ref="header"
v-body-scroll-lock="isInModal"
class="emoji-tabs" class="emoji-tabs"
> >
<span <span
@ -21,6 +28,7 @@
active: activeGroupView === group.id active: activeGroupView === group.id
}" }"
:title="group.text" :title="group.text"
role="button"
@click.prevent="highlight(group.id)" @click.prevent="highlight(group.id)"
> >
<span <span
@ -74,19 +82,32 @@
@input="$event.target.composing = false" @input="$event.target.composing = false"
> >
</div> </div>
<div <!-- Enables scrolling for this element on safari iOS. See comments for header. -->
<DynamicScroller
ref="emoji-groups" ref="emoji-groups"
v-body-scroll-lock="isInModal"
class="emoji-groups" class="emoji-groups"
:class="groupsScrolledClass" :class="groupsScrolledClass"
@scroll="onScroll" :min-item-size="minItemSize"
:items="emojiItems"
:emit-update="true"
@update="onScroll"
@visible="recalculateItemPerRow"
@resize="recalculateItemPerRow"
>
<template #default="{ item: group, index, active }">
<DynamicScrollerItem
:ref="setGroupRef('group-' + group.id)"
:item="group"
:active="active"
:data-index="index"
:size-dependencies="[group.emojis.length]"
> >
<div <div
v-for="group in filteredEmojiGroups"
:key="group.id"
class="emoji-group" class="emoji-group"
> >
<h6 <h6
:ref="setGroupRef('group-' + group.id)" v-if="group.isFirstRow"
class="emoji-group-title" class="emoji-group-title"
> >
{{ group.text }} {{ group.text }}
@ -96,6 +117,7 @@
:key="group.id + emoji.displayText" :key="group.id + emoji.displayText"
:title="maybeLocalizedEmojiName(emoji)" :title="maybeLocalizedEmojiName(emoji)"
class="emoji-item" class="emoji-item"
role="button"
@click.stop.prevent="onEmoji(emoji)" @click.stop.prevent="onEmoji(emoji)"
> >
<span <span
@ -104,15 +126,17 @@
>{{ emoji.replacement }}</span> >{{ emoji.replacement }}</span>
<still-image <still-image
v-else v-else
:ref="setEmojiRef(group.id + emoji.displayText)"
class="emoji-picker-emoji -custom" class="emoji-picker-emoji -custom"
:data-src="emoji.imageUrl" loading="lazy"
:alt="maybeLocalizedEmojiName(emoji)"
:src="emoji.imageUrl"
:data-emoji-name="group.id + emoji.displayText" :data-emoji-name="group.id + emoji.displayText"
/> />
</span> </span>
<span :ref="setGroupRef('group-end-' + group.id)" />
</div>
</div> </div>
</DynamicScrollerItem>
</template>
</DynamicScroller>
<div class="keep-open"> <div class="keep-open">
<Checkbox v-model="keepOpen"> <Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }} {{ $t('emoji.keep_open') }}

View File

@ -1,5 +1,17 @@
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faPlus,
faMinus,
faCheck
} from '@fortawesome/free-solid-svg-icons'
library.add(
faPlus,
faMinus,
faCheck
)
const EMOJI_REACTION_COUNT_CUTOFF = 12 const EMOJI_REACTION_COUNT_CUTOFF = 12
@ -33,6 +45,9 @@ const EmojiReactions = {
}, },
loggedIn () { loggedIn () {
return !!this.$store.state.users.currentUser return !!this.$store.state.users.currentUser
},
remoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
} }
}, },
methods: { methods: {
@ -42,10 +57,10 @@ const EmojiReactions = {
reactedWith (emoji) { reactedWith (emoji) {
return this.status.emoji_reactions.find(r => r.name === emoji).me return this.status.emoji_reactions.find(r => r.name === emoji).me
}, },
fetchEmojiReactionsByIfMissing () { async fetchEmojiReactionsByIfMissing () {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts) const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
if (hasNoAccounts) { if (hasNoAccounts) {
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id) return await this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
} }
}, },
reactWith (emoji) { reactWith (emoji) {
@ -54,14 +69,26 @@ const EmojiReactions = {
unreact (emoji) { unreact (emoji) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
}, },
emojiOnClick (emoji, event) { async emojiOnClick (emoji, event) {
if (!this.loggedIn) return if (!this.loggedIn) return
await this.fetchEmojiReactionsByIfMissing()
if (this.reactedWith(emoji)) { if (this.reactedWith(emoji)) {
this.unreact(emoji) this.unreact(emoji)
} else { } else {
this.reactWith(emoji) this.reactWith(emoji)
} }
},
counterTriggerAttrs (reaction) {
return {
class: [
'btn',
'button-default',
'emoji-reaction-count-button',
{ '-picked-reaction': this.reactedWith(reaction.name) }
],
'aria-label': this.$tc('status.reaction_count_label', reaction.count, { num: reaction.count })
}
} }
} }
} }

View File

@ -1,20 +1,64 @@
<template> <template>
<div class="EmojiReactions"> <div class="EmojiReactions">
<UserListPopover <span
v-for="(reaction) in emojiReactions" v-for="(reaction) in emojiReactions"
:key="reaction.name" :key="reaction.url || reaction.name"
:users="accountsForEmoji[reaction.name]" class="emoji-reaction-container btn-group"
> >
<button <component
:is="loggedIn ? 'button' : 'a'"
v-bind="!loggedIn ? { href: remoteInteractionLink } : {}"
role="button"
class="emoji-reaction btn button-default" class="emoji-reaction btn button-default"
:class="{ '-picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" :class="{ '-picked-reaction': reactedWith(reaction.name) }"
:title="reaction.url ? reaction.name : undefined"
:aria-pressed="reactedWith(reaction.name)"
@click="emojiOnClick(reaction.name, $event)" @click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()"
> >
<span class="reaction-emoji">{{ reaction.name }}</span> <span
<span>{{ reaction.count }}</span> class="reaction-emoji"
</button> >
<img
v-if="reaction.url"
:src="reaction.url"
class="reaction-emoji-content"
width="1em"
>
<span
v-else
class="reaction-emoji reaction-emoji-content"
>{{ reaction.name }}</span>
</span>
<FALayers>
<FAIcon
v-if="reactedWith(reaction.name)"
class="active-marker"
transform="shrink-6 up-9"
icon="check"
/>
<FAIcon
v-if="!reactedWith(reaction.name)"
class="focus-marker"
transform="shrink-6 up-9"
icon="plus"
/>
<FAIcon
v-else
class="focus-marker"
transform="shrink-6 up-9"
icon="minus"
/>
</FALayers>
</component>
<UserListPopover
:users="accountsForEmoji[reaction.name]"
class="emoji-reaction-popover"
:trigger-attrs="counterTriggerAttrs(reaction)"
@show="fetchEmojiReactionsByIfMissing()"
>
<span class="emoji-reaction-counts">{{ reaction.count }}</span>
</UserListPopover> </UserListPopover>
</span>
<a <a
v-if="tooManyReactions" v-if="tooManyReactions"
class="emoji-reaction-expand faint" class="emoji-reaction-expand faint"
@ -28,43 +72,121 @@
<script src="./emoji_reactions.js"></script> <script src="./emoji_reactions.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
@import "../../mixins";
.EmojiReactions { .EmojiReactions {
display: flex; display: flex;
margin-top: 0.25em; margin-top: 0.25em;
flex-wrap: wrap; flex-wrap: wrap;
.emoji-reaction { --emoji-size: calc(1.25em * var(--emojiReactionsScale, 1));
padding: 0 0.5em;
margin-right: 0.5em; .emoji-reaction-container {
display: flex;
align-items: stretch;
margin-top: 0.5em; margin-top: 0.5em;
margin-right: 0.5em;
.emoji-reaction-popover {
padding: 0;
.emoji-reaction-count-button {
background-color: var(--btn);
margin: 0;
height: 100%;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
box-sizing: border-box;
min-width: 2em;
display: inline-flex;
justify-content: center;
align-items: center;
color: $fallback--text;
color: var(--btnText, $fallback--text);
&.-picked-reaction {
border: 1px solid var(--accent, $fallback--link);
margin-right: -1px;
}
}
}
}
.emoji-reaction {
padding-left: 0.5em;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-sizing: border-box; box-sizing: border-box;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
margin: 0;
.reaction-emoji { .reaction-emoji {
width: 1.25em; width: var(--emoji-size);
height: var(--emoji-size);
margin-right: 0.25em; margin-right: 0.25em;
line-height: var(--emoji-size);
display: flex;
justify-content: center;
align-items: center;
}
.reaction-emoji-content {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
line-height: inherit;
overflow: hidden;
font-size: calc(var(--emoji-size) * 0.8);
margin: 0;
} }
&:focus { &:focus {
outline: none; outline: none;
} }
&.not-clickable { .svg-inline--fa {
cursor: default; color: $fallback--text;
&:hover { color: var(--btnText, $fallback--text);
box-shadow: $fallback--buttonShadow;
box-shadow: var(--buttonShadow);
}
} }
&.-picked-reaction { &.-picked-reaction {
border: 1px solid var(--accent, $fallback--link); border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px); margin-right: -1px;
.svg-inline--fa {
color: $fallback--link;
color: var(--accent, $fallback--link);
}
}
@include unfocused-style {
.focus-marker {
visibility: hidden;
}
.active-marker {
visibility: visible;
}
}
@include focused-style {
.svg-inline--fa {
color: $fallback--link;
color: var(--accent, $fallback--link);
}
.focus-marker {
visibility: visible;
}
.active-marker {
visibility: hidden;
}
} }
} }
@ -75,10 +197,10 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
} }
} }
</style> </style>

View File

@ -1,4 +1,5 @@
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faEllipsisH, faEllipsisH,
@ -32,10 +33,14 @@ library.add(
const ExtraButtons = { const ExtraButtons = {
props: ['status'], props: ['status'],
components: { Popover }, components: {
Popover,
ConfirmModal
},
data () { data () {
return { return {
expanded: false expanded: false,
showingDeleteDialog: false
} }
}, },
methods: { methods: {
@ -46,11 +51,22 @@ const ExtraButtons = {
this.expanded = false this.expanded = false
}, },
deleteStatus () { deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm')) if (this.shouldConfirmDelete) {
if (confirmed) { this.showDeleteStatusConfirmDialog()
this.$store.dispatch('deleteStatus', { id: this.status.id }) } else {
this.doDeleteStatus()
} }
}, },
doDeleteStatus () {
this.$store.dispatch('deleteStatus', { id: this.status.id })
this.hideDeleteStatusConfirmDialog()
},
showDeleteStatusConfirmDialog () {
this.showingDeleteDialog = true
},
hideDeleteStatusConfirmDialog () {
this.showingDeleteDialog = false
},
pinStatus () { pinStatus () {
this.$store.dispatch('pinStatus', this.status.id) this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
@ -133,7 +149,10 @@ const ExtraButtons = {
isEdited () { isEdited () {
return this.status.edited_at !== null 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
}
} }
} }

View File

@ -165,6 +165,18 @@
/> />
</FALayers> </FALayers>
</span> </span>
<teleport to="#modal">
<ConfirmModal
v-if="showingDeleteDialog"
:title="$t('status.delete_confirm_title')"
:cancel-text="$t('status.delete_confirm_cancel_button')"
:confirm-text="$t('status.delete_confirm_accept_button')"
@cancelled="hideDeleteStatusConfirmDialog"
@accepted="doDeleteStatus"
>
{{ $t('status.delete_confirm') }}
</ConfirmModal>
</teleport>
</template> </template>
</Popover> </Popover>
</template> </template>
@ -172,15 +184,10 @@
<script src="./extra_buttons.js"></script> <script src="./extra_buttons.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
@import '../../_mixins.scss'; @import "../../mixins";
.ExtraButtons { .ExtraButtons {
/* override of popover internal stuff */
.popover-trigger-button {
width: auto;
}
.popover-trigger { .popover-trigger {
position: static; position: static;
padding: 10px; padding: 10px;
@ -190,10 +197,12 @@
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
} }
} }
.popover-trigger-button { .popover-trigger-button {
/* override of popover internal stuff */
width: auto;
@include unfocused-style { @include unfocused-style {
.focus-marker { .focus-marker {
visibility: hidden; visibility: hidden;

View File

@ -38,13 +38,20 @@
class="button-unstyled interactive" class="button-unstyled interactive"
target="_blank" target="_blank"
role="button" role="button"
:title="$t('tool_tip.favorite')"
:href="remoteInteractionLink" :href="remoteInteractionLink"
> >
<FALayers class="fa-scale-110 fa-old-padding-layer">
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" class="fa-scale-110"
:title="$t('tool_tip.favorite')"
:icon="['far', 'star']" :icon="['far', 'star']"
/> />
<FAIcon
class="focus-marker"
transform="shrink-6 up-9 right-12"
icon="plus"
/>
</FALayers>
</a> </a>
<span <span
v-if="!mergedConfig.hidePostStats && status.fave_num > 0" v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
@ -58,8 +65,8 @@
<script src="./favorite_button.js"></script> <script src="./favorite_button.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
@import '../../_mixins.scss'; @import "../../mixins";
.FavoriteButton { .FavoriteButton {
display: flex; display: flex;

View File

@ -42,7 +42,8 @@
<script src="./flash.js"></script> <script src="./flash.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.Flash { .Flash {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
@ -78,7 +79,7 @@
.hidden { .hidden {
display: none; display: none;
visibility: 'hidden'; visibility: "hidden";
} }
} }
</style> </style>

View File

@ -1,12 +1,20 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default { export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
components: {
ConfirmModal
},
data () { data () {
return { return {
inProgress: false inProgress: false,
showingConfirmUnfollow: false
} }
}, },
computed: { computed: {
shouldConfirmUnfollow () {
return this.$store.getters.mergedConfig.modalOnUnfollow
},
isPressed () { isPressed () {
return this.inProgress || this.relationship.following return this.inProgress || this.relationship.following
}, },
@ -35,6 +43,12 @@ export default {
} }
}, },
methods: { methods: {
showConfirmUnfollow () {
this.showingConfirmUnfollow = true
},
hideConfirmUnfollow () {
this.showingConfirmUnfollow = false
},
onClick () { onClick () {
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
}, },
@ -45,12 +59,21 @@ export default {
}) })
}, },
unfollow () { unfollow () {
if (this.shouldConfirmUnfollow) {
this.showConfirmUnfollow()
} else {
this.doUnfollow()
}
},
doUnfollow () {
const store = this.$store const store = this.$store
this.inProgress = true this.inProgress = true
requestUnfollow(this.relationship.id, store).then(() => { requestUnfollow(this.relationship.id, store).then(() => {
this.inProgress = false this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id }) store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
}) })
this.hideConfirmUnfollow()
} }
} }
} }

View File

@ -7,6 +7,27 @@
@click="onClick" @click="onClick"
> >
{{ label }} {{ label }}
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmUnfollow"
:title="$t('user_card.unfollow_confirm_title')"
:confirm-text="$t('user_card.unfollow_confirm_accept_button')"
:cancel-text="$t('user_card.unfollow_confirm_cancel_button')"
@accepted="doUnfollow"
@cancelled="hideConfirmUnfollow"
>
<i18n-t
keypath="user_card.unfollow_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</button> </button>
</template> </template>

View File

@ -24,6 +24,7 @@
/> />
<RemoveFollowerButton <RemoveFollowerButton
v-if="noFollowsYou && relationship.followed_by" v-if="noFollowsYou && relationship.followed_by"
:user="user"
:relationship="relationship" :relationship="relationship"
class="follow-card-button" class="follow-card-button"
/> />
@ -39,9 +40,8 @@
&-content-container { &-content-container {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: row; flex-flow: row wrap;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
line-height: 1.5em; line-height: 1.5em;
} }

View File

@ -1,10 +1,18 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js' import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
const FollowRequestCard = { const FollowRequestCard = {
props: ['user'], props: ['user'],
components: { components: {
BasicUserCard BasicUserCard,
ConfirmModal
},
data () {
return {
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
}
}, },
methods: { methods: {
findFollowRequestNotificationId () { findFollowRequestNotificationId () {
@ -13,7 +21,26 @@ const FollowRequestCard = {
) )
return notif && notif.id return notif && notif.id
}, },
showApproveConfirmDialog () {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
this.showingDenyConfirmDialog = false
},
approveUser () { approveUser () {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
@ -25,14 +52,34 @@ const FollowRequestCard = {
notification.type = 'follow' notification.type = 'follow'
} }
}) })
this.hideApproveConfirmDialog()
}, },
denyUser () { denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => { .then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId }) this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog()
}
},
computed: {
mergedConfig () {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
} }
} }
} }

View File

@ -14,6 +14,28 @@
{{ $t('user_card.deny') }} {{ $t('user_card.deny') }}
</button> </button>
</div> </div>
<teleport to="#modal">
<confirm-modal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
<confirm-modal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
</teleport>
</basic-user-card> </basic-user-card>
</template> </template>
@ -22,8 +44,8 @@
<style lang="scss"> <style lang="scss">
.follow-request-card-content-container { .follow-request-card-content-container {
display: flex; display: flex;
flex-direction: row; flex-flow: row wrap;
flex-wrap: wrap;
button { button {
margin-top: 0.5em; margin-top: 0.5em;
margin-right: 0.5em; margin-right: 0.5em;

View File

@ -4,6 +4,7 @@
:class="{ custom: isCustom }" :class="{ custom: isCustom }"
> >
<label <label
:id="name + '-label'"
:for="preset === 'custom' ? name : name + '-font-switcher'" :for="preset === 'custom' ? name : name + '-font-switcher'"
class="label" class="label"
> >
@ -12,7 +13,8 @@
<input <input
v-if="typeof fallback !== 'undefined'" v-if="typeof fallback !== 'undefined'"
:id="name + '-o'" :id="name + '-o'"
class="opt exlcude-disabled" :aria-labelledby="name + '-label'"
class="opt exlcude-disabled visible-for-screenreader-only"
type="checkbox" type="checkbox"
:checked="present" :checked="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
@ -21,6 +23,7 @@
v-if="typeof fallback !== 'undefined'" v-if="typeof fallback !== 'undefined'"
class="opt-l" class="opt-l"
:for="name + '-o'" :for="name + '-o'"
:aria-hidden="true"
/> />
{{ ' ' }} {{ ' ' }}
<Select <Select
@ -50,17 +53,20 @@
<script src="./font_control.js"></script> <script src="./font_control.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.font-control { .font-control {
input.custom-font { input.custom-font {
min-width: 10em; min-width: 10em;
} }
&.custom { &.custom {
/* TODO Should make proper joiners... */ /* TODO Should make proper joiners... */
.font-switcher { .font-switcher {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
.custom-font { .custom-font {
border-top-left-radius: 0; border-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;

View File

@ -4,6 +4,7 @@ import { sumBy, set } from 'lodash'
const Gallery = { const Gallery = {
props: [ props: [
'attachments', 'attachments',
'compact',
'limitRows', 'limitRows',
'descriptions', 'descriptions',
'limit', 'limit',

View File

@ -20,6 +20,7 @@
v-for="(attachment, attachmentIndex) in row.items" v-for="(attachment, attachmentIndex) in row.items"
:key="attachment.id" :key="attachment.id"
class="gallery-item" class="gallery-item"
:compact="compact"
:nsfw="nsfw" :nsfw="nsfw"
:attachment="attachment" :attachment="attachment"
:size="size" :size="size"
@ -86,7 +87,7 @@
<script src='./gallery.js'></script> <script src='./gallery.js'></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.Gallery { .Gallery {
.gallery-rows { .gallery-rows {
@ -100,6 +101,53 @@
width: 100%; width: 100%;
flex-grow: 1; flex-grow: 1;
.gallery-row-inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-flow: row wrap;
align-content: stretch;
.gallery-item {
margin: 0 0.5em 0 0;
flex-grow: 1;
height: 100%;
box-sizing: border-box;
// to make failed images a bit more noticeable on chromium
min-width: 2em;
&:last-child {
margin: 0;
}
}
&.-grid {
width: 100%;
height: auto;
position: relative;
display: grid;
grid-gap: 0.5em;
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
.gallery-item {
margin: 0;
height: 200px;
}
}
}
&.-grid,
&.-minimal {
height: auto;
.gallery-row-inner {
position: relative;
}
}
&:not(:first-child) { &:not(:first-child) {
margin-top: 0.5em; margin-top: 0.5em;
} }
@ -114,7 +162,7 @@
linear-gradient(to top, white, white); linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */ /* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor; mask-composite: xor;
mask-composite: exclude; mask-composite: exclude;
} }
} }
@ -138,54 +186,5 @@
padding: 0 2em; padding: 0 2em;
} }
} }
.gallery-row {
&.-grid,
&.-minimal {
height: auto;
.gallery-row-inner {
position: relative;
}
}
}
.gallery-row-inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: stretch;
&.-grid {
width: 100%;
height: auto;
position: relative;
display: grid;
grid-column-gap: 0.5em;
grid-row-gap: 0.5em;
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
.gallery-item {
margin: 0;
height: 200px;
}
}
}
.gallery-item {
margin: 0 0.5em 0 0;
flex-grow: 1;
height: 100%;
box-sizing: border-box;
// to make failed images a bit more noticeable on chromium
min-width: 2em;
&:last-child {
margin: 0;
}
}
} }
</style> </style>

View File

@ -25,7 +25,7 @@
<script src="./global_notice_list.js"></script> <script src="./global_notice_list.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.global-notice-list { .global-notice-list {
position: fixed; position: fixed;
@ -73,6 +73,7 @@
.global-success { .global-success {
background-color: var(--alertPopupSuccess, $fallback--cGreen); background-color: var(--alertPopupSuccess, $fallback--cGreen);
color: var(--alertPopupSuccessText, $fallback--text); color: var(--alertPopupSuccessText, $fallback--text);
.svg-inline--fa { .svg-inline--fa {
color: var(--alertPopupSuccessText, $fallback--text); color: var(--alertPopupSuccessText, $fallback--text);
} }
@ -81,6 +82,7 @@
.global-info { .global-info {
background-color: var(--alertPopupNeutral, $fallback--fg); background-color: var(--alertPopupNeutral, $fallback--fg);
color: var(--alertPopupNeutralText, $fallback--text); color: var(--alertPopupNeutralText, $fallback--text);
.svg-inline--fa { .svg-inline--fa {
color: var(--alertPopupNeutralText, $fallback--text); color: var(--alertPopupNeutralText, $fallback--text);
} }
@ -88,6 +90,7 @@
.close-notice { .close-notice {
padding-right: 0.2em; padding-right: 0.2em;
.svg-inline--fa:hover { .svg-inline--fa:hover {
opacity: 0.6; opacity: 0.6;
} }

View File

@ -1,12 +1,19 @@
<template> <template>
<div> <div class="interface-language-switcher">
<label for="interface-language-switcher"> <label>
{{ promptText }} {{ promptText }}
</label> </label>
{{ ' ' }} <ul class="setting-list">
<li
v-for="index of controlledLanguage.keys()"
:key="index"
>
<label>
{{ index === 0 ? $t('settings.primary_language') : $tc('settings.fallback_language', index, { index }) }}
<Select <Select
id="interface-language-switcher" class="language-select"
v-model="controlledLanguage" :model-value="controlledLanguage[index]"
@update:modelValue="val => setLanguageAt(index, val)"
> >
<option <option
v-for="lang in languages" v-for="lang in languages"
@ -16,6 +23,24 @@
{{ lang.name }} {{ lang.name }}
</option> </option>
</Select> </Select>
</label>
<button
v-if="controlledLanguage.length > 1 && index !== 0"
class="button-default btn"
@click="() => removeLanguageAt(index)"
>
{{ $t('settings.remove_language') }}
</button>
</li>
<li>
<button
class="button-default btn"
@click="addLanguage"
>
{{ $t('settings.add_language') }}
</button>
</li>
</ul>
</div> </div>
</template> </template>
@ -34,7 +59,7 @@ export default {
required: true required: true
}, },
language: { language: {
type: String, type: [Array, String],
required: true required: true
}, },
setLanguage: { setLanguage: {
@ -48,7 +73,9 @@ export default {
}, },
controlledLanguage: { controlledLanguage: {
get: function () { return this.language }, get: function () {
return Array.isArray(this.language) ? this.language : [this.language]
},
set: function (val) { set: function (val) {
this.setLanguage(val) this.setLanguage(val)
} }
@ -58,7 +85,30 @@ export default {
methods: { methods: {
getLanguageName (code) { getLanguageName (code) {
return localeService.getLanguageName(code) return localeService.getLanguageName(code)
},
addLanguage () {
this.controlledLanguage = [...this.controlledLanguage, '']
},
setLanguageAt (index, val) {
const lang = [...this.controlledLanguage]
lang[index] = val
this.controlledLanguage = lang
},
removeLanguageAt (index) {
const lang = [...this.controlledLanguage]
lang.splice(index, 1)
this.controlledLanguage = lang
} }
} }
} }
</script> </script>
<style lang="scss">
@import "../../variables";
.interface-language-switcher {
.language-select {
margin-right: 1em;
}
}
</style>

View File

@ -33,7 +33,7 @@
<script src="./link-preview.js"></script> <script src="./link-preview.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.link-preview-card { .link-preview-card {
display: flex; display: flex;
@ -46,6 +46,7 @@
flex-shrink: 0; flex-shrink: 0;
width: 120px; width: 120px;
max-width: 25%; max-width: 25%;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -67,7 +68,7 @@
} }
.card-description { .card-description {
margin: 0.5em 0 0 0; margin: 0.5em 0 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
word-break: break-word; word-break: break-word;

View File

@ -1,9 +1,13 @@
<template> <template>
<div class="list"> <div
class="list"
role="list"
>
<div <div
v-for="item in items" v-for="item in items"
:key="getKey(item)" :key="getKey(item)"
class="list-item" class="list-item"
role="listitem"
> >
<slot <slot
name="item" name="item"
@ -35,7 +39,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.list { .list {
&-item:not(:last-child) { &-item:not(:last-child) {

View File

@ -21,12 +21,16 @@
<script src="./lists_card.js"></script> <script src="./lists_card.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.list-card { .list-card {
display: flex; display: flex;
} }
.list-name {
flex-grow: 1;
}
.list-name, .list-name,
.button-list-edit { .button-list-edit {
margin: 0; margin: 0;
@ -39,13 +43,10 @@
background-color: var(--selectedMenu, $fallback--lightBg); background-color: var(--selectedMenu, $fallback--lightBg);
color: $fallback--link; color: $fallback--link;
color: var(--selectedMenuText, $fallback--link); color: var(--selectedMenuText, $fallback--link);
--faint: var(--selectedMenuFaintText, $fallback--faint); --faint: var(--selectedMenuFaintText, $fallback--faint);
--faintLink: var(--selectedMenuFaintLink, $fallback--faint); --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
--lightText: var(--selectedMenuLightText, $fallback--lightText); --lightText: var(--selectedMenuLightText, $fallback--lightText);
} }
} }
.list-name {
flex-grow: 1;
}
</style> </style>

View File

@ -95,10 +95,10 @@ const ListsNew = {
return this.addedUserIds.has(user.id) return this.addedUserIds.has(user.id)
}, },
addUser (user) { 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) { removeUser (userId) {
this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id }) this.$store.dispatch('removeListAccount', { accountId: userId, listId: this.id })
}, },
onSearchLoading (results) { onSearchLoading (results) {
this.searchLoading = true this.searchLoading = true

View File

@ -164,7 +164,7 @@
<script src="./lists_edit.js"></script> <script src="./lists_edit.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.ListEdit { .ListEdit {
--panel-body-padding: 0.5em; --panel-body-padding: 0.5em;

View File

@ -27,12 +27,12 @@
<script src="./lists_user_search.js"></script> <script src="./lists_user_search.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.ListsUserSearch { .ListsUserSearch {
.input-wrap { .input-wrap {
display: flex; display: flex;
margin: 0.7em 0.5em 0.7em 0.5em; margin: 0.7em 0.5em;
input { input {
width: 100%; width: 100%;

View File

@ -93,7 +93,7 @@
<script src="./login_form.js"></script> <script src="./login_form.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import "../../variables";
.login-form { .login-form {
display: flex; display: flex;
@ -110,7 +110,7 @@
} }
.login-bottom { .login-bottom {
margin-top: 1.0em; margin-top: 1em;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@ -121,7 +121,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.3em 0.5em 0.6em; padding: 0.3em 0.5em 0.6em;
line-height:24px; line-height: 24px;
} }
.form-bottom { .form-bottom {
@ -142,7 +142,6 @@
.error { .error {
text-align: center; text-align: center;
animation-name: shakeError; animation-name: shakeError;
animation-duration: 0.4s; animation-duration: 0.4s;
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;

View File

@ -63,6 +63,11 @@ const MediaModal = {
}, },
type () { type () {
return this.currentMedia ? this.getType(this.currentMedia) : null return this.currentMedia ? this.getType(this.currentMedia) : null
},
swipeDisableClickThreshold () {
// If there is only one media, allow more mouse movements to close the modal
// because there is less chance that the user wants to switch to another image
return () => this.canNavigate ? 1 : 30
} }
}, },
methods: { methods: {

View File

@ -10,6 +10,7 @@
class="modal-image-container" class="modal-image-container"
:direction="swipeDirection" :direction="swipeDirection"
:threshold="swipeThreshold" :threshold="swipeThreshold"
:disable-click-threshold="swipeDisableClickThreshold"
@preview-requested="handleSwipePreview" @preview-requested="handleSwipePreview"
@swipe-finished="handleSwipeEnd" @swipe-finished="handleSwipeEnd"
@swipeless-clicked="hide" @swipeless-clicked="hide"
@ -120,32 +121,12 @@ $modal-view-button-icon-half-height: calc(#{$modal-view-button-icon-height} / 2)
$modal-view-button-icon-width: 3em; $modal-view-button-icon-width: 3em;
$modal-view-button-icon-margin: 0.5em; $modal-view-button-icon-margin: 0.5em;
.modal-view.media-modal-view {
z-index: var(--ZI_media_modal);
flex-direction: column;
.modal-view-button-arrow,
.modal-view-button-hide {
opacity: 0.75;
&:focus,
&:hover {
outline: none;
box-shadow: none;
}
&:hover {
opacity: 1;
}
}
overflow: hidden;
}
.media-modal-view { .media-modal-view {
@keyframes media-fadein { @keyframes media-fadein {
from { from {
opacity: 0; opacity: 0;
} }
to { to {
opacity: 1; opacity: 1;
} }
@ -226,7 +207,7 @@ $modal-view-button-icon-margin: 0.5em;
appearance: none; appearance: none;
overflow: visible; overflow: visible;
cursor: pointer; cursor: pointer;
transition: opacity 333ms cubic-bezier(.4,0,.22,1); transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
height: $modal-view-button-icon-height; height: $modal-view-button-icon-height;
width: $modal-view-button-icon-width; width: $modal-view-button-icon-width;
@ -236,9 +217,9 @@ $modal-view-button-icon-margin: 0.5em;
width: $modal-view-button-icon-width; width: $modal-view-button-icon-width;
font-size: 1rem; font-size: 1rem;
line-height: $modal-view-button-icon-height; line-height: $modal-view-button-icon-height;
color: #FFF; color: #fff;
text-align: center; text-align: center;
background-color: rgba(0,0,0,.3); background-color: rgb(0 0 0 / 30%);
} }
} }
@ -254,13 +235,14 @@ $modal-view-button-icon-margin: 0.5em;
position: absolute; position: absolute;
top: 0; top: 0;
line-height: $modal-view-button-icon-height; line-height: $modal-view-button-icon-height;
color: #FFF; color: #fff;
text-align: center; text-align: center;
background-color: rgba(0,0,0,.3); background-color: rgb(0 0 0 / 30%);
} }
&--prev { &--prev {
left: 0; left: 0;
.arrow-icon { .arrow-icon {
left: $modal-view-button-icon-margin; left: $modal-view-button-icon-margin;
} }
@ -268,6 +250,7 @@ $modal-view-button-icon-margin: 0.5em;
&--next { &--next {
right: 0; right: 0;
.arrow-icon { .arrow-icon {
right: $modal-view-button-icon-margin; right: $modal-view-button-icon-margin;
} }
@ -278,10 +261,33 @@ $modal-view-button-icon-margin: 0.5em;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
.button-icon { .button-icon {
top: $modal-view-button-icon-margin; top: $modal-view-button-icon-margin;
right: $modal-view-button-icon-margin; right: $modal-view-button-icon-margin;
} }
} }
} }
.modal-view.media-modal-view {
z-index: var(--ZI_media_modal);
flex-direction: column;
.modal-view-button-arrow,
.modal-view-button-hide {
opacity: 0.75;
&:focus,
&:hover {
outline: none;
box-shadow: none;
}
&:hover {
opacity: 1;
}
}
overflow: hidden;
}
</style> </style>

Some files were not shown because too many files have changed in this diff Show More