Merge branch 'develop' into 'master'
Update master branch See merge request pleroma/pleroma-fe!1861
This commit is contained in:
commit
b6accf9e7f
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build/webpack.prod.conf.js export-subst
|
@ -4,11 +4,36 @@
|
||||
image: node:16
|
||||
|
||||
stages:
|
||||
- check-changelog
|
||||
- lint
|
||||
- build
|
||||
- test
|
||||
- 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:
|
||||
stage: lint
|
||||
script:
|
||||
|
@ -1,19 +1,41 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-rscss/config",
|
||||
"stylelint-config-recommended",
|
||||
"stylelint-config-standard"
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended-scss",
|
||||
"stylelint-config-html",
|
||||
"stylelint-config-recommended-vue/scss"
|
||||
],
|
||||
"rules": {
|
||||
"declaration-no-important": true,
|
||||
"rscss/no-descendant-combinator": false,
|
||||
"rscss/class-format": [
|
||||
true,
|
||||
false,
|
||||
{
|
||||
"component": "pascal-case",
|
||||
"variant": "^-[a-z]\\w+",
|
||||
"element": "^[a-z]\\w+"
|
||||
}
|
||||
],
|
||||
"selector-class-pattern": null,
|
||||
"import-notation": null,
|
||||
"custom-property-pattern": null,
|
||||
"keyframes-name-pattern": null,
|
||||
"scss/operator-no-newline-after": null,
|
||||
"declaration-block-no-redundant-longhand-properties": [
|
||||
true,
|
||||
{
|
||||
"ignoreShorthands": [
|
||||
"grid-template",
|
||||
"margin",
|
||||
"padding",
|
||||
"border",
|
||||
"border-width",
|
||||
"border-style",
|
||||
"border-color",
|
||||
"border-radius"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
28
CHANGELOG.md
28
CHANGELOG.md
@ -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/).
|
||||
|
||||
## 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
|
||||
### Fixed
|
||||
- UI no longer lags when switching between mobile and desktop mode
|
||||
|
@ -6,7 +6,7 @@ var ServiceWorkerWebpackPlugin = require('serviceworker-webpack5-plugin')
|
||||
var CopyPlugin = require('copy-webpack-plugin');
|
||||
var { VueLoaderPlugin } = require('vue-loader')
|
||||
var ESLintPlugin = require('eslint-webpack-plugin');
|
||||
|
||||
var StylelintPlugin = require('stylelint-webpack-plugin');
|
||||
|
||||
var env = process.env.NODE_ENV
|
||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||
@ -111,6 +111,7 @@ module.exports = {
|
||||
extensions: ['js', 'vue'],
|
||||
formatter: require('eslint-formatter-friendly')
|
||||
}),
|
||||
new StylelintPlugin({}),
|
||||
new VueLoaderPlugin(),
|
||||
// This copies Ruffle's WASM to a directory so that JS side can access it
|
||||
new CopyPlugin({
|
||||
|
@ -11,9 +11,16 @@ var env = process.env.NODE_ENV === 'testing'
|
||||
? require('../config/test.env')
|
||||
: config.build.env
|
||||
|
||||
let commitHash = require('child_process')
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString();
|
||||
let commitHash = (() => {
|
||||
const subst = "$Format:%h$";
|
||||
if(!subst.match(/Format:/)) {
|
||||
return subst;
|
||||
} else {
|
||||
return require('child_process')
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString();
|
||||
}
|
||||
})();
|
||||
|
||||
var webpackConfig = merge(baseWebpackConfig, {
|
||||
mode: 'production',
|
||||
|
1
changelog.d/add-taiwanese-aka-hokkien-i18n-support.add
Normal file
1
changelog.d/add-taiwanese-aka-hokkien-i18n-support.add
Normal 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
1
changelog.d/adminfe.add
Normal file
@ -0,0 +1 @@
|
||||
Implemented a very basic instance administration screen
|
0
changelog.d/check-changelog.skip
Normal file
0
changelog.d/check-changelog.skip
Normal file
1
changelog.d/custom-emoji-notif-width.fix
Normal file
1
changelog.d/custom-emoji-notif-width.fix
Normal file
@ -0,0 +1 @@
|
||||
Keep aspect ratio of custom emoji reaction in notification
|
1
changelog.d/edit-profile-button.fix
Normal file
1
changelog.d/edit-profile-button.fix
Normal file
@ -0,0 +1 @@
|
||||
Fix openSettingsModalTab so that it correctly opens Settings modal instead of Admin modal
|
1
changelog.d/emoji-picker-button-accessible.fix
Normal file
1
changelog.d/emoji-picker-button-accessible.fix
Normal file
@ -0,0 +1 @@
|
||||
Add alt text to emoji picker buttons
|
1
changelog.d/export-subst-hash.fix
Normal file
1
changelog.d/export-subst-hash.fix
Normal file
@ -0,0 +1 @@
|
||||
Use export-subst gitattribute to allow tarball builds
|
1
changelog.d/fix-reports.fix
Normal file
1
changelog.d/fix-reports.fix
Normal file
@ -0,0 +1 @@
|
||||
fix reports now showing reason/content:w
|
1
changelog.d/html-attribute-parsing.fix
Normal file
1
changelog.d/html-attribute-parsing.fix
Normal file
@ -0,0 +1 @@
|
||||
Fix HTML attribute parsing, discard attributes not strating with a letter
|
1
changelog.d/mention-twice.fix
Normal file
1
changelog.d/mention-twice.fix
Normal file
@ -0,0 +1 @@
|
||||
Fix a bug where mentioning a user twice will not fill the mention into the textarea
|
1
changelog.d/mentionsline-shouldbreak.fix
Normal file
1
changelog.d/mentionsline-shouldbreak.fix
Normal file
@ -0,0 +1 @@
|
||||
Make MentionsLine aware of line breaking by non-br elements
|
1
changelog.d/nonascii-tags.fix
Normal file
1
changelog.d/nonascii-tags.fix
Normal file
@ -0,0 +1 @@
|
||||
Fix parsing non-ascii tags
|
1
changelog.d/oauth2-token-linger.fix
Normal file
1
changelog.d/oauth2-token-linger.fix
Normal file
@ -0,0 +1 @@
|
||||
Fix OAuth2 token lingering after revocation
|
1
changelog.d/quote-hide-oops.fix
Normal file
1
changelog.d/quote-hide-oops.fix
Normal file
@ -0,0 +1 @@
|
||||
fix typo in code that prevented cards from showing at all
|
1
changelog.d/quote-hide.fix
Normal file
1
changelog.d/quote-hide.fix
Normal file
@ -0,0 +1 @@
|
||||
don't display quoted status twice
|
1
changelog.d/quote.add
Normal file
1
changelog.d/quote.add
Normal file
@ -0,0 +1 @@
|
||||
Implement quoting
|
1
changelog.d/react-button-safari.fix
Normal file
1
changelog.d/react-button-safari.fix
Normal file
@ -0,0 +1 @@
|
||||
Fix react button misalignment on safari ios
|
1
changelog.d/react-button.fix
Normal file
1
changelog.d/react-button.fix
Normal file
@ -0,0 +1 @@
|
||||
Fix react button not working if reaction accounts are not loaded
|
1
changelog.d/reload-user-pinned.fix
Normal file
1
changelog.d/reload-user-pinned.fix
Normal file
@ -0,0 +1 @@
|
||||
Fix pinned statuses gone when reloading user timeline
|
1
changelog.d/scroll-emoji-selector-safari.fix
Normal file
1
changelog.d/scroll-emoji-selector-safari.fix
Normal file
@ -0,0 +1 @@
|
||||
Fix scrolling emoji selector in modal in safari ios
|
@ -25,7 +25,17 @@ This could be a bit trickier, you basically need steps 1-4 from *develop build*
|
||||
|
||||
### Replacing your instance's frontend with custom FE build
|
||||
|
||||
This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](../backend/configuration/static_dir.md), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder.
|
||||
#### New way (via AdminFE, a bit janky but works)
|
||||
|
||||
In backend's [static directory](../backend/configuration/static_dir.md) there should be a folder called `frontends` if you installed any frontends from AdminFE before, otherwise you can create it yourself (ensuring correct permissions). Backend will serve given frontend from path `frontends/{frontend}/{reference}`, where `{frontend}` is name of frontend (`pleroma-fe`) and `{reference}` is version. You could make a production build, move `dist` folder into `frontends/pleroma-fe` and rename it into something like `myCustomVersion`. To actually make backend serve this frontend by default, in AdminFE you'll need to set name/reference in Settings -> Frontend -> Frontends -> Primary.
|
||||
|
||||
You could also install from a zip file (i.e. CI build) but AdminFE UI is a bit buggy and lacking, so this approach is not recommended.
|
||||
|
||||
Take note that frontend management is in early development and currently there's no way for user to change frontend or version for themselves, primary frontend becomes default frontend for all users and visitors.
|
||||
|
||||
#### Old way (replaces everything, hard to maintain, not recommended)
|
||||
|
||||
Copy or symlink contents of `dist` folder into backend's [static directory](../backend/configuration/static_dir.md), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder, and this could remove emojis, other frontends etc. and therefore this approach is not recommended.
|
||||
|
||||
### Running production build locally or on a separate server
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
<body class="hidden">
|
||||
<noscript>To use Pleroma, please enable JavaScript.</noscript>
|
||||
<div id="app"></div>
|
||||
<div id="modal"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<div id="popovers" />
|
||||
</body>
|
||||
|
96
package.json
96
package.json
@ -11,115 +11,121 @@
|
||||
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "npm run unit && npm run e2e",
|
||||
"stylelint": "npx stylelint src/components/status/status.scss",
|
||||
"stylelint": "npx stylelint '**/*.scss' '**/*.vue'",
|
||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
|
||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.20.0",
|
||||
"@babel/runtime": "7.21.5",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
|
||||
"@vuelidate/core": "2.0.0",
|
||||
"@vuelidate/core": "2.0.2",
|
||||
"@vuelidate/validators": "2.0.0",
|
||||
"body-scroll-lock": "3.1.5",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "1.5.12",
|
||||
"cropperjs": "1.5.13",
|
||||
"escape-html": "1.0.3",
|
||||
"js-cookie": "3.0.1",
|
||||
"localforage": "1.10.0",
|
||||
"lozad": "1.16.0",
|
||||
"parse-link-header": "2.0.0",
|
||||
"phoenix": "1.6.2",
|
||||
"punycode.js": "2.1.0",
|
||||
"qrcode": "1.5.0",
|
||||
"phoenix": "1.7.7",
|
||||
"punycode.js": "2.3.0",
|
||||
"qrcode": "1.5.3",
|
||||
"querystring-es3": "0.2.1",
|
||||
"url": "0.11.0",
|
||||
"utf8": "3.0.0",
|
||||
"vue": "3.2.41",
|
||||
"vue": "3.2.45",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.6",
|
||||
"vue-template-compiler": "2.7.13",
|
||||
"vue-template-compiler": "2.7.14",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.7",
|
||||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.19.6",
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@babel/plugin-transform-runtime": "7.19.6",
|
||||
"@babel/preset-env": "7.19.4",
|
||||
"@babel/register": "7.18.9",
|
||||
"@intlify/vue-i18n-loader": "5.0.0",
|
||||
"@babel/core": "7.21.8",
|
||||
"@babel/eslint-parser": "7.21.8",
|
||||
"@babel/plugin-transform-runtime": "7.21.4",
|
||||
"@babel/preset-env": "7.21.5",
|
||||
"@babel/register": "7.21.0",
|
||||
"@intlify/vue-i18n-loader": "5.0.1",
|
||||
"@ungap/event-target": "0.2.3",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||
"@vue/babel-plugin-jsx": "1.1.1",
|
||||
"@vue/compiler-sfc": "3.2.41",
|
||||
"@vue/test-utils": "2.2.6",
|
||||
"autoprefixer": "10.4.12",
|
||||
"babel-loader": "8.2.5",
|
||||
"@vue/compiler-sfc": "3.2.45",
|
||||
"@vue/test-utils": "2.2.8",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.2",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "4.3.7",
|
||||
"chalk": "1.1.3",
|
||||
"chromedriver": "104.0.0",
|
||||
"chromedriver": "108.0.0",
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"css-loader": "6.7.1",
|
||||
"css-loader": "6.7.3",
|
||||
"css-minimizer-webpack-plugin": "4.2.2",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"eslint": "8.29.0",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-standard": "17.0.0",
|
||||
"eslint-formatter-friendly": "7.0.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-n": "15.6.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-n": "15.6.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-vue": "9.7.0",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"eslint-webpack-plugin": "3.2.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "4.18.2",
|
||||
"function-bind": "1.1.1",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"html-webpack-plugin": "5.5.1",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"iso-639-1": "2.1.15",
|
||||
"json-loader": "0.5.7",
|
||||
"karma": "6.4.1",
|
||||
"karma": "6.4.2",
|
||||
"karma-coverage": "2.2.0",
|
||||
"karma-firefox-launcher": "2.1.2",
|
||||
"karma-mocha": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-sinon-chai": "2.0.2",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"mocha": "10.0.0",
|
||||
"nightwatch": "2.3.3",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"mocha": "10.2.0",
|
||||
"nightwatch": "2.6.20",
|
||||
"opn": "5.5.0",
|
||||
"ora": "0.4.1",
|
||||
"postcss": "8.4.16",
|
||||
"postcss-loader": "7.0.1",
|
||||
"sass": "1.55.0",
|
||||
"sass-loader": "13.0.2",
|
||||
"postcss": "8.4.23",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-loader": "7.0.2",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"sass": "1.60.0",
|
||||
"sass-loader": "13.2.2",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "7.3.8",
|
||||
"serviceworker-webpack5-plugin": "2.0.0",
|
||||
"shelljs": "0.8.5",
|
||||
"sinon": "14.0.2",
|
||||
"sinon": "15.0.4",
|
||||
"sinon-chai": "3.7.0",
|
||||
"stylelint": "13.13.1",
|
||||
"stylelint-config-standard": "20.0.0",
|
||||
"stylelint": "14.16.1",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recommended-scss": "^8.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "29.0.0",
|
||||
"stylelint-rscss": "0.4.0",
|
||||
"stylelint-webpack-plugin": "^3.3.0",
|
||||
"vue-loader": "17.0.1",
|
||||
"vue-style-loader": "4.1.3",
|
||||
"webpack": "5.74.0",
|
||||
"webpack": "5.75.0",
|
||||
"webpack-dev-middleware": "3.7.3",
|
||||
"webpack-hot-middleware": "2.25.2",
|
||||
"webpack-hot-middleware": "2.25.3",
|
||||
"webpack-merge": "0.20.0"
|
||||
},
|
||||
"engines": {
|
||||
|
113
src/App.scss
113
src/App.scss
@ -1,5 +1,7 @@
|
||||
// stylelint-disable rscss/class-format
|
||||
@import './_variables.scss';
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
@import "./variables";
|
||||
@import "./panel";
|
||||
|
||||
:root {
|
||||
--navbar-height: 3.5rem;
|
||||
@ -123,7 +125,7 @@ h4 {
|
||||
font-weight: 1000;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
i[class*="icon-"],
|
||||
.svg-inline--fa,
|
||||
.iconLetter {
|
||||
color: $fallback--icon;
|
||||
@ -132,7 +134,7 @@ i[class*=icon-],
|
||||
|
||||
.button-unstyled:hover,
|
||||
a:hover {
|
||||
> i[class*=icon-],
|
||||
> i[class*="icon-"],
|
||||
> .svg-inline--fa,
|
||||
> .iconLetter {
|
||||
color: var(--text);
|
||||
@ -141,12 +143,11 @@ a:hover {
|
||||
|
||||
nav {
|
||||
z-index: var(--ZI_navbar);
|
||||
color: var(--topBarText);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--topBar, $fallback--fg);
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
|
||||
box-shadow: 0 0 4px rgb(0 0 0 / 60%);
|
||||
box-shadow: var(--topBarShadow);
|
||||
box-sizing: border-box;
|
||||
height: var(--navbar-height);
|
||||
@ -191,13 +192,11 @@ nav {
|
||||
}
|
||||
|
||||
.underlay {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: span 3;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 1;
|
||||
grid-column: 1 / span 3;
|
||||
grid-row: 1 / 1;
|
||||
pointer-events: none;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
background-color: var(--underlay, rgba(0, 0, 0, 0.15));
|
||||
background-color: rgb(0 0 0 / 15%);
|
||||
background-color: var(--underlay, rgb(0 0 0 / 15%));
|
||||
z-index: -1000;
|
||||
}
|
||||
|
||||
@ -231,8 +230,7 @@ nav {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
box-sizing: border-box;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 1;
|
||||
grid-row: 1 / 1;
|
||||
margin: 0 calc(var(--___columnMargin) / 2);
|
||||
padding: calc(var(--___columnMargin)) 0;
|
||||
row-gap: var(--___columnMargin);
|
||||
@ -307,7 +305,7 @@ nav {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
&.-reverse:not(.-wide):not(.-mobile) {
|
||||
&.-reverse:not(.-wide, .-mobile) {
|
||||
grid-template-columns:
|
||||
var(--effectiveContentColumnWidth)
|
||||
var(--effectiveSidebarColumnWidth);
|
||||
@ -336,11 +334,8 @@ nav {
|
||||
padding: 0;
|
||||
|
||||
.column {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-top: 0;
|
||||
margin-top: var(--navbar-height);
|
||||
margin-bottom: 0;
|
||||
margin: var(--navbar-height) 0 0 0;
|
||||
}
|
||||
|
||||
.panel-heading,
|
||||
@ -389,7 +384,7 @@ nav {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
i[class*="icon-"],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
@ -400,12 +395,15 @@ nav {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 0 4px rgb(255 255 255 / 30%);
|
||||
box-shadow: var(--buttonHoverShadow);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow:
|
||||
0 0 4px 0 rgb(255 255 255 / 30%),
|
||||
0 1px 0 0 rgb(0 0 0 / 20%) inset,
|
||||
0 -1px 0 0 rgb(255 255 255 / 20%) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedText, $fallback--text);
|
||||
@ -438,7 +436,10 @@ nav {
|
||||
color: var(--btnToggledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggled, $fallback--fg);
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow:
|
||||
0 0 4px 0 rgb(255 255 255 / 30%),
|
||||
0 1px 0 0 rgb(0 0 0 / 20%) inset,
|
||||
0 -1px 0 0 rgb(255 255 255 / 20%) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
|
||||
svg,
|
||||
@ -503,7 +504,10 @@ textarea,
|
||||
border: none;
|
||||
border-radius: $fallback--inputRadius;
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
|
||||
box-shadow:
|
||||
0 1px 0 0 rgb(0 0 0 / 20%) inset,
|
||||
0 -1px 0 0 rgb(255 255 255 / 20%) inset,
|
||||
0 0 2px 0 rgb(0 0 0 / 100%) inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
@ -521,13 +525,13 @@ textarea,
|
||||
padding: 0 var(--_padding);
|
||||
|
||||
&:disabled,
|
||||
&[disabled=disabled],
|
||||
&[disabled="disabled"],
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&[type=range] {
|
||||
&[type="range"] {
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
@ -535,7 +539,7 @@ textarea,
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&[type=radio] {
|
||||
&[type="radio"] {
|
||||
display: none;
|
||||
|
||||
&:checked + label::before {
|
||||
@ -555,7 +559,7 @@ textarea,
|
||||
+ label::before {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
content: '';
|
||||
content: "";
|
||||
transition: box-shadow 200ms;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
@ -575,9 +579,7 @@ textarea,
|
||||
}
|
||||
}
|
||||
|
||||
&[type=checkbox] {
|
||||
display: none;
|
||||
|
||||
&[type="checkbox"] {
|
||||
&:checked + label::before {
|
||||
color: $fallback--text;
|
||||
color: var(--inputText, $fallback--text);
|
||||
@ -594,7 +596,7 @@ textarea,
|
||||
+ label::before {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
content: '✓';
|
||||
content: "✓";
|
||||
transition: color 200ms;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
@ -634,15 +636,29 @@ option {
|
||||
}
|
||||
|
||||
.hide-number-spinner {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
|
||||
&[type=number]::-webkit-inner-spin-button,
|
||||
&[type=number]::-webkit-outer-spin-button {
|
||||
&[type="number"]::-webkit-inner-spin-button,
|
||||
&[type="number"]::-webkit-outer-spin-button {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@ -653,24 +669,25 @@ option {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
|
||||
button {
|
||||
button,
|
||||
.button-dropdown {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&:not(:last-child) {
|
||||
&:not(:last-child),
|
||||
&:not(:last-child) .button-default {
|
||||
border-top-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-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import './panel.scss';
|
||||
|
||||
.fa {
|
||||
color: grey;
|
||||
}
|
||||
@ -686,7 +703,7 @@ option {
|
||||
max-width: 10em;
|
||||
min-width: 1.7em;
|
||||
height: 1.3em;
|
||||
padding: 0.15em 0.15em;
|
||||
padding: 0.15em;
|
||||
vertical-align: middle;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
@ -789,7 +806,8 @@ option {
|
||||
|
||||
.fa-old-padding {
|
||||
&.iconLetter,
|
||||
&.svg-inline--fa, &-layer {
|
||||
&.svg-inline--fa,
|
||||
&-layer {
|
||||
padding: 0 0.3em;
|
||||
}
|
||||
}
|
||||
@ -883,3 +901,16 @@ option {
|
||||
.fade-leave-active {
|
||||
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;
|
||||
}
|
||||
|
@ -71,7 +71,6 @@
|
||||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal />
|
||||
<UpdateNotification />
|
||||
<div id="modal" />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,13 +1,14 @@
|
||||
@mixin unfocused-style {
|
||||
@content;
|
||||
|
||||
&:focus:not(:focus-visible):not(:hover) {
|
||||
&:focus:not(:focus-visible, :hover) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin focused-style {
|
||||
&:hover, &:focus {
|
||||
&:hover,
|
||||
&:focus {
|
||||
@content;
|
||||
}
|
||||
|
||||
|
@ -4,20 +4,20 @@ $darkened-background: whitesmoke;
|
||||
|
||||
$fallback--bg: #121a24;
|
||||
$fallback--fg: #182230;
|
||||
$fallback--faint: rgba(185, 185, 186, .5);
|
||||
$fallback--faint: rgb(185 185 186 / 50%);
|
||||
$fallback--text: #b9b9ba;
|
||||
$fallback--link: #d8a070;
|
||||
$fallback--icon: #666;
|
||||
$fallback--lightBg: rgb(21, 30, 42);
|
||||
$fallback--lightBg: rgb(21 30 42);
|
||||
$fallback--lightText: #b9b9ba;
|
||||
$fallback--border: #222;
|
||||
$fallback--cRed: #ff0000;
|
||||
$fallback--cRed: #f00;
|
||||
$fallback--cBlue: #0095ff;
|
||||
$fallback--cGreen: #0fa00f;
|
||||
$fallback--cOrange: orange;
|
||||
|
||||
$fallback--alertError: rgba(211,16,20,.5);
|
||||
$fallback--alertWarning: rgba(111,111,20,.5);
|
||||
$fallback--alertError: rgb(211 16 20 / 50%);
|
||||
$fallback--alertWarning: rgb(111 111 20 / 50%);
|
||||
|
||||
$fallback--panelRadius: 10px;
|
||||
$fallback--checkboxRadius: 2px;
|
||||
@ -29,6 +29,8 @@ $fallback--avatarAltRadius: 10px;
|
||||
$fallback--attachmentRadius: 10px;
|
||||
$fallback--chatMessageRadius: 10px;
|
||||
|
||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%),
|
||||
0 1px 0 0 rgb(255 255 255 / 20%) inset,
|
||||
0 -1px 0 0 rgb(0 0 0 / 20%) inset;
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
@ -58,6 +60,8 @@ const getInstanceConfig = async ({ store }) => {
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma.metadata.birthday_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma.metadata.birthday_min_age || 0 })
|
||||
|
||||
if (vapidPublicKey) {
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
@ -249,11 +253,13 @@ const getNodeInfo = async ({ store }) => {
|
||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
||||
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: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
|
||||
|
||||
const uploadLimits = metadata.uploadLimits
|
||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||
@ -397,6 +403,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||
|
||||
app.use(vClickOutside)
|
||||
app.use(VBodyScrollLock)
|
||||
app.use(VueVirtualScroller)
|
||||
|
||||
app.component('FAIcon', FontAwesomeIcon)
|
||||
app.component('FALayers', FontAwesomeLayers)
|
||||
|
@ -9,6 +9,3 @@
|
||||
</template>
|
||||
|
||||
<script src="./about.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
|
@ -2,6 +2,7 @@ import { mapState } from 'vuex'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisV
|
||||
@ -16,14 +17,30 @@ const AccountActions = {
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
return { }
|
||||
return {
|
||||
showingConfirmBlock: false,
|
||||
showingConfirmRemoveFollower: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Popover,
|
||||
UserListMenu
|
||||
UserListMenu,
|
||||
ConfirmModal
|
||||
},
|
||||
methods: {
|
||||
showConfirmBlock () {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
hideConfirmBlock () {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showConfirmRemoveUserFromFollowers () {
|
||||
this.showingConfirmRemoveFollower = true
|
||||
},
|
||||
hideConfirmRemoveUserFromFollowers () {
|
||||
this.showingConfirmRemoveFollower = false
|
||||
},
|
||||
showRepeats () {
|
||||
this.$store.dispatch('showReblogs', this.user.id)
|
||||
},
|
||||
@ -31,13 +48,29 @@ const AccountActions = {
|
||||
this.$store.dispatch('hideReblogs', this.user.id)
|
||||
},
|
||||
blockUser () {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
this.showConfirmBlock()
|
||||
}
|
||||
},
|
||||
doBlockUser () {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
this.hideConfirmBlock()
|
||||
},
|
||||
unblockUser () {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers () {
|
||||
if (!this.shouldConfirmRemoveUserFromFollowers) {
|
||||
this.doRemoveUserFromFollowers()
|
||||
} else {
|
||||
this.showConfirmRemoveUserFromFollowers()
|
||||
}
|
||||
},
|
||||
doRemoveUserFromFollowers () {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
this.hideConfirmRemoveUserFromFollowers()
|
||||
},
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||
@ -50,6 +83,12 @@ const AccountActions = {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmBlock () {
|
||||
return this.$store.getters.mergedConfig.modalOnBlock
|
||||
},
|
||||
shouldConfirmRemoveUserFromFollowers () {
|
||||
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
|
||||
},
|
||||
...mapState({
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
|
@ -74,13 +74,56 @@
|
||||
</button>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script src="./account_actions.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.AccountActions {
|
||||
.ellipsis-button {
|
||||
width: 2.5em;
|
||||
|
@ -27,6 +27,9 @@ const Announcement = {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
canEditAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
},
|
||||
content () {
|
||||
return this.announcement.content
|
||||
},
|
||||
|
@ -45,14 +45,14 @@
|
||||
{{ $t('announcements.mark_as_read_action') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
v-if="canEditAnnouncement"
|
||||
class="btn button-default"
|
||||
@click="enterEditMode"
|
||||
>
|
||||
{{ $t('announcements.edit_action') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
v-if="canEditAnnouncement"
|
||||
class="btn button-default"
|
||||
@click="deleteAnnouncement"
|
||||
>
|
||||
@ -102,19 +102,19 @@
|
||||
@import "../../variables";
|
||||
|
||||
.announcement {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading, .body {
|
||||
.heading,
|
||||
.body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.times {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -28,6 +28,9 @@ const AnnouncementsPage = {
|
||||
}),
|
||||
announcements () {
|
||||
return this.$store.state.announcements.announcements
|
||||
},
|
||||
canPostAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<section
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
v-if="canPostAnnouncement"
|
||||
>
|
||||
<div class="post-form">
|
||||
<div class="heading">
|
||||
@ -67,7 +67,8 @@
|
||||
.post-form {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading, .body {
|
||||
.heading,
|
||||
.body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
|
@ -34,9 +34,10 @@ export default {
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.btn {
|
||||
margin: .5em;
|
||||
padding: .5em 2em;
|
||||
margin: 0.5em;
|
||||
padding: 0.5em 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -36,6 +36,7 @@ library.add(
|
||||
const Attachment = {
|
||||
props: [
|
||||
'attachment',
|
||||
'compact',
|
||||
'description',
|
||||
'hideDescription',
|
||||
'nsfw',
|
||||
@ -71,7 +72,8 @@ const Attachment = {
|
||||
{
|
||||
'-loading': this.loading,
|
||||
'-nsfw-placeholder': this.hidden,
|
||||
'-editable': this.edit !== undefined
|
||||
'-editable': this.edit !== undefined,
|
||||
'-compact': this.compact
|
||||
},
|
||||
'-type-' + this.type,
|
||||
this.size && '-size-' + this.size,
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.Attachment {
|
||||
display: inline-flex;
|
||||
@ -102,14 +102,13 @@
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
||||
color: rgb(255 255 255 / 75%);
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 40%);
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
@ -135,18 +134,32 @@
|
||||
margin-left: 0.5em;
|
||||
font-size: 1.25em;
|
||||
// TODO: theming? hard to theme with unknown background image color
|
||||
background: rgba(230, 230, 230, 0.7);
|
||||
background: rgb(230 230 230 / 70%);
|
||||
|
||||
.svg-inline--fa {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
color: rgb(0 0 0 / 60%);
|
||||
}
|
||||
|
||||
&:hover .svg-inline--fa {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
color: rgb(0 0 0 / 90%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.-contain-fit {
|
||||
img,
|
||||
canvas {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&.-cover-fit {
|
||||
img,
|
||||
canvas {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.oembed-container {
|
||||
line-height: 1.2em;
|
||||
flex: 1 0 100%;
|
||||
@ -160,8 +173,9 @@
|
||||
|
||||
.image {
|
||||
flex: 1;
|
||||
|
||||
img {
|
||||
border: 0px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
@ -172,9 +186,10 @@
|
||||
flex: 2;
|
||||
margin: 8px;
|
||||
word-break: break-all;
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -252,17 +267,9 @@
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
&.-contain-fit {
|
||||
img,
|
||||
canvas {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&.-cover-fit {
|
||||
img,
|
||||
canvas {
|
||||
object-fit: cover;
|
||||
&.-compact {
|
||||
.placeholder-container {
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -162,10 +162,11 @@
|
||||
target="_blank"
|
||||
>
|
||||
<FAIcon
|
||||
size="5x"
|
||||
:size="compact ? '2x' : '5x'"
|
||||
:icon="placeholderIconClass"
|
||||
:title="localDescription"
|
||||
/>
|
||||
<p>
|
||||
<p v-if="!compact">
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
</a>
|
||||
|
@ -24,7 +24,7 @@
|
||||
<script src="./autosuggest.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.autosuggest {
|
||||
position: relative;
|
||||
@ -50,7 +50,7 @@
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
border-top-left-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);
|
||||
overflow-y: auto;
|
||||
z-index: 1;
|
||||
|
@ -17,7 +17,7 @@
|
||||
<script src="./avatar_list.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
|
@ -49,7 +49,7 @@
|
||||
margin: 0;
|
||||
padding: 0.6em 1em;
|
||||
|
||||
--emoji-size: 14px;
|
||||
--emoji-size: 14px;
|
||||
|
||||
&-collapsed-content {
|
||||
margin-left: 0.7em;
|
||||
|
@ -37,6 +37,7 @@
|
||||
.block-card-content-container {
|
||||
margin-top: 0.5em;
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
min-height: calc(100vh - var(--navbar-height));
|
||||
margin: 0 0 0 0;
|
||||
margin: 0;
|
||||
border-radius: 10px 10px 0 0;
|
||||
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
display: flex;
|
||||
justify-content: 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;
|
||||
transition: 0.35s all;
|
||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||
|
@ -95,6 +95,6 @@
|
||||
|
||||
<script src="./chat.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import './chat.scss';
|
||||
@import "../../variables";
|
||||
@import "./chat";
|
||||
</style>
|
||||
|
@ -45,7 +45,7 @@
|
||||
<script src="./chat_list.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.chat-list {
|
||||
min-height: 25em;
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
&:hover {
|
||||
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 {
|
||||
@ -67,6 +67,7 @@
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
|
||||
img {
|
||||
visibility: visible;
|
||||
}
|
||||
@ -79,13 +80,11 @@
|
||||
|
||||
.chat-preview-body {
|
||||
--emoji-size: 1.4em;
|
||||
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.time-wrapper {
|
||||
line-height: var(--post-line-height);
|
||||
}
|
||||
|
||||
.chat-preview-body {
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,6 @@
|
||||
<script src="./chat_list_item.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import './chat_list_item.scss';
|
||||
@import "../../variables";
|
||||
@import "./chat_list_item";
|
||||
</style>
|
||||
|
@ -1,12 +1,12 @@
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.chat-message-wrapper {
|
||||
|
||||
&.hovered-message-chain {
|
||||
.animated.Avatar {
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
|
||||
img {
|
||||
visibility: visible;
|
||||
}
|
||||
@ -28,7 +28,8 @@
|
||||
.menu-icon {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, .extra-button-popover.open & {
|
||||
&:hover,
|
||||
.extra-button-popover.open & {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
@ -54,27 +55,11 @@
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.link-preview, .attachments {
|
||||
.link-preview,
|
||||
.attachments {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.chat-message-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
max-width: 80%;
|
||||
min-width: 10em;
|
||||
width: 100%;
|
||||
|
||||
&.with-media {
|
||||
width: 100%;
|
||||
|
||||
.status {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
border-radius: $fallback--chatMessageRadius;
|
||||
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
|
||||
@ -86,7 +71,7 @@
|
||||
position: relative;
|
||||
float: right;
|
||||
font-size: 0.8em;
|
||||
margin: -1em 0 -0.5em 0;
|
||||
margin: -1em 0 -0.5em;
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
@ -103,18 +88,54 @@
|
||||
}
|
||||
|
||||
.pending {
|
||||
.status-content.media-body, .created-at {
|
||||
.status-content.media-body,
|
||||
.created-at {
|
||||
color: var(--faint);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
.status-content.media-body, .created-at {
|
||||
.status-content.media-body,
|
||||
.created-at {
|
||||
color: $fallback--cRed;
|
||||
color: var(--badgeNotification, $fallback--cRed);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
max-width: 80%;
|
||||
min-width: 10em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.outgoing {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: end;
|
||||
justify-content: flex-end;
|
||||
|
||||
a {
|
||||
color: var(--chatMessageOutgoingLink, $fallback--link);
|
||||
}
|
||||
|
||||
.status {
|
||||
color: var(--chatMessageOutgoingText, $fallback--text);
|
||||
background-color: var(--chatMessageOutgoingBg, $fallback--lightBg);
|
||||
border: 1px solid var(--chatMessageOutgoingBorder, --lightBg);
|
||||
}
|
||||
|
||||
.chat-message-inner {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.chat-message-menu {
|
||||
right: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.incoming {
|
||||
a {
|
||||
color: var(--chatMessageIncomingLink, $fallback--link);
|
||||
@ -137,36 +158,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.outgoing {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: end;
|
||||
justify-content: flex-end;
|
||||
|
||||
a {
|
||||
color: var(--chatMessageOutgoingLink, $fallback--link);
|
||||
}
|
||||
.chat-message-inner.with-media {
|
||||
width: 100%;
|
||||
|
||||
.status {
|
||||
color: var(--chatMessageOutgoingText, $fallback--text);
|
||||
background-color: var(--chatMessageOutgoingBg, $fallback--lightBg);
|
||||
border: 1px solid var(--chatMessageOutgoingBorder, --lightBg);
|
||||
}
|
||||
|
||||
.chat-message-inner {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.chat-message-menu {
|
||||
right: 0.4rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-message-date-separator {
|
||||
|
@ -33,7 +33,7 @@
|
||||
<div
|
||||
class="media status"
|
||||
:class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
|
||||
style="position: relative"
|
||||
style="position: relative;"
|
||||
@mouseenter="hovered = true"
|
||||
@mouseleave="hovered = false"
|
||||
>
|
||||
@ -98,6 +98,6 @@
|
||||
|
||||
<script src="./chat_message.js"></script>
|
||||
<style lang="scss">
|
||||
@import './chat_message.scss';
|
||||
@import "./chat_message";
|
||||
|
||||
</style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
.chat-new {
|
||||
.input-wrap {
|
||||
display: flex;
|
||||
margin: 0.7em 0.5em 0.7em 0.5em;
|
||||
margin: 0.7em 0.5em;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
|
@ -46,6 +46,6 @@
|
||||
|
||||
<script src="./chat_new.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import './chat_new.scss';
|
||||
@import "../../variables";
|
||||
@import "./chat_new";
|
||||
</style>
|
||||
|
@ -26,7 +26,7 @@
|
||||
<script src="./chat_title.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.chat-title {
|
||||
display: flex;
|
||||
|
@ -1,16 +1,21 @@
|
||||
<template>
|
||||
<label
|
||||
class="checkbox"
|
||||
:class="{ disabled, indeterminate }"
|
||||
:class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="visible-for-screenreader-only"
|
||||
:disabled="disabled"
|
||||
:checked="modelValue"
|
||||
:indeterminate="indeterminate"
|
||||
@change="$emit('update:modelValue', $event.target.checked)"
|
||||
>
|
||||
<i class="checkbox-indicator" />
|
||||
<i
|
||||
class="checkbox-indicator"
|
||||
:aria-hidden="true"
|
||||
@transitionend.capture="onTransitionEnd"
|
||||
/>
|
||||
<span
|
||||
v-if="!!$slots.default"
|
||||
class="label"
|
||||
@ -27,12 +32,30 @@ export default {
|
||||
'indeterminate',
|
||||
'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>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
@import "../../mixins";
|
||||
|
||||
.checkbox {
|
||||
position: relative;
|
||||
@ -49,13 +72,13 @@ export default {
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
content: '✓';
|
||||
content: "✓";
|
||||
transition: color 200ms;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
border-radius: $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);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
@ -71,32 +94,36 @@ export default {
|
||||
&.disabled {
|
||||
.checkbox-indicator::before,
|
||||
.label {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
|
||||
input[type="checkbox"] {
|
||||
&:checked + .checkbox-indicator::before {
|
||||
color: $fallback--text;
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
&:indeterminate + .checkbox-indicator::before {
|
||||
content: '–';
|
||||
content: "–";
|
||||
color: $fallback--text;
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
&.indeterminate-fix {
|
||||
input[type="checkbox"] + .checkbox-indicator::before {
|
||||
content: "–";
|
||||
}
|
||||
}
|
||||
|
||||
& > span {
|
||||
margin-left: .5em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.color-input {
|
||||
display: inline-flex;
|
||||
@ -8,7 +8,7 @@
|
||||
flex: 0 0 0;
|
||||
max-width: 9em;
|
||||
align-items: stretch;
|
||||
padding: .2em 8px;
|
||||
padding: 0.2em 8px;
|
||||
|
||||
input {
|
||||
background: none;
|
||||
@ -31,6 +31,7 @@
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.computedIndicator,
|
||||
.transparentIndicator {
|
||||
flex: 0 0 2em;
|
||||
@ -38,22 +39,27 @@
|
||||
align-self: stretch;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.transparentIndicator {
|
||||
// forgot to install counter-strike source, ooops
|
||||
background-color: #FF00FF;
|
||||
background-color: #f0f;
|
||||
position: relative;
|
||||
&::before, &::after {
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
background-color: #000000;
|
||||
content: "";
|
||||
background-color: #000;
|
||||
position: absolute;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
@ -64,5 +70,4 @@
|
||||
.label {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
37
src/components/confirm_modal/confirm_modal.js
Normal file
37
src/components/confirm_modal/confirm_modal.js
Normal 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
|
29
src/components/confirm_modal/confirm_modal.vue
Normal file
29
src/components/confirm_modal/confirm_modal.vue
Normal 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>
|
@ -87,7 +87,6 @@ export default {
|
||||
.contrast-ratio {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
margin-top: -4px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
|
@ -210,17 +210,16 @@
|
||||
<script src="./conversation.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.Conversation {
|
||||
z-index: 1;
|
||||
|
||||
.conversation-dive-to-top-level-box {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
|
||||
/* Make the button stretch along the whole row */
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
@ -235,52 +234,48 @@
|
||||
.thread-ancestor.-faded .StatusContent {
|
||||
--link: var(--faintLink);
|
||||
--text: var(--faint);
|
||||
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.thread-ancestor-dive-box {
|
||||
padding-left: var(--status-margin, $status-margin);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
|
||||
/* Make the button stretch along the whole row */
|
||||
&, &-inner {
|
||||
&,
|
||||
&-inner {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.thread-ancestor-dive-box-inner {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.conversation-status {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.thread-ancestor-has-other-replies .conversation-status,
|
||||
&:last-child .conversation-status,
|
||||
.thread-ancestor:last-child .conversation-status,
|
||||
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
||||
&:last-child .conversation-status,
|
||||
&.-expanded .thread-tree .conversation-status {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.thread-ancestors + .thread-tree > .conversation-status {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-top-color: var(--border, $fallback--border);
|
||||
border-top: 1px solid var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
/* expanded conversation in timeline */
|
||||
&.status-fadein.-expanded .thread-body {
|
||||
border-left-width: 4px;
|
||||
border-left-style: solid;
|
||||
border-left-color: $fallback--cRed;
|
||||
border-left: 4px solid $fallback--cRed;
|
||||
border-left-color: var(--cRed, $fallback--cRed);
|
||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
|
@ -1,4 +1,5 @@
|
||||
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 {
|
||||
faSignInAlt,
|
||||
@ -30,7 +31,8 @@ library.add(
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SearchBar
|
||||
SearchBar,
|
||||
ConfirmModal
|
||||
},
|
||||
data: () => ({
|
||||
searchBarHidden: true,
|
||||
@ -40,7 +42,8 @@ export default {
|
||||
window.CSS.supports('-moz-mask-size', 'contain') ||
|
||||
window.CSS.supports('-ms-mask-size', 'contain') ||
|
||||
window.CSS.supports('-o-mask-size', 'contain')
|
||||
)
|
||||
),
|
||||
showingConfirmLogout: false
|
||||
}),
|
||||
computed: {
|
||||
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
|
||||
@ -73,21 +76,41 @@ export default {
|
||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
||||
logoLeft () { return this.$store.state.instance.logoLeft },
|
||||
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: {
|
||||
scrollToTop () {
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
showConfirmLogout () {
|
||||
this.showingConfirmLogout = true
|
||||
},
|
||||
hideConfirmLogout () {
|
||||
this.showingConfirmLogout = false
|
||||
},
|
||||
logout () {
|
||||
if (!this.shouldConfirmLogout) {
|
||||
this.doLogout()
|
||||
} else {
|
||||
this.showConfirmLogout()
|
||||
}
|
||||
},
|
||||
doLogout () {
|
||||
this.$router.replace('/main/public')
|
||||
this.$store.dispatch('logout')
|
||||
this.hideConfirmLogout()
|
||||
},
|
||||
onSearchBarToggled (hidden) {
|
||||
this.searchBarHidden = hidden
|
||||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
this.$store.dispatch('openSettingsModal', 'user')
|
||||
},
|
||||
openAdminModal () {
|
||||
this.$store.dispatch('openSettingsModal', 'admin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.DesktopNav {
|
||||
width: 100%;
|
||||
@ -27,20 +27,13 @@
|
||||
--miniColumn: 25rem;
|
||||
--maxiColumn: 45rem;
|
||||
--columnGap: 1em;
|
||||
max-width: calc(
|
||||
var(--sidebarColumnWidth, var(--miniColumn)) +
|
||||
var(--contentColumnWidth, var(--maxiColumn)) +
|
||||
var(--columnGap)
|
||||
);
|
||||
}
|
||||
|
||||
&.-column-stretch.-wide .inner-nav {
|
||||
max-width: calc(
|
||||
var(--sidebarColumnWidth, var(--miniColumn)) +
|
||||
var(--contentColumnWidth, var(--maxiColumn)) +
|
||||
var(--notifsColumnWidth, var(--miniColumn)) +
|
||||
var(--columnGap)
|
||||
);
|
||||
max-width:
|
||||
calc(
|
||||
var(--sidebarColumnWidth, var(--miniColumn)) +
|
||||
var(--contentColumnWidth, var(--maxiColumn)) +
|
||||
var(--columnGap)
|
||||
);
|
||||
}
|
||||
|
||||
&.-logoLeft .inner-nav {
|
||||
@ -48,8 +41,19 @@
|
||||
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 {
|
||||
&, svg {
|
||||
&,
|
||||
svg {
|
||||
color: $fallback--text;
|
||||
color: var(--btnTopBarText, $fallback--text);
|
||||
}
|
||||
@ -70,7 +74,7 @@
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledTopBarText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg)
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg);
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +86,7 @@
|
||||
transition-duration: 100ms;
|
||||
|
||||
@media all and (min-width: 800px) {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
class="logo"
|
||||
:to="{ name: 'root' }"
|
||||
:style="logoBgStyle"
|
||||
:title="sitename"
|
||||
>
|
||||
<div
|
||||
class="mask"
|
||||
@ -38,44 +39,55 @@
|
||||
/>
|
||||
<button
|
||||
class="button-unstyled nav-icon"
|
||||
@click="openSettingsModal"
|
||||
:title="$t('nav.preferences')"
|
||||
@click.stop="openSettingsModal"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="cog"
|
||||
:title="$t('nav.preferences')"
|
||||
/>
|
||||
</button>
|
||||
<a
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
href="/pleroma/admin/#/login-pleroma"
|
||||
class="nav-icon"
|
||||
class="button-unstyled nav-icon"
|
||||
target="_blank"
|
||||
@click.stop
|
||||
:title="$t('nav.administration')"
|
||||
@click.stop="openAdminModal"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="tachometer-alt"
|
||||
:title="$t('nav.administration')"
|
||||
/>
|
||||
</a>
|
||||
</button>
|
||||
<span class="spacer" />
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="button-unstyled nav-icon"
|
||||
@click.prevent="logout"
|
||||
:title="$t('login.logout')"
|
||||
@click.stop.prevent="logout"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="sign-out-alt"
|
||||
:title="$t('login.logout')"
|
||||
/>
|
||||
</button>
|
||||
</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>
|
||||
</template>
|
||||
<script src="./desktop_nav.js"></script>
|
||||
|
@ -25,7 +25,7 @@
|
||||
<script src="./dialog_modal.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
// TODO: unify with other modals.
|
||||
.dark-overlay {
|
||||
@ -38,8 +38,8 @@
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background: rgba(27,31,35,.5);
|
||||
z-index: 99;
|
||||
background: rgb(27 31 35 / 50%);
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
margin: 15vh auto;
|
||||
position: fixed;
|
||||
transform: translateX(-50%);
|
||||
z-index: 999;
|
||||
z-index: 2001;
|
||||
cursor: default;
|
||||
display: block;
|
||||
background-color: $fallback--bg;
|
||||
@ -65,7 +65,7 @@
|
||||
|
||||
.dialog-modal-content {
|
||||
margin: 0;
|
||||
padding: 1rem 1rem;
|
||||
padding: 1rem;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
white-space: normal;
|
||||
@ -73,7 +73,7 @@
|
||||
|
||||
.dialog-modal-footer {
|
||||
margin: 0;
|
||||
padding: .5em .5em;
|
||||
padding: 0.5em;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-top: 1px solid $fallback--border;
|
||||
@ -83,7 +83,7 @@
|
||||
|
||||
button {
|
||||
width: auto;
|
||||
margin-left: .5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@
|
||||
.modal-view.edit-form-modal-view {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.edit-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin-top: 25%;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Completion from '../../services/completion/completion.js'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.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 { take } from 'lodash'
|
||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
@ -109,9 +110,10 @@ const EmojiInput = {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
randomSeed: `${Math.random()}`.replace('.', '-'),
|
||||
input: undefined,
|
||||
caretEl: undefined,
|
||||
highlighted: 0,
|
||||
highlighted: -1,
|
||||
caret: 0,
|
||||
focused: false,
|
||||
blurTimeout: null,
|
||||
@ -125,12 +127,16 @@ const EmojiInput = {
|
||||
components: {
|
||||
Popover,
|
||||
EmojiPicker,
|
||||
UnicodeDomainIndicator
|
||||
UnicodeDomainIndicator,
|
||||
ScreenReaderNotice
|
||||
},
|
||||
computed: {
|
||||
padEmoji () {
|
||||
return this.$store.getters.mergedConfig.padEmoji
|
||||
},
|
||||
defaultCandidateIndex () {
|
||||
return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
|
||||
},
|
||||
preText () {
|
||||
return this.modelValue.slice(0, this.caret)
|
||||
},
|
||||
@ -203,6 +209,12 @@ const EmojiInput = {
|
||||
top: this.input.scrollTop,
|
||||
left: this.input.scrollLeft
|
||||
})
|
||||
},
|
||||
suggestionListId () {
|
||||
return `suggestions-${this.randomSeed}`
|
||||
},
|
||||
suggestionItemId () {
|
||||
return (index) => `suggestion-item-${index}-${this.randomSeed}`
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
@ -278,6 +290,11 @@ const EmojiInput = {
|
||||
...rest,
|
||||
img: imageUrl || ''
|
||||
}))
|
||||
this.highlighted = this.defaultCandidateIndex
|
||||
this.$refs.screenReaderNotice.announce(
|
||||
this.$tc('tool_tip.autocomplete_available',
|
||||
this.suggestions.length,
|
||||
{ number: this.suggestions.length }))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -374,26 +391,27 @@ const EmojiInput = {
|
||||
},
|
||||
cycleBackward (e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 1) {
|
||||
this.highlighted -= 1
|
||||
if (this.highlighted < 0) {
|
||||
this.highlighted = this.suggestions.length - 1
|
||||
}
|
||||
|
||||
this.highlighted -= 1
|
||||
if (this.highlighted === -1) {
|
||||
this.input.focus()
|
||||
} else if (this.highlighted < -1) {
|
||||
this.highlighted = len - 1
|
||||
}
|
||||
if (len > 0) {
|
||||
e.preventDefault()
|
||||
} else {
|
||||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
cycleForward (e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 1) {
|
||||
this.highlighted += 1
|
||||
if (this.highlighted >= len) {
|
||||
this.highlighted = 0
|
||||
}
|
||||
|
||||
this.highlighted += 1
|
||||
if (this.highlighted >= len) {
|
||||
this.highlighted = -1
|
||||
this.input.focus()
|
||||
}
|
||||
if (len > 0) {
|
||||
e.preventDefault()
|
||||
} else {
|
||||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
scrollIntoView () {
|
||||
@ -540,6 +558,13 @@ const EmojiInput = {
|
||||
})
|
||||
},
|
||||
resize () {
|
||||
},
|
||||
autoCompleteItemLabel (suggestion) {
|
||||
if (suggestion.user) {
|
||||
return suggestion.displayText + ' ' + suggestion.detailText
|
||||
} else {
|
||||
return this.maybeLocalizedEmojiName(suggestion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,19 @@
|
||||
class="emoji-input"
|
||||
: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? -->
|
||||
<div
|
||||
ref="hiddenOverlay"
|
||||
class="hidden-overlay"
|
||||
:style="overlayStyle"
|
||||
:aria-hidden="true"
|
||||
>
|
||||
<span>{{ preText }}</span>
|
||||
<span
|
||||
@ -18,11 +25,16 @@
|
||||
>x</span>
|
||||
<span>{{ postText }}</span>
|
||||
</div>
|
||||
<screen-reader-notice
|
||||
ref="screenReaderNotice"
|
||||
aria-live="assertive"
|
||||
/>
|
||||
<template v-if="enableEmojiPicker">
|
||||
<button
|
||||
v-if="!hideEmojiButton"
|
||||
class="button-unstyled emoji-picker-icon"
|
||||
type="button"
|
||||
:title="$t('emoji.add_emoji')"
|
||||
@click.prevent="togglePicker"
|
||||
>
|
||||
<FAIcon :icon="['far', 'smile-beam']" />
|
||||
@ -43,17 +55,24 @@
|
||||
ref="suggestorPopover"
|
||||
class="autocomplete-panel"
|
||||
placement="bottom"
|
||||
:trigger-attrs="{ 'aria-hidden': true }"
|
||||
>
|
||||
<template #content>
|
||||
<div
|
||||
:id="suggestionListId"
|
||||
ref="panel-body"
|
||||
class="autocomplete-panel-body"
|
||||
role="listbox"
|
||||
>
|
||||
<div
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:id="suggestionItemId(index)"
|
||||
:key="index"
|
||||
class="autocomplete-item"
|
||||
role="option"
|
||||
:class="{ highlighted: index === highlighted }"
|
||||
:aria-label="autoCompleteItemLabel(suggestion)"
|
||||
:aria-selected="index === highlighted"
|
||||
@click.stop.prevent="onClick($event, suggestion)"
|
||||
>
|
||||
<span class="image">
|
||||
@ -91,22 +110,18 @@
|
||||
<script src="./emoji_input.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.emoji-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
&.with-picker input {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.emoji-picker-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: .2em .25em;
|
||||
margin: 0.2em 0.25em;
|
||||
font-size: 1.3em;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
@ -123,14 +138,19 @@
|
||||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
input,
|
||||
textarea {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
&.with-picker input {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.hidden-overlay {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
@ -140,8 +160,10 @@
|
||||
right: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
|
||||
/* DEBUG STUFF */
|
||||
color: red;
|
||||
|
||||
/* set opacity to non-zero to see the overlay */
|
||||
|
||||
.caret {
|
||||
@ -151,6 +173,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete {
|
||||
&-panel {
|
||||
position: absolute;
|
||||
@ -160,7 +183,7 @@
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
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;
|
||||
|
||||
.image {
|
||||
@ -169,7 +192,6 @@
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
|
||||
margin-right: 4px;
|
||||
|
||||
img {
|
||||
@ -199,6 +221,7 @@
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--selectedMenuPopover, $fallback--fg);
|
||||
color: var(--selectedMenuPopoverText, $fallback--text);
|
||||
|
||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||
|
@ -94,8 +94,9 @@ export const suggestUsers = ({ dispatch, state }) => {
|
||||
|
||||
const newSuggestions = state.users.users.filter(
|
||||
user =>
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix)
|
||||
user.screen_name && user.name && (
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix))
|
||||
).slice(0, 20).sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
|
@ -3,7 +3,6 @@ import Checkbox from '../checkbox/checkbox.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||
import lozad from 'lozad'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faBoxOpen,
|
||||
@ -19,7 +18,7 @@ import {
|
||||
faCode,
|
||||
faFlag
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { debounce, trim } from 'lodash'
|
||||
import { debounce, trim, chunk } from 'lodash'
|
||||
|
||||
library.add(
|
||||
faBoxOpen,
|
||||
@ -82,14 +81,31 @@ const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
|
||||
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 = {
|
||||
props: {
|
||||
enableStickerPicker: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hideCustomEmoji: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
inject: ['popoversZLayer'],
|
||||
data () {
|
||||
return {
|
||||
keyword: '',
|
||||
@ -102,7 +118,8 @@ const EmojiPicker = {
|
||||
contentLoaded: false,
|
||||
groupRefs: {},
|
||||
emojiRefs: {},
|
||||
filteredEmojiGroups: []
|
||||
filteredEmojiGroups: [],
|
||||
width: 0
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@ -125,9 +142,6 @@ const EmojiPicker = {
|
||||
setGroupRef (name) {
|
||||
return el => { this.groupRefs[name] = el }
|
||||
},
|
||||
setEmojiRef (name) {
|
||||
return el => { this.emojiRefs[name] = el }
|
||||
},
|
||||
onPopoverShown () {
|
||||
this.$emit('show')
|
||||
},
|
||||
@ -147,18 +161,21 @@ const EmojiPicker = {
|
||||
}
|
||||
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
||||
},
|
||||
onScroll (e) {
|
||||
const target = (e && e.target) || this.$refs['emoji-groups']
|
||||
this.updateScrolledClass(target)
|
||||
this.scrolledGroup(target)
|
||||
onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
|
||||
const target = this.$refs['emoji-groups'].$el
|
||||
this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
|
||||
},
|
||||
scrolledGroup (target) {
|
||||
scrolledGroup (target, start, end) {
|
||||
const top = target.scrollTop + 5
|
||||
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]
|
||||
if (ref && ref.offsetTop <= top) {
|
||||
this.activeGroup = group.id
|
||||
if (!ref) { return }
|
||||
const elem = ref.$el.parentElement
|
||||
if (!elem) { return }
|
||||
if (elem && getOffset(elem) <= top) {
|
||||
this.activeGroup = headerId
|
||||
}
|
||||
})
|
||||
this.scrollHeader()
|
||||
@ -181,14 +198,10 @@ const EmojiPicker = {
|
||||
setScroll(right + margin - headerCont.clientWidth)
|
||||
}
|
||||
},
|
||||
highlight (key) {
|
||||
const ref = this.groupRefs['group-' + key]
|
||||
const top = ref.offsetTop
|
||||
highlight (groupId) {
|
||||
this.setShowStickers(false)
|
||||
this.activeGroup = key
|
||||
this.$nextTick(() => {
|
||||
this.$refs['emoji-groups'].scrollTop = top + 1
|
||||
})
|
||||
const indexInList = this.emojiItems.findIndex(k => k.id === groupId)
|
||||
this.$refs['emoji-groups'].scrollToItem(indexInList)
|
||||
},
|
||||
updateScrolledClass (target) {
|
||||
if (target.scrollTop <= 5) {
|
||||
@ -208,43 +221,13 @@ const EmojiPicker = {
|
||||
filterByKeyword (list, keyword) {
|
||||
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 () {
|
||||
const oldContentLoaded = this.contentLoaded
|
||||
this.recalculateItemPerRow()
|
||||
this.$nextTick(() => {
|
||||
this.$refs.search.focus()
|
||||
})
|
||||
this.contentLoaded = true
|
||||
this.waitForDomAndInitializeLazyLoad()
|
||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||
if (!oldContentLoaded) {
|
||||
this.$nextTick(() => {
|
||||
@ -261,6 +244,14 @@ const EmojiPicker = {
|
||||
emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
|
||||
}))
|
||||
.filter(group => group.emojis.length > 0)
|
||||
},
|
||||
recalculateItemPerRow () {
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs['emoji-groups']) {
|
||||
return
|
||||
}
|
||||
this.width = this.$refs['emoji-groups'].$el.clientWidth
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -269,14 +260,22 @@ const EmojiPicker = {
|
||||
this.debouncedHandleKeywordChange()
|
||||
},
|
||||
allCustomGroups () {
|
||||
this.waitForDomAndInitializeLazyLoad()
|
||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
this.destroyLazyLoad()
|
||||
},
|
||||
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 () {
|
||||
return this.showingStickers ? '' : this.activeGroup
|
||||
},
|
||||
@ -287,7 +286,14 @@ const EmojiPicker = {
|
||||
return 0
|
||||
},
|
||||
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 () {
|
||||
return Object.keys(this.allCustomGroups)[0]
|
||||
@ -310,10 +316,20 @@ const EmojiPicker = {
|
||||
},
|
||||
debouncedHandleKeywordChange () {
|
||||
return debounce(() => {
|
||||
this.waitForDomAndInitializeLazyLoad()
|
||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||
}, 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 () {
|
||||
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
|
||||
},
|
||||
@ -335,6 +351,9 @@ const EmojiPicker = {
|
||||
|
||||
return emoji.displayText
|
||||
}
|
||||
},
|
||||
isInModal () {
|
||||
return this.popoversZLayer === 'modals'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
$emoji-picker-header-height: 36px;
|
||||
$emoji-picker-header-picture-width: 32px;
|
||||
@ -7,14 +7,14 @@ $emoji-picker-emoji-size: 32px;
|
||||
|
||||
.emoji-picker {
|
||||
width: 25em;
|
||||
max-width: 100vw;
|
||||
max-width: calc(100vw - 20px); // popover gives 10px margin from window edge
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--lightText: var(--popoverLightText, $fallback--faint);
|
||||
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
@ -28,6 +28,7 @@ $emoji-picker-emoji-size: 32px;
|
||||
max-width: $emoji-picker-header-picture-width;
|
||||
height: $emoji-picker-header-picture-height;
|
||||
max-height: $emoji-picker-header-picture-height;
|
||||
|
||||
.still-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
@ -62,24 +63,18 @@ $emoji-picker-emoji-size: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.emoji-tabs {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
flex-flow: row nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.emoji-groups {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.additional-tabs {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
border-left: 1px solid;
|
||||
border-left-color: $fallback--icon;
|
||||
border-left-color: var(--icon, $fallback--icon);
|
||||
@ -121,7 +116,7 @@ $emoji-picker-emoji-size: 32px;
|
||||
}
|
||||
|
||||
.sticker-picker {
|
||||
flex: 1 1 auto
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.stickers,
|
||||
@ -151,22 +146,27 @@ $emoji-picker-emoji-size: 32px;
|
||||
}
|
||||
|
||||
&-groups {
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
flex: 1 1 1px;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
user-select: none;
|
||||
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 top, white, white);
|
||||
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 top, white, white);
|
||||
transition: mask-size 150ms;
|
||||
mask-size: 100% 20px, 100% 20px, auto;
|
||||
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
|
||||
&.scrolled {
|
||||
&-top {
|
||||
mask-size: 100% 20px, 100% 0, auto;
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
mask-size: 100% 0, 100% 20px, auto;
|
||||
}
|
||||
@ -200,7 +200,6 @@ $emoji-picker-emoji-size: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
.emoji-picker-emoji.-custom {
|
||||
@ -208,12 +207,11 @@ $emoji-picker-emoji-size: 32px;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.emoji-picker-emoji.-unicode {
|
||||
font-size: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,13 +3,20 @@
|
||||
ref="popover"
|
||||
trigger="click"
|
||||
popover-class="emoji-picker popover-default"
|
||||
:trigger-attrs="{ 'aria-hidden': true }"
|
||||
@show="onPopoverShown"
|
||||
@close="onPopoverClosed"
|
||||
>
|
||||
<template #content>
|
||||
<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
|
||||
ref="header"
|
||||
v-body-scroll-lock="isInModal"
|
||||
class="emoji-tabs"
|
||||
>
|
||||
<span
|
||||
@ -21,6 +28,7 @@
|
||||
active: activeGroupView === group.id
|
||||
}"
|
||||
:title="group.text"
|
||||
role="button"
|
||||
@click.prevent="highlight(group.id)"
|
||||
>
|
||||
<span
|
||||
@ -74,45 +82,61 @@
|
||||
@input="$event.target.composing = false"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
<!-- Enables scrolling for this element on safari iOS. See comments for header. -->
|
||||
<DynamicScroller
|
||||
ref="emoji-groups"
|
||||
v-body-scroll-lock="isInModal"
|
||||
class="emoji-groups"
|
||||
:class="groupsScrolledClass"
|
||||
@scroll="onScroll"
|
||||
:min-item-size="minItemSize"
|
||||
:items="emojiItems"
|
||||
:emit-update="true"
|
||||
@update="onScroll"
|
||||
@visible="recalculateItemPerRow"
|
||||
@resize="recalculateItemPerRow"
|
||||
>
|
||||
<div
|
||||
v-for="group in filteredEmojiGroups"
|
||||
:key="group.id"
|
||||
class="emoji-group"
|
||||
>
|
||||
<h6
|
||||
<template #default="{ item: group, index, active }">
|
||||
<DynamicScrollerItem
|
||||
:ref="setGroupRef('group-' + group.id)"
|
||||
class="emoji-group-title"
|
||||
:item="group"
|
||||
:active="active"
|
||||
:data-index="index"
|
||||
:size-dependencies="[group.emojis.length]"
|
||||
>
|
||||
{{ group.text }}
|
||||
</h6>
|
||||
<span
|
||||
v-for="emoji in group.emojis"
|
||||
:key="group.id + emoji.displayText"
|
||||
:title="maybeLocalizedEmojiName(emoji)"
|
||||
class="emoji-item"
|
||||
@click.stop.prevent="onEmoji(emoji)"
|
||||
>
|
||||
<span
|
||||
v-if="!emoji.imageUrl"
|
||||
class="emoji-picker-emoji -unicode"
|
||||
>{{ emoji.replacement }}</span>
|
||||
<still-image
|
||||
v-else
|
||||
:ref="setEmojiRef(group.id + emoji.displayText)"
|
||||
class="emoji-picker-emoji -custom"
|
||||
:data-src="emoji.imageUrl"
|
||||
:data-emoji-name="group.id + emoji.displayText"
|
||||
/>
|
||||
</span>
|
||||
<span :ref="setGroupRef('group-end-' + group.id)" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="emoji-group"
|
||||
>
|
||||
<h6
|
||||
v-if="group.isFirstRow"
|
||||
class="emoji-group-title"
|
||||
>
|
||||
{{ group.text }}
|
||||
</h6>
|
||||
<span
|
||||
v-for="emoji in group.emojis"
|
||||
:key="group.id + emoji.displayText"
|
||||
:title="maybeLocalizedEmojiName(emoji)"
|
||||
class="emoji-item"
|
||||
role="button"
|
||||
@click.stop.prevent="onEmoji(emoji)"
|
||||
>
|
||||
<span
|
||||
v-if="!emoji.imageUrl"
|
||||
class="emoji-picker-emoji -unicode"
|
||||
>{{ emoji.replacement }}</span>
|
||||
<still-image
|
||||
v-else
|
||||
class="emoji-picker-emoji -custom"
|
||||
loading="lazy"
|
||||
:alt="maybeLocalizedEmojiName(emoji)"
|
||||
:src="emoji.imageUrl"
|
||||
:data-emoji-name="group.id + emoji.displayText"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
<div class="keep-open">
|
||||
<Checkbox v-model="keepOpen">
|
||||
{{ $t('emoji.keep_open') }}
|
||||
|
@ -1,5 +1,17 @@
|
||||
import UserAvatar from '../user_avatar/user_avatar.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
|
||||
|
||||
@ -33,6 +45,9 @@ const EmojiReactions = {
|
||||
},
|
||||
loggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
remoteInteractionLink () {
|
||||
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -42,10 +57,10 @@ const EmojiReactions = {
|
||||
reactedWith (emoji) {
|
||||
return this.status.emoji_reactions.find(r => r.name === emoji).me
|
||||
},
|
||||
fetchEmojiReactionsByIfMissing () {
|
||||
async fetchEmojiReactionsByIfMissing () {
|
||||
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
|
||||
if (hasNoAccounts) {
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
|
||||
return await this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
|
||||
}
|
||||
},
|
||||
reactWith (emoji) {
|
||||
@ -54,14 +69,26 @@ const EmojiReactions = {
|
||||
unreact (emoji) {
|
||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||
},
|
||||
emojiOnClick (emoji, event) {
|
||||
async emojiOnClick (emoji, event) {
|
||||
if (!this.loggedIn) return
|
||||
|
||||
await this.fetchEmojiReactionsByIfMissing()
|
||||
if (this.reactedWith(emoji)) {
|
||||
this.unreact(emoji)
|
||||
} else {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,64 @@
|
||||
<template>
|
||||
<div class="EmojiReactions">
|
||||
<UserListPopover
|
||||
<span
|
||||
v-for="(reaction) in emojiReactions"
|
||||
:key="reaction.name"
|
||||
:users="accountsForEmoji[reaction.name]"
|
||||
:key="reaction.url || 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="{ '-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)"
|
||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||
>
|
||||
<span class="reaction-emoji">{{ reaction.name }}</span>
|
||||
<span>{{ reaction.count }}</span>
|
||||
</button>
|
||||
</UserListPopover>
|
||||
<span
|
||||
class="reaction-emoji"
|
||||
>
|
||||
<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>
|
||||
</span>
|
||||
<a
|
||||
v-if="tooManyReactions"
|
||||
class="emoji-reaction-expand faint"
|
||||
@ -28,43 +72,121 @@
|
||||
|
||||
<script src="./emoji_reactions.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
@import "../../mixins";
|
||||
|
||||
.EmojiReactions {
|
||||
display: flex;
|
||||
margin-top: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.emoji-reaction {
|
||||
padding: 0 0.5em;
|
||||
margin-right: 0.5em;
|
||||
--emoji-size: calc(1.25em * var(--emojiReactionsScale, 1));
|
||||
|
||||
.emoji-reaction-container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
margin: 0;
|
||||
|
||||
.reaction-emoji {
|
||||
width: 1.25em;
|
||||
width: var(--emoji-size);
|
||||
height: var(--emoji-size);
|
||||
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 {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.not-clickable {
|
||||
cursor: default;
|
||||
&:hover {
|
||||
box-shadow: $fallback--buttonShadow;
|
||||
box-shadow: var(--buttonShadow);
|
||||
}
|
||||
.svg-inline--fa {
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
}
|
||||
|
||||
&.-picked-reaction {
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Popover from '../popover/popover.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisH,
|
||||
@ -32,10 +33,14 @@ library.add(
|
||||
|
||||
const ExtraButtons = {
|
||||
props: ['status'],
|
||||
components: { Popover },
|
||||
components: {
|
||||
Popover,
|
||||
ConfirmModal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expanded: false
|
||||
expanded: false,
|
||||
showingDeleteDialog: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -46,11 +51,22 @@ const ExtraButtons = {
|
||||
this.expanded = false
|
||||
},
|
||||
deleteStatus () {
|
||||
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
||||
if (confirmed) {
|
||||
this.$store.dispatch('deleteStatus', { id: this.status.id })
|
||||
if (this.shouldConfirmDelete) {
|
||||
this.showDeleteStatusConfirmDialog()
|
||||
} else {
|
||||
this.doDeleteStatus()
|
||||
}
|
||||
},
|
||||
doDeleteStatus () {
|
||||
this.$store.dispatch('deleteStatus', { id: this.status.id })
|
||||
this.hideDeleteStatusConfirmDialog()
|
||||
},
|
||||
showDeleteStatusConfirmDialog () {
|
||||
this.showingDeleteDialog = true
|
||||
},
|
||||
hideDeleteStatusConfirmDialog () {
|
||||
this.showingDeleteDialog = false
|
||||
},
|
||||
pinStatus () {
|
||||
this.$store.dispatch('pinStatus', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
@ -133,7 +149,10 @@ const ExtraButtons = {
|
||||
isEdited () {
|
||||
return this.status.edited_at !== null
|
||||
},
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable }
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
shouldConfirmDelete () {
|
||||
return this.$store.getters.mergedConfig.modalOnDelete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,6 +165,18 @@
|
||||
/>
|
||||
</FALayers>
|
||||
</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>
|
||||
</Popover>
|
||||
</template>
|
||||
@ -172,15 +184,10 @@
|
||||
<script src="./extra_buttons.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../../_mixins.scss';
|
||||
@import "../../variables";
|
||||
@import "../../mixins";
|
||||
|
||||
.ExtraButtons {
|
||||
/* override of popover internal stuff */
|
||||
.popover-trigger-button {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.popover-trigger {
|
||||
position: static;
|
||||
padding: 10px;
|
||||
@ -190,10 +197,12 @@
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.popover-trigger-button {
|
||||
/* override of popover internal stuff */
|
||||
width: auto;
|
||||
|
||||
@include unfocused-style {
|
||||
.focus-marker {
|
||||
visibility: hidden;
|
||||
|
@ -38,13 +38,20 @@
|
||||
class="button-unstyled interactive"
|
||||
target="_blank"
|
||||
role="button"
|
||||
:title="$t('tool_tip.favorite')"
|
||||
:href="remoteInteractionLink"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
:title="$t('tool_tip.favorite')"
|
||||
:icon="['far', 'star']"
|
||||
/>
|
||||
<FALayers class="fa-scale-110 fa-old-padding-layer">
|
||||
<FAIcon
|
||||
class="fa-scale-110"
|
||||
:icon="['far', 'star']"
|
||||
/>
|
||||
<FAIcon
|
||||
class="focus-marker"
|
||||
transform="shrink-6 up-9 right-12"
|
||||
icon="plus"
|
||||
/>
|
||||
</FALayers>
|
||||
</a>
|
||||
<span
|
||||
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
|
||||
@ -58,8 +65,8 @@
|
||||
<script src="./favorite_button.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../../_mixins.scss';
|
||||
@import "../../variables";
|
||||
@import "../../mixins";
|
||||
|
||||
.FavoriteButton {
|
||||
display: flex;
|
||||
|
@ -42,7 +42,8 @@
|
||||
<script src="./flash.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.Flash {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
@ -78,7 +79,7 @@
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
visibility: 'hidden';
|
||||
visibility: "hidden";
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,12 +1,20 @@
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
export default {
|
||||
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
inProgress: false
|
||||
inProgress: false,
|
||||
showingConfirmUnfollow: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmUnfollow () {
|
||||
return this.$store.getters.mergedConfig.modalOnUnfollow
|
||||
},
|
||||
isPressed () {
|
||||
return this.inProgress || this.relationship.following
|
||||
},
|
||||
@ -35,6 +43,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showConfirmUnfollow () {
|
||||
this.showingConfirmUnfollow = true
|
||||
},
|
||||
hideConfirmUnfollow () {
|
||||
this.showingConfirmUnfollow = false
|
||||
},
|
||||
onClick () {
|
||||
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
|
||||
},
|
||||
@ -45,12 +59,21 @@ export default {
|
||||
})
|
||||
},
|
||||
unfollow () {
|
||||
if (this.shouldConfirmUnfollow) {
|
||||
this.showConfirmUnfollow()
|
||||
} else {
|
||||
this.doUnfollow()
|
||||
}
|
||||
},
|
||||
doUnfollow () {
|
||||
const store = this.$store
|
||||
this.inProgress = true
|
||||
requestUnfollow(this.relationship.id, store).then(() => {
|
||||
this.inProgress = false
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
|
||||
})
|
||||
|
||||
this.hideConfirmUnfollow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,27 @@
|
||||
@click="onClick"
|
||||
>
|
||||
{{ 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>
|
||||
</template>
|
||||
|
||||
|
@ -24,6 +24,7 @@
|
||||
/>
|
||||
<RemoveFollowerButton
|
||||
v-if="noFollowsYou && relationship.followed_by"
|
||||
:user="user"
|
||||
:relationship="relationship"
|
||||
class="follow-card-button"
|
||||
/>
|
||||
@ -39,9 +40,8 @@
|
||||
&-content-container {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,18 @@
|
||||
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'
|
||||
|
||||
const FollowRequestCard = {
|
||||
props: ['user'],
|
||||
components: {
|
||||
BasicUserCard
|
||||
BasicUserCard,
|
||||
ConfirmModal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showingApproveConfirmDialog: false,
|
||||
showingDenyConfirmDialog: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
findFollowRequestNotificationId () {
|
||||
@ -13,7 +21,26 @@ const FollowRequestCard = {
|
||||
)
|
||||
return notif && notif.id
|
||||
},
|
||||
showApproveConfirmDialog () {
|
||||
this.showingApproveConfirmDialog = true
|
||||
},
|
||||
hideApproveConfirmDialog () {
|
||||
this.showingApproveConfirmDialog = false
|
||||
},
|
||||
showDenyConfirmDialog () {
|
||||
this.showingDenyConfirmDialog = true
|
||||
},
|
||||
hideDenyConfirmDialog () {
|
||||
this.showingDenyConfirmDialog = false
|
||||
},
|
||||
approveUser () {
|
||||
if (this.shouldConfirmApprove) {
|
||||
this.showApproveConfirmDialog()
|
||||
} else {
|
||||
this.doApprove()
|
||||
}
|
||||
},
|
||||
doApprove () {
|
||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
|
||||
@ -25,14 +52,34 @@ const FollowRequestCard = {
|
||||
notification.type = 'follow'
|
||||
}
|
||||
})
|
||||
this.hideApproveConfirmDialog()
|
||||
},
|
||||
denyUser () {
|
||||
if (this.shouldConfirmDeny) {
|
||||
this.showDenyConfirmDialog()
|
||||
} else {
|
||||
this.doDeny()
|
||||
}
|
||||
},
|
||||
doDeny () {
|
||||
const notifId = this.findFollowRequestNotificationId()
|
||||
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
|
||||
.then(() => {
|
||||
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,28 @@
|
||||
{{ $t('user_card.deny') }}
|
||||
</button>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -22,8 +44,8 @@
|
||||
<style lang="scss">
|
||||
.follow-request-card-content-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
flex-flow: row wrap;
|
||||
|
||||
button {
|
||||
margin-top: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
|
@ -4,6 +4,7 @@
|
||||
:class="{ custom: isCustom }"
|
||||
>
|
||||
<label
|
||||
:id="name + '-label'"
|
||||
:for="preset === 'custom' ? name : name + '-font-switcher'"
|
||||
class="label"
|
||||
>
|
||||
@ -12,7 +13,8 @@
|
||||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exlcude-disabled"
|
||||
:aria-labelledby="name + '-label'"
|
||||
class="opt exlcude-disabled visible-for-screenreader-only"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||
@ -21,6 +23,7 @@
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
:for="name + '-o'"
|
||||
:aria-hidden="true"
|
||||
/>
|
||||
{{ ' ' }}
|
||||
<Select
|
||||
@ -50,17 +53,20 @@
|
||||
<script src="./font_control.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.font-control {
|
||||
input.custom-font {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
&.custom {
|
||||
/* TODO Should make proper joiners... */
|
||||
.font-switcher {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.custom-font {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
@ -4,6 +4,7 @@ import { sumBy, set } from 'lodash'
|
||||
const Gallery = {
|
||||
props: [
|
||||
'attachments',
|
||||
'compact',
|
||||
'limitRows',
|
||||
'descriptions',
|
||||
'limit',
|
||||
|
@ -20,6 +20,7 @@
|
||||
v-for="(attachment, attachmentIndex) in row.items"
|
||||
:key="attachment.id"
|
||||
class="gallery-item"
|
||||
:compact="compact"
|
||||
:nsfw="nsfw"
|
||||
:attachment="attachment"
|
||||
:size="size"
|
||||
@ -86,7 +87,7 @@
|
||||
<script src='./gallery.js'></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.Gallery {
|
||||
.gallery-rows {
|
||||
@ -100,6 +101,53 @@
|
||||
width: 100%;
|
||||
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) {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
@ -114,7 +162,7 @@
|
||||
linear-gradient(to top, white, white);
|
||||
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
}
|
||||
@ -138,54 +186,5 @@
|
||||
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>
|
||||
|
@ -25,7 +25,7 @@
|
||||
<script src="./global_notice_list.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.global-notice-list {
|
||||
position: fixed;
|
||||
@ -73,6 +73,7 @@
|
||||
.global-success {
|
||||
background-color: var(--alertPopupSuccess, $fallback--cGreen);
|
||||
color: var(--alertPopupSuccessText, $fallback--text);
|
||||
|
||||
.svg-inline--fa {
|
||||
color: var(--alertPopupSuccessText, $fallback--text);
|
||||
}
|
||||
@ -81,6 +82,7 @@
|
||||
.global-info {
|
||||
background-color: var(--alertPopupNeutral, $fallback--fg);
|
||||
color: var(--alertPopupNeutralText, $fallback--text);
|
||||
|
||||
.svg-inline--fa {
|
||||
color: var(--alertPopupNeutralText, $fallback--text);
|
||||
}
|
||||
@ -88,6 +90,7 @@
|
||||
|
||||
.close-notice {
|
||||
padding-right: 0.2em;
|
||||
|
||||
.svg-inline--fa:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
@ -1,21 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<label for="interface-language-switcher">
|
||||
<div class="interface-language-switcher">
|
||||
<label>
|
||||
{{ promptText }}
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<Select
|
||||
id="interface-language-switcher"
|
||||
v-model="controlledLanguage"
|
||||
>
|
||||
<option
|
||||
v-for="lang in languages"
|
||||
:key="lang.code"
|
||||
:value="lang.code"
|
||||
<ul class="setting-list">
|
||||
<li
|
||||
v-for="index of controlledLanguage.keys()"
|
||||
:key="index"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</Select>
|
||||
<label>
|
||||
{{ index === 0 ? $t('settings.primary_language') : $tc('settings.fallback_language', index, { index }) }}
|
||||
<Select
|
||||
class="language-select"
|
||||
:model-value="controlledLanguage[index]"
|
||||
@update:modelValue="val => setLanguageAt(index, val)"
|
||||
>
|
||||
<option
|
||||
v-for="lang in languages"
|
||||
:key="lang.code"
|
||||
:value="lang.code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -34,7 +59,7 @@ export default {
|
||||
required: true
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
type: [Array, String],
|
||||
required: true
|
||||
},
|
||||
setLanguage: {
|
||||
@ -48,7 +73,9 @@ export default {
|
||||
},
|
||||
|
||||
controlledLanguage: {
|
||||
get: function () { return this.language },
|
||||
get: function () {
|
||||
return Array.isArray(this.language) ? this.language : [this.language]
|
||||
},
|
||||
set: function (val) {
|
||||
this.setLanguage(val)
|
||||
}
|
||||
@ -58,7 +85,30 @@ export default {
|
||||
methods: {
|
||||
getLanguageName (code) {
|
||||
return localeService.getLanguageName(code)
|
||||
},
|
||||
addLanguage () {
|
||||
this.controlledLanguage = [...this.controlledLanguage, '']
|
||||
},
|
||||
setLanguageAt (index, val) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang[index] = val
|
||||
this.controlledLanguage = lang
|
||||
},
|
||||
removeLanguageAt (index) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang.splice(index, 1)
|
||||
this.controlledLanguage = lang
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.interface-language-switcher {
|
||||
.language-select {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -33,7 +33,7 @@
|
||||
<script src="./link-preview.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.link-preview-card {
|
||||
display: flex;
|
||||
@ -46,6 +46,7 @@
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
max-width: 25%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -67,7 +68,7 @@
|
||||
}
|
||||
|
||||
.card-description {
|
||||
margin: 0.5em 0 0 0;
|
||||
margin: 0.5em 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
|
@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<div class="list">
|
||||
<div
|
||||
class="list"
|
||||
role="list"
|
||||
>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="getKey(item)"
|
||||
class="list-item"
|
||||
role="listitem"
|
||||
>
|
||||
<slot
|
||||
name="item"
|
||||
@ -35,7 +39,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.list {
|
||||
&-item:not(:last-child) {
|
||||
|
@ -21,12 +21,16 @@
|
||||
<script src="./lists_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.list-card {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.list-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.list-name,
|
||||
.button-list-edit {
|
||||
margin: 0;
|
||||
@ -39,13 +43,10 @@
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: $fallback--link;
|
||||
color: var(--selectedMenuText, $fallback--link);
|
||||
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
}
|
||||
}
|
||||
|
||||
.list-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
@ -95,10 +95,10 @@ const ListsNew = {
|
||||
return this.addedUserIds.has(user.id)
|
||||
},
|
||||
addUser (user) {
|
||||
this.$store.dispatch('addListAccount', { accountId: this.user.id, listId: this.id })
|
||||
this.$store.dispatch('addListAccount', { accountId: user.id, listId: this.id })
|
||||
},
|
||||
removeUser (userId) {
|
||||
this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id })
|
||||
this.$store.dispatch('removeListAccount', { accountId: userId, listId: this.id })
|
||||
},
|
||||
onSearchLoading (results) {
|
||||
this.searchLoading = true
|
||||
|
@ -164,7 +164,7 @@
|
||||
<script src="./lists_edit.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.ListEdit {
|
||||
--panel-body-padding: 0.5em;
|
||||
|
@ -27,12 +27,12 @@
|
||||
|
||||
<script src="./lists_user_search.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.ListsUserSearch {
|
||||
.input-wrap {
|
||||
display: flex;
|
||||
margin: 0.7em 0.5em 0.7em 0.5em;
|
||||
margin: 0.7em 0.5em;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
|
@ -93,7 +93,7 @@
|
||||
<script src="./login_form.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
@ -110,7 +110,7 @@
|
||||
}
|
||||
|
||||
.login-bottom {
|
||||
margin-top: 1.0em;
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@ -121,7 +121,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.3em 0.5em 0.6em;
|
||||
line-height:24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.form-bottom {
|
||||
@ -142,7 +142,6 @@
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
|
||||
animation-name: shakeError;
|
||||
animation-duration: 0.4s;
|
||||
animation-timing-function: ease-in-out;
|
||||
|
@ -63,6 +63,11 @@ const MediaModal = {
|
||||
},
|
||||
type () {
|
||||
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: {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user