Merge remote-tracking branch 'origin/develop' into from/develop/tusooa/sw-cache-assets

This commit is contained in:
Henry Jameson 2025-06-25 15:40:56 +03:00
commit a1f43234cd
235 changed files with 6354 additions and 4065 deletions

View file

@ -1 +1 @@
18.20.7 18.20.8

View file

@ -1,6 +1,5 @@
{ {
"extends": [ "extends": [
"stylelint-rscss/config",
"stylelint-config-standard", "stylelint-config-standard",
"stylelint-config-recommended-scss", "stylelint-config-recommended-scss",
"stylelint-config-html", "stylelint-config-html",
@ -8,15 +7,6 @@
], ],
"rules": { "rules": {
"declaration-no-important": true, "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, "selector-class-pattern": null,
"import-notation": null, "import-notation": null,
"custom-property-pattern": null, "custom-property-pattern": null,

View file

@ -2,6 +2,74 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.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
- BREAKING: static/emoji.json is replaced with a properly hashed path under static/js in the production build, meaning server admins cannot provide their own set of unicode emojis by overriding this file (custom (image-based) emojis not affected)
- Speed up initial boot.
- Updated our build system to support browsers:
Safari >= 15
Firefox >= 115
Android > 4
no Opera Mini support
no IE support
no "dead" (unmaintained) browsers support
This does not guarantee that browsers will or will not work.
- Use /api/v1/accounts/:id/follow for account subscriptions instead of the deprecated routes
- Modal layout for mobile has new layout to make it easy to use
- Better display of mute reason on posts
- Simplify the OAuth client_name to 'PleromaFE'
- Partially migrated from vuex to pinia
- Authenticate and subscribe to streaming after connection
- Tabs now have indentation for better visibility of which tab is currently active
- Upgraded Vue to version 3.5
### Added
- Support bookmark folders
- Some new default color schemes
- Added support for fetching /{resource}.custom.ext to allow adding instance-specific themes without altering sourcetree
- Post actions customization
- Support displaying time in absolute format
- Add draft management system
- Compress most kinds of images on upload.
- Added option to always convert images to JPEG format instead of using WebP when compressing images.
- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload.
- Inform users that Smithereen public polls are public
- Splash screen + loading indicator to make process of identifying initialization issues and load performance
- UI for making v3 themes and palettes, support for bundling v3 themes
- Make UserLink wrappable
### Fixed
- Fixed occasional overflows in emoji picker and made header scrollable
- Updated shadow editor, hopefully fixed long-standing bugs, added ability to specify shadow's name.
- Checkbox vertical alignment
- Check for canvas extract permission when initializing favicon service
- Fix some of the color manipulation functions
- Fix draft saving when auto-save is off
- Switch from class hack to normalButton attribute for emoji count popover
- Fix emoji inconsistencies in notifications,
- Fix some emoji not scaling with interface
- Make sure hover style is also applied to :focus-visible
- Improved ToS and registration
- Fix small markup inconsistencies
- Fixed modals buttons overflow
- Fix whitespaces for multiple status mute reasons, display bot status reason
- Create an OAuth app only when needed
- Fix CSS compatibility issues in style_setter.js for older browsers like Palemoon
- Proper sticky header for conversations on user page
- Add text label for more actions button in post status form
- Reply-or-quote buttons now take less space
- Allow repeats of own posts with private scopes
- Bookmarks visible again on mobile
- Remove focusability on hidden popover in subject input
- Show only month and day instead of weird "day, hour" format.
### Removed
- BREAKING: drop support for browsers that do not support `<script type="module">`
- BREAKING: css source map does not work in production (see https://github.com/vitejs/vite/issues/2830 )
- Remove emoji annotations code for unused languages from final build
## 2.7.1 ## 2.7.1
Bugfix release. Added small optimizations to emoji picker that should make it a bit more responsive, however it needs rather large change to make it more performant which might come in a major release. Bugfix release. Added small optimizations to emoji picker that should make it a bit more responsive, however it needs rather large change to make it more performant which might come in a major release.

64
build/emojis_plugin.js Normal file
View file

@ -0,0 +1,64 @@
import { resolve } from 'node:path'
import { access } from 'node:fs/promises'
import { languages, langCodeToCldrName } from '../src/i18n/languages.js'
const annotationsImportPrefix = '@kazvmoe-infra/unicode-emoji-json/annotations/'
const specialAnnotationsLocale = {
ja_easy: 'ja'
}
const internalToAnnotationsLocale = (internal) => specialAnnotationsLocale[internal] || internal
// This gets all the annotations that are accessible (whose language
// can be chosen in the settings). Data for other languages are
// discarded because there is no way for it to be fetched.
const getAllAccessibleAnnotations = async (projectRoot) => {
const imports = (await Promise.all(
languages
.map(async lang => {
const destLang = internalToAnnotationsLocale(lang)
const importModule = `${annotationsImportPrefix}${destLang}.json`
const importFile = resolve(projectRoot, 'node_modules', importModule)
try {
await access(importFile)
return `'${lang}': () => import('${importModule}')`
} catch (e) {
return
}
})))
.filter(k => k)
.join(',\n')
return `
export const annotationsLoader = {
${imports}
}
`
}
const emojiAnnotationsId = 'virtual:pleroma-fe/emoji-annotations'
const emojiAnnotationsIdResolved = '\0' + emojiAnnotationsId
const emojisPlugin = () => {
let projectRoot
return {
name: 'emojis-plugin',
configResolved (conf) {
projectRoot = conf.root
},
resolveId (id) {
if (id === emojiAnnotationsId) {
return emojiAnnotationsIdResolved
}
return null
},
async load (id) {
if (id === emojiAnnotationsIdResolved) {
return await getAllAccessibleAnnotations(projectRoot)
}
return null
}
}
}
export default emojisPlugin

28
build/msw_plugin.js Normal file
View file

@ -0,0 +1,28 @@
import { resolve } from 'node:path'
import { readFile } from 'node:fs/promises'
const target = 'node_modules/msw/lib/mockServiceWorker.js'
const mswPlugin = () => {
let projectRoot
return {
name: 'msw-plugin',
apply: 'serve',
configResolved (conf) {
projectRoot = conf.root
},
configureServer (server) {
server.middlewares.use(async (req, res, next) => {
if (req.path === '/mockServiceWorker.js') {
const file = await readFile(resolve(projectRoot, target))
res.set('Content-Type', 'text/javascript')
res.send(file)
} else {
next()
}
})
}
}
}
export default mswPlugin

View file

@ -0,0 +1 @@
Added support for Akkoma and IceShrimp.NET backend

View file

@ -1 +0,0 @@
Added option to always convert images to JPEG format instead of using WebP when compressing images.

View file

@ -0,0 +1,2 @@
Add arithmetic blend ISS function

View file

@ -0,0 +1 @@
Add support for detachable scrollTop button

View file

@ -1 +0,0 @@
Updated shadow editor, hopefully fixed long-standing bugs, added ability to specify shadow's name.

View file

@ -1 +0,0 @@
Support bookmark folders

View file

@ -1 +0,0 @@
Speed up initial boot.

View file

@ -1,9 +0,0 @@
Updated our build system to support browsers:
Safari >= 15
Firefox >= 115
Android > 4
no Opera Mini support
no IE support
no "dead" (unmaintained) browsers support
This does not guarantee that browsers will or will not work.

View file

@ -1 +0,0 @@
checkbox vertical alignment has been fixed

View file

@ -1 +0,0 @@
Some new default color schemes

View file

@ -1 +0,0 @@
Fix some of the color manipulation functions

View file

@ -1 +0,0 @@
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

View file

@ -1 +0,0 @@
Added support for fetching /{resource}.custom.ext to allow adding instance-specific themes without altering sourcetree

View file

@ -1 +0,0 @@
Post actions can be customized

View file

@ -1 +0,0 @@
Support displaying time in absolute format

View file

@ -1 +0,0 @@
Use /api/v1/accounts/:id/follow for account subscriptions instead of the deprecated routes

View file

@ -1 +0,0 @@
Fix draft saving when auto-save is off

View file

@ -1 +0,0 @@
Add draft management system

View file

@ -1 +0,0 @@
fixed occasional overflows in emoji picker and made header scrollable

View file

@ -1 +0,0 @@
fix emoji inconsistencies in notifications, fix some emoji not scaling with interface

View file

@ -1 +0,0 @@
Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload.

View file

@ -1 +0,0 @@
Fix few markup panel inconsistencies; better ToS and registration

View file

@ -1 +0,0 @@
Fix small markup inconsistencies

View file

@ -1 +0,0 @@
modal layout for mobile has new layout to make it easy to use

View file

@ -1 +0,0 @@
fixed modals buttons overflow

View file

@ -1 +0,0 @@
Fix whitespaces for multiple status mute reasons, display bot status reason

View file

@ -1 +0,0 @@
Added missing EN translation key for status.muted_user

View file

@ -0,0 +1 @@
Synchronized mutes, advanced mute control (regexp, expiry, naming)

View file

@ -1 +0,0 @@
better display of mute reason on posts

View file

@ -1 +0,0 @@
BREAKING: drop support for browsers that do not support `<script type="module">`

View file

@ -1 +0,0 @@
BREAKING: css source map does not work in production (see https://github.com/vitejs/vite/issues/2830 )

View file

@ -1 +0,0 @@
Inform users that Smithereen public polls are public

View file

@ -1 +0,0 @@
Simplify the OAuth client_name to 'PleromaFE'

View file

@ -1 +0,0 @@
proper sticky header for conversations on user page

View file

@ -1 +0,0 @@
Partially migrated from vuex to pinia

View file

@ -1 +0,0 @@
Add text label for more actions button in post status form

View file

@ -0,0 +1 @@
Fix error styling for user profiles

View file

@ -1 +0,0 @@
BREAKING: static/emoji.json is replaced with a properly hashed path under static/js in the production build, meaning server admins cannot provide their own set of unicode emojis by overriding this file (custom (image-based) emojis not affected)

View file

@ -1 +0,0 @@
reply-or-quote buttons now take less space

View file

@ -1 +0,0 @@
Bookmarks visible again on mobile

View file

@ -1 +0,0 @@
Splash screen + loading indicator to make process of identifying initialization issues and load performance

View file

@ -1 +0,0 @@
Authenticate and subscribe to streaming after connection

View file

@ -1 +0,0 @@
Tabs now have indentation for better visibility of which tab is currently active

View file

@ -0,0 +1 @@
Indicate currently active V3 theme as a body element class

View file

@ -1 +0,0 @@
UI for making v3 themes and palettes, support for bundling v3 themes

View file

@ -1 +0,0 @@
Resize most kinds of images on upload.

View file

@ -1 +0,0 @@
Make UserLink wrappable

View file

View file

@ -1 +0,0 @@
Upgraded Vue to version 3.5

View file

@ -1 +0,0 @@
Show only month and day instead of weird "day, hour" format. While at it, fixed typo "defualt" in a comment.

View file

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no"> <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="/static/config.json" as="fetch" crossorigin />
<link rel="preload" href="/api/pleroma/frontend_configurations" 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="/nodeinfo/2.1.json" as="fetch" crossorigin />
<link rel="preload" href="/api/v1/instance" 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" /> <link rel="preload" href="/static/pleromatan_apology_fox_small.webp" as="image" />

View file

@ -17,7 +17,7 @@
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs" "lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.26.9", "@babel/runtime": "7.27.1",
"@chenfengyuan/vue-qrcode": "2.0.0", "@chenfengyuan/vue-qrcode": "2.0.0",
"@fortawesome/fontawesome-svg-core": "6.7.2", "@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2", "@fortawesome/free-regular-svg-icons": "6.7.2",
@ -32,82 +32,83 @@
"body-scroll-lock": "3.1.5", "body-scroll-lock": "3.1.5",
"chromatism": "3.0.0", "chromatism": "3.0.0",
"click-outside-vue3": "4.0.1", "click-outside-vue3": "4.0.1",
"cropperjs": "1.6.2", "cropperjs": "2.0.0",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"globals": "^16.0.0", "globals": "^16.0.0",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"localforage": "1.10.0", "localforage": "1.10.0",
"parse-link-header": "2.0.0", "parse-link-header": "2.0.0",
"phoenix": "1.7.19", "phoenix": "1.7.21",
"pinia": "^2.0.33", "pinia": "^3.0.0",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"qrcode": "1.5.4", "qrcode": "1.5.4",
"querystring-es3": "0.2.1", "querystring-es3": "0.2.1",
"url": "0.11.4", "url": "0.11.4",
"utf8": "3.0.0", "utf8": "3.0.0",
"vue": "3.5.13", "uuid": "11.1.0",
"vue-i18n": "10", "vue": "3.5.17",
"vue-router": "4.5.0", "vue-i18n": "11",
"vue-router": "4.5.1",
"vue-virtual-scroller": "^2.0.0-beta.7", "vue-virtual-scroller": "^2.0.0-beta.7",
"vuex": "4.1.0" "vuex": "4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.26.9", "@babel/core": "7.27.1",
"@babel/eslint-parser": "7.26.8", "@babel/eslint-parser": "7.27.1",
"@babel/plugin-transform-runtime": "7.26.9", "@babel/plugin-transform-runtime": "7.27.1",
"@babel/preset-env": "7.26.9", "@babel/preset-env": "7.27.2",
"@babel/register": "7.25.9", "@babel/register": "7.27.1",
"@ungap/event-target": "0.2.4", "@ungap/event-target": "0.2.4",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "^4.1.1",
"@vitest/browser": "^3.0.7", "@vitest/browser": "^3.0.7",
"@vitest/ui": "^3.0.7", "@vitest/ui": "^3.0.7",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.2.5", "@vue/babel-plugin-jsx": "1.4.0",
"@vue/compiler-sfc": "3.5.13", "@vue/compiler-sfc": "3.5.17",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.21",
"babel-plugin-lodash": "3.3.4", "babel-plugin-lodash": "3.3.4",
"chai": "4.5.0", "chai": "5.2.0",
"chalk": "5.4.1", "chalk": "5.4.1",
"chromedriver": "133.0.2", "chromedriver": "135.0.4",
"connect-history-api-fallback": "2.0.0", "connect-history-api-fallback": "2.0.0",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"custom-event-polyfill": "1.0.7", "custom-event-polyfill": "1.0.7",
"eslint": "9.20.1", "eslint": "9.26.0",
"vue-eslint-parser": "10.1.3",
"eslint-config-standard": "17.1.0", "eslint-config-standard": "17.1.0",
"eslint-formatter-friendly": "7.0.0", "eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-n": "17.15.1", "eslint-plugin-n": "17.18.0",
"eslint-plugin-promise": "7.2.1", "eslint-plugin-promise": "7.2.1",
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "10.1.0",
"eventsource-polyfill": "0.9.6", "eventsource-polyfill": "0.9.6",
"express": "4.21.2", "express": "5.1.0",
"function-bind": "1.1.2", "function-bind": "1.1.2",
"http-proxy-middleware": "3.0.3", "http-proxy-middleware": "3.0.5",
"iso-639-1": "3.1.5", "iso-639-1": "3.1.5",
"lodash": "4.17.21", "lodash": "4.17.21",
"nightwatch": "2.6.25", "msw": "2.10.2",
"opn": "5.5.0", "nightwatch": "3.12.1",
"ora": "0.4.1", "playwright": "1.52.0",
"playwright": "1.49.1", "postcss": "8.5.3",
"postcss": "8.5.2",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6", "postcss-scss": "^4.0.6",
"sass": "1.85.0", "sass": "1.89.2",
"selenium-server": "3.141.59", "selenium-server": "3.141.59",
"semver": "7.7.1", "semver": "7.7.2",
"serve-static": "1.16.2", "serve-static": "2.2.0",
"shelljs": "0.8.5", "shelljs": "0.10.0",
"sinon": "15.2.0", "sinon": "20.0.0",
"sinon-chai": "3.7.0", "sinon-chai": "4.0.0",
"stylelint": "14.16.1", "stylelint": "16.19.1",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-recommended-scss": "^8.0.0", "stylelint-config-recommended": "^16.0.0",
"stylelint-config-recommended-vue": "^1.4.0", "stylelint-config-recommended-scss": "^14.0.0",
"stylelint-config-standard": "29.0.0", "stylelint-config-recommended-vue": "^1.6.0",
"stylelint-rscss": "0.4.0", "stylelint-config-standard": "38.0.0",
"vite": "^6.1.0", "vite": "^6.1.0",
"vite-plugin-eslint2": "^5.0.3", "vite-plugin-eslint2": "^5.0.3",
"vite-plugin-stylelint": "^6.0.0", "vite-plugin-stylelint": "^6.0.0",

View file

@ -25,7 +25,7 @@
accent: #1CA4F3; accent: #1CA4F3;
cBlue: #1CA4F3; cBlue: #1CA4F3;
cRed: #f41a51; cRed: #f41a51;
cGreen: #1af46e; cGreen: #0b6a30;
cOrange: #f4af1a; cOrange: #f4af1a;
border: #d8e6f9; border: #d8e6f9;
link: #1CA4F3; link: #1CA4F3;
@ -39,7 +39,7 @@
accent: #1CA4F3; accent: #1CA4F3;
cRed: #f41a51; cRed: #f41a51;
cBlue: #1CA4F3; cBlue: #1CA4F3;
cGreen: #1af46e; cGreen: #0b6a30;
cOrange: #f4af1a; cOrange: #f4af1a;
} }
@ -47,45 +47,46 @@ Root {
--badgeNotification: color | --cRed; --badgeNotification: color | --cRed;
--buttonDefaultHoverGlow: shadow | inset 0 0 0 1 --accent / 1; --buttonDefaultHoverGlow: shadow | inset 0 0 0 1 --accent / 1;
--buttonDefaultFocusGlow: shadow | inset 0 0 0 1 --accent / 1; --buttonDefaultFocusGlow: shadow | inset 0 0 0 1 --accent / 1;
--buttonDefaultShadow: shadow | inset 0 0 0 1 --parent--text / 0.35, 0 5 5 -5 #000000 / 0.35; --buttonDefaultShadow: shadow | inset 0 0 0 1 --text / 0.35, 0 5 5 -5 #000000 / 0.35;
--buttonDefaultBevel: shadow | inset 0 14 14 -14 #FFFFFF / 0.1; --buttonDefaultBevel: shadow | inset 0 14 14 -14 #FFFFFF / 0.1;
--buttonPressedBevel: shadow | inset 0 -20 20 -20 #000000 / 0.05; --buttonPressedBevel: shadow | inset 0 -20 20 -20 #000000 / 0.05;
--defaultInputBevel: shadow | inset 0 0 0 1 --parent--text / 0.35; --defaultInputBevel: shadow | inset 0 0 0 1 --text / 0.35;
--defaultInputHoverGlow: shadow | 0 0 0 1 --accent / 1; --defaultInputHoverGlow: shadow | 0 0 0 1 --accent / 1;
--defaultInputFocusGlow: shadow | 0 0 0 1 --link / 1; --defaultInputFocusGlow: shadow | 0 0 0 1 --link / 1;
} }
Button {
background: --parent;
}
Button:disabled { Button:disabled {
shadow: --buttonDefaultBevel, --buttonDefaultShadow shadow: --buttonDefaultBevel, --buttonDefaultShadow
} }
Button:hover { Button:hover {
background: --inheritedBackground;
shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow
} }
Button:toggled { Button:toggled {
background: $blend(--bg 0.3 --accent) background: $blend(--inheritedBackground 0.3 --accent)
} }
Button:pressed { Button:pressed {
background: $blend(--bg 0.8 --accent) background: $blend(--inheritedBackground 0.8 --accent)
} }
Button:pressed:toggled { Button:pressed:toggled {
background: $blend(--bg 0.2 --accent) background: $blend(--inheritedBackground 0.2 --accent)
} }
Button:toggled:hover { Button:toggled:hover {
background: $blend(--bg 0.3 --accent) background: $blend(--inheritedBackground 0.3 --accent)
}
Button {
background: --parent;
} }
Input { Input {
shadow: --defaultInputBevel; shadow: --defaultInputBevel;
background: --parent; background: $mod(--bg -10);
} }
PanelHeader { PanelHeader {
@ -97,5 +98,5 @@ Tab:hover {
} }
Tab { Tab {
background: --parent; background: --bg;
} }

View file

@ -129,6 +129,21 @@ PanelHeader {
background: --fg background: --fg
} }
PanelHeader ButtonUnstyled Icon {
textColor: --text;
textAuto: 'no-preserve'
}
PanelHeader Button Icon {
textColor: --text;
textAuto: 'no-preserve'
}
PanelHeader Button Text {
textColor: --text;
textAuto: 'no-preserve'
}
Tab:hover { Tab:hover {
background: --bg; background: --bg;
shadow: --buttonDefaultBevel shadow: --buttonDefaultBevel
@ -172,6 +187,14 @@ MenuItem:hover {
background: --fg background: --fg
} }
MenuItem:active {
background: --fg
}
MenuItem:active:hover {
background: --fg
}
Popover { Popover {
shadow: --buttonDefaultBevel, 5 5 0 0 #000000 / 0.2; shadow: --buttonDefaultBevel, 5 5 0 0 #000000 / 0.2;
roundness: 0 roundness: 0

View file

@ -21,6 +21,8 @@ import { defineAsyncComponent } from 'vue'
import { useShoutStore } from './stores/shout' import { useShoutStore } from './stores/shout'
import { useInterfaceStore } from './stores/interface' import { useInterfaceStore } from './stores/interface'
import { throttle } from 'lodash'
export default { export default {
name: 'app', name: 'app',
components: { components: {
@ -51,6 +53,9 @@ export default {
themeApplied () { themeApplied () {
this.removeSplash() this.removeSplash()
}, },
currentTheme () {
this.setThemeBodyClass()
},
layoutType () { layoutType () {
document.getElementById('modal').classList = ['-' + this.layoutType] document.getElementById('modal').classList = ['-' + this.layoutType]
} }
@ -59,22 +64,41 @@ export default {
// Load the locale from the storage // Load the locale from the storage
const val = this.$store.getters.mergedConfig.interfaceLanguage const val = this.$store.getters.mergedConfig.interfaceLanguage
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
window.addEventListener('resize', this.updateMobileState)
document.getElementById('modal').classList = ['-' + this.layoutType] document.getElementById('modal').classList = ['-' + this.layoutType]
// Create bound handlers
this.updateScrollState = throttle(this.scrollHandler, 200)
this.updateMobileState = throttle(this.resizeHandler, 200)
}, },
mounted () { mounted () {
window.addEventListener('resize', this.updateMobileState)
this.scrollParent.addEventListener('scroll', this.updateScrollState)
if (useInterfaceStore().themeApplied) { if (useInterfaceStore().themeApplied) {
this.setThemeBodyClass()
this.removeSplash() this.removeSplash()
} }
getOrCreateServiceWorker() getOrCreateServiceWorker()
}, },
unmounted () { unmounted () {
window.removeEventListener('resize', this.updateMobileState) window.removeEventListener('resize', this.updateMobileState)
this.scrollParent.removeEventListener('scroll', this.updateScrollState)
}, },
computed: { computed: {
themeApplied () { themeApplied () {
return useInterfaceStore().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 () { layoutModalClass () {
return '-' + this.layoutType return '-' + this.layoutType
}, },
@ -148,13 +172,42 @@ export default {
}, },
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars }, showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
scrollParent () { return window; /* this.$refs.appContentRef */ },
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
methods: { methods: {
updateMobileState () { resizeHandler () {
useInterfaceStore().setLayoutWidth(windowWidth()) useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight()) 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 () { removeSplash () {
document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4)) document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4))
const splashscreenRoot = document.querySelector('#splash') const splashscreenRoot = document.querySelector('#splash')

View file

@ -34,8 +34,7 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
overscroll-behavior-y: none; overscroll-behavior-y: none;
overflow-x: clip; overflow: clip scroll;
overflow-y: scroll;
&.hidden { &.hidden {
display: none; display: none;
@ -224,9 +223,8 @@ nav {
grid-template-rows: 1fr; grid-template-rows: 1fr;
box-sizing: border-box; box-sizing: border-box;
margin: 0 auto; margin: 0 auto;
align-content: flex-start; place-content: flex-start center;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center;
min-height: 100vh; min-height: 100vh;
overflow-x: clip; overflow-x: clip;
@ -262,8 +260,7 @@ nav {
position: sticky; position: sticky;
top: var(--navbar-height); top: var(--navbar-height);
max-height: calc(100vh - var(--navbar-height)); max-height: calc(100vh - var(--navbar-height));
overflow-y: auto; overflow: hidden auto;
overflow-x: hidden;
margin-left: calc(var(--___paddingIncrease) * -1); margin-left: calc(var(--___paddingIncrease) * -1);
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2); padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
@ -388,6 +385,10 @@ nav {
&:disabled { &:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
&:active {
transform: translate(1px, 1px);
}
} }
.menu-item { .menu-item {
@ -828,7 +829,7 @@ option {
.login-hint { .login-hint {
text-align: center; text-align: center;
@media all and (min-width: 801px) { @media all and (width >= 801px) {
display: none; display: none;
} }
@ -850,7 +851,7 @@ option {
flex: 1; flex: 1;
} }
@media all and (max-width: 800px) { @media all and (width <= 800px) {
.mobile-hidden { .mobile-hidden {
display: none; display: none;
} }

View file

@ -16,6 +16,7 @@
<Notifications v-if="currentUser" /> <Notifications v-if="currentUser" />
<div <div
id="content" id="content"
ref="appContentRef"
class="app-layout container" class="app-layout container"
:class="classes" :class="classes"
> >

View file

@ -1,3 +1,4 @@
/* global process */
import { createApp } from 'vue' import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3' import vClickOutside from 'click-outside-vue3'
@ -11,12 +12,12 @@ import routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock' import VBodyScrollLock from 'src/directives/body_scroll_lock'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { applyConfig } from '../services/style_setter/style_setter.js' import { applyConfig } from '../services/style_setter/style_setter.js'
import FaviconService from '../services/favicon_service/favicon_service.js' import FaviconService from '../services/favicon_service/favicon_service.js'
import { initServiceWorker, updateFocus } from '../services/sw/sw.js' import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
import { useOAuthStore } from 'src/stores/oauth'
import { useI18nStore } from 'src/stores/i18n' import { useI18nStore } from 'src/stores/i18n'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
import { useAnnouncementsStore } from 'src/stores/announcements' import { useAnnouncementsStore } from 'src/stores/announcements'
@ -62,10 +63,11 @@ const getInstanceConfig = async ({ store }) => {
const textlimit = data.max_toot_chars const textlimit = data.max_toot_chars
const vapidPublicKey = data.pleroma.vapid_public_key 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: 'textlimit', value: textlimit })
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma.metadata.birthday_required }) store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma?.metadata.birthday_required })
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma.metadata.birthday_min_age || 0 }) store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma?.metadata.birthday_min_age || 0 })
if (vapidPublicKey) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
@ -77,6 +79,8 @@ const getInstanceConfig = async ({ store }) => {
console.error('Could not load instance config, potentially fatal') console.error('Could not load instance config, potentially fatal')
console.error(error) 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 () => { const getBackendProvidedConfig = async () => {
@ -228,17 +232,9 @@ const getStickers = async ({ store }) => {
} }
const getAppSecret = async ({ store }) => { const getAppSecret = async ({ store }) => {
const { state, commit } = store const oauth = useOAuthStore()
const { oauth, instance } = state
if (oauth.userToken) { if (oauth.userToken) {
commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) store.commit('setBackendInteractor', backendInteractorService(oauth.getToken))
} else {
return getOrCreateApp({ ...oauth, instance: instance.server, commit })
.then((app) => getClientToken({ ...app, instance: instance.server }))
.then((token) => {
commit('setAppToken', token.access_token)
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
})
} }
} }
@ -249,7 +245,8 @@ const resolveStaffAccounts = ({ store, accounts }) => {
const getNodeInfo = async ({ store }) => { const getNodeInfo = async ({ store }) => {
try { 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) { if (res.ok) {
const data = await res.json() const data = await res.json()
const metadata = data.metadata const metadata = data.metadata
@ -269,6 +266,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') }) 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: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances ?? [] })
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
@ -287,7 +285,6 @@ const getNodeInfo = async ({ store }) => {
const software = data.software const software = data.software
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository }) store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
const priv = metadata.private const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv }) store.dispatch('setInstanceOption', { name: 'private', value: priv })
@ -332,20 +329,65 @@ const setConfig = async ({ store }) => {
const apiConfig = configInfos[0] const apiConfig = configInfos[0]
const staticConfig = configInfos[1] const staticConfig = configInfos[1]
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store })) getAppSecret({ store })
await setSettings({ store, apiConfig, staticConfig })
} }
const checkOAuthToken = async ({ store }) => { const checkOAuthToken = async ({ store }) => {
if (store.getters.getUserToken()) { const oauth = useOAuthStore()
return store.dispatch('loginUser', store.getters.getUserToken()) if (oauth.getUserToken) {
return store.dispatch('loginUser', oauth.getUserToken)
} }
return Promise.resolve() return Promise.resolve()
} }
const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
const app = createApp(App) const app = createApp(App)
// Must have app use pinia before we do anything that touches the store
// https://pinia.vuejs.org/core-concepts/plugins.html#Introduction
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
app.use(pinia) app.use(pinia)
const waitForAllStoresToLoad = async () => {
// the stores that do not persist technically do not need to be awaited here,
// but that involves either hard-coding the stores in some place (prone to errors)
// or writing another vite plugin to analyze which stores needs persisting (++load time)
const allStores = import.meta.glob('../stores/*.js', { eager: true })
if (process.env.NODE_ENV === 'development') {
// do some checks to avoid common errors
if (!Object.keys(allStores).length) {
throw new Error('No stores are available. Check the code in src/boot/after_store.js')
}
}
await Promise.all(
Object.entries(allStores)
.map(async ([name, mod]) => {
const isStoreName = name => name.startsWith('use')
if (process.env.NODE_ENV === 'development') {
if (Object.keys(mod).filter(isStoreName).length !== 1) {
throw new Error('Each store file must export exactly one store as a named export. Check your code in src/stores/')
}
}
const storeFuncName = Object.keys(mod).find(isStoreName)
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
const p = mod[storeFuncName]().$persistLoaded
if (!(p instanceof Promise)) {
throw new Error(`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`)
}
await p
} else {
throw new Error(`Store module ${name} does not export a 'use...' function`)
}
}))
}
try {
await waitForAllStoresToLoad()
} catch (e) {
console.error('Cannot load stores:', e)
storageError = e
}
if (storageError) { if (storageError) {
useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' }) useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' })
} }

View file

@ -1,4 +1,5 @@
import PublicTimeline from 'components/public_timeline/public_timeline.vue' 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 PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue' import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
import TagTimeline from 'components/tag_timeline/tag_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: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'bubble', path: '/bubble', component: BubbleTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline }, { name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
{ {

View file

@ -56,7 +56,7 @@
.post-textarea { .post-textarea {
resize: vertical; resize: vertical;
height: 10em; height: 10em;
overflow: none; overflow: visible;
box-sizing: content-box; box-sizing: content-box;
} }
} }

View file

@ -177,7 +177,8 @@
.text { .text {
flex: 2; flex: 2;
margin: 8px; margin: 8px;
word-break: break-all; overflow-wrap: break-word;
text-wrap: pretty;
h1 { h1 {
font-size: 1rem; font-size: 1rem;

View file

@ -15,7 +15,9 @@ export default {
}, },
{ {
component: 'Button', component: 'Button',
parent: { component: 'Attachment' }, parent: {
component: 'Attachment'
},
directives: { directives: {
background: '#FFFFFF', background: '#FFFFFF',
opacity: 0.5 opacity: 0.5

View 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

View file

@ -0,0 +1,9 @@
<template>
<Timeline
:title="$t('nav.bubble')"
:timeline="timeline"
:timeline-name="'bubble'"
/>
</template>
<script src="./bubble_timeline.js"></script>

View file

@ -9,9 +9,9 @@ export default {
// However, cascading still works, so resulting state will be result of merging of all relevant states/variants // However, cascading still works, so resulting state will be result of merging of all relevant states/variants
// normal: '' // normal state is implicitly added, it is always included // normal: '' // normal state is implicitly added, it is always included
toggled: '.toggled', toggled: '.toggled',
focused: ':focus-visible', focused: ':focus-within',
pressed: ':focus:active', pressed: ':focus:active',
hover: ':hover:not(:disabled)', hover: ':is(:hover, :focus-visible):not(:disabled)',
disabled: ':disabled' disabled: ':disabled'
}, },
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it. // Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
@ -89,6 +89,13 @@ export default {
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'] shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
} }
}, },
{
state: ['toggled', 'focused'],
directives: {
background: '--accent,-24.2',
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
}
},
{ {
state: ['toggled', 'disabled'], state: ['toggled', 'disabled'],
directives: { directives: {
@ -99,7 +106,7 @@ export default {
{ {
state: ['disabled'], state: ['disabled'],
directives: { directives: {
background: '$blend(--accent 0.25 --parent)', background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--buttonDefaultBevel'] shadow: ['--buttonDefaultBevel']
} }
}, },

View file

@ -6,8 +6,8 @@ export default {
states: { states: {
toggled: '.toggled', toggled: '.toggled',
disabled: ':disabled', disabled: ':disabled',
hover: ':hover:not(:disabled)', hover: ':is(:hover, :focus-visible):not(:disabled)',
focused: ':focus-within' focused: ':focus-within:not(:is(:focus-visible))'
}, },
validInnerComponents: [ validInnerComponents: [
'Text', 'Text',

View file

@ -17,7 +17,6 @@
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
word-wrap: break-word;
} }
.heading { .heading {

View file

@ -107,8 +107,7 @@
.outgoing { .outgoing {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
align-content: end; place-content: end flex-end;
justify-content: flex-end;
.chat-message-inner { .chat-message-inner {
align-items: flex-end; align-items: flex-end;

View file

@ -39,7 +39,6 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
display: inline; display: inline;
word-wrap: break-word;
overflow: hidden; overflow: hidden;
} }

View file

@ -1,7 +1,7 @@
<template> <template>
<label <label
class="checkbox" class="checkbox"
:class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }" :class="[{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }, radio ? '-radio' : '-checkbox']"
> >
<span <span
v-if="!!$slots.before" v-if="!!$slots.before"
@ -19,9 +19,9 @@
@change="$emit('update:modelValue', $event.target.checked)" @change="$emit('update:modelValue', $event.target.checked)"
> >
<i <i
class="input -checkbox checkbox-indicator" class="input checkbox-indicator"
:aria-hidden="true" :aria-hidden="true"
:class="{ disabled }" :class="[{ disabled }, radio ? '-radio' : '-checkbox']"
@transitionend.capture="onTransitionEnd" @transitionend.capture="onTransitionEnd"
/> />
<span <span
@ -37,6 +37,7 @@
<script> <script>
export default { export default {
props: [ props: [
'radio',
'modelValue', 'modelValue',
'indeterminate', 'indeterminate',
'disabled' 'disabled'
@ -107,6 +108,19 @@ export default {
box-sizing: border-box; box-sizing: border-box;
} }
&.-radio {
.checkbox-indicator {
&,
&::before {
border-radius: 9999px;
}
&::before {
content: "•";
}
}
}
.disabled { .disabled {
.checkbox-indicator::before { .checkbox-indicator::before {
background-color: var(--background); background-color: var(--background);

View file

@ -190,21 +190,16 @@ export default {
.header { .header {
grid-area: header; grid-area: header;
justify-self: center; place-self: baseline center;
align-self: baseline;
line-height: 2; line-height: 2;
} }
.invalid-container { .invalid-container {
position: absolute; position: absolute;
top: 0; inset: 0;
bottom: 0;
left: 0;
right: 0;
display: grid; display: grid;
align-items: center; place-items: center center;
justify-items: center; background-color: rgb(100 0 0 / 50%);
background-color: rgba(100 0 0 / 50%);
.alert { .alert {
padding: 0.5em 1em; padding: 0.5em 1em;
@ -214,7 +209,7 @@ export default {
.assists { .assists {
grid-area: assists; grid-area: assists;
display: grid; display: grid;
grid-auto-flow: rows; grid-auto-flow: row;
grid-auto-rows: 2em; grid-auto-rows: 2em;
grid-gap: 0.5em; grid-gap: 0.5em;
} }
@ -266,14 +261,14 @@ export default {
.preview-window { .preview-window {
--__grid-color1: rgb(102 102 102); --__grid-color1: rgb(102 102 102);
--__grid-color2: rgb(153 153 153); --__grid-color2: rgb(153 153 153);
--__grid-color1-disabled: rgba(102 102 102 / 20%); --__grid-color1-disabled: rgb(102 102 102 / 20%);
--__grid-color2-disabled: rgba(153 153 153 / 20%); --__grid-color2-disabled: rgb(153 153 153 / 20%);
&.-light-grid { &.-light-grid {
--__grid-color1: rgb(205 205 205); --__grid-color1: rgb(205 205 205);
--__grid-color2: rgb(255 255 255); --__grid-color2: rgb(255 255 255);
--__grid-color1-disabled: rgba(205 205 205 / 20%); --__grid-color1-disabled: rgb(205 205 205 / 20%);
--__grid-color2-disabled: rgba(255 255 255 / 20%); --__grid-color2-disabled: rgb(255 255 255 / 20%);
} }
position: relative; position: relative;

View file

@ -339,11 +339,6 @@ const conversation = {
canDive () { canDive () {
return this.isTreeView && this.isExpanded return this.isTreeView && this.isExpanded
}, },
focused () {
return (id) => {
return (this.isExpanded) && id === this.highlight
}
},
maybeHighlight () { maybeHighlight () {
return this.isExpanded ? this.highlight : null return this.isExpanded ? this.highlight : null
}, },
@ -406,6 +401,9 @@ const conversation = {
}) })
} }
}, },
isFocused (id) {
return (this.isExpanded) && id === this.highlight
},
getReplies (id) { getReplies (id) {
return this.replies[id] || [] return this.replies[id] || []
}, },

View file

@ -94,7 +94,7 @@
:statusoid="status" :statusoid="status"
:expandable="!isExpanded" :expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:focused="focused(status.id)" :focused="isFocused(status.id)"
:in-conversation="isExpanded" :in-conversation="isExpanded"
:highlight="getHighlight()" :highlight="getHighlight()"
:replies="getReplies(status.id)" :replies="getReplies(status.id)"
@ -168,7 +168,7 @@
:pinned-status-ids-object="pinnedStatusIdsObject" :pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId" :profile-user-id="profileUserId"
:focused="focused" :is-focused-function="isFocused"
:get-replies="getReplies" :get-replies="getReplies"
:highlight="maybeHighlight" :highlight="maybeHighlight"
:set-highlight="setHighlight" :set-highlight="setHighlight"
@ -199,7 +199,7 @@
:statusoid="status" :statusoid="status"
:expandable="!isExpanded" :expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:focused="focused(status.id)" :focused="isFocused(status.id)"
:in-conversation="isExpanded" :in-conversation="isExpanded"
:highlight="getHighlight()" :highlight="getHighlight()"
:replies="getReplies(status.id)" :replies="getReplies(status.id)"
@ -322,10 +322,7 @@
content: ""; content: "";
display: block; display: block;
position: absolute; position: absolute;
top: calc(var(--___margin) * -1); inset: calc(var(--___margin) * -1);
bottom: calc(var(--___margin) * -1);
left: calc(var(--___margin) * -1);
right: calc(var(--___margin) * -1);
background: var(--background); background: var(--background);
backdrop-filter: var(--__panel-backdrop-filter); backdrop-filter: var(--__panel-backdrop-filter);
} }

View file

@ -59,7 +59,7 @@
transition-timing-function: ease-out; transition-timing-function: ease-out;
transition-duration: 100ms; transition-duration: 100ms;
@media all and (min-width: 800px) { @media all and (width >= 800px) {
/* stylelint-disable-next-line declaration-no-important */ /* stylelint-disable-next-line declaration-no-important */
opacity: 1 !important; opacity: 1 !important;
} }
@ -70,10 +70,7 @@
mask-size: contain; mask-size: contain;
background-color: var(--text); background-color: var(--text);
position: absolute; position: absolute;
top: 0; inset: 0;
bottom: 0;
left: 0;
right: 0;
} }
img { img {

View file

@ -29,14 +29,11 @@
// TODO: unify with other modals. // TODO: unify with other modals.
.dark-overlay { .dark-overlay {
&::before { &::before {
bottom: 0; inset: 0;
content: " "; content: " ";
display: block; display: block;
cursor: default; cursor: default;
left: 0;
position: fixed; position: fixed;
right: 0;
top: 0;
background: rgb(27 31 35 / 50%); background: rgb(27 31 35 / 50%);
z-index: 2000; z-index: 2000;
} }
@ -45,13 +42,9 @@
.dialog-container { .dialog-container {
display: grid; display: grid;
position: fixed; position: fixed;
top: 0; inset: 0;
bottom: 0;
left: 0;
right: 0;
justify-content: center; justify-content: center;
align-items: center; place-items: center center;
justify-items: center;
} }
.dialog-modal.panel { .dialog-modal.panel {
@ -98,8 +91,7 @@
#modal.-mobile { #modal.-mobile {
.dialog-container { .dialog-container {
justify-content: stretch; justify-content: stretch;
align-items: end; place-items: end stretch;
justify-items: stretch;
&.-center-mobile { &.-center-mobile {
align-items: center; align-items: center;
@ -114,7 +106,6 @@
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-auto-columns: none;
grid-auto-rows: auto; grid-auto-rows: auto;
grid-auto-flow: row dense; grid-auto-flow: row dense;

View file

@ -127,7 +127,6 @@
max-width: 100%; max-width: 100%;
p { p {
word-wrap: break-word;
white-space: normal; white-space: normal;
overflow-x: hidden; overflow-x: hidden;
} }
@ -135,8 +134,7 @@
.poll-indicator-container { .poll-indicator-container {
border-radius: var(--roundness); border-radius: var(--roundness);
display: grid; display: grid;
justify-items: center; place-items: center center;
align-items: center;
align-self: start; align-self: start;
height: 0; height: 0;
padding-bottom: 62.5%; padding-bottom: 62.5%;
@ -147,13 +145,9 @@
box-sizing: border-box; box-sizing: border-box;
border: 1px solid var(--border); border: 1px solid var(--border);
position: absolute; position: absolute;
top: 0; inset: 0;
bottom: 0;
left: 0;
right: 0;
display: grid; display: grid;
justify-items: center; place-items: center center;
align-items: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

View file

@ -55,7 +55,7 @@
ref="suggestorPopover" ref="suggestorPopover"
class="autocomplete-panel" class="autocomplete-panel"
placement="bottom" placement="bottom"
:trigger-attrs="{ 'aria-hidden': true }" :hide-trigger="true"
> >
<template #content> <template #content>
<div <div
@ -159,10 +159,7 @@
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
top: 0; inset: 0;
bottom: 0;
right: 0;
left: 0;
overflow: hidden; overflow: hidden;
/* DEBUG STUFF */ /* DEBUG STUFF */

View file

@ -64,8 +64,7 @@
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row nowrap;
overflow-x: auto; overflow: auto hidden;
overflow-y: hidden;
} }
.additional-tabs { .additional-tabs {
@ -153,7 +152,13 @@
transition: mask-size 150ms; transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto; mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different // Autoprefixed seem to ignore this one, and also syntax is different
/* stylelint-disable mask-composite */
/* stylelint-disable declaration-property-value-no-unknown */
/* TODO check if this is still needed */
mask-composite: xor; mask-composite: xor;
/* stylelint-enable declaration-property-value-no-unknown */
/* stylelint-enable mask-composite */
mask-composite: exclude; mask-composite: exclude;
&.scrolled { &.scrolled {
@ -197,8 +202,7 @@
&-group { &-group {
display: grid; display: grid;
grid-template-columns: repeat(var(--__amount), 1fr); grid-template-columns: repeat(var(--__amount), 1fr);
align-items: center; place-items: center center;
justify-items: center;
justify-content: center; justify-content: center;
grid-template-rows: repeat(1, auto); grid-template-rows: repeat(1, auto);

View file

@ -3,7 +3,7 @@
ref="popover" ref="popover"
trigger="click" trigger="click"
popover-class="emoji-picker popover-default" popover-class="emoji-picker popover-default"
:trigger-attrs="{ 'aria-hidden': true, tabindex: -1 }" :hide-trigger="true"
@show="onPopoverShown" @show="onPopoverShown"
@close="onPopoverClosed" @close="onPopoverClosed"
> >

View file

@ -84,8 +84,6 @@ const EmojiReactions = {
counterTriggerAttrs (reaction) { counterTriggerAttrs (reaction) {
return { return {
class: [ class: [
'btn',
'button-default',
'emoji-reaction-count-button', 'emoji-reaction-count-button',
{ {
'-picked-reaction': this.reactedWith(reaction.name), '-picked-reaction': this.reactedWith(reaction.name),

View file

@ -52,6 +52,7 @@
<UserListPopover <UserListPopover
:users="accountsForEmoji[reaction.name]" :users="accountsForEmoji[reaction.name]"
class="emoji-reaction-popover" class="emoji-reaction-popover"
:normal-button="true"
:trigger-attrs="counterTriggerAttrs(reaction)" :trigger-attrs="counterTriggerAttrs(reaction)"
@show="fetchEmojiReactionsByIfMissing()" @show="fetchEmojiReactionsByIfMissing()"
> >

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