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.
- [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)
- [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.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.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.
@ -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/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/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/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)
@ -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/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/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/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/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/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)
@ -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/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/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/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)

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

View file

@ -371,7 +371,8 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
content-visibility: auto;
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" */
content-visibility: visible !important;
}
@ -380,10 +381,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
}
.timeline.contextual {
--thread-start: 40px;
--line-start: 40px;
--indent-large-start: 40px;
--indent-small-start: 10px;
--thread-start: var(--indent-small-start);
--line-start: var(--indent-small-start);
--line-width: 3px;
--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-radius: 10px;
--line-diameter: calc(var(--line-radius) * 2);
@ -394,6 +399,68 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
:dir(rtl) & {
--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 {
background-image: linear-gradient(
@ -405,6 +472,28 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
transparent var(--line-end),
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;
transition: opacity 0.3s ease-in-out;
}
@ -426,7 +515,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.timeline.contextual
> li.descendant:not(.thread)
> :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']) {
background-color: var(--bg-color);
@ -1306,6 +1395,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
user-select: none;
width: 100%;
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 {
display: none;
@ -1320,7 +1414,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
width: 100%;
height: 100vh;
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(
closest-side,
var(--accent-color) 10%,
@ -1861,7 +1958,7 @@ body > .szh-menu-container {
box-shadow: 0 3px 24px -3px var(--drop-shadow-color);
}
.szh-menu__header {
margin: -8px 0 8px;
margin: -4px 0 8px;
padding: 8px 16px;
color: var(--text-insignificant-color);
font-size: 90%;
@ -2542,6 +2639,15 @@ ul.link-list li a .icon {
}
#columns > * {
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-radius: 16px; */
/* 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) {
document.addEventListener('visibilitychange', () => {
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');
let $meta;
if (theme) {

View file

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

View file

@ -12,9 +12,16 @@ import Notifications from '../pages/notifications';
import Public from '../pages/public';
import Search from '../pages/search';
import Trending from '../pages/trending';
import isRTL from '../utils/is-rtl';
import states from '../utils/states';
import useTitle from '../utils/useTitle';
const scrollIntoViewOptions = {
block: 'nearest',
inline: 'nearest',
behavior: 'smooth',
};
function Columns() {
useTitle(t`Home`, '/');
const snapStates = useSnapshot(states);
@ -50,12 +57,42 @@ function Columns() {
useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => {
try {
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) {
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 (
<div
id="columns"

View file

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

View file

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

View file

@ -113,6 +113,14 @@ export default memo(function KeyboardShortcutsHelp() {
</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`,
keys: <kbd>c</kbd>,

View file

@ -14,6 +14,7 @@ import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
import isRTL from '../utils/is-rtl';
import showToast from '../utils/show-toast';
import states from '../utils/states';
import store from '../utils/store';
import Icon from './icon';
import Link from './link';
@ -115,39 +116,95 @@ function MediaModal({
return () => clearTimeout(timer);
}, []);
const mediaAccentColors = useMemo(() => {
const mediaOklabColors = useMemo(() => {
return mediaAttachments?.map((media) => {
const { blurhash } = media;
if (blurhash) {
const averageColor = getBlurHashAverageColor(blurhash);
const labAverageColor = rgb2oklab(averageColor);
return oklab2rgb([0.6, labAverageColor[1], labAverageColor[2]]);
return rgb2oklab(averageColor);
}
return null;
});
}, [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 range = 100 / mediaAccentColors.length;
return (
mediaAccentColors
?.map((color, i) => {
const colors = 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}%
`;
if (color?.light && color?.dark) {
return {
light: `
rgb(${color.light?.join(',')}) ${start}%,
rgb(${color.light?.join(',')}) ${end}%
`,
dark: `
rgb(${color.dark?.join(',')}) ${start}%,
rgb(${color.dark?.join(',')}) ${end}%
`,
};
}
return `
return {
light: `
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]);
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 (
<div
class={`media-modal-container media-modal-count-${mediaAttachments?.length}`}
@ -179,8 +276,10 @@ function MediaModal({
mediaAttachments.length > 1
? {
backgroundAttachment: 'local',
backgroundImage: `linear-gradient(
to ${isRTL() ? 'left' : 'right'}, ${mediaAccentGradient})`,
'--accent-gradient-light': mediaAccentGradients?.light,
'--accent-gradient-dark': mediaAccentGradients?.dark,
// backgroundImage: `linear-gradient(
// to ${isRTL() ? 'left' : 'right'}, ${mediaAccentGradient})`,
}
: {}
}
@ -194,8 +293,14 @@ function MediaModal({
style={
accentColor
? {
'--accent-color': `rgb(${accentColor?.join(',')})`,
'--accent-alpha-color': `rgba(${accentColor?.join(
'--accent-color': `rgb(${accentColor.default.join(',')})`,
'--accent-light-color': `rgb(${accentColor.light?.join(
',',
)})`,
'--accent-dark-color': `rgb(${accentColor.dark?.join(
',',
)})`,
'--accent-alpha-color': `rgba(${accentColor.default.join(
',',
)}, 0.4)`,
}
@ -260,8 +365,8 @@ function MediaModal({
e.stopPropagation();
const left =
carouselRef.current.clientWidth * i * (isRTL() ? -1 : 1);
carouselRef.current.scrollTo({ left, behavior: 'smooth' });
carouselRef.current.focus();
carouselRef.current.scrollTo({ left, behavior: 'smooth' });
}}
>
<Icon icon="round" size="s" alt="⸱" />

View file

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

View file

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

View file

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

View file

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

View file

@ -247,8 +247,11 @@
:is(.content.truncated, .poll, .media-container.truncated) {
mask-image: linear-gradient(to bottom, #000 40px, transparent);
}
.status-card .card {
.status-card {
.card,
.card-byline {
display: none;
}
}
.timeline-deck .status-card .content.truncated:after {
/* Don't show "Read more" in status cards */
@ -1995,7 +1998,11 @@ a.card:is(:hover, :focus):visited {
gap: 4px;
color: var(--text-insignificant-color);
padding: 2px 8px;
align-items: center;
align-items: flex-start;
.icon {
min-height: 1em;
}
.avatar {
width: 16px !important;
@ -2124,8 +2131,8 @@ a.card:is(:hover, :focus):visited {
color: var(--text-insignificant-color);
font-size: 90%;
}
.status .extra-meta * {
vertical-align: middle;
.status .extra-meta .icon {
vertical-align: text-bottom;
}
.status .extra-meta a {
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 PostContent =
/*memo(*/
@ -343,32 +383,10 @@ function Status({
useEffect(() => {
if (!content) return;
if (_language) return;
if (languageAutoDetected) return;
let timer;
timer = setTimeout(async () => {
let detected = await detectLang(
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();
}
});
},
}),
);
let detected = await detectLang(getHTMLTextForDetectLang(content));
setLanguageAutoDetected(detected);
}, 1000);
return () => clearTimeout(timer);
@ -2061,6 +2079,7 @@ function Status({
class="content"
ref={contentRef}
data-read-more={_(readMoreText)}
inert={!!spoilerText && !showSpoiler ? true : undefined}
>
<PostContent
post={status}
@ -2101,8 +2120,7 @@ function Status({
/>
)}
{(((enableTranslate || inlineTranslate) &&
!!content.trim() &&
!!getHTMLText(emojifyText(content, emojis)) &&
isTranslateble(content) &&
differentLanguage) ||
forceTranslate) && (
<TranslationBlock
@ -2202,6 +2220,7 @@ function Status({
}
: undefined
}
checkAspectRatio={mediaAttachments.length === 1}
/>
))}
</div>
@ -2248,6 +2267,19 @@ function Status({
/> */}
<span>{_(visibilityText[visibility])}</span> &bull;{' '}
<a href={url} target="_blank" rel="noopener noreferrer">
{
// within a day
new Date().getTime() - createdAtDate.getTime() <
86400000 && (
<>
<RelativeTime
datetime={createdAtDate}
format="micro"
/>{' '}
{' '}
</>
)
}
<time
class="created"
datetime={createdAtDate.toISOString()}

View file

@ -15,13 +15,13 @@
"code": "cs-CZ",
"nativeName": "čeština",
"name": "Czech",
"completion": 80
"completion": 79
},
{
"code": "de-DE",
"nativeName": "Deutsch",
"name": "German",
"completion": 96
"completion": 95
},
{
"code": "eo-UY",
@ -57,13 +57,13 @@
"code": "fr-FR",
"nativeName": "français",
"name": "French",
"completion": 99
"completion": 98
},
{
"code": "gl-ES",
"nativeName": "galego",
"name": "Galician",
"completion": 100
"completion": 99
},
{
"code": "he-IL",
@ -75,7 +75,7 @@
"code": "it-IT",
"nativeName": "italiano",
"name": "Italian",
"completion": 100
"completion": 99
},
{
"code": "ja-JP",
@ -87,13 +87,13 @@
"code": "kab",
"nativeName": "Taqbaylit",
"name": "Kabyle",
"completion": 100
"completion": 99
},
{
"code": "ko-KR",
"nativeName": "한국어",
"name": "Korean",
"completion": 97
"completion": 96
},
{
"code": "lt-LT",
@ -105,19 +105,19 @@
"code": "nb-NO",
"nativeName": "norsk bokmål",
"name": "Norwegian Bokmål",
"completion": 31
"completion": 51
},
{
"code": "nl-NL",
"nativeName": "Nederlands",
"name": "Dutch",
"completion": 83
"completion": 82
},
{
"code": "pl-PL",
"nativeName": "polski",
"name": "Polish",
"completion": 6
"completion": 10
},
{
"code": "pt-BR",
@ -147,7 +147,7 @@
"code": "uk-UA",
"nativeName": "українська",
"name": "Ukrainian",
"completion": 26
"completion": 100
},
{
"code": "zh-CN",
@ -159,6 +159,6 @@
"code": "zh-TW",
"nativeName": "繁體中文",
"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();
let title = t`Account posts`;
if (account?.acct) {
const acctDisplay = /@/.test(account.acct) ? '' : '@' + account.acct;
const acctDisplay = (/@/.test(account.acct) ? '' : '@') + account.acct;
const accountDisplay = account?.displayName
? `${account.displayName} (${acctDisplay})`
: `${acctDisplay}`;

View file

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

View file

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

View file

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

View file

@ -24,6 +24,12 @@ const SHORT_LIMIT = 5;
const LIMIT = 40;
const emptySearchParams = new URLSearchParams();
const scrollIntoViewOptions = {
block: 'nearest',
inline: 'center',
behavior: 'smooth',
};
function Search({ columnMode, ...props }) {
const params = columnMode ? {} : useParams();
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();
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">
<header class={uiState === 'loading' ? 'loading' : ''}>
<div class="header-grid">

View file

@ -14,6 +14,7 @@ import targetLanguages from '../data/lingva-target-languages';
import { api } from '../utils/api';
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
import localeCode2Text from '../utils/localeCode2Text';
import prettyBytes from '../utils/pretty-bytes';
import {
initSubscription,
isPushSupported,
@ -856,6 +857,13 @@ function Settings({ onClose }) {
>
Show keys count
</button>{' '}
<button
type="button"
class="plain2 small"
onClick={async () => alert(await getCachesSize())}
>
Show cache size
</button>{' '}
<button
type="button"
class="plain2 small"
@ -902,6 +910,33 @@ async function getCachesKeys() {
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) {
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 }) {
const [searchParams, setSearchParams] = useSearchParams();
const mediaParam = searchParams.get('media');
@ -321,6 +326,9 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
const context = await contextFetch;
const { ancestors, descendants } = context;
ancestors.sort(createdAtSort);
descendants.sort(createdAtSort);
totalDescendants.current = descendants?.length || 0;
const missingStatuses = new Set();
@ -360,7 +368,11 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
nestedDescendants.push(status);
} else if (
!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
) {
// 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 });
if (missingStatuses.size) {
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) => ({
id: _r.id,
account: _r.account,
repliesCount: _r.repliesCount,
content: _r.content,
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,
thread: s.account.id === heroStatus.account.id,
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
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) {
setLimit(heroLimit + 1);
}
console.log({ allStatuses });
console.log({ allStatuses, descendantLevelsCount });
setStatuses(allStatuses);
cachedStatusesMap[id] = allStatuses;
@ -564,7 +594,10 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
useTitle(
heroDisplayName && heroContentText
? `${heroDisplayName}: "${heroContentText}"`
: t`Post`,
: t({
id: 'post.title',
message: 'Post',
}),
'/:instance?/s/:id',
);
@ -747,6 +780,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
replies,
repliesCount,
weight,
level,
} = status;
const isHero = statusID === id;
const isLinkable = isThread || ancestor;
@ -920,7 +954,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
instance={instance}
replies={replies}
hasParentThread={thread}
level={1}
level={level}
accWeight={weight}
openAll={totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT}
parentLink={{
@ -1094,7 +1128,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
</>
) : (
<>
Post{' '}
<Trans id="post.title">Post</Trans>{' '}
<button
type="button"
class="ancestors-indicator light small"
@ -1403,22 +1437,40 @@ 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 (
<details
<Container
ref={detailsRef}
class="replies"
open={openBefore || open}
onToggle={(e) => {
open={isDetails ? openBefore || open : undefined}
onToggle={
isDetails
? (e) => {
const { open } = e.target;
// use first reply as ID
cachedRepliesToggle[replies[0].id] = open;
}}
}
: undefined
}
style={{
'--comments-level': level,
}}
data-comments-level={level}
data-comments-level-overflow={level > 4}
>
{!open && (
<summary class="replies-summary" hidden={open}>
<span class="avatars">
{accounts.map((a) => (
@ -1478,6 +1530,8 @@ function SubComments({
</Link>
)}
</summary>
)}
{renderReplies && (
<ul>
{replies.map((r) => (
<li key={r.id}>
@ -1512,7 +1566,7 @@ function SubComments({
<SubComments
instance={instance}
replies={r.replies}
level={level + 1}
level={r.level}
accWeight={!open ? r.weight : totalWeight}
openAll={openAll}
parentLink={{
@ -1526,7 +1580,8 @@ function SubComments({
</li>
))}
</ul>
</details>
)}
</Container>
);
}

View file

@ -27,12 +27,19 @@ const CODE_BLOCK_END_REGEX = /```$/;
const INLINE_CODE_REGEX = /`[^`]+`/;
const TWITTER_DOMAIN_REGEX = /(twitter|x)\.com/i;
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) {
if (isDocumentFragment) {
const tpl = document.createElement('template');
tpl.innerHTML = html;
return isDocumentFragment ? tpl.content : tpl;
return tpl.content;
} else {
const tpl = document.createElement('div');
tpl.innerHTML = html;
return tpl;
}
}
function _enhanceContent(content, opts = {}) {
@ -110,13 +117,11 @@ function _enhanceContent(content, opts = {}) {
// ======
// Convert :shortcode: to <img />
let textNodes;
if (enhancedContent.includes(':')) {
if (enhancedContent.includes(':') && emojis?.length) {
textNodes = extractTextNodes(dom);
for (const node of textNodes) {
let html = escapeHTML(node.nodeValue);
if (emojis) {
html = emojifyText(html, emojis);
}
fauxDiv.innerHTML = html;
node.replaceWith(...fauxDiv.childNodes);
}
@ -193,7 +198,7 @@ function _enhanceContent(content, opts = {}) {
for (const node of textNodes) {
let html = escapeHTML(node.nodeValue);
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;
// 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) {
queueMicrotask(() => postEnhanceDOM(dom));
// postEnhanceDOM(dom); // mutate dom

View file

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

View file

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

View file

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