Merge branch 'release/2.9.x' into 'master'
Release/2.9.x See merge request pleroma/pleroma-fe!2248
This commit is contained in:
commit
0ecbae9675
206 changed files with 8195 additions and 5745 deletions
8
.gitlab/merge_request_templates/Release.md
Normal file
8
.gitlab/merge_request_templates/Release.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
### Release checklist
|
||||
* [ ] Bump version in `package.json`
|
||||
* [ ] Compile a changelog with the `tools/collect-changelog` script
|
||||
* [ ] Create an MR with an announcement to pleroma.social
|
||||
#### post-merge
|
||||
* [ ] Tag the release on the merge commit
|
||||
* [ ] Make the tag into a Gitlab Release™
|
||||
* [ ] Merge `master` into `develop` (in case the fixes are already in develop, use `git merge -s ours --no-commit` and manually merge the changelogs)
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-rscss/config",
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended-scss",
|
||||
"stylelint-config-html",
|
||||
|
|
@ -8,20 +7,13 @@
|
|||
],
|
||||
"rules": {
|
||||
"declaration-no-important": true,
|
||||
"rscss/no-descendant-combinator": false,
|
||||
"rscss/class-format": [
|
||||
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-property-value-no-unknown": true,
|
||||
"scss/declaration-property-value-no-unknown": true,
|
||||
"declaration-block-no-redundant-longhand-properties": [
|
||||
true,
|
||||
{
|
||||
|
|
|
|||
36
CHANGELOG.md
36
CHANGELOG.md
|
|
@ -2,6 +2,42 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## 2.9.2
|
||||
### Changed
|
||||
- BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible
|
||||
- User card/profile got an overhaul
|
||||
- Profile editing overhaul
|
||||
- Visually combined subject and content fields in post form
|
||||
- Moved post form's emoji button into input field
|
||||
- Minor visual changes and fixes
|
||||
- Clicking on fav/rt/emoji notifications' contents expands/collapses it
|
||||
- Reduced time taken processing theme by half
|
||||
- Splash screen only appears if loading takes more than 2 seconds
|
||||
|
||||
### Added
|
||||
- Mutes received an update, adding support for regex, muting based on username and expiration time.
|
||||
- Mutes are now synchronized across sessions
|
||||
- Support for expiring mutes and blocks (if available)
|
||||
- Clicking on emoji shows bigger version of it alongside with its shortcode
|
||||
- Admins also are able to copy it into a local pack
|
||||
- Added support for Akkoma and IceShrimp.NET backends
|
||||
- Compatibility with stricter CSP (Akkoma backend)
|
||||
- Added a way to upload new packs from a URL or ZIP file via the Admin Dashboard
|
||||
- Unify show/hide content buttons
|
||||
- Add support for detachable scrollTop button
|
||||
- Option to left-align user bio
|
||||
- Cache assets and emojis with service worker
|
||||
- Indicate currently active V3 theme as a body element class
|
||||
- Add arithmetic blend ISS function
|
||||
|
||||
### Fixed
|
||||
- Display counter for status action buttons when they are in the menu
|
||||
- Fix bookmark button alignment in the extra actions menu
|
||||
- Instance favicons are no longer stretched
|
||||
- A lot more scalable UI fixes
|
||||
- Emoji picker now should work fine when emoji size is increased
|
||||
|
||||
## 2.8.0
|
||||
### Changed
|
||||
- BREAKING: static/img/nsfw.2958239.png is now static/img/nsfw.DepQPhG0.png, which may affect people who specify exactly this path as the cover image
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ const getSWMessagesAsText = async () => {
|
|||
}
|
||||
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||
|
||||
const swEnvName = 'virtual:pleroma-fe/service_worker_env'
|
||||
const swEnvNameResolved = '\0' + swEnvName
|
||||
const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };`
|
||||
const getProdSwEnv = ({ assets }) => `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
|
||||
|
||||
export const devSwPlugin = ({
|
||||
swSrc,
|
||||
swDest,
|
||||
|
|
@ -32,12 +37,16 @@ export const devSwPlugin = ({
|
|||
const name = id.startsWith('/') ? id.slice(1) : id
|
||||
if (name === swDest) {
|
||||
return swFullSrc
|
||||
} else if (name === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
async load (id) {
|
||||
if (id === swFullSrc) {
|
||||
return readFile(swFullSrc, 'utf-8')
|
||||
} else if (id === swEnvNameResolved) {
|
||||
return getDevSwEnv()
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
|
@ -79,6 +88,21 @@ export const devSwPlugin = ({
|
|||
contents: await getSWMessagesAsText()
|
||||
}))
|
||||
}
|
||||
}, {
|
||||
name: 'sw-env',
|
||||
setup (b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp('^' + swEnvName + '$') },
|
||||
args => ({
|
||||
path: args.path,
|
||||
namespace: 'sw-env'
|
||||
}))
|
||||
b.onLoad(
|
||||
{ filter: /.*/, namespace: 'sw-env' },
|
||||
() => ({
|
||||
contents: getDevSwEnv()
|
||||
}))
|
||||
}
|
||||
}]
|
||||
})
|
||||
const text = res.outputFiles[0].text
|
||||
|
|
@ -126,6 +150,30 @@ export const buildSwPlugin = ({
|
|||
configFile: false
|
||||
}
|
||||
},
|
||||
generateBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler (_, bundle) {
|
||||
const assets = Object.keys(bundle)
|
||||
.filter(name => !/\.map$/.test(name))
|
||||
.map(name => '/' + name)
|
||||
config.plugins.push({
|
||||
name: 'build-sw-env-plugin',
|
||||
resolveId (id) {
|
||||
if (id === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
load (id) {
|
||||
if (id === swEnvNameResolved) {
|
||||
return getProdSwEnv({ assets })
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
|
|
|
|||
132
index.html
132
index.html
|
|
@ -5,140 +5,18 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||
<link rel="preload" href="/static/config.json" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/api/pleroma/frontend_configurations" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/nodeinfo/2.0.json" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/nodeinfo/2.1.json" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/api/v1/instance" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/static/pleromatan_apology_fox_small.webp" as="image" />
|
||||
<!-- putting styles here to avoid having to wait for styles to load up -->
|
||||
<style id="splashscreen">
|
||||
#splash {
|
||||
--scale: 1;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
flex-direction: column;
|
||||
background: #0f161e;
|
||||
font-family: sans-serif;
|
||||
color: #b9b9ba;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
}
|
||||
|
||||
#splash-credit {
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#mascot-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
perspective: 60em;
|
||||
perspective-origin: 0 -15em;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
#mascot {
|
||||
width: calc(10em * var(--scale));
|
||||
height: calc(10em * var(--scale));
|
||||
object-fit: contain;
|
||||
object-position: bottom;
|
||||
transform: translateZ(-2em);
|
||||
}
|
||||
|
||||
#throbber {
|
||||
display: grid;
|
||||
width: calc(5em * 0.5 * var(--scale));
|
||||
height: calc(8em * 0.5 * var(--scale));
|
||||
margin-left: 4.1em;
|
||||
z-index: 2;
|
||||
grid-template-rows: repeat(8, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-areas: "P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . . ."
|
||||
"P P . . ."
|
||||
"P P . E E"
|
||||
"P P . E E";
|
||||
|
||||
--logoChunkSize: calc(2em * 0.5 * var(--scale))
|
||||
}
|
||||
|
||||
.chunk {
|
||||
background-color: #e2b188;
|
||||
box-shadow: 0.01em 0.01em 0.1em 0 #e2b188;
|
||||
}
|
||||
|
||||
#chunk-P {
|
||||
grid-area: P;
|
||||
border-top-left-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#chunk-L {
|
||||
grid-area: L;
|
||||
border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#chunk-E {
|
||||
grid-area: E;
|
||||
border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 1em;
|
||||
line-height: 2;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#statusError {
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
line-height: 2;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#statusStack {
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
font-size: calc((1vw + 1vh + 1vmin) / 2.5);
|
||||
width: calc(100vw - 5em);
|
||||
padding: 1em;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
text-align: left;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
#throbber {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style id="pleroma-eager-styles" type="text/css"></style>
|
||||
<style id="pleroma-lazy-styles" type="text/css"></style>
|
||||
<link rel="stylesheet" id="splashscreen" href="/static/splash.css" />
|
||||
<link rel="stylesheet" id="custom-styles-holder" type="text/css" href="/static/empty.css" />
|
||||
<!--server-generated-meta-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0">
|
||||
<body>
|
||||
<noscript>To use Pleroma, please enable JavaScript.</noscript>
|
||||
<div id="splash">
|
||||
<div id="splash" class="initial-hidden">
|
||||
<!-- we are hiding entire graphic so no point showing credit -->
|
||||
<div aria-hidden="true" id="splash-credit">
|
||||
Art by pipivovott
|
||||
|
|
|
|||
78
package.json
78
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "2.7.1",
|
||||
"version": "2.9.2",
|
||||
"description": "Pleroma frontend, the default frontend of Pleroma social network server",
|
||||
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
|
||||
"private": false,
|
||||
|
|
@ -17,96 +17,98 @@
|
|||
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.27.0",
|
||||
"@babel/runtime": "7.28.3",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "3.0.8",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"@fortawesome/vue-fontawesome": "3.1.1",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.3.0",
|
||||
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.1.13",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22",
|
||||
"@vuelidate/core": "2.0.3",
|
||||
"@vuelidate/validators": "2.0.4",
|
||||
"@web3-storage/parse-link-header": "^3.1.0",
|
||||
"body-scroll-lock": "3.1.5",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"cropperjs": "2.0.1",
|
||||
"escape-html": "1.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "2.0.0",
|
||||
"phoenix": "1.7.21",
|
||||
"phoenix": "1.8.0",
|
||||
"pinia": "^3.0.0",
|
||||
"punycode.js": "2.3.1",
|
||||
"qrcode": "1.5.4",
|
||||
"querystring-es3": "0.2.1",
|
||||
"url": "0.11.4",
|
||||
"utf8": "3.0.0",
|
||||
"vue": "3.5.13",
|
||||
"vue-i18n": "10",
|
||||
"vue-router": "4.5.0",
|
||||
"uuid": "11.1.0",
|
||||
"vue": "3.5.19",
|
||||
"vue-i18n": "11",
|
||||
"vue-router": "4.5.1",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.7",
|
||||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.10",
|
||||
"@babel/eslint-parser": "7.27.0",
|
||||
"@babel/plugin-transform-runtime": "7.26.10",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/register": "7.25.9",
|
||||
"@babel/core": "7.28.3",
|
||||
"@babel/eslint-parser": "7.28.0",
|
||||
"@babel/plugin-transform-runtime": "7.28.3",
|
||||
"@babel/preset-env": "7.28.3",
|
||||
"@babel/register": "7.28.3",
|
||||
"@ungap/event-target": "0.2.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vitest/browser": "^3.0.7",
|
||||
"@vitest/ui": "^3.0.7",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||
"@vue/babel-plugin-jsx": "1.4.0",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"@vue/babel-plugin-jsx": "1.5.0",
|
||||
"@vue/compiler-sfc": "3.5.19",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"autoprefixer": "10.4.21",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "5.2.0",
|
||||
"chalk": "5.4.1",
|
||||
"chromedriver": "134.0.5",
|
||||
"chai": "5.3.2",
|
||||
"chalk": "5.6.0",
|
||||
"chromedriver": "135.0.4",
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"eslint": "9.23.0",
|
||||
"eslint": "9.33.0",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-formatter-friendly": "7.0.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-n": "17.17.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-n": "17.21.3",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"eslint-plugin-vue": "9.33.0",
|
||||
"eslint-plugin-vue": "10.4.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "4.21.2",
|
||||
"express": "5.1.0",
|
||||
"function-bind": "1.1.2",
|
||||
"http-proxy-middleware": "3.0.3",
|
||||
"http-proxy-middleware": "3.0.5",
|
||||
"iso-639-1": "3.1.5",
|
||||
"lodash": "4.17.21",
|
||||
"msw": "2.7.3",
|
||||
"nightwatch": "3.12.1",
|
||||
"playwright": "1.49.1",
|
||||
"postcss": "8.5.3",
|
||||
"msw": "2.10.5",
|
||||
"nightwatch": "3.12.2",
|
||||
"playwright": "1.55.0",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"sass": "1.86.0",
|
||||
"sass": "1.89.2",
|
||||
"selenium-server": "3.141.59",
|
||||
"semver": "7.7.1",
|
||||
"semver": "7.7.2",
|
||||
"serve-static": "2.2.0",
|
||||
"shelljs": "0.9.2",
|
||||
"shelljs": "0.10.0",
|
||||
"sinon": "20.0.0",
|
||||
"sinon-chai": "4.0.0",
|
||||
"stylelint": "14.16.1",
|
||||
"stylelint": "16.19.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-config-recommended": "^16.0.0",
|
||||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.6.0",
|
||||
"stylelint-config-standard": "38.0.0",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-eslint2": "^5.0.3",
|
||||
"vite-plugin-stylelint": "^6.0.0",
|
||||
|
|
|
|||
132
public/static/splash.css
Normal file
132
public/static/splash.css
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#splash {
|
||||
--scale: 1;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto;
|
||||
align-content: center;
|
||||
place-items: center;
|
||||
flex-direction: column;
|
||||
background: #0f161e;
|
||||
font-family: sans-serif;
|
||||
color: #b9b9ba;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
opacity: 1;
|
||||
transition: opacity 500ms ease-out 2s;
|
||||
}
|
||||
|
||||
#splash.hidden,
|
||||
#splash.initial-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#splash-credit {
|
||||
position: absolute;
|
||||
font-size: 1em;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#mascot-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
perspective: 60em;
|
||||
perspective-origin: 0 -15em;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
#mascot {
|
||||
width: calc(10em * var(--scale));
|
||||
height: calc(10em * var(--scale));
|
||||
object-fit: contain;
|
||||
object-position: bottom;
|
||||
transform: translateZ(-2em);
|
||||
}
|
||||
|
||||
#throbber {
|
||||
display: grid;
|
||||
width: calc(5em * 0.5 * var(--scale));
|
||||
height: calc(8em * 0.5 * var(--scale));
|
||||
margin-left: 4.1em;
|
||||
z-index: 2;
|
||||
grid-template-rows: repeat(8, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-areas: "P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . . ."
|
||||
"P P . . ."
|
||||
"P P . E E"
|
||||
"P P . E E";
|
||||
|
||||
--logoChunkSize: calc(2em * 0.5 * var(--scale))
|
||||
}
|
||||
|
||||
.chunk {
|
||||
background-color: #e2b188;
|
||||
box-shadow: 0.01em 0.01em 0.1em 0 #e2b188;
|
||||
}
|
||||
|
||||
#chunk-P {
|
||||
grid-area: P;
|
||||
border-top-left-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#chunk-L {
|
||||
grid-area: L;
|
||||
border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#chunk-E {
|
||||
grid-area: E;
|
||||
border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 1em;
|
||||
line-height: 2;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#statusError {
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
line-height: 2;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#statusStack {
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
font-size: calc((1vw + 1vh + 1vmin) / 2.5);
|
||||
width: calc(100vw - 5em);
|
||||
padding: 1em;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
text-align: left;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
#throbber {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
62
src/App.js
62
src/App.js
|
|
@ -14,12 +14,15 @@ import EditStatusModal from './components/edit_status_modal/edit_status_modal.vu
|
|||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import { getOrCreateServiceWorker } from './services/sw/sw'
|
||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { useShoutStore } from './stores/shout'
|
||||
import { useInterfaceStore } from './stores/interface'
|
||||
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
|
|
@ -50,6 +53,9 @@ export default {
|
|||
themeApplied () {
|
||||
this.removeSplash()
|
||||
},
|
||||
currentTheme () {
|
||||
this.setThemeBodyClass()
|
||||
},
|
||||
layoutType () {
|
||||
document.getElementById('modal').classList = ['-' + this.layoutType]
|
||||
}
|
||||
|
|
@ -58,21 +64,41 @@ export default {
|
|||
// Load the locale from the storage
|
||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
window.addEventListener('resize', this.updateMobileState)
|
||||
document.getElementById('modal').classList = ['-' + this.layoutType]
|
||||
|
||||
// Create bound handlers
|
||||
this.updateScrollState = throttle(this.scrollHandler, 200)
|
||||
this.updateMobileState = throttle(this.resizeHandler, 200)
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('resize', this.updateMobileState)
|
||||
this.scrollParent.addEventListener('scroll', this.updateScrollState)
|
||||
|
||||
if (useInterfaceStore().themeApplied) {
|
||||
this.setThemeBodyClass()
|
||||
this.removeSplash()
|
||||
}
|
||||
getOrCreateServiceWorker()
|
||||
},
|
||||
unmounted () {
|
||||
window.removeEventListener('resize', this.updateMobileState)
|
||||
this.scrollParent.removeEventListener('scroll', this.updateScrollState)
|
||||
},
|
||||
computed: {
|
||||
themeApplied () {
|
||||
return useInterfaceStore().themeApplied
|
||||
},
|
||||
currentTheme () {
|
||||
if (useInterfaceStore().styleDataUsed) {
|
||||
const styleMeta = useInterfaceStore().styleDataUsed.find(x => x.component === '@meta')
|
||||
|
||||
if (styleMeta !== undefined) {
|
||||
return styleMeta.directives.name.replaceAll(" ", "-").toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
return 'stock'
|
||||
},
|
||||
layoutModalClass () {
|
||||
return '-' + this.layoutType
|
||||
},
|
||||
|
|
@ -146,19 +172,51 @@ export default {
|
|||
},
|
||||
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
||||
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
|
||||
scrollParent () { return window; /* this.$refs.appContentRef */ },
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
methods: {
|
||||
updateMobileState () {
|
||||
resizeHandler () {
|
||||
useInterfaceStore().setLayoutWidth(windowWidth())
|
||||
useInterfaceStore().setLayoutHeight(windowHeight())
|
||||
},
|
||||
scrollHandler () {
|
||||
const scrollPosition = this.scrollParent === window ? window.scrollY : this.scrollParent.scrollTop
|
||||
|
||||
if (scrollPosition != 0) {
|
||||
this.$refs.appContentRef.classList.add(['-scrolled'])
|
||||
} else {
|
||||
this.$refs.appContentRef.classList.remove(['-scrolled'])
|
||||
}
|
||||
},
|
||||
setThemeBodyClass () {
|
||||
const themeName = this.currentTheme
|
||||
const classList = Array.from(document.body.classList)
|
||||
const oldTheme = classList.filter(c => c.startsWith('theme-'))
|
||||
|
||||
if (themeName !== null && themeName !== '') {
|
||||
const newTheme = `theme-${themeName.toLowerCase()}`
|
||||
|
||||
// remove old theme reference if there are any
|
||||
if (oldTheme.length) {
|
||||
document.body.classList.replace(oldTheme[0], newTheme)
|
||||
} else {
|
||||
document.body.classList.add(newTheme)
|
||||
}
|
||||
} else {
|
||||
// remove theme reference if non-V3 theme is used
|
||||
document.body.classList.remove(...oldTheme)
|
||||
}
|
||||
},
|
||||
removeSplash () {
|
||||
document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4))
|
||||
const splashscreenRoot = document.querySelector('#splash')
|
||||
splashscreenRoot.addEventListener('transitionend', () => {
|
||||
splashscreenRoot.remove()
|
||||
})
|
||||
setTimeout(() => {
|
||||
splashscreenRoot.remove() // forcibly remove it, should fix my plasma browser widget t. HJ
|
||||
}, 600)
|
||||
splashscreenRoot.classList.add('hidden')
|
||||
document.querySelector('#app').classList.remove('hidden')
|
||||
}
|
||||
|
|
|
|||
63
src/App.scss
63
src/App.scss
|
|
@ -2,6 +2,9 @@
|
|||
/* stylelint-disable no-descending-specificity */
|
||||
@use "panel";
|
||||
|
||||
@import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
@import '@kazvmoe-infra/pinch-zoom-element/dist/pinch-zoom.css';
|
||||
|
||||
:root {
|
||||
--status-margin: 0.75em;
|
||||
--post-line-height: 1.4;
|
||||
|
|
@ -18,7 +21,7 @@
|
|||
}
|
||||
|
||||
html {
|
||||
font-size: var(--textSize, 14px);
|
||||
font-size: var(--textSize, 1rem);
|
||||
|
||||
--navbar-height: var(--navbarSize, 3.5rem);
|
||||
--emoji-size: var(--emojiSize, 32px);
|
||||
|
|
@ -30,12 +33,12 @@ body {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--font);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overscroll-behavior-y: none;
|
||||
overflow-x: clip;
|
||||
overflow-y: scroll;
|
||||
overflow: clip scroll;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
|
|
@ -224,9 +227,8 @@ nav {
|
|||
grid-template-rows: 1fr;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
align-content: flex-start;
|
||||
place-content: flex-start center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
overflow-x: clip;
|
||||
|
||||
|
|
@ -262,8 +264,7 @@ nav {
|
|||
position: sticky;
|
||||
top: var(--navbar-height);
|
||||
max-height: calc(100vh - var(--navbar-height));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden auto;
|
||||
margin-left: calc(var(--___paddingIncrease) * -1);
|
||||
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
|
||||
|
||||
|
|
@ -381,6 +382,10 @@ nav {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--font);
|
||||
|
||||
&.-transparent {
|
||||
backdrop-filter: blur(0.125em) contrast(60%);
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -524,6 +529,10 @@ textarea {
|
|||
height: unset;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--textFaint)
|
||||
}
|
||||
|
||||
--_padding: 0.5em;
|
||||
|
||||
border: none;
|
||||
|
|
@ -678,11 +687,6 @@ option {
|
|||
}
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
|
@ -741,17 +745,17 @@ option {
|
|||
}
|
||||
|
||||
&.-dot {
|
||||
min-height: 8px;
|
||||
max-height: 8px;
|
||||
min-width: 8px;
|
||||
max-width: 8px;
|
||||
min-height: 0.6em;
|
||||
max-height: 0.6em;
|
||||
min-width: 0.6em;
|
||||
max-width: 0.6em;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
font-size: 0;
|
||||
left: calc(50% - 4px);
|
||||
top: calc(50% - 4px);
|
||||
margin-left: 6px;
|
||||
margin-top: -6px;
|
||||
left: calc(50% - 0.6em);
|
||||
top: calc(50% - 0.6em);
|
||||
margin-left: 0.4em;
|
||||
margin-top: -0.4em;
|
||||
}
|
||||
|
||||
&.-counter {
|
||||
|
|
@ -782,12 +786,6 @@ option {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.visibility-notice {
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--textFaint);
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
.notice-dismissible {
|
||||
padding-right: 4rem;
|
||||
position: relative;
|
||||
|
|
@ -832,7 +830,7 @@ option {
|
|||
.login-hint {
|
||||
text-align: center;
|
||||
|
||||
@media all and (min-width: 801px) {
|
||||
@media all and (width >= 801px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -854,7 +852,7 @@ option {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
@media all and (width <= 800px) {
|
||||
.mobile-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -935,12 +933,7 @@ option {
|
|||
|
||||
#splash {
|
||||
pointer-events: none;
|
||||
transition: opacity 0.5s;
|
||||
opacity: 1;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
// transition: opacity 0.5s;
|
||||
|
||||
#status {
|
||||
&.css-ok {
|
||||
|
|
@ -1079,7 +1072,7 @@ option {
|
|||
scale: 1.0063 0.9938;
|
||||
translate: 0 -10%;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-in-ou;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
90% {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
<Notifications v-if="currentUser" />
|
||||
<div
|
||||
id="content"
|
||||
ref="appContentRef"
|
||||
class="app-layout container"
|
||||
:class="classes"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import VueVirtualScroller from 'vue-virtual-scroller'
|
|||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||
config.autoAddCss = false
|
||||
|
||||
import App from '../App.vue'
|
||||
import routes from './routes'
|
||||
|
|
@ -21,6 +23,7 @@ import { useOAuthStore } from 'src/stores/oauth'
|
|||
import { useI18nStore } from 'src/stores/i18n'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
|
||||
let staticInitialResults = null
|
||||
|
||||
|
|
@ -63,10 +66,11 @@ const getInstanceConfig = async ({ store }) => {
|
|||
const textlimit = data.max_toot_chars
|
||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaExtensionsAvailable', value: data.pleroma })
|
||||
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 })
|
||||
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 })
|
||||
|
|
@ -78,6 +82,8 @@ const getInstanceConfig = async ({ store }) => {
|
|||
console.error('Could not load instance config, potentially fatal')
|
||||
console.error(error)
|
||||
}
|
||||
// We should check for scrobbles support here but it requires userId
|
||||
// so instead we check for it where it's fetched (statuses.js)
|
||||
}
|
||||
|
||||
const getBackendProvidedConfig = async () => {
|
||||
|
|
@ -153,7 +159,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
: config.logoMargin
|
||||
})
|
||||
copyInstanceOption('logoLeft')
|
||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||
useAuthFlowStore().setInitialStrategy(config.loginMethod)
|
||||
|
||||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
|
|
@ -242,7 +248,8 @@ const resolveStaffAccounts = ({ store, accounts }) => {
|
|||
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
try {
|
||||
const res = await preloadFetch('/nodeinfo/2.1.json')
|
||||
let res = await preloadFetch('/nodeinfo/2.1.json')
|
||||
if (!res.ok) res = await preloadFetch('/nodeinfo/2.0.json')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const metadata = data.metadata
|
||||
|
|
@ -253,7 +260,12 @@ 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: 'pleromaCustomEmojiReactionsAvailable',
|
||||
value:
|
||||
features.includes('pleroma_custom_emoji_reactions') ||
|
||||
features.includes('custom_emoji_reactions')
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
|
|
@ -262,6 +274,8 @@ const getNodeInfo = async ({ store }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
|
||||
store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
|
||||
store.dispatch('setInstanceOption', { name: 'blockExpiration', value: features.includes('pleroma:block_expiration') })
|
||||
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances ?? [] })
|
||||
|
||||
const uploadLimits = metadata.uploadLimits
|
||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||
|
|
@ -280,7 +294,6 @@ const getNodeInfo = async ({ store }) => {
|
|||
const software = data.software
|
||||
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
|
||||
store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
|
||||
|
||||
const priv = metadata.private
|
||||
store.dispatch('setInstanceOption', { name: 'private', value: priv })
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import PublicTimeline from 'components/public_timeline/public_timeline.vue'
|
||||
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
|
||||
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
||||
|
|
@ -54,6 +55,7 @@ export default (store) => {
|
|||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||
{ name: 'bubble', path: '/bubble', component: BubbleTimeline },
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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 UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisV
|
||||
|
|
@ -27,15 +28,10 @@ const AccountActions = {
|
|||
ProgressButton,
|
||||
Popover,
|
||||
UserListMenu,
|
||||
ConfirmModal
|
||||
ConfirmModal,
|
||||
UserTimedFilterModal
|
||||
},
|
||||
methods: {
|
||||
showConfirmBlock () {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
hideConfirmBlock () {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showConfirmRemoveUserFromFollowers () {
|
||||
this.showingConfirmRemoveFollower = true
|
||||
},
|
||||
|
|
@ -49,10 +45,14 @@ const AccountActions = {
|
|||
this.$store.dispatch('hideReblogs', this.user.id)
|
||||
},
|
||||
blockUser () {
|
||||
if (this.$refs.timedBlockDialog) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
} else {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
this.showConfirmBlock()
|
||||
this.showingConfirmBlock = true
|
||||
}
|
||||
}
|
||||
},
|
||||
doBlockUser () {
|
||||
|
|
@ -91,6 +91,7 @@ const AccountActions = {
|
|||
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
|
||||
},
|
||||
...mapState({
|
||||
blockExpirationSupported: state => state.instance.blockExpiration,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
<Popover
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template #content>
|
||||
|
|
@ -96,7 +95,8 @@
|
|||
</Popover>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
v-if="showingConfirmBlock"
|
||||
v-if="showingConfirmBlock && !blockExpirationSupported"
|
||||
ref="blockDialog"
|
||||
: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')"
|
||||
|
|
@ -137,6 +137,12 @@
|
|||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
<UserTimedFilterModal
|
||||
v-if="blockExpirationSupported"
|
||||
ref="timedBlockDialog"
|
||||
:is-mute="false"
|
||||
:user="user"
|
||||
/>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
.post-textarea {
|
||||
resize: vertical;
|
||||
height: 10em;
|
||||
overflow: none;
|
||||
overflow: visible;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,9 +107,9 @@
|
|||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
font-size: 4.5em;
|
||||
top: calc(50% - 2.25rem);
|
||||
left: calc(50% - 2.25rem);
|
||||
color: rgb(255 255 255 / 75%);
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 40%);
|
||||
|
||||
|
|
@ -177,7 +177,8 @@
|
|||
.text {
|
||||
flex: 2;
|
||||
margin: 8px;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
export default {
|
||||
name: 'Attachment',
|
||||
selector: '.Attachment',
|
||||
notEditable: true,
|
||||
validInnerComponents: [
|
||||
'Border',
|
||||
'Button',
|
||||
'Input'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
roundness: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Button',
|
||||
parent: {
|
||||
component: 'Attachment'
|
||||
},
|
||||
directives: {
|
||||
background: '#FFFFFF',
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
<FAIcon icon="trash-alt" />
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
>
|
||||
<button
|
||||
v-if="type === 'flash' && flashLoaded"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.attachment_stop_flash')"
|
||||
@click.prevent="stopFlash"
|
||||
>
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
>
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="!useModal && type !== 'unknown'"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.show_attachment_in_modal')"
|
||||
@click.prevent="openModalForce"
|
||||
>
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="nsfw && hideNsfwLocal"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.hide_attachment')"
|
||||
@click.prevent="toggleHidden"
|
||||
>
|
||||
|
|
@ -113,7 +113,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftUp"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.move_up')"
|
||||
@click.prevent="onShiftUp"
|
||||
>
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftDn"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.move_down')"
|
||||
@click.prevent="onShiftDn"
|
||||
>
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.remove_attachment')"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { h, resolveComponent } from 'vue'
|
|||
import LoginForm from '../login_form/login_form.vue'
|
||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
|
|
@ -15,7 +16,7 @@ const AuthForm = {
|
|||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||
...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery'])
|
||||
},
|
||||
components: {
|
||||
MFARecoveryForm,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
|
||||
const BlockCard = {
|
||||
props: ['userId'],
|
||||
data () {
|
||||
return {
|
||||
progress: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
|
|
@ -16,23 +13,32 @@ const BlockCard = {
|
|||
},
|
||||
blocked () {
|
||||
return this.relationship.blocking
|
||||
}
|
||||
},
|
||||
blockExpiryAvailable () {
|
||||
return this.user.block_expires_at !== undefined
|
||||
},
|
||||
blockExpiry () {
|
||||
return this.user.block_expires_at == null
|
||||
? this.$t('user_card.block_expires_forever')
|
||||
: this.$t('user_card.block_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
|
||||
},
|
||||
...mapState({
|
||||
blockExpirationSupported: state => state.instance.blockExpiration,
|
||||
})
|
||||
},
|
||||
components: {
|
||||
BasicUserCard
|
||||
},
|
||||
methods: {
|
||||
unblockUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('unblockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
blockUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('blockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
if (this.blockExpirationSupported) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
} else {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,35 @@
|
|||
<template>
|
||||
<basic-user-card :user="user">
|
||||
<div class="block-card-content-container">
|
||||
<span
|
||||
v-if="blocked && blockExpiryAvailable"
|
||||
class="alert neutral"
|
||||
>
|
||||
{{ blockExpiry }}
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
v-if="blocked"
|
||||
class="btn button-default"
|
||||
:disabled="progress"
|
||||
@click="unblockUser"
|
||||
>
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.unblock_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.unblock') }}
|
||||
</template>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn button-default"
|
||||
:disabled="progress"
|
||||
@click="blockUser"
|
||||
>
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.block_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.block') }}
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<UserTimedFilterModal
|
||||
ref="timedBlockDialog"
|
||||
:user="user"
|
||||
:is-mute="false"
|
||||
/>
|
||||
</teleport>
|
||||
</basic-user-card>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
18
src/components/bubble_timeline/bubble_timeline.js
Normal file
18
src/components/bubble_timeline/bubble_timeline.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
const BubbleTimeline = {
|
||||
components: {
|
||||
Timeline
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.bubble }
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||
},
|
||||
unmounted () {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BubbleTimeline
|
||||
9
src/components/bubble_timeline/bubble_timeline.vue
Normal file
9
src/components/bubble_timeline/bubble_timeline.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<Timeline
|
||||
:title="$t('nav.bubble')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'bubble'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./bubble_timeline.js"></script>
|
||||
|
|
@ -10,7 +10,7 @@ export default {
|
|||
// normal: '' // normal state is implicitly added, it is always included
|
||||
toggled: '.toggled',
|
||||
focused: ':focus-within',
|
||||
pressed: ':focus:active',
|
||||
pressed: ':active',
|
||||
hover: ':is(:hover, :focus-visible):not(:disabled)',
|
||||
disabled: ':disabled'
|
||||
},
|
||||
|
|
@ -18,7 +18,8 @@ export default {
|
|||
variants: {
|
||||
// Variants save on computation time since adding new variant just adds one more "set".
|
||||
// normal: '', // you can override normal variant, it will be appenended to the main class
|
||||
danger: '.danger'
|
||||
danger: '.-danger',
|
||||
transparent: '.-transparent'
|
||||
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
|
||||
// This (currently) is further multipled by number of places where component can exist.
|
||||
},
|
||||
|
|
@ -51,6 +52,38 @@ export default {
|
|||
roundness: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'danger',
|
||||
directives: {
|
||||
background: '--cRed'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'transparent',
|
||||
directives: {
|
||||
opacity: 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
variant: 'transparent'
|
||||
},
|
||||
directives: {
|
||||
textColor: '--text'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
variant: 'transparent'
|
||||
},
|
||||
directives: {
|
||||
textColor: '--text'
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.heading {
|
||||
|
|
|
|||
|
|
@ -107,8 +107,7 @@
|
|||
.outgoing {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: end;
|
||||
justify-content: flex-end;
|
||||
place-content: end flex-end;
|
||||
|
||||
.chat-message-inner {
|
||||
align-items: flex-end;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ export default {
|
|||
'Text',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'RichContent',
|
||||
'Attachment',
|
||||
'PollGraph'
|
||||
],
|
||||
defaultRules: [
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
:title="'@'+(user && user.screen_name_ui)"
|
||||
:html="htmlTitle"
|
||||
:emoji="user.emoji || []"
|
||||
:is-local="user.is_local"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -39,7 +40,6 @@
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: inline;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@
|
|||
/>
|
||||
<div
|
||||
class="input color-input-field"
|
||||
:class="{ disabled: !present || disabled }"
|
||||
:class="{ disabled: !present || disabled, unstyled }"
|
||||
>
|
||||
<input
|
||||
:id="name + '-t'"
|
||||
class="textColor unstyled"
|
||||
:class="{ disabled: !present || disabled }"
|
||||
type="text"
|
||||
:value="modelValue || fallback"
|
||||
:value="modelValue ?? fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="updateValue($event.target.value)"
|
||||
>
|
||||
|
|
@ -92,6 +92,11 @@ export default {
|
|||
required: true,
|
||||
type: String
|
||||
},
|
||||
// use unstyled, uh, style
|
||||
unstyled: {
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
// Color value, should be required but vue cannot tell the difference
|
||||
// between "property missing" and "property set to undefined"
|
||||
modelValue: {
|
||||
|
|
|
|||
82
src/components/component_preview/component_preview.js
Normal file
82
src/components/component_preview/component_preview.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
|
||||
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
|
||||
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
ColorInput
|
||||
},
|
||||
props: [
|
||||
'shadow',
|
||||
'shadowControl',
|
||||
'previewClass',
|
||||
'previewStyle',
|
||||
'previewCss',
|
||||
'disabled',
|
||||
'invalid',
|
||||
'noColorControl'
|
||||
],
|
||||
emits: ['update:shadow'],
|
||||
data () {
|
||||
return {
|
||||
colorOverride: undefined,
|
||||
lightGrid: false,
|
||||
zoom: 100,
|
||||
randomSeed: genRandomSeed()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.update()
|
||||
},
|
||||
computed: {
|
||||
hideControls () {
|
||||
return typeof this.shadow === 'string'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
previewCss () {
|
||||
this.update()
|
||||
},
|
||||
previewStyle () {
|
||||
this.update()
|
||||
},
|
||||
zoom () {
|
||||
this.update()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProperty (axis, value) {
|
||||
this.$emit('update:shadow', { axis, value: Number(value) })
|
||||
},
|
||||
update () {
|
||||
const sheet = createStyleSheet('style-component-preview', 90)
|
||||
|
||||
sheet.clear()
|
||||
|
||||
const result = [this.previewCss]
|
||||
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
|
||||
|
||||
const styleRule = [
|
||||
'#component-preview-', this.randomSeed, ' {\n',
|
||||
'.preview-block {\n',
|
||||
`zoom: ${this.zoom / 100};`,
|
||||
this.previewStyle,
|
||||
'\n}',
|
||||
'\n}'
|
||||
].join('')
|
||||
|
||||
sheet.addRule(styleRule)
|
||||
sheet.addRule([
|
||||
'#component-preview-', this.randomSeed, ' {\n',
|
||||
...result,
|
||||
'\n}'
|
||||
].join(''))
|
||||
|
||||
sheet.ready = true
|
||||
adoptStyleSheets()
|
||||
}
|
||||
}
|
||||
}
|
||||
151
src/components/component_preview/component_preview.scss
Normal file
151
src/components/component_preview/component_preview.scss
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
.ComponentPreview {
|
||||
display: grid;
|
||||
grid-template-columns: 1em 1fr 1fr 1em;
|
||||
grid-template-rows: 2em 1fr 1fr 1fr 1em 2em max-content;
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"x-slide x-slide x-slide . "
|
||||
"x-num x-num y-num y-num "
|
||||
"assists assists assists assists";
|
||||
grid-gap: 0.5em;
|
||||
|
||||
&:not(.-shadow-controls) {
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"assists assists assists assists";
|
||||
grid-template-rows: 2em 1fr 1fr 1fr max-content;
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-area: header;
|
||||
place-self: baseline center;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.invalid-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center center;
|
||||
background-color: rgb(100 0 0 / 50%);
|
||||
|
||||
.alert {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.assists {
|
||||
grid-area: assists;
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: 2em;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
||||
.input-light-grid {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.input-number {
|
||||
min-width: 2em;
|
||||
}
|
||||
|
||||
.x-shift-number {
|
||||
grid-area: x-num;
|
||||
justify-self: right;
|
||||
}
|
||||
|
||||
.y-shift-number {
|
||||
grid-area: y-num;
|
||||
justify-self: left;
|
||||
}
|
||||
|
||||
.x-shift-number,
|
||||
.y-shift-number {
|
||||
input {
|
||||
max-width: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.x-shift-slider {
|
||||
grid-area: x-slide;
|
||||
height: auto;
|
||||
align-self: start;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.y-shift-slider {
|
||||
grid-area: y-slide;
|
||||
writing-mode: vertical-lr;
|
||||
justify-self: left;
|
||||
min-height: 10em;
|
||||
}
|
||||
|
||||
.x-shift-slider,
|
||||
.y-shift-slider {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.preview-window {
|
||||
--__grid-color1: rgb(102 102 102);
|
||||
--__grid-color2: rgb(153 153 153);
|
||||
--__grid-color1-disabled: rgb(102 102 102 / 20%);
|
||||
--__grid-color2-disabled: rgb(153 153 153 / 20%);
|
||||
|
||||
&.-light-grid {
|
||||
--__grid-color1: rgb(205 205 205);
|
||||
--__grid-color2: rgb(255 255 255);
|
||||
--__grid-color1-disabled: rgb(205 205 205 / 20%);
|
||||
--__grid-color2-disabled: rgb(255 255 255 / 20%);
|
||||
}
|
||||
|
||||
position: relative;
|
||||
grid-area: preview;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 10em;
|
||||
min-height: 10em;
|
||||
background-color: var(--__grid-color2);
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--__grid-color1) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--__grid-color1) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--__grid-color1) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--__grid-color1) 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
|
||||
border-radius: var(--roundness);
|
||||
|
||||
&.disabled {
|
||||
background-color: var(--__grid-color2-disabled);
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--__grid-color1-disabled) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--__grid-color1-disabled) 75%);
|
||||
}
|
||||
|
||||
.preview-block {
|
||||
background: var(--background, var(--bg));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 33%;
|
||||
min-height: 33%;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: var(--border);
|
||||
border-radius: var(--roundness);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
<template>
|
||||
<div
|
||||
:id="'component-preview-' + randomSeed"
|
||||
class="ComponentPreview"
|
||||
:class="{ '-shadow-controls': shadowControl }"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="previewCss"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<label
|
||||
v-show="shadowControl"
|
||||
role="heading"
|
||||
|
|
@ -74,7 +69,6 @@
|
|||
<div
|
||||
class="preview-block"
|
||||
:class="previewClass"
|
||||
:style="style"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.test_string') }}
|
||||
</div>
|
||||
|
|
@ -116,208 +110,5 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
ColorInput
|
||||
},
|
||||
props: [
|
||||
'shadow',
|
||||
'shadowControl',
|
||||
'previewClass',
|
||||
'previewStyle',
|
||||
'previewCss',
|
||||
'disabled',
|
||||
'invalid',
|
||||
'noColorControl'
|
||||
],
|
||||
emits: ['update:shadow'],
|
||||
data () {
|
||||
return {
|
||||
colorOverride: undefined,
|
||||
lightGrid: false,
|
||||
zoom: 100
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style () {
|
||||
const result = [
|
||||
this.previewStyle,
|
||||
`zoom: ${this.zoom / 100}`
|
||||
]
|
||||
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
|
||||
return result
|
||||
},
|
||||
hideControls () {
|
||||
return typeof this.shadow === 'string'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProperty (axis, value) {
|
||||
this.$emit('update:shadow', { axis, value: Number(value) })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.ComponentPreview {
|
||||
display: grid;
|
||||
grid-template-columns: 1em 1fr 1fr 1em;
|
||||
grid-template-rows: 2em 1fr 1fr 1fr 1em 2em max-content;
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"x-slide x-slide x-slide . "
|
||||
"x-num x-num y-num y-num "
|
||||
"assists assists assists assists";
|
||||
grid-gap: 0.5em;
|
||||
|
||||
&:not(.-shadow-controls) {
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"assists assists assists assists";
|
||||
grid-template-rows: 2em 1fr 1fr 1fr max-content;
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-area: header;
|
||||
justify-self: center;
|
||||
align-self: baseline;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.invalid-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
background-color: rgba(100 0 0 / 50%);
|
||||
|
||||
.alert {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.assists {
|
||||
grid-area: assists;
|
||||
display: grid;
|
||||
grid-auto-flow: rows;
|
||||
grid-auto-rows: 2em;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
||||
.input-light-grid {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.input-number {
|
||||
min-width: 2em;
|
||||
}
|
||||
|
||||
.x-shift-number {
|
||||
grid-area: x-num;
|
||||
justify-self: right;
|
||||
}
|
||||
|
||||
.y-shift-number {
|
||||
grid-area: y-num;
|
||||
justify-self: left;
|
||||
}
|
||||
|
||||
.x-shift-number,
|
||||
.y-shift-number {
|
||||
input {
|
||||
max-width: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.x-shift-slider {
|
||||
grid-area: x-slide;
|
||||
height: auto;
|
||||
align-self: start;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.y-shift-slider {
|
||||
grid-area: y-slide;
|
||||
writing-mode: vertical-lr;
|
||||
justify-self: left;
|
||||
min-height: 10em;
|
||||
}
|
||||
|
||||
.x-shift-slider,
|
||||
.y-shift-slider {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.preview-window {
|
||||
--__grid-color1: rgb(102 102 102);
|
||||
--__grid-color2: rgb(153 153 153);
|
||||
--__grid-color1-disabled: rgba(102 102 102 / 20%);
|
||||
--__grid-color2-disabled: rgba(153 153 153 / 20%);
|
||||
|
||||
&.-light-grid {
|
||||
--__grid-color1: rgb(205 205 205);
|
||||
--__grid-color2: rgb(255 255 255);
|
||||
--__grid-color1-disabled: rgba(205 205 205 / 20%);
|
||||
--__grid-color2-disabled: rgba(255 255 255 / 20%);
|
||||
}
|
||||
|
||||
position: relative;
|
||||
grid-area: preview;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 10em;
|
||||
min-height: 10em;
|
||||
background-color: var(--__grid-color2);
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--__grid-color1) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--__grid-color1) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--__grid-color1) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--__grid-color1) 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
|
||||
border-radius: var(--roundness);
|
||||
|
||||
&.disabled {
|
||||
background-color: var(--__grid-color2-disabled);
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--__grid-color1-disabled) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--__grid-color1-disabled) 75%);
|
||||
}
|
||||
|
||||
.preview-block {
|
||||
background: var(--background, var(--bg));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 33%;
|
||||
min-height: 33%;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: var(--border);
|
||||
border-radius: var(--roundness);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="./component_preview.js" />
|
||||
<style src="./component_preview.scss" lang="scss" />
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<slot />
|
||||
|
||||
<template #footer>
|
||||
<slot name="footerLeft" />
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click.prevent="onAccept"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { unitToSeconds } from 'src/services/date_utils/date_utils.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import ConfirmModal from './confirm_modal.vue'
|
||||
|
|
@ -8,21 +7,13 @@ export default {
|
|||
props: ['type', 'user', 'status'],
|
||||
emits: ['hide', 'show', 'muted'],
|
||||
data: () => ({
|
||||
showing: false,
|
||||
muteExpiryAmount: 2,
|
||||
muteExpiryUnit: 'hours'
|
||||
showing: false
|
||||
}),
|
||||
components: {
|
||||
ConfirmModal,
|
||||
Select
|
||||
},
|
||||
computed: {
|
||||
muteExpiryValue () {
|
||||
unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount)
|
||||
},
|
||||
muteExpiryUnits () {
|
||||
return ['minutes', 'hours', 'days']
|
||||
},
|
||||
domain () {
|
||||
return this.user.fqn.split('@')[1]
|
||||
},
|
||||
|
|
@ -31,13 +22,8 @@ export default {
|
|||
return 'status.mute_domain_confirm'
|
||||
} else if (this.type === 'conversation') {
|
||||
return 'status.mute_conversation_confirm'
|
||||
} else {
|
||||
return 'user_card.mute_confirm'
|
||||
}
|
||||
},
|
||||
userIsMuted () {
|
||||
return this.$store.getters.relationship(this.user.id).muting
|
||||
},
|
||||
conversationIsMuted () {
|
||||
return this.status.conversation_muted
|
||||
},
|
||||
|
|
@ -49,12 +35,9 @@ export default {
|
|||
case 'domain': {
|
||||
return this.mergedConfig.modalOnMuteDomain
|
||||
}
|
||||
case 'conversation': {
|
||||
default: { // conversation
|
||||
return this.mergedConfig.modalOnMuteConversation
|
||||
}
|
||||
default: {
|
||||
return this.mergedConfig.modalOnMute
|
||||
}
|
||||
}
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
|
|
@ -79,7 +62,7 @@ export default {
|
|||
switch (this.type) {
|
||||
case 'domain': {
|
||||
if (!this.domainIsMuted) {
|
||||
this.$store.dispatch('muteDomain', { id: this.domain, expiresIn: this.muteExpiryValue })
|
||||
this.$store.dispatch('muteDomain', { id: this.domain })
|
||||
} else {
|
||||
this.$store.dispatch('unmuteDomain', { id: this.domain })
|
||||
}
|
||||
|
|
@ -87,20 +70,12 @@ export default {
|
|||
}
|
||||
case 'conversation': {
|
||||
if (!this.conversationIsMuted) {
|
||||
this.$store.dispatch('muteConversation', { id: this.status.id, expiresIn: this.muteExpiryValue })
|
||||
this.$store.dispatch('muteConversation', { id: this.status.id })
|
||||
} else {
|
||||
this.$store.dispatch('unmuteConversation', { id: this.status.id })
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (!this.userIsMuted) {
|
||||
this.$store.dispatch('muteUser', { id: this.user.id, expiresIn: this.muteExpiryValue })
|
||||
} else {
|
||||
this.$store.dispatch('unmuteUser', { id: this.user.id })
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
this.$emit('muted')
|
||||
this.hide()
|
||||
|
|
|
|||
|
|
@ -18,36 +18,6 @@
|
|||
<span v-text="user.screen_name_ui" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
<div
|
||||
v-if="type !== 'domain'"
|
||||
class="mute-expiry"
|
||||
>
|
||||
<p>
|
||||
<label>
|
||||
{{ $t('user_card.mute_duration_prompt') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="muteExpiryAmount"
|
||||
type="number"
|
||||
class="input expiry-amount hide-number-spinner"
|
||||
:min="0"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<Select
|
||||
v-model="muteExpiryUnit"
|
||||
unstyled="true"
|
||||
class="expiry-unit"
|
||||
>
|
||||
<option
|
||||
v-for="unit in muteExpiryUnits"
|
||||
:key="unit"
|
||||
:value="unit"
|
||||
>
|
||||
{{ $t(`time.unit.${unit}_short`, ['']) }}
|
||||
</option>
|
||||
</Select>
|
||||
</p>
|
||||
</div>
|
||||
</confirm-modal>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -339,11 +339,6 @@ const conversation = {
|
|||
canDive () {
|
||||
return this.isTreeView && this.isExpanded
|
||||
},
|
||||
focused () {
|
||||
return (id) => {
|
||||
return (this.isExpanded) && id === this.highlight
|
||||
}
|
||||
},
|
||||
maybeHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
|
|
@ -406,6 +401,9 @@ const conversation = {
|
|||
})
|
||||
}
|
||||
},
|
||||
isFocused (id) {
|
||||
return (this.isExpanded) && id === this.highlight
|
||||
},
|
||||
getReplies (id) {
|
||||
return this.replies[id] || []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:focused="focused(status.id)"
|
||||
:focused="isFocused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
:replies="getReplies(status.id)"
|
||||
|
|
@ -168,7 +168,7 @@
|
|||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
:focused="focused"
|
||||
:is-focused-function="isFocused"
|
||||
:get-replies="getReplies"
|
||||
:highlight="maybeHighlight"
|
||||
:set-highlight="setHighlight"
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:focused="focused(status.id)"
|
||||
:focused="isFocused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
:replies="getReplies(status.id)"
|
||||
|
|
@ -322,10 +322,7 @@
|
|||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: calc(var(--___margin) * -1);
|
||||
bottom: calc(var(--___margin) * -1);
|
||||
left: calc(var(--___margin) * -1);
|
||||
right: calc(var(--___margin) * -1);
|
||||
inset: calc(var(--___margin) * -1);
|
||||
background: var(--background);
|
||||
backdrop-filter: var(--__panel-backdrop-filter);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
transition-timing-function: ease-out;
|
||||
transition-duration: 100ms;
|
||||
|
||||
@media all and (min-width: 800px) {
|
||||
@media all and (width >= 800px) {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
|
@ -70,10 +70,7 @@
|
|||
mask-size: contain;
|
||||
background-color: var(--text);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
|
|
|
|||
|
|
@ -29,14 +29,11 @@
|
|||
// TODO: unify with other modals.
|
||||
.dark-overlay {
|
||||
&::before {
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
content: " ";
|
||||
display: block;
|
||||
cursor: default;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background: rgb(27 31 35 / 50%);
|
||||
z-index: 2000;
|
||||
}
|
||||
|
|
@ -45,17 +42,13 @@
|
|||
.dialog-container {
|
||||
display: grid;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
place-items: center center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dialog-modal.panel {
|
||||
max-height: 80vh;
|
||||
max-width: 90vw;
|
||||
z-index: 2001;
|
||||
cursor: default;
|
||||
|
|
@ -98,8 +91,7 @@
|
|||
#modal.-mobile {
|
||||
.dialog-container {
|
||||
justify-content: stretch;
|
||||
align-items: end;
|
||||
justify-items: stretch;
|
||||
place-items: end stretch;
|
||||
|
||||
&.-center-mobile {
|
||||
align-items: center;
|
||||
|
|
@ -114,7 +106,6 @@
|
|||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-columns: none;
|
||||
grid-auto-rows: auto;
|
||||
grid-auto-flow: row dense;
|
||||
|
||||
|
|
|
|||
|
|
@ -127,7 +127,6 @@
|
|||
max-width: 100%;
|
||||
|
||||
p {
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
|
@ -135,8 +134,7 @@
|
|||
.poll-indicator-container {
|
||||
border-radius: var(--roundness);
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
place-items: center center;
|
||||
align-self: start;
|
||||
height: 0;
|
||||
padding-bottom: 62.5%;
|
||||
|
|
@ -147,13 +145,9 @@
|
|||
box-sizing: border-box;
|
||||
border: 1px solid var(--border);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
place-items: center center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,12 +205,6 @@ const EmojiInput = {
|
|||
return emoji.displayText
|
||||
}
|
||||
},
|
||||
onInputScroll () {
|
||||
this.$refs.hiddenOverlay.scrollTo({
|
||||
top: this.input.scrollTop,
|
||||
left: this.input.scrollLeft
|
||||
})
|
||||
},
|
||||
suggestionListId () {
|
||||
return `suggestions-${this.randomSeed}`
|
||||
},
|
||||
|
|
@ -239,7 +233,6 @@ const EmojiInput = {
|
|||
this.overlayStyle.fontSize = style.fontSize
|
||||
this.overlayStyle.wordWrap = style.wordWrap
|
||||
this.overlayStyle.whiteSpace = style.whiteSpace
|
||||
this.resize()
|
||||
input.addEventListener('blur', this.onBlur)
|
||||
input.addEventListener('focus', this.onFocus)
|
||||
input.addEventListener('paste', this.onPaste)
|
||||
|
|
@ -302,6 +295,13 @@ const EmojiInput = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
onInputScroll (e) {
|
||||
this.$refs.hiddenOverlay.scrollTo({
|
||||
top: this.input.scrollTop,
|
||||
left: this.input.scrollLeft
|
||||
})
|
||||
this.setCaret(e)
|
||||
},
|
||||
triggerShowPicker () {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.picker.showPicker()
|
||||
|
|
@ -561,8 +561,6 @@ const EmojiInput = {
|
|||
this.$refs.suggestorPopover.updateStyles()
|
||||
})
|
||||
},
|
||||
resize () {
|
||||
},
|
||||
autoCompleteItemLabel (suggestion) {
|
||||
if (suggestion.user) {
|
||||
return suggestion.displayText + ' ' + suggestion.detailText
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
margin: 0.2em 0.25em;
|
||||
font-size: 1.3em;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
line-height: 1.2em;
|
||||
|
||||
&:hover i {
|
||||
color: var(--text);
|
||||
|
|
@ -133,7 +133,7 @@
|
|||
.emoji-picker-panel {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
margin-top: 2px;
|
||||
margin-top: 0.2em;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
|
|
@ -152,17 +152,14 @@
|
|||
}
|
||||
|
||||
&.with-picker input {
|
||||
padding-right: 30px;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
.hidden-overlay {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
|
||||
/* DEBUG STUFF */
|
||||
|
|
@ -218,8 +215,8 @@
|
|||
}
|
||||
|
||||
.detailText {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-size: 0.6em;
|
||||
line-height: 0.6em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,13 +140,13 @@ const EmojiPicker = {
|
|||
},
|
||||
updateEmojiSize () {
|
||||
const css = window.getComputedStyle(this.$refs.popover.$el)
|
||||
const fontSize = css.getPropertyValue('font-size') || '14px'
|
||||
const fontSize = css.getPropertyValue('font-size') || '1rem'
|
||||
const emojiSize = css.getPropertyValue('--emojiSize') || '2.2rem'
|
||||
|
||||
const fontSizeUnit = fontSize.replace(/[0-9,.]+/, '')
|
||||
const fontSizeUnit = fontSize.replace(/[0-9,.]+/, '').trim()
|
||||
const fontSizeValue = Number(fontSize.replace(/[^0-9,.]+/, ''))
|
||||
|
||||
const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '')
|
||||
const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '').trim()
|
||||
const emojiSizeValue = Number(emojiSize.replace(/[^0-9,.]+/, ''))
|
||||
|
||||
let fontSizeMultiplier
|
||||
|
|
|
|||
|
|
@ -64,8 +64,7 @@
|
|||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overflow: auto hidden;
|
||||
}
|
||||
|
||||
.additional-tabs {
|
||||
|
|
@ -153,7 +152,15 @@
|
|||
transition: mask-size 150ms;
|
||||
mask-size: 100% 20px, 100% 20px, auto;
|
||||
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||
/* stylelint-disable mask-composite */
|
||||
/* stylelint-disable declaration-property-value-no-unknown */
|
||||
/* stylelint-disable scss/declaration-property-value-no-unknown */
|
||||
|
||||
/* TODO check if this is still needed */
|
||||
mask-composite: xor;
|
||||
/* stylelint-enable declaration-property-value-no-unknown */
|
||||
/* stylelint-enable scss/declaration-property-value-no-unknown */
|
||||
/* stylelint-enable mask-composite */
|
||||
mask-composite: exclude;
|
||||
|
||||
&.scrolled {
|
||||
|
|
@ -197,8 +204,7 @@
|
|||
&-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--__amount), 1fr);
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
place-items: center center;
|
||||
justify-content: center;
|
||||
grid-template-rows: repeat(1, auto);
|
||||
|
||||
|
|
|
|||
|
|
@ -72,12 +72,11 @@
|
|||
flex: 1 1 0;
|
||||
line-height: 1.2;
|
||||
white-space: normal;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
visibility: "hidden";
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -101,10 +101,7 @@
|
|||
|
||||
.gallery-row-inner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: stretch;
|
||||
|
|
@ -132,7 +129,7 @@
|
|||
|
||||
.gallery-item {
|
||||
margin: 0;
|
||||
height: 200px;
|
||||
height: 15em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -160,7 +157,15 @@
|
|||
linear-gradient(to top, white, white);
|
||||
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
/* stylelint-disable mask-composite */
|
||||
/* stylelint-disable declaration-property-value-no-unknown */
|
||||
/* stylelint-disable scss/declaration-property-value-no-unknown */
|
||||
|
||||
/* TODO check if this is still needed */
|
||||
mask-composite: xor;
|
||||
/* stylelint-enable scss/declaration-property-value-no-unknown */
|
||||
/* stylelint-enable declaration-property-value-no-unknown */
|
||||
/* stylelint-enable mask-composite */
|
||||
mask-composite: exclude;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import Cropper from 'cropperjs'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
import 'cropperjs' // This adds all of the cropperjs's components into DOM
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCircleNotch
|
||||
|
|
@ -11,86 +10,47 @@ library.add(
|
|||
|
||||
const ImageCropper = {
|
||||
props: {
|
||||
trigger: {
|
||||
type: [String, window.Element],
|
||||
required: true
|
||||
},
|
||||
submitHandler: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
cropperOptions: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
aspectRatio: 1,
|
||||
autoCropArea: 1,
|
||||
viewMode: 1,
|
||||
movable: false,
|
||||
zoomable: false,
|
||||
guides: false
|
||||
}
|
||||
}
|
||||
},
|
||||
// Mime-types to accept, i.e. which filetypes to accept (.gif, .png, etc.)
|
||||
mimes: {
|
||||
type: String,
|
||||
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
|
||||
},
|
||||
saveButtonLabel: {
|
||||
type: String
|
||||
},
|
||||
saveWithoutCroppingButtonlabel: {
|
||||
type: String
|
||||
},
|
||||
cancelButtonLabel: {
|
||||
type: String
|
||||
// Fixed aspect-ratio for selection box
|
||||
aspectRatio: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
cropper: undefined,
|
||||
dataUrl: undefined,
|
||||
filename: undefined,
|
||||
submitting: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
saveText () {
|
||||
return this.saveButtonLabel || this.$t('image_cropper.save')
|
||||
},
|
||||
saveWithoutCroppingText () {
|
||||
return this.saveWithoutCroppingButtonlabel || this.$t('image_cropper.save_without_cropping')
|
||||
},
|
||||
cancelText () {
|
||||
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
|
||||
filename: undefined
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'submit', // cropping complete or uncropped image returned
|
||||
'close', // cropper is closed
|
||||
],
|
||||
methods: {
|
||||
destroy () {
|
||||
if (this.cropper) {
|
||||
this.cropper.destroy()
|
||||
}
|
||||
this.$refs.input.value = ''
|
||||
this.dataUrl = undefined
|
||||
this.$emit('close')
|
||||
},
|
||||
submit (cropping = true) {
|
||||
this.submitting = true
|
||||
this.submitHandler(cropping && this.cropper, this.file)
|
||||
.then(() => this.destroy())
|
||||
.finally(() => {
|
||||
this.submitting = false
|
||||
let cropperPromise
|
||||
if (cropping) {
|
||||
cropperPromise = this.$refs.cropperSelection.$toCanvas()
|
||||
} else {
|
||||
cropperPromise = Promise.resolve()
|
||||
}
|
||||
|
||||
cropperPromise.then(canvas => {
|
||||
this.$emit('submit', { canvas, file: this.file })
|
||||
})
|
||||
},
|
||||
pickImage () {
|
||||
this.$refs.input.click()
|
||||
},
|
||||
createCropper () {
|
||||
this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
|
||||
},
|
||||
getTriggerDOM () {
|
||||
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
|
||||
},
|
||||
readFile () {
|
||||
const fileInput = this.$refs.input
|
||||
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||
|
|
@ -103,26 +63,37 @@ const ImageCropper = {
|
|||
reader.readAsDataURL(this.file)
|
||||
this.$emit('changed', this.file, reader)
|
||||
}
|
||||
},
|
||||
inSelection(selection, maxSelection) {
|
||||
return (
|
||||
selection.x >= maxSelection.x
|
||||
&& selection.y >= maxSelection.y
|
||||
&& (selection.x + selection.width) <= (maxSelection.x + maxSelection.width)
|
||||
&& (selection.y + selection.height) <= (maxSelection.y + maxSelection.height)
|
||||
)
|
||||
},
|
||||
onCropperSelectionChange(event) {
|
||||
const cropperCanvas = this.$refs.cropperCanvas
|
||||
const cropperCanvasRect = cropperCanvas.getBoundingClientRect()
|
||||
const selection = event.detail
|
||||
const maxSelection = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: cropperCanvasRect.width,
|
||||
height: cropperCanvasRect.height,
|
||||
}
|
||||
|
||||
if (!this.inSelection(selection, maxSelection)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// listen for click event on trigger
|
||||
const trigger = this.getTriggerDOM()
|
||||
if (!trigger) {
|
||||
this.$emit('error', 'No image make trigger found.', 'user')
|
||||
} else {
|
||||
trigger.addEventListener('click', this.pickImage)
|
||||
}
|
||||
// listen for input file changes
|
||||
const fileInput = this.$refs.input
|
||||
fileInput.addEventListener('change', this.readFile)
|
||||
},
|
||||
beforeUnmount: function () {
|
||||
// remove the event listeners
|
||||
const trigger = this.getTriggerDOM()
|
||||
if (trigger) {
|
||||
trigger.removeEventListener('click', this.pickImage)
|
||||
}
|
||||
const fileInput = this.$refs.input
|
||||
fileInput.removeEventListener('change', this.readFile)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,53 @@
|
|||
<template>
|
||||
<div class="image-cropper">
|
||||
<div v-if="dataUrl">
|
||||
<div class="image-cropper-image-container">
|
||||
<img
|
||||
ref="img"
|
||||
:src="dataUrl"
|
||||
alt=""
|
||||
@load.stop="createCropper"
|
||||
<div class="image">
|
||||
<cropper-canvas
|
||||
ref="cropperCanvas"
|
||||
background
|
||||
class="image-cropper-canvas"
|
||||
height="100%"
|
||||
>
|
||||
</div>
|
||||
<div class="image-cropper-buttons-wrapper">
|
||||
<button
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
:disabled="submitting"
|
||||
@click="submit()"
|
||||
v-text="saveText"
|
||||
<cropper-image
|
||||
v-if="dataUrl"
|
||||
ref="cropperImage"
|
||||
:src="dataUrl"
|
||||
alt="Picture"
|
||||
class="image-cropper-image"
|
||||
translatable
|
||||
scalable
|
||||
/>
|
||||
<button
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
:disabled="submitting"
|
||||
@click="destroy"
|
||||
v-text="cancelText"
|
||||
<cropper-shade hidden />
|
||||
<cropper-handle
|
||||
action="select"
|
||||
plain
|
||||
/>
|
||||
<button
|
||||
class="button-default btn"
|
||||
type="button"
|
||||
:disabled="submitting"
|
||||
@click="submit(false)"
|
||||
v-text="saveWithoutCroppingText"
|
||||
<cropper-selection
|
||||
ref="cropperSelection"
|
||||
initial-coverage="0.9"
|
||||
:aspect-ratio="aspectRatio"
|
||||
movable
|
||||
resizable
|
||||
@change="onCropperSelectionChange"
|
||||
>
|
||||
<cropper-grid
|
||||
role="grid"
|
||||
covered
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="submitting"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
<cropper-crosshair centered />
|
||||
<cropper-handle
|
||||
action="move"
|
||||
theme-color="rgba(255, 255, 255, 0.35)"
|
||||
/>
|
||||
</div>
|
||||
<cropper-handle action="n-resize" />
|
||||
<cropper-handle action="e-resize" />
|
||||
<cropper-handle action="s-resize" />
|
||||
<cropper-handle action="w-resize" />
|
||||
<cropper-handle action="ne-resize" />
|
||||
<cropper-handle action="nw-resize" />
|
||||
<cropper-handle action="se-resize" />
|
||||
<cropper-handle action="sw-resize" />
|
||||
</cropper-selection>
|
||||
</cropper-canvas>
|
||||
</div>
|
||||
<input
|
||||
ref="input"
|
||||
|
|
@ -51,24 +62,24 @@
|
|||
|
||||
<style lang="scss">
|
||||
.image-cropper {
|
||||
&-img-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-canvas, .image {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
& &-img-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-image-container {
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-buttons-wrapper {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
|
||||
button {
|
||||
margin-top: 5px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import localeService from '../../services/locale/locale.service.js'
|
||||
|
||||
import Select from '../select/select.vue'
|
||||
import ProfileSettingIndicator from 'src/components/settings_modal/helpers/profile_setting_indicator.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Select,
|
||||
ProfileSettingIndicator
|
||||
},
|
||||
props: {
|
||||
// List of languages (or just one language)
|
||||
modelValue: {
|
||||
type: [Array, String],
|
||||
required: true
|
||||
},
|
||||
// Is this setting stored in user profile (true) or elsewhere (false)
|
||||
// Doesn't affect storage, just shows an icon if true
|
||||
profile: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
languages () {
|
||||
return localeService.languages
|
||||
},
|
||||
|
||||
controlledLanguage: {
|
||||
get: function () {
|
||||
return Array.isArray(this.modelValue) ? this.modelValue : [this.modelValue]
|
||||
},
|
||||
set: function (val) {
|
||||
this.$emit('update:modelValue', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLanguageName (code) {
|
||||
return localeService.getLanguageName(code)
|
||||
},
|
||||
addLanguage () {
|
||||
this.controlledLanguage = [...this.controlledLanguage, '']
|
||||
},
|
||||
setLanguageAt (index, val) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang[index] = val
|
||||
this.controlledLanguage = lang
|
||||
},
|
||||
removeLanguageAt (index) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang.splice(index, 1)
|
||||
this.controlledLanguage = lang
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<div class="interface-language-switcher">
|
||||
<label>
|
||||
{{ promptText }}
|
||||
<slot />
|
||||
<ProfileSettingIndicator :is-profile="profile" />
|
||||
</label>
|
||||
<ul class="setting-list">
|
||||
<li
|
||||
|
|
@ -44,64 +45,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
import Select from '../select/select.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Select
|
||||
},
|
||||
props: {
|
||||
promptText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
language: {
|
||||
type: [Array, String],
|
||||
required: true
|
||||
},
|
||||
setLanguage: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
languages () {
|
||||
return localeService.languages
|
||||
},
|
||||
|
||||
controlledLanguage: {
|
||||
get: function () {
|
||||
return Array.isArray(this.language) ? this.language : [this.language]
|
||||
},
|
||||
set: function (val) {
|
||||
this.setLanguage(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLanguageName (code) {
|
||||
return localeService.getLanguageName(code)
|
||||
},
|
||||
addLanguage () {
|
||||
this.controlledLanguage = [...this.controlledLanguage, '']
|
||||
},
|
||||
setLanguageAt (index, val) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang[index] = val
|
||||
this.controlledLanguage = lang
|
||||
},
|
||||
removeLanguageAt (index) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang.splice(index, 1)
|
||||
this.controlledLanguage = lang
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="./interface_language_switcher.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.interface-language-switcher {
|
||||
|
|
|
|||
|
|
@ -68,10 +68,13 @@
|
|||
margin: 0.5em 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
line-height: 1.2em;
|
||||
// cap description at 3 lines, the 1px is to clean up some stray pixels
|
||||
// TODO: fancier fade-out at the bottom to show off that it's too long?
|
||||
|
||||
/* cap description at 3 lines, the 1px is to clean up some stray pixels
|
||||
TODO: fancier fade-out at the bottom to show off that it's too long?
|
||||
*/
|
||||
max-height: calc(1.2em * 3 - 1px);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
export default {
|
||||
name: 'ListItem',
|
||||
selector: '.list-item',
|
||||
states: {
|
||||
active: '.-active',
|
||||
hover: ':is(:hover, :focus-visible, :has(:focus-visible)):not(.-non-interactive)'
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'RichContent',
|
||||
'Input',
|
||||
'Avatar'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--bg',
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['active'],
|
||||
directives: {
|
||||
background: '--inheritedBackground, 10',
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
background: '--inheritedBackground, 10',
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['hover', 'active'],
|
||||
directives: {
|
||||
background: '--inheritedBackground, 20',
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { mapStores } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapStores, mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import oauthApi from '../../services/new_api/oauth.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
|
|
@ -25,13 +26,10 @@ const LoginForm = {
|
|||
instance: state => state.instance,
|
||||
loggingIn: state => state.users.loggingIn,
|
||||
}),
|
||||
...mapGetters(
|
||||
'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA']
|
||||
)
|
||||
...mapPiniaState(useAuthFlowStore, ['requiredPassword', 'requiredToken', 'requiredMFA'])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
...mapActions(useAuthFlowStore, ['requireMFA', 'login']),
|
||||
submit () {
|
||||
this.isTokenAuth ? this.submitToken() : this.submitPassword()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -168,9 +168,10 @@ $modal-view-button-icon-margin: 0.5em;
|
|||
flex: 0 0 auto;
|
||||
overflow-y: auto;
|
||||
min-height: 1em;
|
||||
max-width: 500px;
|
||||
max-width: 35.8em;
|
||||
max-height: 9.5em;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
& .new,
|
||||
& .original {
|
||||
display: inline;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.mention-avatar {
|
||||
|
|
@ -27,7 +26,6 @@
|
|||
top: 100%;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 1;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
.MentionsLine {
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
|
||||
.mention-link:not(:first-child)::before {
|
||||
content: " ";
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@ export default {
|
|||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Input',
|
||||
'Border',
|
||||
'ButtonUnstyled',
|
||||
'Badge',
|
||||
'Avatar'
|
||||
'Border'
|
||||
],
|
||||
states: {
|
||||
hover: ':is(:hover, :focus-visible, :has(:focus-visible)):not(.disabled)',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { mapStores } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapStores, mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
|
|
@ -17,8 +18,8 @@ export default {
|
|||
error: false
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters({
|
||||
authSettings: 'authFlow/settings'
|
||||
...mapPiniaState(useAuthFlowStore, {
|
||||
authSettings: store => store.settings
|
||||
}),
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
|
|
@ -26,8 +27,7 @@ export default {
|
|||
})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireTOTP', 'abortMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
...mapActions(useAuthFlowStore, ['requireTOTP', 'abortMFA', 'login']),
|
||||
clearError () { this.error = false },
|
||||
|
||||
focusOnCodeInput () {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { mapStores } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapStores, mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
|
|
@ -17,8 +18,8 @@ export default {
|
|||
error: false
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters({
|
||||
authSettings: 'authFlow/settings'
|
||||
...mapPiniaState(useAuthFlowStore, {
|
||||
authSettings: store => store.settings
|
||||
}),
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
|
|
@ -26,8 +27,7 @@ export default {
|
|||
})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireRecovery', 'abortMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
...mapActions(useAuthFlowStore, ['requireRecovery', 'abortMFA', 'login']),
|
||||
clearError () { this.error = false },
|
||||
|
||||
focusOnCodeInput () {
|
||||
|
|
|
|||
|
|
@ -2,18 +2,7 @@ export default {
|
|||
name: 'MobileDrawer',
|
||||
selector: '.mobile-drawer',
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'Input',
|
||||
'PanelHeader',
|
||||
'MenuItem',
|
||||
'Notification',
|
||||
'Alert',
|
||||
'UserCard'
|
||||
'MenuItem'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
|
|
@ -21,21 +10,6 @@ export default {
|
|||
background: '--bg',
|
||||
backgroundNoCssColor: 'yes'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'PanelHeader',
|
||||
parent: { component: 'MobileDrawer' },
|
||||
directives: {
|
||||
background: '--fg',
|
||||
shadow: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||
import Notifications from '../notifications/notifications.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
|
||||
import {
|
||||
unseenNotificationsFromStore,
|
||||
countExtraNotifications
|
||||
} from '../../services/notification_utils/notification_utils'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
|
|
@ -18,7 +23,6 @@ import {
|
|||
faMinus,
|
||||
faCheckDouble
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
|
|
@ -71,10 +75,9 @@ const MobileNav = {
|
|||
return this.$route.name === 'chat'
|
||||
},
|
||||
...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']),
|
||||
...mapGetters(['unreadChatCount']),
|
||||
chatsPinned () {
|
||||
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||
},
|
||||
...mapState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||
}),
|
||||
shouldConfirmLogout () {
|
||||
return this.$store.getters.mergedConfig.modalOnLogout
|
||||
},
|
||||
|
|
|
|||
|
|
@ -220,8 +220,7 @@
|
|||
margin-top: 3.5em;
|
||||
width: 100vw;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
overflow: hidden scroll;
|
||||
|
||||
.notifications {
|
||||
padding: 0;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 801px) {
|
||||
@media all and (width >= 801px) {
|
||||
.new-status-button:not(.always-show) {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,10 +41,7 @@ export default {
|
|||
.modal-view {
|
||||
z-index: var(--ZI_modals);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -208,6 +208,8 @@
|
|||
}
|
||||
|
||||
.moderation-tools-button {
|
||||
white-space: nowrap;
|
||||
|
||||
svg,
|
||||
i {
|
||||
font-size: 0.8em;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
padding-bottom: 20px;
|
||||
padding-left: 0.5em;
|
||||
padding-bottom: 1.1em;
|
||||
|
||||
th,
|
||||
td {
|
||||
width: 180px;
|
||||
max-width: 360px;
|
||||
width: 11em;
|
||||
max-width: 25em;
|
||||
overflow: hidden;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
|
||||
const MuteCard = {
|
||||
props: ['userId'],
|
||||
data () {
|
||||
return {
|
||||
progress: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
|
|
@ -16,23 +12,26 @@ const MuteCard = {
|
|||
},
|
||||
muted () {
|
||||
return this.relationship.muting
|
||||
},
|
||||
muteExpiryAvailable () {
|
||||
return this.user.mute_expires_at !== undefined
|
||||
},
|
||||
muteExpiry () {
|
||||
return this.user.mute_expires_at == null
|
||||
? this.$t('user_card.mute_expires_forever')
|
||||
: this.$t('user_card.mute_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BasicUserCard
|
||||
BasicUserCard,
|
||||
UserTimedFilterModal
|
||||
},
|
||||
methods: {
|
||||
unmuteUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('unmuteUser', this.userId).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
this.$store.dispatch('unmuteUser', this.userId)
|
||||
},
|
||||
muteUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('muteUser', this.userId).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
this.$refs.timedMuteDialog.optionallyPrompt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,35 @@
|
|||
<template>
|
||||
<basic-user-card :user="user">
|
||||
<div class="mute-card-content-container">
|
||||
<span
|
||||
v-if="muted && muteExpiryAvailable"
|
||||
class="alert neutral"
|
||||
>
|
||||
{{ muteExpiry }}
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
v-if="muted"
|
||||
class="btn button-default"
|
||||
:disabled="progress"
|
||||
@click="unmuteUser"
|
||||
>
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.unmute') }}
|
||||
</template>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn button-default"
|
||||
:disabled="progress"
|
||||
@click="muteUser"
|
||||
>
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.mute_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.mute') }}
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<UserTimedFilterModal
|
||||
ref="timedMuteDialog"
|
||||
:user="user"
|
||||
:is-mute="true"
|
||||
/>
|
||||
</teleport>
|
||||
</basic-user-card>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import { filterNavigation } from 'src/components/navigation/filter.js'
|
|||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faChevronDown,
|
||||
|
|
@ -29,6 +32,7 @@ import {
|
|||
library.add(
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faChevronDown,
|
||||
|
|
@ -76,19 +80,19 @@ const NavPanel = {
|
|||
this.editMode = !this.editMode
|
||||
},
|
||||
toggleCollapse () {
|
||||
this.$store.commit('setPreference', { path: 'simple.collapseNav', value: !this.collapsed })
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().setPreference({ path: 'simple.collapseNav', value: !this.collapsed })
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
},
|
||||
isPinned (item) {
|
||||
return this.pinnedItems.has(item)
|
||||
},
|
||||
togglePin (item) {
|
||||
if (this.isPinned(item)) {
|
||||
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
|
||||
useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value: item })
|
||||
} else {
|
||||
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
|
||||
useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value: item })
|
||||
}
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -96,20 +100,25 @@ const NavPanel = {
|
|||
unreadAnnouncementCount: 'unreadAnnouncementCount',
|
||||
supportsAnnouncements: store => store.supportsAnnouncements
|
||||
}),
|
||||
...mapPiniaState(useServerSideStorageStore, {
|
||||
collapsed: store => store.prefsStorage.simple.collapseNav,
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
|
||||
}),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
followRequestCount: state => state.api.followRequests.length,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems),
|
||||
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav,
|
||||
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable
|
||||
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable,
|
||||
bubbleTimeline: state => state.instance.localBubbleInstances.length > 0
|
||||
}),
|
||||
timelinesItems () {
|
||||
return filterNavigation(
|
||||
Object
|
||||
.entries({ ...TIMELINES })
|
||||
// do not show in timeliens list since it's in a better place now
|
||||
.filter(([key]) => key !== 'bookmarks')
|
||||
.map(([k, v]) => ({ ...v, name: k })),
|
||||
{
|
||||
hasChats: this.pleromaChatMessagesAvailable,
|
||||
|
|
@ -117,6 +126,7 @@ const NavPanel = {
|
|||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarkFolders
|
||||
}
|
||||
)
|
||||
|
|
@ -132,6 +142,7 @@ const NavPanel = {
|
|||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarkFolders
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFederating, isPrivate, currentUser, supportsBookmarkFolders }) => {
|
||||
export const filterNavigation = (list = [], {
|
||||
hasChats,
|
||||
hasAnnouncements,
|
||||
isFederating,
|
||||
isPrivate,
|
||||
currentUser,
|
||||
supportsBookmarkFolders,
|
||||
supportsBubbleTimeline
|
||||
}) => {
|
||||
return list.filter(({ criteria, anon, anonRoute }) => {
|
||||
const set = new Set(criteria || [])
|
||||
if (!isFederating && set.has('federating')) return false
|
||||
|
|
@ -7,6 +15,8 @@ export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFede
|
|||
if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false
|
||||
if (!hasChats && set.has('chats')) return false
|
||||
if (!hasAnnouncements && set.has('announcements')) return false
|
||||
if (!supportsBubbleTimeline && set.has('supportsBubbleTimeline')) return false
|
||||
if (!supportsBookmarkFolders && set.has('supportsBookmarkFolders')) return false
|
||||
if (supportsBookmarkFolders && set.has('!supportsBookmarkFolders')) return false
|
||||
return true
|
||||
})
|
||||
|
|
@ -19,11 +29,11 @@ export const getListEntries = store => store.allLists.map(list => ({
|
|||
iconLetter: list.title[0]
|
||||
}))
|
||||
|
||||
export const getBookmarkFolderEntries = store => store.allFolders.map(folder => ({
|
||||
export const getBookmarkFolderEntries = store => store.allFolders ? store.allFolders.map(folder => ({
|
||||
name: 'bookmark-folder-' + folder.id,
|
||||
routeObject: { name: 'bookmark-folder', params: { id: folder.id } },
|
||||
labelRaw: folder.name,
|
||||
iconEmoji: folder.emoji,
|
||||
iconEmojiUrl: folder.emoji_url,
|
||||
iconLetter: folder.name[0]
|
||||
}))
|
||||
})) : []
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ export const TIMELINES = {
|
|||
label: 'nav.public_tl',
|
||||
criteria: ['!private']
|
||||
},
|
||||
bubble: {
|
||||
route: 'bubble',
|
||||
anon: true,
|
||||
icon: 'city',
|
||||
label: 'nav.bubble',
|
||||
criteria: ['!private', 'federating', 'supportsBubbleTimeline']
|
||||
},
|
||||
twkn: {
|
||||
route: 'public-external-timeline',
|
||||
anon: true,
|
||||
|
|
@ -34,11 +41,11 @@ export const TIMELINES = {
|
|||
label: 'nav.twkn',
|
||||
criteria: ['!private', 'federating']
|
||||
},
|
||||
// bookmarks are still technically a timeline so we should show it in the dropdown
|
||||
bookmarks: {
|
||||
route: 'bookmarks',
|
||||
icon: 'bookmark',
|
||||
label: 'nav.bookmarks',
|
||||
criteria: ['!supportsBookmarkFolders']
|
||||
},
|
||||
favorites: {
|
||||
routeObject: { name: 'user-profile', query: { tab: 'favorites' } },
|
||||
|
|
@ -53,6 +60,15 @@ export const TIMELINES = {
|
|||
}
|
||||
|
||||
export const ROOT_ITEMS = {
|
||||
bookmarks: {
|
||||
route: 'bookmarks',
|
||||
icon: 'bookmark',
|
||||
label: 'nav.bookmarks',
|
||||
// shows bookmarks entry in a better suited location
|
||||
// hides it when bookmark folders are supported since
|
||||
// we show custom component instead of it
|
||||
criteria: ['!supportsBookmarkFolders']
|
||||
},
|
||||
interactions: {
|
||||
route: 'interactions',
|
||||
icon: 'bell',
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import { routeTo } from 'src/components/navigation/navigation.js'
|
|||
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
|
||||
import { mapStores } from 'pinia'
|
||||
import { mapStores, mapState as mapPiniaState } from 'pinia'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
library.add(faThumbtack)
|
||||
|
||||
|
|
@ -19,11 +21,11 @@ const NavigationEntry = {
|
|||
},
|
||||
togglePin (value) {
|
||||
if (this.isPinned(value)) {
|
||||
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value })
|
||||
useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value })
|
||||
} else {
|
||||
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value })
|
||||
useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value })
|
||||
}
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -35,9 +37,11 @@ const NavigationEntry = {
|
|||
},
|
||||
...mapStores(useAnnouncementsStore),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||
})
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
...mapPiniaState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
|||
import {
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faComments,
|
||||
|
|
@ -20,10 +21,12 @@ import {
|
|||
import { useListsStore } from 'src/stores/lists'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
library.add(
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faComments,
|
||||
|
|
@ -54,7 +57,10 @@ const NavPanel = {
|
|||
supportsAnnouncements: store => store.supportsAnnouncements
|
||||
}),
|
||||
...mapPiniaState(useBookmarkFoldersStore, {
|
||||
bookmarks: getBookmarkFolderEntries
|
||||
bookmarks: getBookmarkFolderEntries,
|
||||
}),
|
||||
...mapPiniaState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
|
||||
}),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
|
|
@ -62,7 +68,7 @@ const NavPanel = {
|
|||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||
bubbleTimeline: state => state.instance.localBubbleInstances.length > 0
|
||||
}),
|
||||
pinnedList () {
|
||||
if (!this.currentUser) {
|
||||
|
|
@ -76,7 +82,9 @@ const NavPanel = {
|
|||
hasAnnouncements: this.supportsAnnouncements,
|
||||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser
|
||||
currentUser: this.currentUser,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarks
|
||||
})
|
||||
}
|
||||
return filterNavigation(
|
||||
|
|
@ -95,6 +103,8 @@ const NavPanel = {
|
|||
{
|
||||
hasChats: this.pleromaChatMessagesAvailable,
|
||||
hasAnnouncements: this.supportsAnnouncements,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarks,
|
||||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ library.add(
|
|||
const Notification = {
|
||||
data () {
|
||||
return {
|
||||
selecting: false,
|
||||
statusExpanded: false,
|
||||
unmuted: false,
|
||||
showingApproveConfirmDialog: false,
|
||||
|
|
@ -62,10 +63,35 @@ const Notification = {
|
|||
UserLink,
|
||||
ConfirmModal
|
||||
},
|
||||
mounted () {
|
||||
document.addEventListener('selectionchange', this.onContentSelect)
|
||||
},
|
||||
unmounted () {
|
||||
document.removeEventListener('selectionchange', this.onContentSelect)
|
||||
},
|
||||
methods: {
|
||||
toggleStatusExpanded () {
|
||||
if (!this.expandable) return
|
||||
this.statusExpanded = !this.statusExpanded
|
||||
},
|
||||
onContentSelect () {
|
||||
const { isCollapsed, anchorNode, offsetNode } = document.getSelection()
|
||||
if (isCollapsed) {
|
||||
this.selecting = false
|
||||
return
|
||||
}
|
||||
const within = this.$refs.root.contains(anchorNode) || this.$refs.root.contains(offsetNode)
|
||||
if (within) {
|
||||
this.selecting = true
|
||||
} else {
|
||||
this.selecting = false
|
||||
}
|
||||
},
|
||||
onContentClick (e) {
|
||||
if (!this.selecting && !e.target.closest('a') && !e.target.closest('button')) {
|
||||
this.toggleStatusExpanded()
|
||||
}
|
||||
},
|
||||
generateUserProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
|
|
@ -136,6 +162,9 @@ const Notification = {
|
|||
const user = this.notification.from_profile
|
||||
return highlightStyle(highlight[user.screen_name])
|
||||
},
|
||||
expandable () {
|
||||
return (new Set(['like', 'pleroma:emoji_reaction', 'repeat'])).has(this.notification.type)
|
||||
},
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.notification.from_profile.id)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
// TODO Copypaste from Status, should unify it somehow
|
||||
|
||||
.Notification {
|
||||
border-bottom: 1px solid;
|
||||
border-color: var(--border);
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
|
||||
.status-content {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.Status {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
|
|
@ -31,8 +36,6 @@
|
|||
& .status-username,
|
||||
& .mute-thread,
|
||||
& .mute-words {
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@ export default {
|
|||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'RichContent',
|
||||
'Input',
|
||||
'Avatar',
|
||||
'Attachment',
|
||||
'PollGraph'
|
||||
],
|
||||
defaultRules: []
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<article
|
||||
v-if="notification.type === 'mention' || notification.type === 'status'"
|
||||
ref="root"
|
||||
>
|
||||
<Status
|
||||
class="Notification"
|
||||
|
|
@ -9,9 +10,17 @@
|
|||
@interacted="interacted"
|
||||
/>
|
||||
</article>
|
||||
<article v-else>
|
||||
<article
|
||||
v-else
|
||||
ref="root"
|
||||
class="NotificationParent"
|
||||
:class="{ '-expandable': expandable }"
|
||||
>
|
||||
<div
|
||||
v-if="needMute && !unmuted"
|
||||
:id="'notif-' +notification.id"
|
||||
:aria-expanded="statusExpanded"
|
||||
:aria-controls="'notif-' +notification.id"
|
||||
class="Notification container -muted"
|
||||
>
|
||||
<small>
|
||||
|
|
@ -62,6 +71,7 @@
|
|||
:title="'@'+notification.from_profile.screen_name_ui"
|
||||
:html="notification.from_profile.name_html"
|
||||
:emoji="notification.from_profile.emoji"
|
||||
:is-local="notification.from_profile.is_local"
|
||||
/>
|
||||
</bdi>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
|
|
@ -245,8 +255,11 @@
|
|||
/>
|
||||
<template v-else>
|
||||
<StatusContent
|
||||
class="status-content"
|
||||
:compact="!statusExpanded"
|
||||
:status="notification.status"
|
||||
:collapse="!statusExpanded"
|
||||
@click="onContentClick"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@
|
|||
|
||||
.notification-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +128,6 @@
|
|||
|
||||
.notification-details {
|
||||
min-width: 0;
|
||||
word-wrap: break-word;
|
||||
line-height: var(--post-line-height);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
|
@ -164,7 +160,8 @@
|
|||
}
|
||||
|
||||
h1 {
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
margin: 0 0 0.3em;
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
|
|
|
|||
|
|
@ -6,29 +6,17 @@ export default {
|
|||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'Input',
|
||||
'PanelHeader',
|
||||
'MenuItem',
|
||||
'Post',
|
||||
'Notification',
|
||||
'Alert',
|
||||
'UserCard',
|
||||
'Chat',
|
||||
'Attachment',
|
||||
'Tab',
|
||||
'ListItem'
|
||||
'MenuItem'
|
||||
],
|
||||
validInnerComponentsLite: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'Input',
|
||||
'PanelHeader',
|
||||
'Alert'
|
||||
'PanelHeader'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ export default {
|
|||
'Icon',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'Badge',
|
||||
'Alert',
|
||||
'Avatar'
|
||||
'Alert'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@
|
|||
align-items: center;
|
||||
padding: 0.1em 0.25em;
|
||||
z-index: 1;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.result-percentage {
|
||||
|
|
|
|||
|
|
@ -6,16 +6,7 @@ export default {
|
|||
modal: '.modal'
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'ButtonUnstyled',
|
||||
'Input',
|
||||
'MenuItem',
|
||||
'Post',
|
||||
'UserCard'
|
||||
'MenuItem'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,11 +15,7 @@
|
|||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
bottom: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
z-index: -1px;
|
||||
inset: -1px;
|
||||
box-shadow: var(--_shadow);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
@ -64,11 +60,14 @@
|
|||
}
|
||||
|
||||
.extra-button {
|
||||
border-left: 1px solid var(--icon);
|
||||
border-left: 1px solid;
|
||||
border-image-source: linear-gradient(to bottom, transparent 0%, var(--icon) var(--__horizontal-gap) calc(100% - var(--__horizontal-gap)), transparent 100%);
|
||||
border-image-slice: 1;
|
||||
padding-left: calc(var(--__horizontal-gap) - 1px);
|
||||
border-right: var(--__horizontal-gap) solid transparent;
|
||||
border-top: var(--__horizontal-gap) solid transparent;
|
||||
border-bottom: var(--__horizontal-gap) solid transparent;
|
||||
padding-right: var(--__horizontal-gap);
|
||||
padding-top: var(--__horizontal-gap);
|
||||
padding-bottom: var(--__horizontal-gap);
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.main-button {
|
||||
|
|
|
|||
|
|
@ -363,12 +363,6 @@ const PostStatusForm = {
|
|||
}
|
||||
},
|
||||
safeToSaveDraft () {
|
||||
console.log('safe', (
|
||||
this.newStatus.status ||
|
||||
this.newStatus.spoilerText ||
|
||||
this.newStatus.files?.length ||
|
||||
this.newStatus.hasPoll
|
||||
) && this.saveable)
|
||||
return (
|
||||
this.newStatus.status ||
|
||||
this.newStatus.spoilerText ||
|
||||
|
|
@ -732,10 +726,6 @@ const PostStatusForm = {
|
|||
scrollerRef.scrollTop = targetScroll
|
||||
}
|
||||
},
|
||||
showEmojiPicker () {
|
||||
this.$refs.textarea.focus()
|
||||
this.$refs['emoji-input'].triggerShowPicker()
|
||||
},
|
||||
clearError () {
|
||||
this.error = null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,10 +41,12 @@
|
|||
|
||||
.form-bottom-left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding-right: 7px;
|
||||
margin-right: 7px;
|
||||
max-width: 10em;
|
||||
gap: 1.5em;
|
||||
|
||||
button {
|
||||
padding: 0.5em;
|
||||
margin: -0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-heading {
|
||||
|
|
@ -102,10 +104,16 @@
|
|||
.visibility-tray {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: 5px;
|
||||
padding-top: 0.5em;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.visibility-notice {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--roundness);
|
||||
padding: 0.5em 1em
|
||||
}
|
||||
|
||||
.visibility-notice.edit-warning {
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
|
|
@ -193,6 +201,10 @@
|
|||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.inputs-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input.form-post-body {
|
||||
// TODO: make a resizable textarea component?
|
||||
box-sizing: content-box; // needed for easier computation of dynamic size
|
||||
|
|
@ -200,11 +212,13 @@
|
|||
transition: min-height 200ms 100ms;
|
||||
// stock padding + 1 line of text (for counter)
|
||||
padding-bottom: calc(var(--_padding) + var(--post-line-height) * 1em);
|
||||
padding-right: 1.5em;
|
||||
// two lines of text
|
||||
height: calc(var(--post-line-height) * 1em);
|
||||
min-height: calc(var(--post-line-height) * 1em);
|
||||
resize: none;
|
||||
background: transparent;
|
||||
text-wrap: stable;
|
||||
|
||||
&.scrollable-form {
|
||||
overflow-y: auto;
|
||||
|
|
@ -215,6 +229,10 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.subject-input {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.character-counter {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
|
|
|||
|
|
@ -156,12 +156,13 @@
|
|||
class="preview-status"
|
||||
/>
|
||||
</div>
|
||||
<div class="input inputs-wrapper">
|
||||
<EmojiInput
|
||||
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
|
||||
v-model="newStatus.spoilerText"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
class="input form-control"
|
||||
class="input form-control subject-input unstyled"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<input
|
||||
|
|
@ -171,7 +172,7 @@
|
|||
:disabled="posting && !optimisticPosting"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
size="1"
|
||||
class="input form-post-subject"
|
||||
class="input form-post-subject unstyled"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
|
|
@ -180,10 +181,9 @@
|
|||
v-model="newStatus.status"
|
||||
:suggest="emojiUserSuggestor"
|
||||
:placement="emojiPickerPlacement"
|
||||
class="input form-control main-input"
|
||||
class="input form-control main-input unstyled"
|
||||
enable-sticker-picker
|
||||
enable-emoji-picker
|
||||
hide-emoji-button
|
||||
:newline-on-ctrl-enter="submitOnEnter"
|
||||
@input="onEmojiInputInput"
|
||||
@sticker-uploaded="addMediaFile"
|
||||
|
|
@ -217,6 +217,7 @@
|
|||
</p>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
</div>
|
||||
<div
|
||||
v-if="!disableScopeSelector"
|
||||
class="visibility-tray"
|
||||
|
|
@ -236,8 +237,9 @@
|
|||
>
|
||||
<Select
|
||||
v-model="newStatus.contentType"
|
||||
class="input form-control"
|
||||
class="input form-control unstyled"
|
||||
:attrs="{ 'aria-label': $t('post_status.content_type_selection') }"
|
||||
unstyled="true"
|
||||
>
|
||||
<option
|
||||
v-for="postFormat in postFormats"
|
||||
|
|
@ -285,13 +287,6 @@
|
|||
@upload-failed="uploadFailed"
|
||||
@all-uploaded="finishedUploadingFiles"
|
||||
/>
|
||||
<button
|
||||
class="emoji-icon button-unstyled"
|
||||
:title="$t('emoji.add_emoji')"
|
||||
@click="showEmojiPicker"
|
||||
>
|
||||
<FAIcon icon="smile-beam" />
|
||||
</button>
|
||||
<button
|
||||
v-if="pollsAvailable"
|
||||
class="poll-icon button-unstyled"
|
||||
|
|
|
|||
|
|
@ -418,7 +418,7 @@
|
|||
margin: 0.6em;
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
@media all and (width <= 800px) {
|
||||
.registration-form .container {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { unescape, flattenDeep } from 'lodash'
|
|||
import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
|
||||
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
|
||||
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
|
||||
import StillImage from 'src/components/still-image/still-image.vue'
|
||||
import StillImageEmojiPopover from 'src/components/still-image/still-image-emoji-popover.vue'
|
||||
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
||||
import { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.js'
|
||||
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
|
||||
|
|
@ -86,6 +86,24 @@ export default {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Collapse newlines
|
||||
collapse: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/* Content comes from current instance
|
||||
*
|
||||
* This is used for emoji stealing popover.
|
||||
* By default we assume it is, so that steal
|
||||
* emoji button isn't shown where it probably
|
||||
* should not be.
|
||||
*/
|
||||
isLocal: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
// NEVER EVER TOUCH DATA INSIDE RENDER
|
||||
|
|
@ -162,11 +180,14 @@ export default {
|
|||
item,
|
||||
this.emoji,
|
||||
({ shortcode, url }) => {
|
||||
return <StillImage
|
||||
return <StillImageEmojiPopover
|
||||
class="emoji img"
|
||||
src={url}
|
||||
title={`:${shortcode}:`}
|
||||
alt={`:${shortcode}:`}
|
||||
|
||||
shortcode={shortcode}
|
||||
isLocal={this.isLocal}
|
||||
/>
|
||||
}
|
||||
)]
|
||||
|
|
@ -281,11 +302,20 @@ export default {
|
|||
|
||||
const pass1 = convertHtmlToTree(html).map(processItem)
|
||||
const pass2 = [...pass1].reverse().map(processItemReverse).reverse()
|
||||
|
||||
// DO NOT USE SLOTS they cause a re-render feedback loop here.
|
||||
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
|
||||
// at least until vue3?
|
||||
const result = <span class={['RichContent', this.faint ? '-faint' : '']}>
|
||||
{ pass2 }
|
||||
const result =
|
||||
<span class={['RichContent', this.faint ? '-faint' : '']}>
|
||||
{
|
||||
this.collapse
|
||||
? pass2.map(x => {
|
||||
if (!Array.isArray(x)) return x.replace(/\n/g, ' ')
|
||||
return x.map(y => y.type === 'br' ? ' ' : y)
|
||||
})
|
||||
: pass2
|
||||
}
|
||||
</span>
|
||||
|
||||
const event = {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
font-family: var(--font);
|
||||
|
||||
&.-faint {
|
||||
color: var(--text);
|
||||
/* stylelint-disable declaration-no-important */
|
||||
--text: var(--textFaint) !important;
|
||||
--link: var(--linkFaint) !important;
|
||||
|
|
@ -63,6 +64,11 @@
|
|||
|
||||
.img {
|
||||
display: inline-block;
|
||||
|
||||
// fix vertical alignment of stealable emoji
|
||||
button {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
export default {
|
||||
name: 'RichContent',
|
||||
selector: '.RichContent',
|
||||
notEditable: true,
|
||||
transparent: true,
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'FunText',
|
||||
'Link'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
'--font': 'generic | inherit',
|
||||
'--monoFont': 'generic | monospace',
|
||||
textNoCssColor: 'yes'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
18
src/components/scroll_top_button/scroll_top_button.js
Normal file
18
src/components/scroll_top_button/scroll_top_button.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
const ScrollTopButton = {
|
||||
props: {
|
||||
fast: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scrollToTop() {
|
||||
const speed = this.fast ? 'instant' : 'smooth';
|
||||
|
||||
window.scrollTo({ top: 0, behavior: speed })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ScrollTopButton
|
||||
29
src/components/scroll_top_button/scroll_top_button.vue
Normal file
29
src/components/scroll_top_button/scroll_top_button.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="rightside-button scroll-to-top">
|
||||
<button
|
||||
class="button-unstyled scroll-to-top-button"
|
||||
type="button"
|
||||
:title="$t('general.scroll_to_top')"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
<FALayers class="fa-scale-110 fa-old-padding-layer">
|
||||
<FAIcon icon="arrow-up" />
|
||||
<FAIcon
|
||||
icon="minus"
|
||||
transform="up-7"
|
||||
/>
|
||||
</FALayers>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./scroll_top_button.js"></script>
|
||||
<style lang="scss">
|
||||
.scroll-to-top {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.-scrolled .scroll-to-top {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -157,7 +157,7 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
@media all and (width <= 800px) {
|
||||
.search-nav-heading {
|
||||
.tab-switcher .tabs .tab-wrapper {
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Popover from 'components/popover/popover.vue'
|
|||
import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
|
||||
import ModifiedIndicator from '../helpers/modified_indicator.vue'
|
||||
import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
const EmojiTab = {
|
||||
components: {
|
||||
|
|
@ -31,7 +32,10 @@ const EmojiTab = {
|
|||
newPackName: '',
|
||||
deleteModalVisible: false,
|
||||
remotePackInstance: '',
|
||||
remotePackDownloadAs: ''
|
||||
remotePackDownloadAs: '',
|
||||
|
||||
remotePackURL: '',
|
||||
remotePackFile: null
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -141,9 +145,9 @@ const EmojiTab = {
|
|||
})
|
||||
},
|
||||
|
||||
updatePackFiles (newFiles) {
|
||||
this.pack.files = newFiles
|
||||
this.sortPackFiles(this.packName)
|
||||
updatePackFiles (newFiles, packName) {
|
||||
this.knownPacks[packName].files = newFiles
|
||||
this.sortPackFiles(packName)
|
||||
},
|
||||
|
||||
loadPacksPaginated (listFunction) {
|
||||
|
|
@ -219,7 +223,7 @@ const EmojiTab = {
|
|||
.then(data => data.json())
|
||||
.then(resp => {
|
||||
if (resp === 'ok') {
|
||||
this.$refs.dlPackPopover.hidePopover()
|
||||
this.$refs.downloadPackPopover.hidePopover()
|
||||
|
||||
return this.refreshPackList()
|
||||
} else {
|
||||
|
|
@ -231,8 +235,49 @@ const EmojiTab = {
|
|||
this.remotePackDownloadAs = ''
|
||||
})
|
||||
},
|
||||
downloadRemoteURLPack () {
|
||||
this.$store.state.api.backendInteractor.downloadRemoteEmojiPackZIP({
|
||||
url: this.remotePackURL, packName: this.newPackName
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(resp => {
|
||||
if (resp === 'ok') {
|
||||
this.$refs.additionalRemotePopover.hidePopover()
|
||||
|
||||
return this.refreshPackList()
|
||||
} else {
|
||||
this.displayError(resp.error)
|
||||
return Promise.reject(resp)
|
||||
}
|
||||
}).then(() => {
|
||||
this.packName = this.newPackName
|
||||
this.newPackName = ''
|
||||
this.remotePackURL = ''
|
||||
})
|
||||
},
|
||||
downloadRemoteFilePack () {
|
||||
this.$store.state.api.backendInteractor.downloadRemoteEmojiPackZIP({
|
||||
file: this.remotePackFile[0], packName: this.newPackName
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(resp => {
|
||||
if (resp === 'ok') {
|
||||
this.$refs.additionalRemotePopover.hidePopover()
|
||||
|
||||
return this.refreshPackList()
|
||||
} else {
|
||||
this.displayError(resp.error)
|
||||
return Promise.reject(resp)
|
||||
}
|
||||
}).then(() => {
|
||||
this.packName = this.newPackName
|
||||
this.newPackName = ''
|
||||
this.remotePackURL = ''
|
||||
})
|
||||
},
|
||||
|
||||
displayError (msg) {
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'admin_dash.emoji.error',
|
||||
messageArgs: [msg],
|
||||
level: 'error'
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
.emoji-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em 1em;
|
||||
gap: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,64 @@
|
|||
</template>
|
||||
</Popover>
|
||||
</button>
|
||||
<button
|
||||
class="button button-default emoji-panel-additional-actions"
|
||||
@click="$refs.additionalRemotePopover.showPopover"
|
||||
>
|
||||
<FAIcon
|
||||
icon="chevron-down"
|
||||
/>
|
||||
|
||||
<Popover
|
||||
ref="additionalRemotePopover"
|
||||
popover-class="emoji-tab-edit-popover popover-default"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
bound-to-selector=".emoji-tab"
|
||||
:bound-to="{ x: 'container' }"
|
||||
:offset="{ y: 5 }"
|
||||
>
|
||||
<template #content>
|
||||
<div class="emoji-tab-popover-input">
|
||||
<h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3>
|
||||
<input
|
||||
v-model="newPackName"
|
||||
:placeholder="$t('admin_dash.emoji.new_pack_name')"
|
||||
class="input"
|
||||
>
|
||||
<h3>Import pack from URL</h3>
|
||||
<input
|
||||
v-model="remotePackURL"
|
||||
class="input"
|
||||
placeholder="Pack .zip URL"
|
||||
>
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
:disabled="newPackName.trim() === '' || remotePackURL.trim() === ''"
|
||||
@click="downloadRemoteURLPack"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<h3>Import pack from a file</h3>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/zip"
|
||||
class="emoji-tab-popover-file input"
|
||||
@change="remotePackFile = $event.target.files"
|
||||
>
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
:disabled="newPackName.trim() === '' || remotePackFile === null || remotePackFile.length === 0"
|
||||
@click="downloadRemoteFilePack"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<h3>{{ $t('admin_dash.emoji.emoji_packs') }}</h3>
|
||||
|
|
@ -240,12 +298,12 @@
|
|||
v-if="pack.remote !== undefined"
|
||||
class="button button-default btn"
|
||||
type="button"
|
||||
@click="$refs.dlPackPopover.showPopover"
|
||||
@click="$refs.downloadPackPopover.showPopover"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.download_pack') }}
|
||||
|
||||
<Popover
|
||||
ref="dlPackPopover"
|
||||
ref="downloadPackPopover"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
bound-to-selector=".emoji-tab"
|
||||
|
|
@ -329,11 +387,12 @@
|
|||
ref="emojiPopovers"
|
||||
:key="shortcode"
|
||||
placement="top"
|
||||
:title="$t('admin_dash.emoji.editing', [shortcode])"
|
||||
:disabled="pack.remote !== undefined"
|
||||
:title="$t(`admin_dash.emoji.${pack.remote === undefined ? 'editing' : 'copying'}`, [shortcode])"
|
||||
:shortcode="shortcode"
|
||||
:file="file"
|
||||
:pack-name="packName"
|
||||
:remote="pack.remote"
|
||||
:known-local-packs="knownLocalPacks"
|
||||
@update-pack-files="updatePackFiles"
|
||||
@display-error="displayError"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import StringSetting from '../helpers/string_setting.vue'
|
|||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
|
@ -80,7 +81,7 @@ const FrontendsTab = {
|
|||
this.$store.dispatch('loadFrontendsStuff')
|
||||
if (response.error) {
|
||||
const reason = await response.error.json()
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
level: 'error',
|
||||
messageKey: 'admin_dash.frontend.failure_installing_frontend',
|
||||
messageArgs: {
|
||||
|
|
@ -90,7 +91,7 @@ const FrontendsTab = {
|
|||
timeout: 5000
|
||||
})
|
||||
} else {
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
level: 'success',
|
||||
messageKey: 'admin_dash.frontend.success_installing_frontend',
|
||||
messageArgs: {
|
||||
|
|
|
|||
|
|
@ -13,15 +13,11 @@
|
|||
// fix buttons showing through
|
||||
z-index: 2;
|
||||
opacity: 0.9;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
max-width: 10em;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
popover-class="emoji-tab-edit-popover popover-default"
|
||||
:bound-to="{ x: 'container' }"
|
||||
:offset="{ y: 5 }"
|
||||
:disabled="disabled"
|
||||
:class="{'emoji-unsaved': isEdited}"
|
||||
>
|
||||
<template #trigger>
|
||||
|
|
@ -30,8 +29,11 @@
|
|||
|
||||
<div
|
||||
v-if="newUpload"
|
||||
class="emoji-tab-popover-input"
|
||||
class="emoji-tab-popover-new-upload"
|
||||
>
|
||||
<h4>{{ $t('admin_dash.emoji.emoji_source') }}</h4>
|
||||
|
||||
<div class="emoji-tab-popover-input">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
|
|
@ -39,6 +41,15 @@
|
|||
@change="uploadFile = $event.target.files"
|
||||
>
|
||||
</div>
|
||||
<div class="emoji-tab-popover-input ">
|
||||
<input
|
||||
v-model="uploadURL"
|
||||
:placeholder="$t('admin_dash.emoji.upload_url')"
|
||||
class="emoji-data-input input"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="emoji-tab-popover-input">
|
||||
<label>
|
||||
|
|
@ -63,16 +74,50 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="remote !== undefined"
|
||||
class="emoji-tab-popover-input"
|
||||
>
|
||||
<label>
|
||||
{{ $t('admin_dash.emoji.copy_to') }}
|
||||
|
||||
<SelectComponent
|
||||
v-model="copyToPack"
|
||||
class="form-control"
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
hidden
|
||||
>
|
||||
{{ $t('admin_dash.emoji.emoji_pack') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(pack, listPackName) in knownLocalPacks"
|
||||
:key="listPackName"
|
||||
:label="listPackName"
|
||||
>
|
||||
{{ listPackName }}
|
||||
</option>
|
||||
</SelectComponent>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
For local emojis, disable the button if nothing was edited.
|
||||
For remote emojis, also disable it if a local pack is not selected.
|
||||
Remote emojis are processed by the same function that uploads new ones, as that is effectively what it does
|
||||
-->
|
||||
<button
|
||||
class="button button-default btn"
|
||||
type="button"
|
||||
:disabled="newUpload ? uploadFile.length == 0 : !isEdited"
|
||||
@click="newUpload ? uploadEmoji() : saveEditedEmoji()"
|
||||
:disabled="saveButtonDisabled"
|
||||
@click="(newUpload || remote !== undefined) ? uploadEmoji() : saveEditedEmoji()"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.save') }}
|
||||
</button>
|
||||
|
||||
<template v-if="!newUpload">
|
||||
<template v-if="!newUpload && remote === undefined">
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
|
|
@ -107,19 +152,16 @@
|
|||
import Popover from 'components/popover/popover.vue'
|
||||
import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
|
||||
import StillImage from 'components/still-image/still-image.vue'
|
||||
import SelectComponent from 'components/select/select.vue'
|
||||
|
||||
export default {
|
||||
components: { Popover, ConfirmModal, StillImage },
|
||||
components: { Popover, ConfirmModal, StillImage, SelectComponent },
|
||||
inject: ['emojiAddr'],
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
newUpload: Boolean,
|
||||
|
||||
|
|
@ -140,21 +182,35 @@ export default {
|
|||
type: String,
|
||||
// Only exists when this is not a new upload
|
||||
default: ''
|
||||
},
|
||||
|
||||
// Only exists for emojis from remote packs
|
||||
remote: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
knownLocalPacks: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
emits: ['updatePackFiles', 'displayError'],
|
||||
data () {
|
||||
return {
|
||||
uploadFile: [],
|
||||
uploadURL: '',
|
||||
editedShortcode: this.shortcode,
|
||||
editedFile: this.file,
|
||||
deleteModalVisible: false
|
||||
deleteModalVisible: false,
|
||||
copyToPack: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
emojiPreview () {
|
||||
if (this.newUpload && this.uploadFile.length > 0) {
|
||||
return URL.createObjectURL(this.uploadFile[0])
|
||||
} else if (this.newUpload && this.uploadURL !== '') {
|
||||
return this.uploadURL
|
||||
} else if (!this.newUpload) {
|
||||
return this.emojiAddr(this.file)
|
||||
}
|
||||
|
|
@ -163,6 +219,12 @@ export default {
|
|||
},
|
||||
isEdited () {
|
||||
return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file)
|
||||
},
|
||||
saveButtonDisabled() {
|
||||
if (this.remote === undefined)
|
||||
return this.newUpload ? (this.uploadURL === "" && this.uploadFile.length == 0) : !this.isEdited
|
||||
else
|
||||
return this.copyToPack === ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -181,9 +243,12 @@ export default {
|
|||
}).then(resp => this.$emit('updatePackFiles', resp))
|
||||
},
|
||||
uploadEmoji () {
|
||||
let packName = this.remote === undefined ? this.packName : this.copyToPack
|
||||
this.$store.state.api.backendInteractor.addNewEmojiFile({
|
||||
packName: this.packName,
|
||||
file: this.uploadFile[0],
|
||||
packName: packName,
|
||||
file: this.remote === undefined
|
||||
? (this.uploadURL !== "" ? this.uploadURL : this.uploadFile[0])
|
||||
: this.emojiAddr(this.file),
|
||||
shortcode: this.editedShortcode,
|
||||
filename: this.editedFile
|
||||
}).then(resp => resp.json()).then(resp => {
|
||||
|
|
@ -192,7 +257,7 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
this.$emit('updatePackFiles', resp)
|
||||
this.$emit('updatePackFiles', resp, packName)
|
||||
this.$refs.emojiPopover.hidePopover()
|
||||
|
||||
this.editedFile = ''
|
||||
|
|
@ -215,7 +280,7 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
this.$emit('updatePackFiles', resp)
|
||||
this.$emit('updatePackFiles', resp, this.packName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -228,9 +293,22 @@ export default {
|
|||
padding-right: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
|
||||
.emoji-tab-popover-new-upload {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 2.3em;
|
||||
height: 2.3em;
|
||||
}
|
||||
|
||||
.Select {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
46
src/components/settings_modal/helpers/help_indicator.vue
Normal file
46
src/components/settings_modal/helpers/help_indicator.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<span class="HelpIndicator">
|
||||
<Popover
|
||||
trigger="click"
|
||||
:trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
|
||||
>
|
||||
<template #trigger>
|
||||
|
||||
<FAIcon icon="circle-question" />
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="help-tooltip">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleQuestion
|
||||
)
|
||||
|
||||
export default {
|
||||
components: { Popover }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.HelpIndicator {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.help-tooltip {
|
||||
margin: 0.5em 1em;
|
||||
min-width: 10em;
|
||||
max-width: 30vw;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue