Merge branch 'main' into feature-detect

This commit is contained in:
Scott Feeney 2024-11-11 15:36:06 -08:00
commit 7b926f73fb
63 changed files with 21154 additions and 19559 deletions

View file

@ -1,41 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
- Which site: [e.g. dev.phanpy.social OR phanpy.social]
- Which site version: [On Phanpy, go to Settings -> About]
- Which instance: [e.g. mastodon.social]
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

81
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,81 @@
name: "Bug report"
description: "Create a report to help us improve"
labels:
- "bug"
body:
- type: input
id: "site"
attributes:
label: "Site"
description: |-
What site(s) did you encounter this bug on?
placeholder: |-
phanpy.social
- type: input
id: "version"
attributes:
label: "Version"
description: |-
Which Phanpy version(s) did you encounter this bug on?
You can see and copy your current version by opening the Settings menu and scrolling down to the About section.
placeholder: |-
2024.10.08.0a176e2
- type: input
id: "instance"
attributes:
label: "Instance"
description: |-
Which instance(s) did you encounter this bug on?
placeholder: |-
mastodon.social
- type: textarea
id: "Browser"
attributes:
label: "Browser"
description: |-
Which browser(s) did you encounter this bug on?
placeholder: |-
- Firefox 132.0b5 on Windows 11
- Safari 18 on iOS 18 on iPhone 16 Pro Max
- type: textarea
id: "description"
attributes:
label: "Bug description"
description: |-
A concise description of what the bug is.
If applicable, add screenshots to help explain your problem.
You can paste screenshots here and GitHub will convert them to Markdown for you.
- type: textarea
id: "steps"
attributes:
label: "To reproduce"
description: |-
A list of steps that can be performed to make the bug happen again.
If possible, add screenshots to help demonstrate the steps.
You can paste screenshots here and GitHub will convert them to Markdown for you.
placeholder: |-
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
- type: textarea
id: "behavior"
attributes:
label: "Expected behavior"
description: |-
A concise description of what you expected to happen.
- type: textarea
id: "other"
attributes:
label: "Other"
description: |-
Anything you want to add?

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: true

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'enhancement'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -0,0 +1,37 @@
name: "Feature request"
description: "Suggest an idea for this project"
labels:
- "enhancement"
body:
- type: textarea
id: "problem"
attributes:
label: "Problem I have"
description: |-
If your request is related to a problem, please provide a clear and concise description of what the problem is.
placeholder: |-
I'm always frustrated when [...]
- type: textarea
id: "solution"
attributes:
label: "Solution I'd like"
description: |-
A clear and concise description of what you want to happen.
- type: textarea
id: "alternatives"
attributes:
label: "Alternatives considered"
description: |-
A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: "other"
attributes:
label: "Other"
description: |-
Anything you want to add?

View file

@ -259,18 +259,20 @@ See documentation for [lingva-translate](https://github.com/thedaviddelta/lingva
These are self-hosted by other wonderful folks. These are self-hosted by other wonderful folks.
- [ferengi.one](https://m.ferengi.one/) by [@david@weaknotes.com](https://weaknotes.com/@david) - [ferengi.one](https://m.ferengi.one/) by [@david@weaknotes.com](https://weaknotes.com/@david)
- [phanpy.blaede.family](https://phanpy.blaede.family/) by [@cassidy@blaede.family](https://mastodon.blaede.family/@cassidy)
- [phanpy.mstdn.mx](https://phanpy.mstdn.mx/) by [@maop@mstdn.mx](https://mstdn.mx/@maop)
- [phanpy.vmst.io](https://phanpy.vmst.io/) by [@vmstan@vmst.io](https://vmst.io/@vmstan)
- [phanpy.gotosocial.social](https://phanpy.gotosocial.social/) by [@admin@gotosocial.social](https://gotosocial.social/@admin)
- [phanpy.bauxite.tech](https://phanpy.bauxite.tech) by [@b4ux1t3@hachyderm.io](https://hachyderm.io/@b4ux1t3)
- [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin)
- [phanpy.fulda.social](https://phanpy.fulda.social) by [@Ganneff@fulda.social](https://fulda.social/@Ganneff)
- [phanpy.crmbl.uk](https://phanpy.crmbl.uk) by [@snail@crmbl.uk](https://mstdn.crmbl.uk/@snail)
- [halo.mookiesplace.com](https://halo.mookiesplace.com) by [@mookie@mookiesplace.com](https://mookiesplace.com/@mookie) - [halo.mookiesplace.com](https://halo.mookiesplace.com) by [@mookie@mookiesplace.com](https://mookiesplace.com/@mookie)
- [social.qrk.one](https://social.qrk.one) by [@kev@fosstodon.org](https://fosstodon.org/@kev) - [phanpy.bauxite.tech](https://phanpy.bauxite.tech) by [@b4ux1t3@hachyderm.io](https://hachyderm.io/@b4ux1t3)
- [phanpy.blaede.family](https://phanpy.blaede.family/) by [@cassidy@blaede.family](https://mastodon.blaede.family/@cassidy)
- [phanpy.crmbl.uk](https://phanpy.crmbl.uk) by [@snail@crmbl.uk](https://mstdn.crmbl.uk/@snail)
- [phanpy.cz](https://phanpy.cz) by [@zdendys@mamutovo.cz](https://mamutovo.cz/@zdendys) - [phanpy.cz](https://phanpy.cz) by [@zdendys@mamutovo.cz](https://mamutovo.cz/@zdendys)
- [phanpy.fulda.social](https://phanpy.fulda.social) by [@Ganneff@fulda.social](https://fulda.social/@Ganneff)
- [phanpy.gotosocial.social](https://phanpy.gotosocial.social/) by [@admin@gotosocial.social](https://gotosocial.social/@admin)
- [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin)
- [phanpy.mastodon.world](https://phanpy.mastodon.world) by [@ruud@mastodon.world](https://mastodon.world/@ruud)
- [phanpy.mstdn.mx](https://phanpy.mstdn.mx/) by [@maop@mstdn.mx](https://mstdn.mx/@maop)
- [phanpy.social.tchncs.de](https://phanpy.social.tchncs.de) by [@milan@social.tchncs.de](https://social.tchncs.de/@milan) - [phanpy.social.tchncs.de](https://phanpy.social.tchncs.de) by [@milan@social.tchncs.de](https://social.tchncs.de/@milan)
- [phanpy.tilde.zone](https://phanpy.tilde.zone) by [@ben@tilde.zone](https://tilde.zone/@ben)
- [phanpy.vmst.io](https://phanpy.vmst.io/) by [@vmstan@vmst.io](https://vmst.io/@vmstan)
- [social.qrk.one](https://social.qrk.one) by [@kev@fosstodon.org](https://fosstodon.org/@kev)
> Note: Add yours by creating a pull request. > Note: Add yours by creating a pull request.
@ -303,7 +305,7 @@ Costs involved in running and developing this web app:
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15525631/medium/51293156034d0236f1a1020c10f7d539_default.png" alt="" width="16" height="16" /> cbo92 (French) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/15525631/medium/51293156034d0236f1a1020c10f7d539_default.png" alt="" width="16" height="16" /> cbo92 (French)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15910131/medium/67fab7eeab5551853450e76e2ef19e59.jpeg" alt="" width="16" height="16" /> CDN (Chinese Simplified) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/15910131/medium/67fab7eeab5551853450e76e2ef19e59.jpeg" alt="" width="16" height="16" /> CDN (Chinese Simplified)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16556801/medium/ed5e501ca1f3cc6525d2da28db646346.jpeg" alt="" width="16" height="16" /> dannypsnl (Chinese Traditional) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16556801/medium/ed5e501ca1f3cc6525d2da28db646346.jpeg" alt="" width="16" height="16" /> dannypsnl (Chinese Traditional)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/3711/medium/d95ddd44e8dcb3a039f8a3463aed781d_default.png" alt="" width="16" height="16" /> databio (Catalan) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/3711/medium/c97239bb54623a50eb43cc6b801bb156.jpg" alt="" width="16" height="16" /> databio (Catalan)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16533843/medium/ac7af8776858a992d992cf6702d1aaae.jpg" alt="" width="16" height="16" /> Dizro (Italian) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16533843/medium/ac7af8776858a992d992cf6702d1aaae.jpg" alt="" width="16" height="16" /> Dizro (Italian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16574625/medium/f2ac3a4f32f104a3a6d4085d4bcb3924_default.png" alt="" width="16" height="16" /> Drift6944 (Czech) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16574625/medium/f2ac3a4f32f104a3a6d4085d4bcb3924_default.png" alt="" width="16" height="16" /> Drift6944 (Czech)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12618120/medium/ccb11bd042bbf4c7189033f7af2dbd32_default.png" alt="" width="16" height="16" /> drydenwu (Chinese Traditional) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/12618120/medium/ccb11bd042bbf4c7189033f7af2dbd32_default.png" alt="" width="16" height="16" /> drydenwu (Chinese Traditional)
@ -315,11 +317,13 @@ Costs involved in running and developing this web app:
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15248754/medium/0dac6334ea0f4e8d4194a605c0a5594a.jpeg" alt="" width="16" height="16" /> hongminhee (Korean) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/15248754/medium/0dac6334ea0f4e8d4194a605c0a5594a.jpeg" alt="" width="16" height="16" /> hongminhee (Korean)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16529833/medium/2122d0c5d61c00786ab6d5e5672d4098.png" alt="" width="16" height="16" /> Hugoglyph (Esperanto, Spanish) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16529833/medium/2122d0c5d61c00786ab6d5e5672d4098.png" alt="" width="16" height="16" /> Hugoglyph (Esperanto, Spanish)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13454728/medium/1f78b7124b3c962bc4ae55e8d701fc91_default.png" alt="" width="16" height="16" /> isard (Catalan) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/13454728/medium/1f78b7124b3c962bc4ae55e8d701fc91_default.png" alt="" width="16" height="16" /> isard (Catalan)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16646485/medium/5d76c44212a4048a815ab437fb170856_default.png" alt="" width="16" height="16" /> kaliuwu (Polish)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16532403/medium/4cefb19623bcc44d7cdb9e25aebf5250.jpeg" alt="" width="16" height="16" /> karlafej (Czech) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16532403/medium/4cefb19623bcc44d7cdb9e25aebf5250.jpeg" alt="" width="16" height="16" /> karlafej (Czech)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15791971/medium/88bdda3090339f16f6083390d32bb434_default.png" alt="" width="16" height="16" /> katullo11 (Italian) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/15791971/medium/88bdda3090339f16f6083390d32bb434_default.png" alt="" width="16" height="16" /> katullo11 (Italian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14677260/medium/e53420d200961f48602324e18c091bdc.png" alt="" width="16" height="16" /> Kytta (German) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/14677260/medium/e53420d200961f48602324e18c091bdc.png" alt="" width="16" height="16" /> Kytta (German)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16529521/medium/ae6add93a901b0fefa2d9b1077920d73.png" alt="" width="16" height="16" /> llun (Thai) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16529521/medium/ae6add93a901b0fefa2d9b1077920d73.png" alt="" width="16" height="16" /> llun (Thai)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16291756/medium/2366972cc86287353708aff1ded3f3c1.jpg" alt="" width="16" height="16" /> lucasofchirst (Occitan, Portuguese, Portuguese, Brazilian) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16291756/medium/c008af10bc117fa9c9dcb70f2b291ee6.jpg" alt="" width="16" height="16" /> lucasofchirst (Occitan, Portuguese, Portuguese, Brazilian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16640089/medium/4b7d8d275d7a7bff564adde51e09b473_default.png" alt="" width="16" height="16" /> LukeHong (Chinese Traditional)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16537713/medium/825f0bf1a14fc545a76891a52839d86e_default.png" alt="" width="16" height="16" /> marcin.kozinski (Polish) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16537713/medium/825f0bf1a14fc545a76891a52839d86e_default.png" alt="" width="16" height="16" /> marcin.kozinski (Polish)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13521465/medium/76cb9aa6b753ce900a70478bff7fcea0.png" alt="" width="16" height="16" /> mkljczkk (Polish) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/13521465/medium/76cb9aa6b753ce900a70478bff7fcea0.png" alt="" width="16" height="16" /> mkljczkk (Polish)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12882812/medium/77744d8db46e9a3e09030e1a02b7a572.jpeg" alt="" width="16" height="16" /> mojosoeun (Korean) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/12882812/medium/77744d8db46e9a3e09030e1a02b7a572.jpeg" alt="" width="16" height="16" /> mojosoeun (Korean)
@ -337,7 +341,7 @@ Costs involved in running and developing this web app:
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16539171/medium/0ce95ef6b3b0566136191fbedc1563d0.png" alt="" width="16" height="16" /> SadmL_AI (Russian) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16539171/medium/0ce95ef6b3b0566136191fbedc1563d0.png" alt="" width="16" height="16" /> SadmL_AI (Russian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12381015/medium/35e3557fd61d85f9a5b84545d9e3feb4.png" alt="" width="16" height="16" /> shuuji3 (Japanese) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/12381015/medium/35e3557fd61d85f9a5b84545d9e3feb4.png" alt="" width="16" height="16" /> shuuji3 (Japanese)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14565190/medium/79100599131b7776e9803e4b696915a3_default.png" alt="" width="16" height="16" /> Sky_NiniKo (French) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/14565190/medium/79100599131b7776e9803e4b696915a3_default.png" alt="" width="16" height="16" /> Sky_NiniKo (French)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13143526/medium/2f15fa6d8e1703c7b82bb608b116a30a.png" alt="" width="16" height="16" /> Steffo99 (Italian) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/13143526/medium/30871da23d51d7e41bb02f3c92d7f104.png" alt="" width="16" height="16" /> Steffo99 (Italian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16532441/medium/1a47e8d80c95636e02d2260f6e233ca5.png" alt="" width="16" height="16" /> Su5hicz (Czech) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16532441/medium/1a47e8d80c95636e02d2260f6e233ca5.png" alt="" width="16" height="16" /> Su5hicz (Czech)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16530049/medium/683f3581620c6b4a5c753b416ed695a7.jpeg" alt="" width="16" height="16" /> tferrermo (Spanish) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/16530049/medium/683f3581620c6b4a5c753b416ed695a7.jpeg" alt="" width="16" height="16" /> tferrermo (Spanish)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15752199/medium/7e9efd828c4691368d063b19d19eb894.png" alt="" width="16" height="16" /> tkbremnes (Norwegian Bokmal) - <img src="https://crowdin-static.downloads.crowdin.com/avatar/15752199/medium/7e9efd828c4691368d063b19d19eb894.png" alt="" width="16" height="16" /> tkbremnes (Norwegian Bokmal)

694
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,15 +16,15 @@
"readme:i18n-volunteers": "node scripts/update-i18n-volunteers-readme.js" "readme:i18n-volunteers": "node scripts/update-i18n-volunteers-readme.js"
}, },
"dependencies": { "dependencies": {
"@formatjs/intl-localematcher": "~0.5.4", "@formatjs/intl-localematcher": "~0.5.5",
"@formatjs/intl-segmenter": "~11.5.7", "@formatjs/intl-segmenter": "~11.6.0",
"@formkit/auto-animate": "~0.8.2", "@formkit/auto-animate": "~0.8.2",
"@github/text-expander-element": "~2.7.2", "@github/text-expander-element": "~2.8.0",
"@iconify-icons/mingcute": "~1.2.9", "@iconify-icons/mingcute": "~1.2.9",
"@justinribeiro/lite-youtube": "~1.5.0", "@justinribeiro/lite-youtube": "~1.5.0",
"@lingui/detect-locale": "~4.11.4", "@lingui/detect-locale": "~4.13.0",
"@lingui/macro": "~4.11.4", "@lingui/macro": "~4.13.0",
"@lingui/react": "~4.11.4", "@lingui/react": "~4.13.0",
"@szhsin/react-menu": "~4.2.2", "@szhsin/react-menu": "~4.2.2",
"compare-versions": "~6.1.1", "compare-versions": "~6.1.1",
"fast-blurhash": "~1.1.4", "fast-blurhash": "~1.1.4",
@ -36,11 +36,11 @@
"js-cookie": "~3.0.5", "js-cookie": "~3.0.5",
"just-debounce-it": "~3.2.0", "just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0", "lz-string": "~1.5.0",
"masto": "~6.8.0", "masto": "~6.10.1",
"moize": "~6.1.6", "moize": "~6.1.6",
"p-retry": "~6.2.0", "p-retry": "~6.2.0",
"p-throttle": "~6.2.0", "p-throttle": "~6.2.0",
"preact": "~10.24.2", "preact": "~10.24.3",
"punycode": "~2.3.1", "punycode": "~2.3.1",
"react-hotkeys-hook": "~4.5.1", "react-hotkeys-hook": "~4.5.1",
"react-intersection-observer": "~9.13.1", "react-intersection-observer": "~9.13.1",
@ -51,22 +51,22 @@
"tinyld": "~1.3.4", "tinyld": "~1.3.4",
"toastify-js": "~1.12.0", "toastify-js": "~1.12.0",
"uid": "~2.0.2", "uid": "~2.0.2",
"use-debounce": "~10.0.3", "use-debounce": "~10.0.4",
"use-long-press": "~3.2.0", "use-long-press": "~3.2.0",
"use-resize-observer": "~9.1.0", "use-resize-observer": "~9.1.0",
"valtio": "2.0.0" "valtio": "2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "~4.3.1", "@ianvs/prettier-plugin-sort-imports": "~4.3.1",
"@lingui/cli": "~4.11.4", "@lingui/cli": "~4.13.0",
"@lingui/vite-plugin": "~4.11.4", "@lingui/vite-plugin": "~4.13.0",
"@preact/preset-vite": "~2.9.1", "@preact/preset-vite": "~2.9.1",
"babel-plugin-macros": "~3.1.0", "babel-plugin-macros": "~3.1.0",
"postcss": "~8.4.47", "postcss": "~8.4.47",
"postcss-dark-theme-class": "~1.3.0", "postcss-dark-theme-class": "~1.3.0",
"postcss-preset-env": "~10.0.6", "postcss-preset-env": "~10.0.8",
"twitter-text": "~3.1.0", "twitter-text": "~3.1.0",
"vite": "~5.4.8", "vite": "~5.4.10",
"vite-plugin-generate-file": "~0.2.0", "vite-plugin-generate-file": "~0.2.0",
"vite-plugin-html-config": "~2.0.2", "vite-plugin-html-config": "~2.0.2",
"vite-plugin-pwa": "~0.20.5", "vite-plugin-pwa": "~0.20.5",

View file

@ -371,7 +371,8 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
content-visibility: auto; content-visibility: auto;
contain-intrinsic-size: auto 160px; contain-intrinsic-size: auto 160px;
} }
.timeline.contextual > li:is(:hover, :focus-visible) { .timeline.contextual
> li:is(:hover, :focus-visible, :focus-within, :has(.status-menu-open)) {
/* Needed to undo the overflow: hidden "effect" due to "content-visibility: auto" */ /* Needed to undo the overflow: hidden "effect" due to "content-visibility: auto" */
content-visibility: visible !important; content-visibility: visible !important;
} }
@ -380,10 +381,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
} }
.timeline.contextual { .timeline.contextual {
--thread-start: 40px; --indent-large-start: 40px;
--line-start: 40px; --indent-small-start: 10px;
--thread-start: var(--indent-small-start);
--line-start: var(--indent-small-start);
--line-width: 3px; --line-width: 3px;
--line-end: calc(var(--line-start) + var(--line-width)); --line-end: calc(var(--line-start) + var(--line-width));
--indent-large-end: calc(var(--indent-large-start) + var(--line-width));
--indent-small-end: calc(var(--indent-small-start) + var(--line-width));
--line-margin-end: 16px; --line-margin-end: 16px;
--line-radius: 10px; --line-radius: 10px;
--line-diameter: calc(var(--line-radius) * 2); --line-diameter: calc(var(--line-radius) * 2);
@ -394,6 +399,68 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
:dir(rtl) & { :dir(rtl) & {
--line-curve: -45deg; --line-curve: -45deg;
} }
> li:is(.hero:has(+ .thread), .hero:last-child, .thread, .ancestor) {
--thread-start: var(--indent-large-start);
--line-start: var(--indent-large-start);
--line-end: calc(var(--line-start) + var(--line-width));
}
> li.descendant.thread:has(+ .descendant:not(.thread)):after {
position: absolute;
inset-inline-start: 10px;
bottom: 0;
content: '';
display: block;
--curves-width: calc(var(--line-start) + var(--line-width) - 10px);
width: var(--curves-width);
background-color: var(--bg-color);
background-repeat: no-repeat;
/* border-bottom: var(--line-width) dotted var(--comment-line-color); */
/* height: calc(var(--line-diameter) - var(--line-width));
background-image: linear-gradient(
transparent calc(var(--line-radius) - var(--line-width)),
var(--comment-line-color) calc(var(--line-radius) - var(--line-width))
var(--line-radius),
transparent var(--line-radius)
),
radial-gradient(
circle at bottom var(--forward),
transparent calc(var(--line-radius) - var(--line-width)),
var(--comment-line-color) calc(var(--line-radius) - var(--line-width))
var(--line-radius),
transparent var(--line-radius)
),
radial-gradient(
circle at top var(--backward),
transparent calc(var(--line-radius) - var(--line-width)),
var(--comment-line-color) calc(var(--line-radius) - var(--line-width))
var(--line-radius),
transparent var(--line-radius)
);
background-position: var(--line-radius) 0, bottom var(--backward),
top var(--forward);
background-size: calc(100% - var(--line-radius) * 2) auto,
var(--line-radius), var(--line-radius); */
--curves-radius: calc(var(--curves-width) / 2);
height: calc(var(--curves-width) - var(--line-width));
background-image: radial-gradient(
circle at bottom var(--forward),
transparent calc(var(--curves-radius) - var(--line-width)),
var(--comment-line-color) calc(var(--curves-radius) - var(--line-width))
var(--curves-radius),
transparent var(--curves-radius)
),
radial-gradient(
circle at top var(--backward),
transparent calc(var(--curves-radius) - var(--line-width)),
var(--comment-line-color) calc(var(--curves-radius) - var(--line-width))
var(--curves-radius),
transparent var(--curves-radius)
);
background-size: var(--curves-radius);
background-position: top var(--backward), bottom var(--forward);
}
} }
.timeline.contextual > li { .timeline.contextual > li {
background-image: linear-gradient( background-image: linear-gradient(
@ -405,6 +472,28 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
transparent var(--line-end), transparent var(--line-end),
transparent transparent
); );
&.hero:not(:has(+ .thread), :first-child, :only-child, :last-child) {
background-image: linear-gradient(
var(--line-dir),
transparent,
transparent var(--indent-small-start),
var(--comment-line-color) var(--indent-small-start),
var(--comment-line-color) var(--indent-small-end),
transparent var(--indent-small-end),
transparent
),
linear-gradient(
var(--line-dir),
transparent,
transparent var(--indent-large-start),
var(--comment-line-color) var(--indent-large-start),
var(--comment-line-color) var(--indent-large-end),
transparent var(--indent-large-end),
transparent
);
background-size: 100% 50%;
background-position: bottom, top;
}
background-repeat: no-repeat; background-repeat: no-repeat;
transition: opacity 0.3s ease-in-out; transition: opacity 0.3s ease-in-out;
} }
@ -426,7 +515,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.timeline.contextual .timeline.contextual
> li.descendant:not(.thread) > li.descendant:not(.thread)
> :is(.status-link, .status-focus) { > :is(.status-link, .status-focus) {
padding-inline-start: 40px; padding-inline-start: var(--line-start);
} }
.timeline.contextual .replies[data-scroll-left]:not([data-scroll-left='0']) { .timeline.contextual .replies[data-scroll-left]:not([data-scroll-left='0']) {
background-color: var(--bg-color); background-color: var(--bg-color);
@ -1306,6 +1395,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
user-select: none; user-select: none;
width: 100%; width: 100%;
gap: 16px; gap: 16px;
--accent-gradient: var(--accent-gradient-light);
@media (prefers-color-scheme: dark) {
--accent-gradient: var(--accent-gradient-dark);
}
background-image: linear-gradient(to var(--forward), var(--accent-gradient));
} }
.carousel::-webkit-scrollbar { .carousel::-webkit-scrollbar {
display: none; display: none;
@ -1320,7 +1414,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
width: 100%; width: 100%;
height: 100vh; height: 100vh;
height: 100dvh; height: 100dvh;
background-color: var(--accent-alpha-color); background-color: var(--accent-light-color);
@media (prefers-color-scheme: dark) {
background-color: var(--accent-dark-color);
}
/* background-image: radial-gradient( /* background-image: radial-gradient(
closest-side, closest-side,
var(--accent-color) 10%, var(--accent-color) 10%,
@ -1861,7 +1958,7 @@ body > .szh-menu-container {
box-shadow: 0 3px 24px -3px var(--drop-shadow-color); box-shadow: 0 3px 24px -3px var(--drop-shadow-color);
} }
.szh-menu__header { .szh-menu__header {
margin: -8px 0 8px; margin: -4px 0 8px;
padding: 8px 16px; padding: 8px 16px;
color: var(--text-insignificant-color); color: var(--text-insignificant-color);
font-size: 90%; font-size: 90%;
@ -2542,6 +2639,15 @@ ul.link-list li a .icon {
} }
#columns > * { #columns > * {
padding: 0 16px; padding: 0 16px;
&:is(#notifications-page) {
padding-inline: 0;
.timeline-deck > header {
margin-inline: 16px;
}
}
border-inline: var(--hairline-width) solid var(--bg-faded-color); border-inline: var(--hairline-width) solid var(--bg-faded-color);
/* border-radius: 16px; */ /* border-radius: 16px; */
/* box-shadow: -4px 0 16px -8px var(--drop-shadow-color); */ /* box-shadow: -4px 0 16px -8px var(--drop-shadow-color); */

View file

@ -210,6 +210,12 @@ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
if (isIOS) { if (isIOS) {
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
// Don't reset theme color if media modal is showing
// Media modal will set its own theme color based on the media's color
const showingMediaModal =
document.getElementsByClassName('media-modal-container').length > 0;
if (showingMediaModal) return;
const theme = store.local.get('theme'); const theme = store.local.get('theme');
let $meta; let $meta;
if (theme) { if (theme) {

View file

@ -413,7 +413,7 @@ function AccountInfo({
<span></span> <Trans>Followers</Trans> <span></span> <Trans>Followers</Trans>
</div> </div>
<div> <div>
<span></span> <Trans>Following</Trans> <span></span> <Trans id="following.stats">Following</Trans>
</div> </div>
<div> <div>
<span></span> <Trans>Posts</Trans> <span></span> <Trans>Posts</Trans>
@ -739,7 +739,10 @@ function AccountInfo({
// states.showAccount = false; // states.showAccount = false;
setTimeout(() => { setTimeout(() => {
states.showGenericAccounts = { states.showGenericAccounts = {
heading: t`Following`, heading: t({
id: 'following.stats',
message: 'Following',
}),
fetchAccounts: fetchFollowing, fetchAccounts: fetchFollowing,
instance, instance,
excludeRelationshipAttrs: isSelf ? ['following'] : [], excludeRelationshipAttrs: isSelf ? ['following'] : [],
@ -753,7 +756,7 @@ function AccountInfo({
<span title={followingCount}> <span title={followingCount}>
{shortenNumber(followingCount)} {shortenNumber(followingCount)}
</span>{' '} </span>{' '}
<Trans>Following</Trans> <Trans id="following.stats">Following</Trans>
<br /> <br />
</LinkOrDiv> </LinkOrDiv>
<LinkOrDiv <LinkOrDiv

View file

@ -12,9 +12,16 @@ import Notifications from '../pages/notifications';
import Public from '../pages/public'; import Public from '../pages/public';
import Search from '../pages/search'; import Search from '../pages/search';
import Trending from '../pages/trending'; import Trending from '../pages/trending';
import isRTL from '../utils/is-rtl';
import states from '../utils/states'; import states from '../utils/states';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const scrollIntoViewOptions = {
block: 'nearest',
inline: 'nearest',
behavior: 'smooth',
};
function Columns() { function Columns() {
useTitle(t`Home`, '/'); useTitle(t`Home`, '/');
const snapStates = useSnapshot(states); const snapStates = useSnapshot(states);
@ -50,12 +57,42 @@ function Columns() {
useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => { useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => {
try { try {
const index = parseInt(handler.keys[0], 10) - 1; const index = parseInt(handler.keys[0], 10) - 1;
document.querySelectorAll('#columns > *')[index].focus(); const $column = document.querySelectorAll('#columns > *')[index];
if ($column) {
$column.focus();
$column.scrollIntoView(scrollIntoViewOptions);
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
}); });
useHotkeys(['[', ']'], (e, handler) => {
const key = handler.keys[0];
const currentFocusedColumn = document.activeElement.closest('#columns > *');
const rtl = isRTL();
const prevColKey = rtl ? ']' : '[';
const nextColKey = rtl ? '[' : ']';
let $column;
if (key === prevColKey) {
// If [, focus on left of focused column, else first column
$column = currentFocusedColumn
? currentFocusedColumn.previousElementSibling
: document.querySelectorAll('#columns > *')[0];
} else if (key === nextColKey) {
// If ], focus on right of focused column, else 2nd column
$column = currentFocusedColumn
? currentFocusedColumn.nextElementSibling
: document.querySelectorAll('#columns > *')[1];
}
if ($column) {
$column.focus();
$column.scrollIntoView(scrollIntoViewOptions);
}
});
return ( return (
<div <div
id="columns" id="columns"

View file

@ -197,6 +197,7 @@ function highlightText(text, { maxCharacters = Infinity }) {
// const rtf = new Intl.RelativeTimeFormat(); // const rtf = new Intl.RelativeTimeFormat();
const RTF = mem((locale) => new Intl.RelativeTimeFormat(locale || undefined)); const RTF = mem((locale) => new Intl.RelativeTimeFormat(locale || undefined));
const LF = mem((locale) => new Intl.ListFormat(locale || undefined));
const CUSTOM_EMOJIS_COUNT = 100; const CUSTOM_EMOJIS_COUNT = 100;
@ -210,6 +211,7 @@ function Compose({
}) { }) {
const { i18n } = useLingui(); const { i18n } = useLingui();
const rtf = RTF(i18n.locale); const rtf = RTF(i18n.locale);
const lf = LF(i18n.locale);
console.warn('RENDER COMPOSER'); console.warn('RENDER COMPOSER');
const { masto, instance } = api(); const { masto, instance } = api();
@ -226,11 +228,11 @@ function Compose({
const { const {
statuses: { statuses: {
maxCharacters, maxCharacters,
maxMediaAttachments, maxMediaAttachments, // Beware: it can be undefined!
charactersReservedPerUrl, charactersReservedPerUrl,
} = {}, } = {},
mediaAttachments: { mediaAttachments: {
supportedMimeTypes = [], supportedMimeTypes,
imageSizeLimit, imageSizeLimit,
imageMatrixLimit, imageMatrixLimit,
videoSizeLimit, videoSizeLimit,
@ -600,15 +602,31 @@ function Compose({
const handleItems = (e) => { const handleItems = (e) => {
const { items } = e.clipboardData || e.dataTransfer; const { items } = e.clipboardData || e.dataTransfer;
const files = []; const files = [];
const unsupportedFiles = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i]; const item = items[i];
if (item.kind === 'file') { if (item.kind === 'file') {
const file = item.getAsFile(); const file = item.getAsFile();
if (file && supportedMimeTypes.includes(file.type)) { if (
supportedMimeTypes !== undefined &&
!supportedMimeTypes.includes(file.type)
) {
unsupportedFiles.push(file);
} else {
files.push(file); files.push(file);
} }
} }
} }
if (unsupportedFiles.length > 0) {
alert(
plural(unsupportedFiles.length, {
one: `File ${unsupportedFiles[0].name} is not supported.`,
other: `Files ${lf.format(
unsupportedFiles.map((f) => f.name),
)} are not supported.`,
}),
);
}
if (files.length > 0 && mediaAttachments.length >= maxMediaAttachments) { if (files.length > 0 && mediaAttachments.length >= maxMediaAttachments) {
alert( alert(
plural(maxMediaAttachments, { plural(maxMediaAttachments, {
@ -623,16 +641,19 @@ function Compose({
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// Auto-cut-off files to avoid exceeding maxMediaAttachments // Auto-cut-off files to avoid exceeding maxMediaAttachments
const max = maxMediaAttachments - mediaAttachments.length; let allowedFiles = files;
const allowedFiles = files.slice(0, max); if (maxMediaAttachments !== undefined) {
if (allowedFiles.length <= 0) { const max = maxMediaAttachments - mediaAttachments.length;
alert( allowedFiles = allowedFiles.slice(0, max);
plural(maxMediaAttachments, { if (allowedFiles.length <= 0) {
one: 'You can only attach up to 1 file.', alert(
other: 'You can only attach up to # files.', plural(maxMediaAttachments, {
}), one: 'You can only attach up to 1 file.',
); other: 'You can only attach up to # files.',
return; }),
);
return;
}
} }
const mediaFiles = allowedFiles.map((file) => ({ const mediaFiles = allowedFiles.map((file) => ({
file, file,
@ -1307,8 +1328,11 @@ function Compose({
<label class="toolbar-button"> <label class="toolbar-button">
<input <input
type="file" type="file"
accept={supportedMimeTypes.join(',')} accept={supportedMimeTypes?.join(',')}
multiple={mediaAttachments.length < maxMediaAttachments - 1} multiple={
maxMediaAttachments === undefined ||
maxMediaAttachments - mediaAttachments >= 2
}
disabled={ disabled={
uiState === 'loading' || uiState === 'loading' ||
mediaAttachments.length >= maxMediaAttachments || mediaAttachments.length >= maxMediaAttachments ||
@ -1400,7 +1424,8 @@ function Compose({
class="toolbar-button gif-picker-button" class="toolbar-button gif-picker-button"
disabled={ disabled={
uiState === 'loading' || uiState === 'loading' ||
mediaAttachments.length >= maxMediaAttachments || (maxMediaAttachments !== undefined &&
mediaAttachments.length >= maxMediaAttachments) ||
!!poll !!poll
} }
onClick={() => { onClick={() => {

View file

@ -23,7 +23,7 @@
pointer-events: auto; pointer-events: auto;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
width: max(var(--width), 480px); width: max(var(--width), var(--main-width));
height: auto; height: auto;
aspect-ratio: var(--aspect-ratio); aspect-ratio: var(--aspect-ratio);
} }

View file

@ -113,6 +113,14 @@ export default memo(function KeyboardShortcutsHelp() {
</Trans> </Trans>
), ),
}, },
{
action: t`Focus next column in multi-column mode`,
keys: <kbd>]</kbd>,
},
{
action: t`Focus previous column in multi-column mode`,
keys: <kbd>[</kbd>,
},
{ {
action: t`Compose new post`, action: t`Compose new post`,
keys: <kbd>c</kbd>, keys: <kbd>c</kbd>,

View file

@ -14,6 +14,7 @@ import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
import isRTL from '../utils/is-rtl'; import isRTL from '../utils/is-rtl';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states from '../utils/states'; import states from '../utils/states';
import store from '../utils/store';
import Icon from './icon'; import Icon from './icon';
import Link from './link'; import Link from './link';
@ -115,39 +116,95 @@ function MediaModal({
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, []);
const mediaAccentColors = useMemo(() => { const mediaOklabColors = useMemo(() => {
return mediaAttachments?.map((media) => { return mediaAttachments?.map((media) => {
const { blurhash } = media; const { blurhash } = media;
if (blurhash) { if (blurhash) {
const averageColor = getBlurHashAverageColor(blurhash); const averageColor = getBlurHashAverageColor(blurhash);
const labAverageColor = rgb2oklab(averageColor); return rgb2oklab(averageColor);
return oklab2rgb([0.6, labAverageColor[1], labAverageColor[2]]);
} }
return null; return null;
}); });
}, [mediaAttachments]); }, [mediaAttachments]);
const mediaAccentGradient = useMemo(() => { // const mediaAccentColors = useMemo(() => {
// return mediaOklabColors?.map((labAverageColor) => {
// if (labAverageColor) {
// return oklab2rgb([0.6, labAverageColor[1], labAverageColor[2]]);
// }
// return null;
// });
// }, [mediaOklabColors]);
const mediaAccentColors = useMemo(() => {
return mediaOklabColors?.map((labAverageColor) => {
if (labAverageColor) {
return {
light: oklab2rgb([0.95, labAverageColor[1], labAverageColor[2]]),
dark: oklab2rgb([0.25, labAverageColor[1], labAverageColor[2]]),
default: oklab2rgb([0.6, labAverageColor[1], labAverageColor[2]]),
};
}
return {};
});
});
// const mediaAccentGradient = useMemo(() => {
// const gap = 5;
// const range = 100 / mediaAccentColors.length;
// return (
// mediaAccentColors
// ?.map((color, i) => {
// const start = i * range + gap;
// const end = (i + 1) * range - gap;
// if (color) {
// return `
// rgba(${color?.join(',')}, 0.4) ${start}%,
// rgba(${color?.join(',')}, 0.4) ${end}%
// `;
// }
// return `
// transparent ${start}%,
// transparent ${end}%
// `;
// })
// ?.join(', ') || 'transparent'
// );
// }, [mediaAccentColors]);
const mediaAccentGradients = useMemo(() => {
const gap = 5; const gap = 5;
const range = 100 / mediaAccentColors.length; const range = 100 / mediaAccentColors.length;
return ( const colors = mediaAccentColors.map((color, i) => {
mediaAccentColors const start = i * range + gap;
?.map((color, i) => { const end = (i + 1) * range - gap;
const start = i * range + gap; if (color?.light && color?.dark) {
const end = (i + 1) * range - gap; return {
if (color) { light: `
return ` rgb(${color.light?.join(',')}) ${start}%,
rgba(${color?.join(',')}, 0.4) ${start}%, rgb(${color.light?.join(',')}) ${end}%
rgba(${color?.join(',')}, 0.4) ${end}% `,
`; dark: `
} rgb(${color.dark?.join(',')}) ${start}%,
rgb(${color.dark?.join(',')}) ${end}%
`,
};
}
return ` return {
transparent ${start}%, light: `
transparent ${end}% transparent ${start}%,
`; transparent ${end}%
}) `,
?.join(', ') || 'transparent' dark: `
); transparent ${start}%,
transparent ${end}%
`,
};
});
const lightGradient = colors.map((color) => color.light).join(', ');
const darkGradient = colors.map((color) => color.dark).join(', ');
return {
light: lightGradient,
dark: darkGradient,
};
}, [mediaAccentColors]); }, [mediaAccentColors]);
let toastRef = useRef(null); let toastRef = useRef(null);
@ -157,6 +214,46 @@ function MediaModal({
}; };
}, []); }, []);
useLayoutEffect(() => {
const currentColor = mediaAccentColors[currentIndex];
let $meta;
let metaColor;
if (currentColor) {
const theme = store.local.get('theme');
if (theme) {
const mediaColor = `rgb(${currentColor[theme].join(',')})`;
console.log({ mediaColor });
$meta = document.querySelector(
`meta[name="theme-color"][data-theme-setting="manual"]`,
);
if ($meta) {
metaColor = $meta.content;
$meta.content = mediaColor;
}
} else {
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light';
const mediaColor = `rgb(${currentColor[colorScheme].join(',')})`;
console.log({ mediaColor });
$meta = document.querySelector(
`meta[name="theme-color"][media*="${colorScheme}"]`,
);
if ($meta) {
metaColor = $meta.content;
$meta.content = mediaColor;
}
}
}
return () => {
// Reset meta color
if ($meta && metaColor) {
$meta.content = metaColor;
}
};
}, [currentIndex, mediaAccentColors]);
return ( return (
<div <div
class={`media-modal-container media-modal-count-${mediaAttachments?.length}`} class={`media-modal-container media-modal-count-${mediaAttachments?.length}`}
@ -179,8 +276,10 @@ function MediaModal({
mediaAttachments.length > 1 mediaAttachments.length > 1
? { ? {
backgroundAttachment: 'local', backgroundAttachment: 'local',
backgroundImage: `linear-gradient( '--accent-gradient-light': mediaAccentGradients?.light,
to ${isRTL() ? 'left' : 'right'}, ${mediaAccentGradient})`, '--accent-gradient-dark': mediaAccentGradients?.dark,
// backgroundImage: `linear-gradient(
// to ${isRTL() ? 'left' : 'right'}, ${mediaAccentGradient})`,
} }
: {} : {}
} }
@ -194,8 +293,14 @@ function MediaModal({
style={ style={
accentColor accentColor
? { ? {
'--accent-color': `rgb(${accentColor?.join(',')})`, '--accent-color': `rgb(${accentColor.default.join(',')})`,
'--accent-alpha-color': `rgba(${accentColor?.join( '--accent-light-color': `rgb(${accentColor.light?.join(
',',
)})`,
'--accent-dark-color': `rgb(${accentColor.dark?.join(
',',
)})`,
'--accent-alpha-color': `rgba(${accentColor.default.join(
',', ',',
)}, 0.4)`, )}, 0.4)`,
} }
@ -260,8 +365,8 @@ function MediaModal({
e.stopPropagation(); e.stopPropagation();
const left = const left =
carouselRef.current.clientWidth * i * (isRTL() ? -1 : 1); carouselRef.current.clientWidth * i * (isRTL() ? -1 : 1);
carouselRef.current.scrollTo({ left, behavior: 'smooth' });
carouselRef.current.focus(); carouselRef.current.focus();
carouselRef.current.scrollTo({ left, behavior: 'smooth' });
}} }}
> >
<Icon icon="round" size="s" alt="⸱" /> <Icon icon="round" size="s" alt="⸱" />

View file

@ -74,6 +74,7 @@ function Media({
showCaption, showCaption,
allowLongerCaption, allowLongerCaption,
altIndex, altIndex,
checkAspectRatio = true,
onClick = () => {}, onClick = () => {},
}) { }) {
let { let {
@ -354,7 +355,7 @@ function Media({
} }
// Check natural aspect ratio vs display aspect ratio // Check natural aspect ratio vs display aspect ratio
if ($media) { if (checkAspectRatio && $media) {
const { const {
clientWidth, clientWidth,
clientHeight, clientHeight,

View file

@ -190,7 +190,7 @@ function NavMenu(props) {
<MenuLink to="/following"> <MenuLink to="/following">
<Icon icon="following" size="l" />{' '} <Icon icon="following" size="l" />{' '}
<span> <span>
<Trans>Following</Trans> <Trans id="following.title">Following</Trans>
</span> </span>
</MenuLink> </MenuLink>
)} )}

View file

@ -134,7 +134,10 @@ const fetchAccountTitle = pmem(async ({ id }) => {
export const SHORTCUTS_META = { export const SHORTCUTS_META = {
following: { following: {
id: 'home', id: 'home',
title: (_, index) => (index === 0 ? t`Home` : t`Following`), title: (_, index) =>
index === 0
? t`Home`
: t({ id: 'following.title', message: 'Following' }),
path: '/', path: '/',
icon: 'home', icon: 'home',
}, },

View file

@ -30,10 +30,10 @@ function Shortcuts() {
if (!shortcuts.length) { if (!shortcuts.length) {
return null; return null;
} }
if ( const isMultiColumnMode =
settings.shortcutsViewMode === 'multi-column' || settings.shortcutsViewMode === 'multi-column' ||
(!settings.shortcutsViewMode && settings.shortcutsColumnsMode) (!settings.shortcutsViewMode && settings.shortcutsColumnsMode);
) { if (isMultiColumnMode) {
return null; return null;
} }
@ -87,16 +87,22 @@ function Shortcuts() {
.filter(Boolean); .filter(Boolean);
const navigate = useNavigate(); const navigate = useNavigate();
useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => { useHotkeys(
const index = parseInt(handler.keys[0], 10) - 1; ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
if (index < formattedShortcuts.length) { (e, handler) => {
const { path } = formattedShortcuts[index]; const index = parseInt(handler.keys[0], 10) - 1;
if (path) { if (index < formattedShortcuts.length) {
navigate(path); const { path } = formattedShortcuts[index];
menuRef.current?.closeMenu?.(); if (path) {
navigate(path);
menuRef.current?.closeMenu?.();
}
} }
} },
}); {
enabled: !isMultiColumnMode,
},
);
const [lists, setLists] = useState([]); const [lists, setLists] = useState([]);

View file

@ -247,8 +247,11 @@
:is(.content.truncated, .poll, .media-container.truncated) { :is(.content.truncated, .poll, .media-container.truncated) {
mask-image: linear-gradient(to bottom, #000 40px, transparent); mask-image: linear-gradient(to bottom, #000 40px, transparent);
} }
.status-card .card { .status-card {
display: none; .card,
.card-byline {
display: none;
}
} }
.timeline-deck .status-card .content.truncated:after { .timeline-deck .status-card .content.truncated:after {
/* Don't show "Read more" in status cards */ /* Don't show "Read more" in status cards */
@ -1995,7 +1998,11 @@ a.card:is(:hover, :focus):visited {
gap: 4px; gap: 4px;
color: var(--text-insignificant-color); color: var(--text-insignificant-color);
padding: 2px 8px; padding: 2px 8px;
align-items: center; align-items: flex-start;
.icon {
min-height: 1em;
}
.avatar { .avatar {
width: 16px !important; width: 16px !important;
@ -2124,8 +2131,8 @@ a.card:is(:hover, :focus):visited {
color: var(--text-insignificant-color); color: var(--text-insignificant-color);
font-size: 90%; font-size: 90%;
} }
.status .extra-meta * { .status .extra-meta .icon {
vertical-align: middle; vertical-align: text-bottom;
} }
.status .extra-meta a { .status .extra-meta a {
color: inherit; color: inherit;

View file

@ -125,6 +125,46 @@ function getPostText(status) {
); );
} }
function isTranslateble(content) {
if (!content) return false;
content = content.trim();
if (!content) return false;
const text = getHTMLText(content, {
preProcess: (dom) => {
// Remove .mention, pre, code, a:has(.invisible)
for (const a of dom.querySelectorAll(
'.mention, pre, code, a:has(.invisible)',
)) {
a.remove();
}
},
});
return !!text;
}
function getHTMLTextForDetectLang(content) {
return getHTMLText(content, {
preProcess: (dom) => {
// Remove anything that can skew the language detection
// Remove .mention, .hashtag, pre, code, a:has(.invisible)
for (const a of dom.querySelectorAll(
'.mention, .hashtag, pre, code, a:has(.invisible)',
)) {
a.remove();
}
// Remove links that contains text that starts with https?://
for (const a of dom.querySelectorAll('a')) {
const text = a.innerText.trim();
if (text.startsWith('https://') || text.startsWith('http://')) {
a.remove();
}
}
},
});
}
const HTTP_REGEX = /^http/i; const HTTP_REGEX = /^http/i;
const PostContent = const PostContent =
/*memo(*/ /*memo(*/
@ -343,32 +383,10 @@ function Status({
useEffect(() => { useEffect(() => {
if (!content) return; if (!content) return;
if (_language) return; if (_language) return;
if (languageAutoDetected) return;
let timer; let timer;
timer = setTimeout(async () => { timer = setTimeout(async () => {
let detected = await detectLang( let detected = await detectLang(getHTMLTextForDetectLang(content));
getHTMLText(content, {
preProcess: (dom) => {
// Remove anything that can skew the language detection
// Remove .mention, .hashtag, pre, code, a:has(.invisible)
dom
.querySelectorAll(
'.mention, .hashtag, pre, code, a:has(.invisible)',
)
.forEach((a) => {
a.remove();
});
// Remove links that contains text that starts with https?://
dom.querySelectorAll('a').forEach((a) => {
const text = a.innerText.trim();
if (text.startsWith('https://') || text.startsWith('http://')) {
a.remove();
}
});
},
}),
);
setLanguageAutoDetected(detected); setLanguageAutoDetected(detected);
}, 1000); }, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
@ -2061,6 +2079,7 @@ function Status({
class="content" class="content"
ref={contentRef} ref={contentRef}
data-read-more={_(readMoreText)} data-read-more={_(readMoreText)}
inert={!!spoilerText && !showSpoiler ? true : undefined}
> >
<PostContent <PostContent
post={status} post={status}
@ -2101,8 +2120,7 @@ function Status({
/> />
)} )}
{(((enableTranslate || inlineTranslate) && {(((enableTranslate || inlineTranslate) &&
!!content.trim() && isTranslateble(content) &&
!!getHTMLText(emojifyText(content, emojis)) &&
differentLanguage) || differentLanguage) ||
forceTranslate) && ( forceTranslate) && (
<TranslationBlock <TranslationBlock
@ -2202,6 +2220,7 @@ function Status({
} }
: undefined : undefined
} }
checkAspectRatio={mediaAttachments.length === 1}
/> />
))} ))}
</div> </div>
@ -2248,6 +2267,19 @@ function Status({
/> */} /> */}
<span>{_(visibilityText[visibility])}</span> &bull;{' '} <span>{_(visibilityText[visibility])}</span> &bull;{' '}
<a href={url} target="_blank" rel="noopener noreferrer"> <a href={url} target="_blank" rel="noopener noreferrer">
{
// within a day
new Date().getTime() - createdAtDate.getTime() <
86400000 && (
<>
<RelativeTime
datetime={createdAtDate}
format="micro"
/>{' '}
{' '}
</>
)
}
<time <time
class="created" class="created"
datetime={createdAtDate.toISOString()} datetime={createdAtDate.toISOString()}

View file

@ -15,13 +15,13 @@
"code": "cs-CZ", "code": "cs-CZ",
"nativeName": "čeština", "nativeName": "čeština",
"name": "Czech", "name": "Czech",
"completion": 80 "completion": 79
}, },
{ {
"code": "de-DE", "code": "de-DE",
"nativeName": "Deutsch", "nativeName": "Deutsch",
"name": "German", "name": "German",
"completion": 96 "completion": 95
}, },
{ {
"code": "eo-UY", "code": "eo-UY",
@ -57,13 +57,13 @@
"code": "fr-FR", "code": "fr-FR",
"nativeName": "français", "nativeName": "français",
"name": "French", "name": "French",
"completion": 99 "completion": 98
}, },
{ {
"code": "gl-ES", "code": "gl-ES",
"nativeName": "galego", "nativeName": "galego",
"name": "Galician", "name": "Galician",
"completion": 100 "completion": 99
}, },
{ {
"code": "he-IL", "code": "he-IL",
@ -75,7 +75,7 @@
"code": "it-IT", "code": "it-IT",
"nativeName": "italiano", "nativeName": "italiano",
"name": "Italian", "name": "Italian",
"completion": 100 "completion": 99
}, },
{ {
"code": "ja-JP", "code": "ja-JP",
@ -87,13 +87,13 @@
"code": "kab", "code": "kab",
"nativeName": "Taqbaylit", "nativeName": "Taqbaylit",
"name": "Kabyle", "name": "Kabyle",
"completion": 100 "completion": 99
}, },
{ {
"code": "ko-KR", "code": "ko-KR",
"nativeName": "한국어", "nativeName": "한국어",
"name": "Korean", "name": "Korean",
"completion": 97 "completion": 96
}, },
{ {
"code": "lt-LT", "code": "lt-LT",
@ -105,19 +105,19 @@
"code": "nb-NO", "code": "nb-NO",
"nativeName": "norsk bokmål", "nativeName": "norsk bokmål",
"name": "Norwegian Bokmål", "name": "Norwegian Bokmål",
"completion": 31 "completion": 51
}, },
{ {
"code": "nl-NL", "code": "nl-NL",
"nativeName": "Nederlands", "nativeName": "Nederlands",
"name": "Dutch", "name": "Dutch",
"completion": 83 "completion": 82
}, },
{ {
"code": "pl-PL", "code": "pl-PL",
"nativeName": "polski", "nativeName": "polski",
"name": "Polish", "name": "Polish",
"completion": 6 "completion": 10
}, },
{ {
"code": "pt-BR", "code": "pt-BR",
@ -147,7 +147,7 @@
"code": "uk-UA", "code": "uk-UA",
"nativeName": "українська", "nativeName": "українська",
"name": "Ukrainian", "name": "Ukrainian",
"completion": 26 "completion": 100
}, },
{ {
"code": "zh-CN", "code": "zh-CN",
@ -159,6 +159,6 @@
"code": "zh-TW", "code": "zh-TW",
"nativeName": "繁體中文", "nativeName": "繁體中文",
"name": "Traditional Chinese", "name": "Traditional Chinese",
"completion": 15 "completion": 32
} }
] ]

1302
src/locales/ar-SA.po generated

File diff suppressed because it is too large Load diff

1246
src/locales/ca-ES.po generated

File diff suppressed because it is too large Load diff

1302
src/locales/cs-CZ.po generated

File diff suppressed because it is too large Load diff

1302
src/locales/de-DE.po generated

File diff suppressed because it is too large Load diff

1142
src/locales/en.po generated

File diff suppressed because it is too large Load diff

1198
src/locales/eo-UY.po generated

File diff suppressed because it is too large Load diff

1210
src/locales/es-ES.po generated

File diff suppressed because it is too large Load diff

1194
src/locales/eu-ES.po generated

File diff suppressed because it is too large Load diff

982
src/locales/fa-IR.po generated

File diff suppressed because it is too large Load diff

1306
src/locales/fi-FI.po generated

File diff suppressed because it is too large Load diff

1306
src/locales/fr-FR.po generated

File diff suppressed because it is too large Load diff

1306
src/locales/gl-ES.po generated

File diff suppressed because it is too large Load diff

1300
src/locales/he-IL.po generated

File diff suppressed because it is too large Load diff

1192
src/locales/it-IT.po generated

File diff suppressed because it is too large Load diff

1304
src/locales/ja-JP.po generated

File diff suppressed because it is too large Load diff

1306
src/locales/kab.po generated

File diff suppressed because it is too large Load diff

1324
src/locales/ko-KR.po generated

File diff suppressed because it is too large Load diff

1236
src/locales/lt-LT.po generated

File diff suppressed because it is too large Load diff

1538
src/locales/nb-NO.po generated

File diff suppressed because it is too large Load diff

1314
src/locales/nl-NL.po generated

File diff suppressed because it is too large Load diff

1300
src/locales/oc-FR.po generated

File diff suppressed because it is too large Load diff

1286
src/locales/pl-PL.po generated

File diff suppressed because it is too large Load diff

1326
src/locales/pt-BR.po generated

File diff suppressed because it is too large Load diff

1334
src/locales/pt-PT.po generated

File diff suppressed because it is too large Load diff

1308
src/locales/ru-RU.po generated

File diff suppressed because it is too large Load diff

1328
src/locales/th-TH.po generated

File diff suppressed because it is too large Load diff

2632
src/locales/uk-UA.po generated

File diff suppressed because it is too large Load diff

1312
src/locales/zh-CN.po generated

File diff suppressed because it is too large Load diff

1598
src/locales/zh-TW.po generated

File diff suppressed because it is too large Load diff

View file

@ -232,7 +232,7 @@ function AccountStatuses() {
const { i18n } = useLingui(); const { i18n } = useLingui();
let title = t`Account posts`; let title = t`Account posts`;
if (account?.acct) { if (account?.acct) {
const acctDisplay = /@/.test(account.acct) ? '' : '@' + account.acct; const acctDisplay = (/@/.test(account.acct) ? '' : '@') + account.acct;
const accountDisplay = account?.displayName const accountDisplay = account?.displayName
? `${account.displayName} (${acctDisplay})` ? `${account.displayName} (${acctDisplay})`
: `${acctDisplay}`; : `${acctDisplay}`;

View file

@ -1061,11 +1061,11 @@ function Catchup() {
onClick={async () => { onClick={async () => {
const yes = confirm(t`Remove this catch-up?`); const yes = confirm(t`Remove this catch-up?`);
if (yes) { if (yes) {
let t = showToast( let st = showToast(
t`Removing Catch-up ${pc.id}`, t`Removing Catch-up ${pc.id}`,
); );
await db.catchup.del(pc.id); await db.catchup.del(pc.id);
t?.hideToast?.(); st?.hideToast?.();
showToast(t`Catch-up ${pc.id} removed`); showToast(t`Catch-up ${pc.id} removed`);
reloadCatchups(); reloadCatchups();
} }
@ -1468,7 +1468,7 @@ function Catchup() {
))} ))}
</fieldset> */} </fieldset> */}
<span class="filter-label"> <span class="filter-label">
<Trans>Group</Trans> <Trans id="group.filter">Group</Trans>
</span>{' '} </span>{' '}
<fieldset class="radio-field-group"> <fieldset class="radio-field-group">
{FILTER_GROUPS.map((key) => ( {FILTER_GROUPS.map((key) => (

View file

@ -17,7 +17,14 @@ import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
function Following({ title, path, id, ...props }) { function Following({ title, path, id, ...props }) {
useTitle(title || t`Following`, path || '/following'); useTitle(
title ||
t({
id: 'following.title',
message: 'Following',
}),
path || '/following',
);
const { masto, streaming, instance } = api(); const { masto, streaming, instance } = api();
const snapStates = useSnapshot(states); const snapStates = useSnapshot(states);
const homeIterator = useRef(); const homeIterator = useRef();
@ -131,7 +138,7 @@ function Following({ title, path, id, ...props }) {
return ( return (
<Timeline <Timeline
title={title || t`Following`} title={title || t({ id: 'following.title', message: 'Following' })}
id={id || 'following'} id={id || 'following'}
emptyText={t`Nothing to see here.`} emptyText={t`Nothing to see here.`}
errorText={t`Unable to load posts.`} errorText={t`Unable to load posts.`}

View file

@ -27,6 +27,11 @@
margin: 0; margin: 0;
opacity: 1; opacity: 1;
#columns & {
position: static;
background-color: inherit;
}
small { small {
font-weight: normal; font-weight: normal;
font-size: var(--text-size); font-size: var(--text-size);
@ -556,6 +561,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
small {
flex: 1;
min-width: 0;
}
} }
} }

View file

@ -24,6 +24,12 @@ const SHORT_LIMIT = 5;
const LIMIT = 40; const LIMIT = 40;
const emptySearchParams = new URLSearchParams(); const emptySearchParams = new URLSearchParams();
const scrollIntoViewOptions = {
block: 'nearest',
inline: 'center',
behavior: 'smooth',
};
function Search({ columnMode, ...props }) { function Search({ columnMode, ...props }) {
const params = columnMode ? {} : useParams(); const params = columnMode ? {} : useParams();
const { masto, instance, authenticated } = api({ const { masto, instance, authenticated } = api({
@ -186,10 +192,79 @@ function Search({ columnMode, ...props }) {
}, },
); );
const itemsSelector = '.timeline > li > a, .hashtag-list > li > a';
const jRef = useHotkeys('j', () => {
const activeItem = document.activeElement.closest(itemsSelector);
const activeItemRect = activeItem?.getBoundingClientRect();
const allItems = Array.from(
scrollableRef.current.querySelectorAll(itemsSelector),
);
if (
activeItem &&
activeItemRect.top < scrollableRef.current.clientHeight &&
activeItemRect.bottom > 0
) {
const activeItemIndex = allItems.indexOf(activeItem);
let nextItem = allItems[activeItemIndex + 1];
if (nextItem) {
nextItem.focus();
nextItem.scrollIntoView(scrollIntoViewOptions);
}
} else {
const topmostItem = allItems.find((item) => {
const itemRect = item.getBoundingClientRect();
return itemRect.top >= 44 && itemRect.left >= 0;
});
if (topmostItem) {
topmostItem.focus();
topmostItem.scrollIntoView(scrollIntoViewOptions);
}
}
});
const kRef = useHotkeys('k', () => {
// focus on previous status after active item
const activeItem = document.activeElement.closest(itemsSelector);
const activeItemRect = activeItem?.getBoundingClientRect();
const allItems = Array.from(
scrollableRef.current.querySelectorAll(itemsSelector),
);
if (
activeItem &&
activeItemRect.top < scrollableRef.current.clientHeight &&
activeItemRect.bottom > 0
) {
const activeItemIndex = allItems.indexOf(activeItem);
let prevItem = allItems[activeItemIndex - 1];
if (prevItem) {
prevItem.focus();
prevItem.scrollIntoView(scrollIntoViewOptions);
}
} else {
const topmostItem = allItems.find((item) => {
const itemRect = item.getBoundingClientRect();
return itemRect.top >= 44 && itemRect.left >= 0;
});
if (topmostItem) {
topmostItem.focus();
topmostItem.scrollIntoView(scrollIntoViewOptions);
}
}
});
const [filterBarParent] = useAutoAnimate(); const [filterBarParent] = useAutoAnimate();
return ( return (
<div id="search-page" class="deck-container" ref={scrollableRef}> <div
id="search-page"
class="deck-container"
tabIndex="-1"
ref={(node) => {
scrollableRef.current = node;
jRef(node);
kRef(node);
}}
>
<div class="timeline-deck deck"> <div class="timeline-deck deck">
<header class={uiState === 'loading' ? 'loading' : ''}> <header class={uiState === 'loading' ? 'loading' : ''}>
<div class="header-grid"> <div class="header-grid">

View file

@ -14,6 +14,7 @@ import targetLanguages from '../data/lingva-target-languages';
import { api } from '../utils/api'; import { api } from '../utils/api';
import getTranslateTargetLanguage from '../utils/get-translate-target-language'; import getTranslateTargetLanguage from '../utils/get-translate-target-language';
import localeCode2Text from '../utils/localeCode2Text'; import localeCode2Text from '../utils/localeCode2Text';
import prettyBytes from '../utils/pretty-bytes';
import { import {
initSubscription, initSubscription,
isPushSupported, isPushSupported,
@ -856,6 +857,13 @@ function Settings({ onClose }) {
> >
Show keys count Show keys count
</button>{' '} </button>{' '}
<button
type="button"
class="plain2 small"
onClick={async () => alert(await getCachesSize())}
>
Show cache size
</button>{' '}
<button <button
type="button" type="button"
class="plain2 small" class="plain2 small"
@ -902,6 +910,33 @@ async function getCachesKeys() {
return total; return total;
} }
async function getCachesSize() {
const keys = await caches.keys();
let total = {};
let TOTAL = 0;
for (const key of keys) {
const cache = await caches.open(key);
const k = await cache.keys();
for (const item of k) {
try {
const response = await cache.match(item);
const blob = await response.blob();
total[key] = (total[key] || 0) + blob.size;
TOTAL += blob.size;
} catch (e) {
alert('Failed to get cache size for ' + item);
alert(e);
}
}
}
return {
...Object.fromEntries(
Object.entries(total).map(([k, v]) => [k, prettyBytes(v)]),
),
TOTAL: prettyBytes(TOTAL),
};
}
function clearCacheKey(key) { function clearCacheKey(key) {
return caches.delete(key); return caches.delete(key);
} }

View file

@ -208,6 +208,11 @@ function StatusParent(props) {
); );
} }
// oldest first
function createdAtSort(a, b) {
return new Date(b.created_at) - new Date(a.created_at);
}
function StatusThread({ id, closeLink = '/', instance: propInstance }) { function StatusThread({ id, closeLink = '/', instance: propInstance }) {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const mediaParam = searchParams.get('media'); const mediaParam = searchParams.get('media');
@ -321,6 +326,9 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
const context = await contextFetch; const context = await contextFetch;
const { ancestors, descendants } = context; const { ancestors, descendants } = context;
ancestors.sort(createdAtSort);
descendants.sort(createdAtSort);
totalDescendants.current = descendants?.length || 0; totalDescendants.current = descendants?.length || 0;
const missingStatuses = new Set(); const missingStatuses = new Set();
@ -360,7 +368,11 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
nestedDescendants.push(status); nestedDescendants.push(status);
} else if ( } else if (
!status.inReplyToAccountId && !status.inReplyToAccountId &&
nestedDescendants.find((s) => s.id === status.inReplyToId) && nestedDescendants.find(
(s) =>
s.id === status.inReplyToId &&
s.account.id === heroStatus.account.id,
) &&
status.account.id === heroStatus.account.id status.account.id === heroStatus.account.id
) { ) {
// If replying to hero's own statuses, it's part of the thread, level 1 // If replying to hero's own statuses, it's part of the thread, level 1
@ -380,19 +392,35 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
} }
}); });
// sort hero author to top
nestedDescendants.sort((a, b) => {
const heroAccountID = heroStatus.account.id;
if (a.account.id === heroAccountID && b.account.id !== heroAccountID)
return -1;
if (b.account.id === heroAccountID && a.account.id !== heroAccountID)
return 1;
return 0;
});
console.log({ ancestors, descendants, nestedDescendants }); console.log({ ancestors, descendants, nestedDescendants });
if (missingStatuses.size) { if (missingStatuses.size) {
console.error('Missing statuses', [...missingStatuses]); console.error('Missing statuses', [...missingStatuses]);
} }
function expandReplies(_replies) { let descendantLevelsCount = 1;
function expandReplies(_replies, level) {
const nextLevel = level + 1;
if (nextLevel > descendantLevelsCount) {
descendantLevelsCount = level;
}
return _replies?.map((_r) => ({ return _replies?.map((_r) => ({
id: _r.id, id: _r.id,
account: _r.account, account: _r.account,
repliesCount: _r.repliesCount, repliesCount: _r.repliesCount,
content: _r.content, content: _r.content,
weight: calcStatusWeight(_r), weight: calcStatusWeight(_r),
replies: expandReplies(_r.__replies), level: nextLevel,
replies: expandReplies(_r.__replies, nextLevel),
})); }));
} }
@ -418,7 +446,8 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
descendant: true, descendant: true,
thread: s.account.id === heroStatus.account.id, thread: s.account.id === heroStatus.account.id,
weight: calcStatusWeight(s), weight: calcStatusWeight(s),
replies: expandReplies(s.__replies), level: 1,
replies: expandReplies(s.__replies, 1),
})), })),
]; ];
@ -429,12 +458,13 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
}; };
// Set limit to hero's index // Set limit to hero's index
const heroLimit = allStatuses.findIndex((s) => s.id === id); // const heroLimit = allStatuses.findIndex((s) => s.id === id);
const heroLimit = ancestors.length || 0; // 0-indexed
if (heroLimit >= limit) { if (heroLimit >= limit) {
setLimit(heroLimit + 1); setLimit(heroLimit + 1);
} }
console.log({ allStatuses }); console.log({ allStatuses, descendantLevelsCount });
setStatuses(allStatuses); setStatuses(allStatuses);
cachedStatusesMap[id] = allStatuses; cachedStatusesMap[id] = allStatuses;
@ -564,7 +594,10 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
useTitle( useTitle(
heroDisplayName && heroContentText heroDisplayName && heroContentText
? `${heroDisplayName}: "${heroContentText}"` ? `${heroDisplayName}: "${heroContentText}"`
: t`Post`, : t({
id: 'post.title',
message: 'Post',
}),
'/:instance?/s/:id', '/:instance?/s/:id',
); );
@ -747,6 +780,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
replies, replies,
repliesCount, repliesCount,
weight, weight,
level,
} = status; } = status;
const isHero = statusID === id; const isHero = statusID === id;
const isLinkable = isThread || ancestor; const isLinkable = isThread || ancestor;
@ -920,7 +954,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
instance={instance} instance={instance}
replies={replies} replies={replies}
hasParentThread={thread} hasParentThread={thread}
level={1} level={level}
accWeight={weight} accWeight={weight}
openAll={totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT} openAll={totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT}
parentLink={{ parentLink={{
@ -1094,7 +1128,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
</> </>
) : ( ) : (
<> <>
Post{' '} <Trans id="post.title">Post</Trans>{' '}
<button <button
type="button" type="button"
class="ancestors-indicator light small" class="ancestors-indicator light small"
@ -1403,130 +1437,151 @@ function SubComments({
}; };
}, []); }, []);
// If not open, delay render replies
const [renderReplies, setRenderReplies] = useState(openBefore || open);
useEffect(() => {
let timer;
if (!openBefore && !open) {
timer = setTimeout(() => setRenderReplies(true), 100);
}
return () => clearTimeout(timer);
}, [openBefore, open]);
const Container = open ? 'div' : 'details';
const isDetails = Container === 'details';
return ( return (
<details <Container
ref={detailsRef} ref={detailsRef}
class="replies" class="replies"
open={openBefore || open} open={isDetails ? openBefore || open : undefined}
onToggle={(e) => { onToggle={
const { open } = e.target; isDetails
// use first reply as ID ? (e) => {
cachedRepliesToggle[replies[0].id] = open; const { open } = e.target;
}} // use first reply as ID
cachedRepliesToggle[replies[0].id] = open;
}
: undefined
}
style={{ style={{
'--comments-level': level, '--comments-level': level,
}} }}
data-comments-level={level} data-comments-level={level}
data-comments-level-overflow={level > 4} data-comments-level-overflow={level > 4}
> >
<summary class="replies-summary" hidden={open}> {!open && (
<span class="avatars"> <summary class="replies-summary" hidden={open}>
{accounts.map((a) => ( <span class="avatars">
<Avatar {accounts.map((a) => (
key={a.id} <Avatar
url={a.avatarStatic} key={a.id}
title={`${a.displayName} @${a.username}`} url={a.avatarStatic}
squircle={a?.bot} title={`${a.displayName} @${a.username}`}
/> squircle={a?.bot}
))} />
</span> ))}
<span class="replies-counts"> </span>
<b> <span class="replies-counts">
<Plural <b>
value={replies.length} <Plural
one="# reply" value={replies.length}
other={ one="# reply"
<Trans> other={
<span title={replies.length}> <Trans>
{shortenNumber(replies.length)} <span title={replies.length}>
</span>{' '} {shortenNumber(replies.length)}
replies </span>{' '}
</Trans> replies
} </Trans>
/> }
</b> />
{!sameCount && totalComments > 1 && ( </b>
<> {!sameCount && totalComments > 1 && (
{' '} <>
&middot;{' '} {' '}
<span> &middot;{' '}
<Plural <span>
value={totalComments} <Plural
one="# comment" value={totalComments}
other={ one="# comment"
<Trans> other={
<span title={totalComments}> <Trans>
{shortenNumber(totalComments)} <span title={totalComments}>
</span>{' '} {shortenNumber(totalComments)}
comments </span>{' '}
</Trans> comments
} </Trans>
/> }
</span> />
</> </span>
</>
)}
</span>
<Icon icon="chevron-down" class="replies-summary-chevron" />
{!!parentLink && (
<Link
class="replies-parent-link"
to={parentLink.to}
onClick={parentLink.onClick}
title={t`View post with its replies`}
>
&raquo;
</Link>
)} )}
</span> </summary>
<Icon icon="chevron-down" class="replies-summary-chevron" /> )}
{!!parentLink && ( {renderReplies && (
<Link <ul>
class="replies-parent-link" {replies.map((r) => (
to={parentLink.to} <li key={r.id}>
onClick={parentLink.onClick} {/* <Link
title={t`View post with its replies`}
>
&raquo;
</Link>
)}
</summary>
<ul>
{replies.map((r) => (
<li key={r.id}>
{/* <Link
class="status-link" class="status-link"
to={instance ? `/${instance}/s/${r.id}` : `/s/${r.id}`} to={instance ? `/${instance}/s/${r.id}` : `/s/${r.id}`}
onClick={() => { onClick={() => {
resetScrollPosition(r.id); resetScrollPosition(r.id);
}} }}
> */} > */}
<div class="status-focus" tabIndex={0}> <div class="status-focus" tabIndex={0}>
<Status <Status
statusID={r.id} statusID={r.id}
instance={instance} instance={instance}
withinContext withinContext
size="s" size="s"
enableTranslate enableTranslate
onMediaClick={handleMediaClick} onMediaClick={handleMediaClick}
showActionsBar showActionsBar
/> />
{!r.replies?.length && r.repliesCount > 0 && ( {!r.replies?.length && r.repliesCount > 0 && (
<div class="replies-link"> <div class="replies-link">
<Icon icon="comment2" alt={t`Replies`} />{' '} <Icon icon="comment2" alt={t`Replies`} />{' '}
<span title={r.repliesCount}> <span title={r.repliesCount}>
{shortenNumber(r.repliesCount)} {shortenNumber(r.repliesCount)}
</span> </span>
</div> </div>
)}
</div>
{/* </Link> */}
{r.replies?.length && (
<SubComments
instance={instance}
replies={r.replies}
level={r.level}
accWeight={!open ? r.weight : totalWeight}
openAll={openAll}
parentLink={{
to: instance ? `/${instance}/s/${r.id}` : `/s/${r.id}`,
onClick: () => {
resetScrollPosition(r.id);
},
}}
/>
)} )}
</div> </li>
{/* </Link> */} ))}
{r.replies?.length && ( </ul>
<SubComments )}
instance={instance} </Container>
replies={r.replies}
level={level + 1}
accWeight={!open ? r.weight : totalWeight}
openAll={openAll}
parentLink={{
to: instance ? `/${instance}/s/${r.id}` : `/s/${r.id}`,
onClick: () => {
resetScrollPosition(r.id);
},
}}
/>
)}
</li>
))}
</ul>
</details>
); );
} }

View file

@ -27,12 +27,19 @@ const CODE_BLOCK_END_REGEX = /```$/;
const INLINE_CODE_REGEX = /`[^`]+`/; const INLINE_CODE_REGEX = /`[^`]+`/;
const TWITTER_DOMAIN_REGEX = /(twitter|x)\.com/i; const TWITTER_DOMAIN_REGEX = /(twitter|x)\.com/i;
const TWITTER_MENTION_REGEX = /@[a-zA-Z0-9_]+@(twitter|x)\.com/; const TWITTER_MENTION_REGEX = /@[a-zA-Z0-9_]+@(twitter|x)\.com/;
const TWITTER_MENTION_CAPTURE_REGEX = /(@([a-zA-Z0-9_]+)@(twitter|x)\.com)/; const TWITTER_MENTION_CAPTURE_REGEX = /(@([a-zA-Z0-9_]+)@(twitter|x)\.com)/g;
const CODE_INLINE_CAPTURE_REGEX = /(`[^]+?`)/g;
function createDOM(html, isDocumentFragment) { function createDOM(html, isDocumentFragment) {
const tpl = document.createElement('template'); if (isDocumentFragment) {
tpl.innerHTML = html; const tpl = document.createElement('template');
return isDocumentFragment ? tpl.content : tpl; tpl.innerHTML = html;
return tpl.content;
} else {
const tpl = document.createElement('div');
tpl.innerHTML = html;
return tpl;
}
} }
function _enhanceContent(content, opts = {}) { function _enhanceContent(content, opts = {}) {
@ -110,13 +117,11 @@ function _enhanceContent(content, opts = {}) {
// ====== // ======
// Convert :shortcode: to <img /> // Convert :shortcode: to <img />
let textNodes; let textNodes;
if (enhancedContent.includes(':')) { if (enhancedContent.includes(':') && emojis?.length) {
textNodes = extractTextNodes(dom); textNodes = extractTextNodes(dom);
for (const node of textNodes) { for (const node of textNodes) {
let html = escapeHTML(node.nodeValue); let html = escapeHTML(node.nodeValue);
if (emojis) { html = emojifyText(html, emojis);
html = emojifyText(html, emojis);
}
fauxDiv.innerHTML = html; fauxDiv.innerHTML = html;
node.replaceWith(...fauxDiv.childNodes); node.replaceWith(...fauxDiv.childNodes);
} }
@ -193,7 +198,7 @@ function _enhanceContent(content, opts = {}) {
for (const node of textNodes) { for (const node of textNodes) {
let html = escapeHTML(node.nodeValue); let html = escapeHTML(node.nodeValue);
if (INLINE_CODE_REGEX.test(html)) { if (INLINE_CODE_REGEX.test(html)) {
html = html.replaceAll(/(`[^]+?`)/g, '<code>$1</code>'); html = html.replaceAll(CODE_INLINE_CAPTURE_REGEX, '<code>$1</code>');
} }
fauxDiv.innerHTML = html; fauxDiv.innerHTML = html;
// const nodes = [...fauxDiv.childNodes]; // const nodes = [...fauxDiv.childNodes];
@ -285,6 +290,17 @@ function _enhanceContent(content, opts = {}) {
} }
} }
// FIX CLOAK MODE FOR SAFARI
// Workaround for Safari so that `text-decoration-thickness` works
// Wrap child text nodes in spans
for (const node of dom.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
const span = document.createElement('span');
span.textContent = node.textContent;
dom.replaceChild(span, node);
}
}
if (postEnhanceDOM) { if (postEnhanceDOM) {
queueMicrotask(() => postEnhanceDOM(dom)); queueMicrotask(() => postEnhanceDOM(dom));
// postEnhanceDOM(dom); // mutate dom // postEnhanceDOM(dom); // mutate dom

View file

@ -3,7 +3,7 @@ const focusDeck = () => {
const columns = document.getElementById('columns'); const columns = document.getElementById('columns');
if (columns) { if (columns) {
// Focus first column // Focus first column
// columns.querySelector('.deck-container')?.focus?.(); columns.querySelector('.deck-container')?.focus?.();
} else { } else {
const modals = document.querySelectorAll('#modal-container > *'); const modals = document.querySelectorAll('#modal-container > *');
if (modals?.length) { if (modals?.length) {

View file

@ -9,6 +9,10 @@ const notificationTypeKeys = {
poll: ['status'], poll: ['status'],
update: ['status'], update: ['status'],
}; };
const GROUP_TYPES = ['favourite', 'reblog', 'follow'];
const groupable = (type) => GROUP_TYPES.includes(type);
export function fixNotifications(notifications) { export function fixNotifications(notifications) {
return notifications.filter((notification) => { return notifications.filter((notification) => {
const { type, id, createdAt } = notification; const { type, id, createdAt } = notification;
@ -85,8 +89,8 @@ export function groupNotifications2(groupNotifications) {
} = gn; } = gn;
const date = createdAt ? new Date(createdAt).toLocaleDateString() : ''; const date = createdAt ? new Date(createdAt).toLocaleDateString() : '';
let virtualType = type; let virtualType = type;
const sameCount = // const sameCount =
notificationsCount > 0 && notificationsCount === sampleAccounts?.length; notificationsCount > 0 && notificationsCount === sampleAccounts?.length;
// if (sameCount && (type === 'favourite' || type === 'reblog')) { // if (sameCount && (type === 'favourite' || type === 'reblog')) {
if (type === 'favourite' || type === 'reblog') { if (type === 'favourite' || type === 'reblog') {
virtualType = 'favourite+reblog'; virtualType = 'favourite+reblog';
@ -94,7 +98,9 @@ export function groupNotifications2(groupNotifications) {
// const key = `${status?.id}-${virtualType}-${date}-${sameCount ? 1 : 0}`; // const key = `${status?.id}-${virtualType}-${date}-${sameCount ? 1 : 0}`;
const key = `${status?.id}-${virtualType}-${date}`; const key = `${status?.id}-${virtualType}-${date}`;
const mappedNotification = notificationsMap[key]; const mappedNotification = notificationsMap[key];
if (mappedNotification) { if (!groupable(type)) {
newGroupNotifications1.push(gn);
} else if (mappedNotification) {
// Merge sampleAccounts + merge _types // Merge sampleAccounts + merge _types
sampleAccounts.forEach((a) => { sampleAccounts.forEach((a) => {
const mappedAccount = mappedNotification.sampleAccounts.find( const mappedAccount = mappedNotification.sampleAccounts.find(
@ -199,7 +205,7 @@ export default function groupNotifications(notifications) {
} }
const key = `${status?.id}-${virtualType}-${date}`; const key = `${status?.id}-${virtualType}-${date}`;
const mappedNotification = notificationsMap[key]; const mappedNotification = notificationsMap[key];
if (virtualType === 'follow_request') { if (!groupable(type)) {
cleanNotifications[j++] = notification; cleanNotifications[j++] = notification;
} else if (mappedNotification?.account) { } else if (mappedNotification?.account) {
const mappedAccount = mappedNotification._accounts.find( const mappedAccount = mappedNotification._accounts.find(

View file

@ -132,18 +132,14 @@ export function getCurrentNodeInfo() {
// Massage these instance configurations to match the Mastodon API // Massage these instance configurations to match the Mastodon API
// - Pleroma // - Pleroma
function getInstanceConfiguration(instance) { function getInstanceConfiguration(instance) {
const { const { configuration, maxMediaAttachments, maxTootChars, pollLimits } =
configuration, instance;
maxMediaAttachments,
maxTootChars,
pleroma,
pollLimits,
} = instance;
const statuses = configuration?.statuses || {}; const statuses = configuration?.statuses || {};
if (maxMediaAttachments) { if (maxMediaAttachments) {
statuses.maxMediaAttachments ??= maxMediaAttachments; statuses.maxMediaAttachments ??= maxMediaAttachments;
} }
if (maxTootChars) { if (maxTootChars) {
statuses.maxCharacters ??= maxTootChars; statuses.maxCharacters ??= maxTootChars;
} }